Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
2 changed files
with
156 additions
and
145 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,162 +1,128 @@ | ||
'use strict'; | ||
const TEMPLATE_REGEX = /(?:\\(u[a-f0-9]{4}|x[a-f0-9]{2}|.))|(?:\{(~)?(\w+(?:\([^)]*\))?(?:\.\w+(?:\([^)]*\))?)*)(?:[ \t]|(?=\r?\n)))|(\})|((?:.|[\r\n\f])+?)/gi; | ||
const STYLE_REGEX = /(?:^|\.)(\w+)(?:\(([^)]*)\))?/g; | ||
const STRING_REGEX = /^(['"])((?:\\.|(?!\1)[^\\])*)\1$/; | ||
const ESCAPE_REGEX = /\\(u[0-9a-f]{4}|x[0-9a-f]{2}|.)|([^\\])/gi; | ||
|
||
const ESCAPES = { | ||
n: '\n', | ||
r: '\r', | ||
t: '\t', | ||
b: '\b', | ||
f: '\f', | ||
v: '\v', | ||
0: '\0', | ||
'\\': '\\', | ||
e: '\u001b', | ||
a: '\u0007' | ||
}; | ||
|
||
function data(parent) { | ||
return { | ||
styles: [], | ||
parent, | ||
contents: [] | ||
}; | ||
} | ||
|
||
const zeroBound = n => n < 0 ? 0 : n; | ||
const lastIndex = a => zeroBound(a.length - 1); | ||
function unescape(c) { | ||
if ((c[0] === 'u' && c.length === 5) || (c[0] === 'x' && c.length === 3)) { | ||
return String.fromCharCode(parseInt(c.slice(1), 16)); | ||
} | ||
|
||
const last = a => a[lastIndex(a)]; | ||
return ESCAPES[c] || c; | ||
} | ||
|
||
const takeWhileReverse = (array, predicate, start) => { | ||
const out = []; | ||
function parseArguments(name, args) { | ||
const results = []; | ||
const chunks = args.trim().split(/\s*,\s*/g); | ||
let matches; | ||
|
||
for (let i = start; i >= 0 && i <= start; i--) { | ||
if (predicate(array[i])) { | ||
out.unshift(array[i]); | ||
for (const chunk of chunks) { | ||
if (!isNaN(chunk)) { | ||
results.push(Number(chunk)); | ||
} else if ((matches = chunk.match(STRING_REGEX))) { | ||
results.push(matches[2].replace(ESCAPE_REGEX, (m, escape, chr) => escape ? unescape(escape) : chr)); | ||
} else { | ||
break; | ||
throw new Error(`Invalid Chalk template style argument: ${chunk} (in style '${name}')`); | ||
} | ||
} | ||
|
||
return out; | ||
}; | ||
|
||
// Check if the character at position `i` in string is a normal character (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 results; | ||
} | ||
|
||
return n % 2 === 1; | ||
}; | ||
function parseStyle(style) { | ||
STYLE_REGEX.lastIndex = 0; | ||
|
||
const collectStyles = data => data ? collectStyles(data.parent).concat(data.styles) : ['reset']; | ||
const results = []; | ||
let matches; | ||
|
||
// Compute the style for a given data based on its style and the style of its parent. | ||
// Also accounts for `!style` styles which remove a style from the list if present. | ||
const sumStyles = data => { | ||
const negateRegex = /^~.+/; | ||
let out = []; | ||
while ((matches = STYLE_REGEX.exec(style)) !== null) { | ||
const name = matches[1]; | ||
|
||
for (const style of collectStyles(data)) { | ||
if (negateRegex.test(style)) { | ||
const exclude = style.slice(1); | ||
out = out.filter(x => x !== exclude); | ||
if (matches[2]) { | ||
const args = parseArguments(name, matches[2]); | ||
results.push([name].concat(args)); | ||
} else { | ||
out.push(style); | ||
results.push([name]); | ||
} | ||
} | ||
|
||
return out; | ||
}; | ||
|
||
// Take a string and parse 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; | ||
return results; | ||
} | ||
|
||
for (let i = 0; i < string.length; i++) { | ||
const char = string[i]; | ||
function buildStyle(chalk, styles) { | ||
const enabled = {}; | ||
|
||
const addNormalCharacter = () => { | ||
const lastChunk = last(current.contents); | ||
for (const layer of styles) { | ||
for (const style of layer.styles) { | ||
enabled[style[0]] = layer.inverse ? null : style.slice(1); | ||
} | ||
} | ||
|
||
if (typeof lastChunk === 'string') { | ||
current.contents[lastIndex(current.contents)] = lastChunk + char; | ||
} else { | ||
current.contents.push(char); | ||
let current = chalk; | ||
for (const styleName of Object.keys(enabled)) { | ||
if (Array.isArray(enabled[styleName])) { | ||
if (!(styleName in current)) { | ||
throw new Error(`Unknown Chalk style: ${styleName}`); | ||
} | ||
}; | ||
|
||
if (pushingStyle) { | ||
if (' \t'.includes(char)) { | ||
pushingStyle = false; | ||
} else if (char === '\n') { | ||
pushingStyle = false; | ||
addNormalCharacter(); | ||
} else if (char === '.') { | ||
current.styles.push(''); | ||
|
||
if (enabled[styleName].length > 0) { | ||
current = current[styleName].apply(current, enabled[styleName]); | ||
} else { | ||
current.styles[lastIndex(current.styles)] = (last(current.styles) || '') + char; | ||
current = current[styleName]; | ||
} | ||
} 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('Template literal has an unclosed block'); | ||
} | ||
|
||
return root; | ||
return current; | ||
} | ||
|
||
// Take a tree of data objects and flatten 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 | ||
}); | ||
module.exports = (chalk, tmp) => { | ||
const styles = []; | ||
const chunks = []; | ||
let chunk = []; | ||
|
||
// eslint-disable-next-line max-params | ||
tmp.replace(TEMPLATE_REGEX, (m, escapeChar, inverse, style, close, chr) => { | ||
if (escapeChar) { | ||
chunk.push(unescape(escapeChar)); | ||
} else if (style) { | ||
const str = chunk.join(''); | ||
chunk = []; | ||
chunks.push(styles.length === 0 ? str : buildStyle(chalk, styles)(str)); | ||
styles.push({inverse, styles: parseStyle(style)}); | ||
} else if (close) { | ||
if (styles.length === 0) { | ||
throw new Error('Found extraneous } in Chalk template literal'); | ||
} | ||
|
||
chunks.push(buildStyle(chalk, styles)(chunk.join(''))); | ||
chunk = []; | ||
styles.pop(); | ||
} else { | ||
flat = flat.concat(flatten(content)); | ||
chunk.push(chr); | ||
} | ||
} | ||
}); | ||
|
||
return flat; | ||
} | ||
chunks.push(chunk.join('')); | ||
|
||
function assertStyle(chalk, style) { | ||
if (!chalk[style]) { | ||
throw new Error(`Invalid Chalk style: ${style}`); | ||
if (styles.length > 0) { | ||
const errMsg = `Chalk template literal is missing ${styles.length} closing bracket${styles.length === 1 ? '' : 's'} (\`}\`)`; | ||
throw new Error(errMsg); | ||
} | ||
} | ||
|
||
// Check if a given style is valid and parse 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); | ||
} | ||
|
||
// Perform the actual styling of the string | ||
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))); | ||
return chunks.join(''); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters