diff options
Diffstat (limited to 'src/util/replacer.js')
-rw-r--r-- | src/util/replacer.js | 284 |
1 files changed, 145 insertions, 139 deletions
diff --git a/src/util/replacer.js b/src/util/replacer.js index 50a90004..7240940d 100644 --- a/src/util/replacer.js +++ b/src/util/replacer.js @@ -1,23 +1,6 @@ import {logError, logWarn} from './cli.js'; import {escapeRegex} from './sugar.js'; -export function validateReplacerSpec(replacerSpec, {find, link}) { - let success = true; - - for (const [key, {link: linkKey, find: findKey, html}] of Object.entries(replacerSpec)) { - if (!html && !link[linkKey]) { - logError`The replacer spec ${key} has invalid link key ${linkKey}! Specify it in link specs or fix typo.`; - success = false; - } - if (findKey && !find[findKey]) { - logError`The replacer spec ${key} has invalid find key ${findKey}! Specify it in find specs or fix typo.`; - success = false; - } - } - - return success; -} - // Syntax literals. const tagBeginning = '[['; const tagEnding = ']]'; @@ -292,13 +275,157 @@ function parseNodes(input, i, stopAt, textOnly) { return nodes; } +function parseAttributes(string) { + const attributes = Object.create(null); + + const skipWhitespace = i => { + if (!/\s/.test(string[i])) { + return i; + } + + const match = string.slice(i).match(/[^\s]/); + if (match) { + return i + match.index; + } + + return string.length; + }; + + for (let i = 0; i < string.length; ) { + i = skipWhitespace(i); + const aStart = i; + const aEnd = i + string.slice(i).match(/[\s=]|$/).index; + const attribute = string.slice(aStart, aEnd); + i = skipWhitespace(aEnd); + if (string[i] === '=') { + i = skipWhitespace(i + 1); + let end, endOffset; + if (string[i] === '"' || string[i] === "'") { + end = string[i]; + endOffset = 1; + i++; + } else { + end = '\\s'; + endOffset = 0; + } + const vStart = i; + const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index; + const value = string.slice(vStart, vEnd); + i = vEnd + endOffset; + attributes[attribute] = value; + } else { + attributes[attribute] = attribute; + } + } + + return ( + Object.fromEntries( + Object.entries(attributes) + .map(([key, val]) => [ + key, + val === 'true' + ? true + : val === 'false' + ? false + : val === key + ? true + : val, + ]))); +} + +export function postprocessImages(inputNodes) { + const outputNodes = []; + + let atStartOfLine = true; + + const lastNode = inputNodes[inputNodes.length - 1]; + + for (const node of inputNodes) { + if (node.type === 'tag') { + atStartOfLine = false; + } + + if (node.type === 'text') { + const imageRegexp = /<img (.*?)>/g; + + let match = null, parseFrom = 0; + while (match = imageRegexp.exec(node.data)) { + const previousText = node.data.slice(parseFrom, match.index); + outputNodes.push({type: 'text', data: previousText}); + parseFrom = match.index + match[0].length; + + const imageNode = {type: 'image'}; + const attributes = parseAttributes(match[1]); + + imageNode.src = attributes.src; + + if (previousText.endsWith('\n')) { + atStartOfLine = true; + } + + imageNode.inline = (() => { + // If we've already determined we're in the middle of a line, + // we're inline. (Of course!) + if (!atStartOfLine) { + return true; + } + + // If there's more text to go in this text node, and what's + // remaining doesn't start with a line break, we're inline. + if ( + parseFrom !== node.data.length && + node.data[parseFrom] !== '\n' + ) { + return true; + } + + // If we're at the end of this text node, but this text node + // isn't the last node overall, we're inline. + if ( + parseFrom === node.data.length && + node !== lastNode + ) { + return true; + } + + // If no other condition matches, this image is on its own line. + return false; + })(); + + if (attributes.width) imageNode.width = parseInt(attributes.width); + if (attributes.height) imageNode.height = parseInt(attributes.height); + + outputNodes.push(imageNode); + + // No longer at the start of a line after an image - there will at + // least be a text node with only '\n' before the next image that's + // on its own line. + atStartOfLine = false; + } + + if (parseFrom !== node.data.length) { + outputNodes.push({ + type: 'text', + data: node.data.slice(parseFrom), + }); + } + + continue; + } + + outputNodes.push(node); + } + + return outputNodes; +} + export function parseInput(input) { if (typeof input !== 'string') { throw new TypeError(`Expected input to be string, got ${input}`); } try { - return parseNodes(input, 0); + return postprocessImages(parseNodes(input, 0)); } catch (errorNode) { if (errorNode.type !== 'error') { throw errorNode; @@ -334,124 +461,3 @@ export function parseInput(input) { ].join('\n')); } } - -function evaluateTag(node, opts) { - const {find, input, language, link, replacerSpec, to} = opts; - - const source = input.slice(node.i, node.iEnd); - - const replacerKeyImplied = !node.data.replacerKey; - const replacerKey = replacerKeyImplied ? 'track' : node.data.replacerKey.data; - - if (!replacerSpec[replacerKey]) { - logWarn`The link ${source} has an invalid replacer key!`; - return source; - } - - const { - find: findKey, - link: linkKey, - value: valueFn, - html: htmlFn, - transformName, - } = replacerSpec[replacerKey]; - - const replacerValue = transformNodes(node.data.replacerValue, opts); - - const value = valueFn - ? valueFn(replacerValue) - : findKey - ? find[findKey]( - replacerKeyImplied ? replacerValue : replacerKey + `:` + replacerValue - ) - : { - directory: replacerValue, - name: null, - }; - - if (!value) { - logWarn`The link ${source} does not match anything!`; - return source; - } - - const enteredLabel = node.data.label && transformNode(node.data.label, opts); - - const label = - enteredLabel || - (transformName && transformName(value.name, node, input)) || - null; - - const hash = node.data.hash && transformNode(node.data.hash, opts); - - const args = - node.data.args && - Object.fromEntries( - node.data.args.map(({key, value}) => [ - transformNode(key, opts), - transformNodes(value, opts), - ]) - ); - - const fn = htmlFn ? htmlFn : link[linkKey]; - - try { - return fn(value, {text: label, hash, args, language, to}); - } catch (error) { - logError`The link ${source} failed to be processed: ${error}`; - return source; - } -} - -function transformNode(node, opts) { - if (!node) { - throw new Error('Expected a node!'); - } - - if (Array.isArray(node)) { - throw new Error('Got an array - use transformNodes here!'); - } - - switch (node.type) { - case 'text': - return node.data; - case 'tag': - return evaluateTag(node, opts); - default: - throw new Error(`Unknown node type ${node.type}`); - } -} - -function transformNodes(nodes, opts) { - if (!nodes || !Array.isArray(nodes)) { - throw new Error(`Expected an array of nodes! Got: ${nodes}`); - } - - return nodes.map((node) => transformNode(node, opts)).join(''); -} - -export function transformInline(input, { - replacerSpec, - find, - language, - link, - to, - wikiData, -}) { - if (!replacerSpec) throw new Error('Expected replacerSpec'); - if (!find) throw new Error('Expected find'); - if (!language) throw new Error('Expected language'); - if (!link) throw new Error('Expected link'); - if (!to) throw new Error('Expected to'); - if (!wikiData) throw new Error('Expected wikiData'); - - const nodes = parseInput(input); - return transformNodes(nodes, { - input, - find, - link, - replacerSpec, - language, - to, - wikiData, - }); -} |