« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/util/replacer.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/replacer.js')
-rw-r--r--src/util/replacer.js679
1 files changed, 355 insertions, 324 deletions
diff --git a/src/util/replacer.js b/src/util/replacer.js
index b29044f..311f763 100644
--- a/src/util/replacer.js
+++ b/src/util/replacer.js
@@ -1,429 +1,460 @@
-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, 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;
-        }
+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, 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;
+  return success;
 }
 
 // Syntax literals.
-const tagBeginning = '[[';
-const tagEnding = ']]';
-const tagReplacerValue = ':';
-const tagHash = '#';
-const tagArgument = '*';
-const tagArgumentValue = '=';
-const tagLabel = '|';
+const tagBeginning = "[[";
+const tagEnding = "]]";
+const tagReplacerValue = ":";
+const tagHash = "#";
+const tagArgument = "*";
+const tagArgumentValue = "=";
+const tagLabel = "|";
 
-const noPrecedingWhitespace = '(?<!\\s)';
+const noPrecedingWhitespace = "(?<!\\s)";
 
-const R_tagBeginning =
-    escapeRegex(tagBeginning);
+const R_tagBeginning = escapeRegex(tagBeginning);
 
-const R_tagEnding =
-    escapeRegex(tagEnding);
+const R_tagEnding = escapeRegex(tagEnding);
 
 const R_tagReplacerValue =
-    noPrecedingWhitespace +
-    escapeRegex(tagReplacerValue);
+  noPrecedingWhitespace + escapeRegex(tagReplacerValue);
 
-const R_tagHash =
-    noPrecedingWhitespace +
-    escapeRegex(tagHash);
+const R_tagHash = noPrecedingWhitespace + escapeRegex(tagHash);
 
-const R_tagArgument =
-    escapeRegex(tagArgument);
+const R_tagArgument = escapeRegex(tagArgument);
 
-const R_tagArgumentValue =
-    escapeRegex(tagArgumentValue);
+const R_tagArgumentValue = escapeRegex(tagArgumentValue);
 
-const R_tagLabel =
-    escapeRegex(tagLabel);
+const R_tagLabel = escapeRegex(tagLabel);
 
 const regexpCache = {};
 
