From d41853b617e1b0e7fa41309ff0d42611305c3149 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 15 May 2021 19:08:48 -0300 Subject: bigass code refactor (no more legacy page writes) --- src/util/html.js | 16 +- src/util/link.js | 10 +- src/util/replacer.js | 424 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/util/strings.js | 11 +- src/util/sugar.js | 11 ++ 5 files changed, 458 insertions(+), 14 deletions(-) create mode 100644 src/util/replacer.js (limited to 'src/util') diff --git a/src/util/html.js b/src/util/html.js index 4895301..9475698 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -73,18 +73,20 @@ export function escapeAttributeValue(value) { export function attributes(attribs) { return Object.entries(attribs) .map(([ key, val ]) => { - if (!val) - return [key, val]; - else if (typeof val === 'string' || typeof val === 'boolean') - return [key, val]; + if (typeof val === 'undefined' || val === null) + return [key, val, false]; + else if (typeof val === 'string') + return [key, val, true]; + else if (typeof val === 'boolean') + return [key, val, val]; else if (typeof val === 'number') - return [key, val.toString()]; + return [key, val.toString(), true]; else if (Array.isArray(val)) - return [key, val.join(' ')]; + return [key, val.filter(Boolean).join(' '), val.length > 0]; else throw new Error(`Attribute value for ${key} should be primitive or array, got ${typeof val}`); }) - .filter(([ key, val ]) => val) + .filter(([ key, val, keep ]) => keep) .map(([ key, val ]) => (typeof val === 'boolean' ? `${key}` : `${key}="${escapeAttributeValue(val)}"`)) diff --git a/src/util/link.js b/src/util/link.js index e5c3c59..107b35f 100644 --- a/src/util/link.js +++ b/src/util/link.js @@ -14,15 +14,21 @@ import { getLinkThemeString } from './colors.js' const linkHelper = (hrefFn, {color = true, attr = null} = {}) => (thing, { - strings, to, + to, text = '', + attributes = null, class: className = '', + color: color2 = true, hash = '' }) => ( html.tag('a', { ...attr ? attr(thing) : {}, + ...attributes ? attributes : {}, href: hrefFn(thing, {to}) + (hash ? (hash.startsWith('#') ? '' : '#') + hash : ''), - style: color ? getLinkThemeString(thing.color) : '', + style: ( + typeof color2 === 'string' ? getLinkThemeString(color2) : + color2 && color ? getLinkThemeString(thing.color) : + ''), class: className }, text || thing.name) ); diff --git a/src/util/replacer.js b/src/util/replacer.js new file mode 100644 index 0000000..a1e880e --- /dev/null +++ b/src/util/replacer.js @@ -0,0 +1,424 @@ +import find from './find.js'; +import {logError} from './cli.js'; +import {escapeRegex} from './sugar.js'; + +export function validateReplacerSpec(replacerSpec, link) { + let success = true; + + for (const [key, {link: linkKey, find: findKey, value, 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 = ']]'; +const tagReplacerValue = ':'; +const tagHash = '#'; +const tagArgument = '*'; +const tagArgumentValue = '='; +const tagLabel = '|'; + +const noPrecedingWhitespace = '(? ({i, type: 'error', data: {message}}); +const endOfInput = (i, comment) => makeError(i, `Unexpected end of input (${comment}).`); + +// These are 8asically stored on the glo8al scope, which might seem odd +// for a recursive function, 8ut the values are only ever used immediately +// after they're set. +let stopped, + stop_iMatch, + stop_iParse, + stop_literal; + +function parseOneTextNode(input, i, stopAt) { + return parseNodes(input, i, stopAt, true)[0]; +} + +function parseNodes(input, i, stopAt, textOnly) { + let nodes = []; + let escapeNext = false; + let string = ''; + let iString = 0; + + stopped = false; + + const pushTextNode = (isLast) => { + string = input.slice(iString, i); + + // If this is the last text node 8efore stopping (at a stopAt match + // or the end of the input), trim off whitespace at the end. + if (isLast) { + string = string.trimEnd(); + } + + if (string.length) { + nodes.push({i: iString, iEnd: i, type: 'text', data: string}); + string = ''; + } + }; + + const literalsToMatch = stopAt ? stopAt.concat([R_tagBeginning]) : [R_tagBeginning]; + + // The 8ackslash stuff here is to only match an even (or zero) num8er + // of sequential 'slashes. Even amounts always cancel out! Odd amounts + // don't, which would mean the following literal is 8eing escaped and + // should 8e counted only as part of the current string/text. + // + // Inspired 8y this: https://stackoverflow.com/a/41470813 + const regexpSource = `(?= 0) { + lineStart += 1; + } else { + lineStart = 0; + } + + let lineEnd = input.slice(i).indexOf('\n'); + if (lineEnd >= 0) { + lineEnd += i; + } else { + lineEnd = input.length; + } + + const line = input.slice(lineStart, lineEnd); + + const cursor = i - lineStart; + + throw new SyntaxError(fixWS` + Parse error (at pos ${i}): ${message} + ${line} + ${'-'.repeat(cursor) + '^'} + `); + } +} + +function evaluateTag(node, opts) { + const { input, link, replacerSpec, strings, to, wikiData } = opts; + + const source = input.slice(node.i, node.iEnd); + + const replacerKey = node.data.replacerKey?.data || 'track'; + + 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](replacerValue, {wikiData}) : + { + 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) + || value.name); + + if (!valueFn && !label) { + logWarn`The link ${source} requires a label be entered!`; + return source; + } + + const hash = node.data.hash && transformNodes(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, strings, 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, link, strings, to, wikiData}) { + if (!replacerSpec) throw new Error('Expected replacerSpec'); + if (!link) throw new Error('Expected link'); + if (!strings) throw new Error('Expected strings'); + if (!to) throw new Error('Expected to'); + if (!wikiData) throw new Error('Expected wikiData'); + + const nodes = parseInput(input); + return transformNodes(nodes, {input, link, replacerSpec, strings, to, wikiData}); +} diff --git a/src/util/strings.js b/src/util/strings.js index 99104aa..c066435 100644 --- a/src/util/strings.js +++ b/src/util/strings.js @@ -1,4 +1,5 @@ import { logWarn } from './cli.js'; +import { bindOpts } from './sugar.js'; // Localiz8tion time! Or l10n as the neeeeeeeerds call it. Which is a terri8le // name and not one I intend on using, thank you very much. (Don't even get me @@ -194,13 +195,13 @@ export function genStrings(stringsJSON, { } }; - const bindOpts = (obj, bind) => Object.fromEntries(Object.entries(obj).map( - ([ key, fn ]) => [key, (value, opts = {}) => fn(value, {...bind, ...opts})] - )); - // And the provided utility dictionaries themselves, of course! for (const [key, utilDict] of Object.entries(bindUtilities)) { - strings[key] = bindOpts(utilDict, {strings}); + const boundUtilDict = {}; + for (const [key, fn] of Object.entries(utilDict)) { + boundUtilDict[key] = bindOpts(fn, {strings}); + } + strings[key] = boundUtilDict; } return strings; diff --git a/src/util/sugar.js b/src/util/sugar.js index c24c617..79a271b 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -76,3 +76,14 @@ export function delay(ms) { export function escapeRegex(string) { return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } + +export function bindOpts(fn, bind) { + const bindIndex = bind[bindOpts.bindIndex] ?? 1; + + return (...args) => { + const opts = args[bindIndex] ?? {}; + return fn(...args.slice(0, bindIndex), {...bind, ...opts}); + }; +} + +bindOpts.bindIndex = Symbol(); -- cgit 1.3.0-6-gf8a5