176 lines
3.8 KiB
JavaScript
176 lines
3.8 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
function data(parent) {
|
||
|
|
return {
|
||
|
|
styles: [],
|
||
|
|
parent,
|
||
|
|
contents: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const zeroBound = n => n < 0 ? 0 : n;
|
||
|
|
const lastIndex = a => zeroBound(a.length - 1);
|
||
|
|
|
||
|
|
const last = a => a[lastIndex(a)];
|
||
|
|
|
||
|
|
const takeWhileReverse = (array, predicate, start) => {
|
||
|
|
const out = [];
|
||
|
|
|
||
|
|
for (let i = start; i >= 0 && i <= start; i--) {
|
||
|
|
if (predicate(array[i])) {
|
||
|
|
out.unshift(array[i]);
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return out;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if the character at position i in string is a normal character a.k.a a non control character.
|
||
|
|
* */
|
||
|
|
const isNormalCharacter = (string, i) => {
|
||
|
|
const char = string[i];
|
||
|
|
const backslash = '\\';
|
||
|
|
|
||
|
|
if (!(char === backslash || char === '{' || char === '}')) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
const n = i === 0 ? 0 : takeWhileReverse(string, x => x === '\\', zeroBound(i - 1)).length;
|
||
|
|
|
||
|
|
return n % 2 === 1;
|
||
|
|
};
|
||
|
|
|
||
|
|
const collectStyles = data => data ? collectStyles(data.parent).concat(data.styles) : ['reset'];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Computes the style for a given data based on it's style and the style of it's parent. Also accounts for !style styles
|
||
|
|
* which remove a style from the list if present.
|
||
|
|
* */
|
||
|
|
const sumStyles = data => {
|
||
|
|
const negateRegex = /^~.+/;
|
||
|
|
let out = [];
|
||
|
|
|
||
|
|
for (const style of collectStyles(data)) {
|
||
|
|
if (negateRegex.test(style)) {
|
||
|
|
const exclude = style.slice(1);
|
||
|
|
out = out.filter(x => x !== exclude);
|
||
|
|
} else {
|
||
|
|
out.push(style);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return out;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Takes a string and parses it into a tree of data objects which inherit styles from their parent.
|
||
|
|
* */
|
||
|
|
function parse(string) {
|
||
|
|
const root = data(null);
|
||
|
|
let pushingStyle = false;
|
||
|
|
|
||
|
|
let current = root;
|
||
|
|
|
||
|
|
for (let i = 0; i < string.length; i++) {
|
||
|
|
const char = string[i];
|
||
|
|
|
||
|
|
const addNormalCharacter = () => {
|
||
|
|
const lastChunk = last(current.contents);
|
||
|
|
|
||
|
|
if (typeof lastChunk === 'string') {
|
||
|
|
current.contents[lastIndex(current.contents)] = lastChunk + char;
|
||
|
|
} else {
|
||
|
|
current.contents.push(char);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (pushingStyle) {
|
||
|
|
if (' \t'.indexOf(char) > -1) {
|
||
|
|
pushingStyle = false;
|
||
|
|
} else if (char === '\n') {
|
||
|
|
pushingStyle = false;
|
||
|
|
addNormalCharacter();
|
||
|
|
} else if (char === '.') {
|
||
|
|
current.styles.push('');
|
||
|
|
} else {
|
||
|
|
current.styles[lastIndex(current.styles)] = (last(current.styles) || '') + char;
|
||
|
|
}
|
||
|
|
} else if (isNormalCharacter(string, i)) {
|
||
|
|
addNormalCharacter();
|
||
|
|
} else if (char === '{') {
|
||
|
|
pushingStyle = true;
|
||
|
|
const nCurrent = data(current);
|
||
|
|
current.contents.push(nCurrent);
|
||
|
|
current = nCurrent;
|
||
|
|
} else if (char === '}') {
|
||
|
|
current = current.parent;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (current !== root) {
|
||
|
|
throw new Error('literal template has an unclosed block');
|
||
|
|
}
|
||
|
|
|
||
|
|
return root;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Takes a tree of data objects and flattens it to a list of data objects with the inherited and negations styles
|
||
|
|
* accounted for.
|
||
|
|
* */
|
||
|
|
function flatten(data) {
|
||
|
|
let flat = [];
|
||
|
|
|
||
|
|
for (const content of data.contents) {
|
||
|
|
if (typeof content === 'string') {
|
||
|
|
flat.push({
|
||
|
|
styles: sumStyles(data),
|
||
|
|
content
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
flat = flat.concat(flatten(content));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return flat;
|
||
|
|
}
|
||
|
|
|
||
|
|
function assertStyle(chalk, style) {
|
||
|
|
if (!chalk[style]) {
|
||
|
|
throw new Error(`invalid Chalk style: ${style}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if a given style is valid and parses style functions.
|
||
|
|
* */
|
||
|
|
function parseStyle(chalk, style) {
|
||
|
|
const fnMatch = style.match(/^\s*(\w+)\s*\(\s*([^)]*)\s*\)\s*/);
|
||
|
|
if (!fnMatch) {
|
||
|
|
assertStyle(chalk, style);
|
||
|
|
return chalk[style];
|
||
|
|
}
|
||
|
|
|
||
|
|
const name = fnMatch[1].trim();
|
||
|
|
const args = fnMatch[2].split(/,/g).map(s => s.trim());
|
||
|
|
|
||
|
|
assertStyle(chalk, name);
|
||
|
|
|
||
|
|
return chalk[name].apply(chalk, args);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Performs the actual styling of the string, essentially lifted from cli.js.
|
||
|
|
* */
|
||
|
|
function style(chalk, flat) {
|
||
|
|
return flat.map(data => {
|
||
|
|
const fn = data.styles.reduce(parseStyle, chalk);
|
||
|
|
return fn(data.content.replace(/\n$/, ''));
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = (chalk, string) => style(chalk, flatten(parse(string)));
|