@torchlight-api/torchlight-cli
Version:
A CLI for Torchlight - the syntax highlighting API
332 lines (262 loc) • 7.57 kB
JavaScript
import axios from 'axios';
import md5 from 'md5';
import get from 'lodash.get';
import chunk from 'lodash.chunk';
import chalk from 'chalk';
let silent = false;
function log(fn, args) {
args = Array.from(args);
if (!silent) {
console.log(fn(args.shift()), ...args);
}
}
function error() {
log(chalk.bold.bgRed, arguments);
}
function info() {
log(chalk.green, arguments);
}
function silence(silence = true) {
silent = silence;
}
var log$1 = {
error,
info,
silence
};
let store = {};
function Memory() {//
}
/**
* Get an item from the cache.
*
* @param {string} key
* @param {*} def
* @return {*}
*/
Memory.prototype.get = function (key, def) {
if (!Object.prototype.hasOwnProperty.call(store, key)) {
return def;
}
const entry = store[key];
if (Date.now() / 1000 > entry.expires) {
this.delete(key);
return def;
}
return entry.value;
};
/**
* Set an item in the cache.
*
* @param {string} key
* @param {*} value
* @param {number} ttlSeconds
*/
Memory.prototype.set = function (key, value, ttlSeconds = 60 * 24 * 7) {
store[key] = {
expires: Date.now() / 1000 + ttlSeconds,
value: value
};
};
/**
* Remove a key from the cache.
*
* @param key
*/
Memory.prototype.delete = function (key) {
delete store[key];
};
/**
* Clear the cache.
*/
Memory.prototype.clear = function () {
store = {};
};
/**
* @constructor
*/
const Torchlight = function () {
this.initialized = false;
this.chunkSize = 30;
this.configuration = {};
};
/**
* @param config
* @return {Torchlight}
*/
Torchlight.prototype.init = function (config, cache) {
var _process, _process$env, _config;
if (this.initialized) {
return this;
}
config = config || {};
if ((_process = process) !== null && _process !== void 0 && (_process$env = _process.env) !== null && _process$env !== void 0 && _process$env.TORCHLIGHT_TOKEN && !((_config = config) !== null && _config !== void 0 && _config.token)) {
config.token = process.env.TORCHLIGHT_TOKEN;
}
this.initialized = true;
this.configuration = config;
this.cache = cache || new Memory();
return this;
};
/**
* Get a value out of the configuration.
*
* @param {string} key
* @param {*} def
* @return {*}
*/
Torchlight.prototype.config = function (key, def = undefined) {
return get(this.configuration, key, def);
};
/**
* Hash of the Torchlight configuration.
*
* @return {string}
*/
Torchlight.prototype.configHash = function () {
return md5(this.configuration);
};
/**
* @param blocks
* @return {Promise<*>}
*/
Torchlight.prototype.highlight = function (blocks) {
// Set the data from cache if it's there.
blocks.map(block => block.setResponseData(this.cache.get(block.hash(), {}))); // Reject the blocks that have already been highlighted from the cache.
const needed = blocks.filter(block => !block.highlighted); // Only send the un-highlighted blocks to the API.
return this.request(needed).then(highlighted => {
needed.forEach(block => {
// Look through the response and match em up by ID.
const found = highlighted.find(b => block.id === b.id);
if (!found || !found.highlighted) {
return;
} // Store it in the cache for later.
this.cache.set(block.hash(), {
highlighted: found.highlighted,
classes: found.classes,
styles: found.styles
}); // Set the info on the block.
block.setResponseData(found);
}); // Look for the blocks that weren't highlighted and add a default.
blocks.filter(block => !block.highlighted).forEach(block => {
log$1.error(`A block failed to highlight. The code was: \`${block.code.substring(0, 20)} [...]\``); // Add the `line` divs so everyone's CSS will work even on default blocks.
block.highlighted = block.code.split('\n').map(line => `<div class="line">${htmlEntities(line)}</div>`).join('');
block.classes = 'torchlight';
});
return blocks;
});
};
/**
* @param blocks
* @return {Promise<*[]>}
*/
Torchlight.prototype.request = function (blocks) {
if (!blocks.length) {
return Promise.resolve([]);
}
const token = this.config('token'); // For huge sites, we need to send blocks in chunks so
// that we don't send e.g. 500 blocks in one request.
if (blocks.length > this.chunkSize) {
return this.fan(blocks);
}
const host = this.config('host', 'https://api.torchlight.dev');
return axios.post(`${host}/highlight`, {
blocks: blocks.map(block => block.toRequestParams()),
options: this.config('options', {})
}, {
headers: {
Authorization: `Bearer ${token}`,
'X-Torchlight-Client': 'Torchlight CLI'
}
}).then(response => response.data.blocks);
};
Torchlight.prototype.fan = function (blocks) {
const highlighted = [];
const errors = [];
const requests = chunk(blocks, this.chunkSize).map(chunk => this.request(chunk)); // Let all of the promises settle, even if some of them fail.
return Promise.allSettled(requests).then(responses => {
responses.forEach(response => {
// For a successful request, add the blocks to the array.
if (response.status === 'fulfilled') {
highlighted.push(...response.value);
} // For an error, stash it as well.
if (response.status === 'rejected') {
errors.push(response.reason);
}
}); // We got some blocks...
if (highlighted.length) {
// ...and some errors. In this case we just log the
// error and go ahead and use the blocks.
if (errors.length) {
log$1.error(`${errors.length} fanned request(s) failed, but others succeeded. Error: ${errors[0]}.`);
}
return highlighted;
} // Errors only, throw a proper error.
if (errors.length) {
throw new Error(errors[0]);
}
return [];
});
};
function htmlEntities(str) {
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
var torchlight = new Torchlight();
function guid() {
const S4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
return `${S4()}${S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`.toLowerCase();
}
function Block(opts = {}) {
opts = {
id: guid(),
theme: torchlight.config('theme', 'nord'),
...opts
};
this.id = opts.id;
this.code = opts.code;
this.language = opts.language;
this.theme = opts.theme;
this.highlighted = null;
this.classes = null;
this.styles = null;
}
Block.prototype.hash = function () {
return md5('' + this.language + this.theme + this.code + torchlight.config('bust', 0) + JSON.stringify(torchlight.config('options')));
};
Block.prototype.code = function (code) {
this.code = code;
return this;
};
Block.prototype.language = function (language) {
this.language = language;
return this;
};
Block.prototype.theme = function (theme) {
this.theme = theme;
return this;
};
Block.prototype.placeholder = function (extra = '') {
if (extra) {
extra = `-${extra}`;
}
return `__torchlight-block-[${this.id}]${extra}__`;
};
Block.prototype.setResponseData = function (data) {
if (data) {
this.highlighted = data.highlighted;
this.classes = data.classes;
this.styles = data.styles;
}
return this;
};
Block.prototype.toRequestParams = function () {
return {
id: this.id,
hash: this.hash,
language: this.language,
theme: this.theme,
code: this.code
};
};
export { Block, torchlight };
//# sourceMappingURL=torchlight-cli.esm.js.map