-const makeError = (i, message) => ({i, type: 'error', data: {message}});
-const endOfInput = (i, comment) => makeError(i, `Unexpected end of input (${comment}).`);
+const makeError = (i, message) => ({ 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;
+let stopped, stop_iMatch, stop_iParse, stop_literal;
 
 function parseOneTextNode(input, i, stopAt) {
-    return parseNodes(input, i, stopAt, true)[0];
+  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;
+  let nodes = [];
+  let escapeNext = false;
+  let string = "";
+  let iString = 0;
 
-    const pushTextNode = (isLast) => {
-        string = input.slice(iString, i);
+  stopped = false;
 
-        // 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();
-        }
+  const pushTextNode = (isLast) => {
+    string = input.slice(iString, i);
 
-        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 = `(?<!\\\\)(?:\\\\{2})*(${literalsToMatch.join('|')})`;
-
-    // There are 8asically only a few regular expressions we'll ever use,
-    // 8ut it's a pain to hard-code them all, so we dynamically gener8te
-    // and cache them for reuse instead.
-    let regexp;
-    if (regexpCache.hasOwnProperty(regexpSource)) {
-        regexp = regexpCache[regexpSource];
-    } else {
-        regexp = new RegExp(regexpSource);
-        regexpCache[regexpSource] = regexp;
+    // 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();
     }
 
-    // Skip whitespace at the start of parsing. This is run every time
-    // parseNodes is called (and thus parseOneTextNode too), so spaces
-    // at the start of syntax elements will always 8e skipped. We don't
-    // skip whitespace that shows up inside content (i.e. once we start
-    // parsing below), though!
-    const whitespaceOffset = input.slice(i).search(/[^\s]/);
-
-    // If the string is all whitespace, that's just zero content, so
-    // return the empty nodes array.
-    if (whitespaceOffset === -1) {
-        return nodes;
+    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 = `(?<!\\\\)(?:\\\\{2})*(${literalsToMatch.join("|")})`;
+
+  // There are 8asically only a few regular expressions we'll ever use,
+  // 8ut it's a pain to hard-code them all, so we dynamically gener8te
+  // and cache them for reuse instead.
+  let regexp;
+  if (regexpCache.hasOwnProperty(regexpSource)) {
+    regexp = regexpCache[regexpSource];
+  } else {
+    regexp = new RegExp(regexpSource);
+    regexpCache[regexpSource] = regexp;
+  }
+
+  // Skip whitespace at the start of parsing. This is run every time
+  // parseNodes is called (and thus parseOneTextNode too), so spaces
+  // at the start of syntax elements will always 8e skipped. We don't
+  // skip whitespace that shows up inside content (i.e. once we start
+  // parsing below), though!
+  const whitespaceOffset = input.slice(i).search(/[^\s]/);
+
+  // If the string is all whitespace, that's just zero content, so
+  // return the empty nodes array.
+  if (whitespaceOffset === -1) {
+    return nodes;
+  }
 
-    i += whitespaceOffset;
+  i += whitespaceOffset;
 
-    while (i < input.length) {
-        const match = input.slice(i).match(regexp);
+  while (i < input.length) {
+    const match = input.slice(i).match(regexp);
 
-        if (!match) {
-            iString = i;
-            i = input.length;
-            pushTextNode(true);
-            break;
-        }
+    if (!match) {
+      iString = i;
+      i = input.length;
+      pushTextNode(true);
+      break;
+    }
 
-        const closestMatch = match[0];
-        const closestMatchIndex = i + match.index;
+    const closestMatch = match[0];
+    const closestMatchIndex = i + match.index;
 
-        if (textOnly && closestMatch === tagBeginning)
-            throw makeError(i, `Unexpected [[tag]] - expected only text here.`);
+    if (textOnly && closestMatch === tagBeginning)
+      throw makeError(i, `Unexpected [[tag]] - expected only text here.`);
 
-        const stopHere = (closestMatch !== tagBeginning);
+    const stopHere = closestMatch !== tagBeginning;
 
-        iString = i;
-        i = closestMatchIndex;
-        pushTextNode(stopHere);
+    iString = i;
+    i = closestMatchIndex;
+    pushTextNode(stopHere);
 
-        i += closestMatch.length;
+    i += closestMatch.length;
 
-        if (stopHere) {
-            stopped = true;
-            stop_iMatch = closestMatchIndex;
-            stop_iParse = i;
-            stop_literal = closestMatch;
-            break;
-        }
+    if (stopHere) {
+      stopped = true;
+      stop_iMatch = closestMatchIndex;
+      stop_iParse = i;
+      stop_literal = closestMatch;
+      break;
+    }
 
-        if (closestMatch === tagBeginning) {
-            const iTag = closestMatchIndex;
+    if (closestMatch === tagBeginning) {
+      const iTag = closestMatchIndex;
 
-            let N;
+      let N;
 
-            // Replacer key (or value)
+      // Replacer key (or value)
 
-            N = parseOneTextNode(input, i, [R_tagReplacerValue, R_tagHash, R_tagArgument, R_tagLabel, R_tagEnding]);
+      N = parseOneTextNode(input, i, [
+        R_tagReplacerValue,
+        R_tagHash,
+        R_tagArgument,
+        R_tagLabel,
+        R_tagEnding,
+      ]);
 
-            if (!stopped) throw endOfInput(i, `reading replacer key`);
+      if (!stopped) throw endOfInput(i, `reading replacer key`);
 
-            if (!N) {
-                switch (stop_literal) {
-                    case tagReplacerValue:
-                    case tagArgument:
-                        throw makeError(i, `Expected text (replacer key).`);
-                    case tagLabel:
-                    case tagHash:
-                    case tagEnding:
-                        throw makeError(i, `Expected text (replacer key/value).`);
-                }
-            }
+      if (!N) {
+        switch (stop_literal) {
+          case tagReplacerValue:
+          case tagArgument:
+            throw makeError(i, `Expected text (replacer key).`);
+          case tagLabel:
+          case tagHash:
+          case tagEnding:
+            throw makeError(i, `Expected text (replacer key/value).`);
+        }
+      }
 
-            const replacerFirst = N;
-            i = stop_iParse;
+      const replacerFirst = N;
+      i = stop_iParse;
 
-            // Replacer value (if explicit)
+      // Replacer value (if explicit)
 
-            let replacerSecond;
+      let replacerSecond;
 
-            if (stop_literal === tagReplacerValue) {
-                N = parseNodes(input, i, [R_tagHash, R_tagArgument, R_tagLabel, R_tagEnding]);
+      if (stop_literal === tagReplacerValue) {
+        N = parseNodes(input, i, [
+          R_tagHash,
+          R_tagArgument,
+          R_tagLabel,
+          R_tagEnding,
+        ]);
 
-                if (!stopped) throw endOfInput(i, `reading replacer value`);
-                if (!N.length) throw makeError(i, `Expected content (replacer value).`);
+        if (!stopped) throw endOfInput(i, `reading replacer value`);
+        if (!N.length) throw makeError(i, `Expected content (replacer value).`);
 
-                replacerSecond = N;
-                i = stop_iParse
-            }
+        replacerSecond = N;
+        i = stop_iParse;
+      }
 
-            // Assign first & second to replacer key/value
+      // Assign first & second to replacer key/value
 
-            let replacerKey,
-                replacerValue;
+      let replacerKey, replacerValue;
 
-            // Value is an array of nodes, 8ut key is just one (or null).
-            // So if we use replacerFirst as the value, we need to stick
-            // it in an array (on its own).
-            if (replacerSecond) {
-                replacerKey = replacerFirst;
-                replacerValue = replacerSecond;
-            } else {
-                replacerKey = null;
-                replacerValue = [replacerFirst];
-            }
+      // Value is an array of nodes, 8ut key is just one (or null).
+      // So if we use replacerFirst as the value, we need to stick
+      // it in an array (on its own).
+      if (replacerSecond) {
+        replacerKey = replacerFirst;
+        replacerValue = replacerSecond;
+      } else {
+        replacerKey = null;
+        replacerValue = [replacerFirst];
+      }
 
-            // Hash
+      // Hash
 
-            let hash;
+      let hash;
 
-            if (stop_literal === tagHash) {
-                N = parseNodes(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]);
+      if (stop_literal === tagHash) {
+        N = parseNodes(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]);
 
-                if (!stopped) throw endOfInput(i, `reading hash`);
+        if (!stopped) throw endOfInput(i, `reading hash`);
 
-                if (!N)
-                    throw makeError(i, `Expected content (hash).`);
+        if (!N) throw makeError(i, `Expected content (hash).`);
 
-                hash = N;
-                i = stop_iParse;
-            }
+        hash = N;
+        i = stop_iParse;
+      }
 
-            // Arguments
+      // Arguments
 
-            const args = [];
+      const args = [];
 
-            while (stop_literal === tagArgument) {
-                N = parseOneTextNode(input, i, [R_tagArgumentValue, R_tagArgument, R_tagLabel, R_tagEnding]);
+      while (stop_literal === tagArgument) {
+        N = parseOneTextNode(input, i, [
+          R_tagArgumentValue,
+          R_tagArgument,
+          R_tagLabel,
+          R_tagEnding,
+        ]);
 
-                if (!stopped) throw endOfInput(i, `reading argument key`);
+        if (!stopped) throw endOfInput(i, `reading argument key`);
 
-                if (stop_literal !== tagArgumentValue)
-                    throw makeError(i, `Expected ${tagArgumentValue.literal} (tag argument).`);
+        if (stop_literal !== tagArgumentValue)
+          throw makeError(
+            i,
+            `Expected ${tagArgumentValue.literal} (tag argument).`
+          );
 
-                if (!N)
-                    throw makeError(i, `Expected text (argument key).`);
+        if (!N) throw makeError(i, `Expected text (argument key).`);
 
-                const key = N;
-                i = stop_iParse;
+        const key = N;
+        i = stop_iParse;
 
-                N = parseNodes(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]);
+        N = parseNodes(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]);
 
-                if (!stopped) throw endOfInput(i, `reading argument value`);
-                if (!N.length) throw makeError(i, `Expected content (argument value).`);
+        if (!stopped) throw endOfInput(i, `reading argument value`);
+        if (!N.length) throw makeError(i, `Expected content (argument value).`);
 
-                const value = N;
-                i = stop_iParse;
+        const value = N;
+        i = stop_iParse;
 
-                args.push({key, value});
-            }
+        args.push({ key, value });
+      }
 
-            let label;
+      let label;
 
-            if (stop_literal === tagLabel) {
-                N = parseOneTextNode(input, i, [R_tagEnding]);
+      if (stop_literal === tagLabel) {
+        N = parseOneTextNode(input, i, [R_tagEnding]);
 
-                if (!stopped) throw endOfInput(i, `reading label`);
-                if (!N) throw makeError(i, `Expected text (label).`);
+        if (!stopped) throw endOfInput(i, `reading label`);
+        if (!N) throw makeError(i, `Expected text (label).`);
 
-                label = N;
-                i = stop_iParse;
-            }
+        label = N;
+        i = stop_iParse;
+      }
 
-            nodes.push({i: iTag, iEnd: i, type: 'tag', data: {replacerKey, replacerValue, hash, args, label}});
+      nodes.push({
+        i: iTag,
+        iEnd: i,
+        type: "tag",
+        data: { replacerKey, replacerValue, hash, args, label },
+      });
 
-            continue;
-        }
+      continue;
     }
+  }
 
-    return nodes;
-};
+  return nodes;
+}
 
 export function parseInput(input) {
-    try {
-        return parseNodes(input, 0);
-    } catch (errorNode) {
-        if (errorNode.type !== 'error') {
-            throw errorNode;
-        }
+  try {
+    return parseNodes(input, 0);
+  } catch (errorNode) {
+    if (errorNode.type !== "error") {
+      throw errorNode;
+    }
 
-        const { i, data: { message } } = errorNode;
+    const {
+      i,
+      data: { message },
+    } = errorNode;
 
-        let lineStart = input.slice(0, i).lastIndexOf('\n');
-        if (lineStart >= 0) {
-            lineStart += 1;
-        } else {
-            lineStart = 0;
-        }
+    let lineStart = input.slice(0, i).lastIndexOf("\n");
+    if (lineStart >= 0) {
+      lineStart += 1;
+    } else {
+      lineStart = 0;
+    }
 
-        let lineEnd = input.slice(i).indexOf('\n');
-        if (lineEnd >= 0) {
-            lineEnd += i;
-        } else {
-            lineEnd = input.length;
-        }
+    let lineEnd = input.slice(i).indexOf("\n");
+    if (lineEnd >= 0) {
+      lineEnd += i;
+    } else {
+      lineEnd = input.length;
+    }
 
-        const line = input.slice(lineStart, lineEnd);
+    const line = input.slice(lineStart, lineEnd);
 
-        const cursor = i - lineStart;
+    const cursor = i - lineStart;
 
-        throw new SyntaxError(fixWS`
+    throw new SyntaxError(fixWS`
             Parse error (at pos ${i}): ${message}
             ${line}
-            ${'-'.repeat(cursor) + '^'}
+            ${"-".repeat(cursor) + "^"}
         `);
-    }
+  }
 }
 
 function evaluateTag(node, opts) {
-    const { find, input, language, link, replacerSpec, to, wikiData } = 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)
-        || 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, language, to});
-    } catch (error) {
-        logError`The link ${source} failed to be processed: ${error}`;
-        return source;
-    }
+  const { find, input, language, link, replacerSpec, to, wikiData } = 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)) ||
+    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, 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}`);
-    }
+  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}`);
-    }
+  if (!nodes || !Array.isArray(nodes)) {
+    throw new Error(`Expected an array of nodes! Got: ${nodes}`);
+  }
 
-    return nodes.map(node => transformNode(node, opts)).join('');
+  return nodes.map((node) => transformNode(node, opts)).join("");
 }
 
-export function transformInline(input, {replacerSpec, find, link, language, to, wikiData}) {
-    if (!replacerSpec) throw new Error('Expected replacerSpec');
-    if (!find) throw new Error('Expected find');
-    if (!link) throw new Error('Expected link');
-    if (!language) throw new Error('Expected language');
-    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});
+export function transformInline(
+  input,
+  { replacerSpec, find, link, language, to, wikiData }
+) {
+  if (!replacerSpec) throw new Error("Expected replacerSpec");
+  if (!find) throw new Error("Expected find");
+  if (!link) throw new Error("Expected link");
+  if (!language) throw new Error("Expected language");
+  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,
+  });
 }