diff --git a/examples/basic.html b/examples/basic.html index 1c85155111..aa2291180b 100644 --- a/examples/basic.html +++ b/examples/basic.html @@ -15,6 +15,15 @@ name: "Your Name", url: "https://your-site.com", }], + extensions: [ + { + name: "my-extension", + on: "before-markdown", + async run(conf, utils) { + utils.showWarning("oops from extension", this.name); + } + } + ], github: "https://github.com/w3c/some-API/", testSuiteURI: "https://w3c-test.org/some-API/", implementationReportURI: "https://w3c.github.io/test-results/some-API", diff --git a/src/core/base-runner.js b/src/core/base-runner.js index 792386ae1e..16078327cc 100644 --- a/src/core/base-runner.js +++ b/src/core/base-runner.js @@ -1,6 +1,7 @@ // @ts-check // Module core/base-runner // The module in charge of running the whole processing pipeline. +import { utils as extensionUtils, setupExtensions } from "./extensions.js"; import { run as includeConfig } from "./include-config.js"; import { init as initReSpecGlobal } from "./respec-global.js"; import { run as overrideConfig } from "./override-configuration.js"; @@ -51,7 +52,9 @@ async function executePreparePass(runnables, config) { } } -async function executeRunPass(runnables, config) { +async function executeRunPass(corePlugins, config) { + const runnables = setupExtensions(corePlugins, respecConfig); + for (const plug of runnables) { const name = plug.name || ""; @@ -69,6 +72,10 @@ async function executeRunPass(runnables, config) { if (plug.Plugin) { await new plug.Plugin(config).run(); resolve(); + } else if (plug.on && plug.run) { + // is extension + await plug.run(config, extensionUtils); + resolve(); } else if (plug.run) { await plug.run(config); resolve(); diff --git a/src/core/extensions.js b/src/core/extensions.js new file mode 100644 index 0000000000..35662cbc25 --- /dev/null +++ b/src/core/extensions.js @@ -0,0 +1,73 @@ +// @ts-check +import { showError, showWarning } from "./utils.js"; + +export const name = "core/extensions"; + +/** + * @param {any[]} corePlugins + * @param {Conf} conf + */ +export function setupExtensions(corePlugins, conf) { + /** @type {Map} */ + const extMap = new Map(); + for (const [i, ext] of Object.entries(conf.extensions || [])) { + if (!ext.name) { + const msg = `Extension #${i} does not have a \`name\`.`; + showWarning(msg, name); + } + ext.name = `extensions/${ext.name || i}`; + + if (ext.run instanceof Function == false) { + const msg = `${ext.name} does not have a \`run()\` function.`; + showError(msg, name); + continue; + } + + const { on } = ext; + const extsByHookName = extMap.get(on) || extMap.set(on, []).get(on); + extsByHookName.push(ext); + } + + const allPlugins = []; + + if (extMap.has("before-all")) { + allPlugins.push(...extMap.get("before-all")); + extMap.delete("before-all"); + } + + for (const corePlugin of corePlugins) { + const beforeHookName = corePlugin.hooks?.find(s => s.startsWith("before-")); + if (beforeHookName && extMap.has(beforeHookName)) { + allPlugins.push(...extMap.get(beforeHookName)); + extMap.delete(beforeHookName); + } + + allPlugins.push(corePlugin); + + const afterHookName = corePlugin.hooks?.find(s => s.startsWith("after-")); + if (afterHookName && extMap.has(afterHookName)) { + allPlugins.push(...extMap.get(afterHookName)); + extMap.delete(afterHookName); + } + } + + if (extMap.has("after-all")) { + allPlugins.push(...extMap.get("after-all")); + extMap.delete("after-all"); + } + + // remaining extensions have no corresponding hooks. + for (const [on, exts] of extMap) { + for (const ext of exts) { + const msg = `${ext.name} does not have a valid \`on\` hook. Found "${on}".`; + showError(msg, name); + } + } + + return allPlugins; +} + +export const utils = { + showError, + showWarning, +}; diff --git a/src/core/markdown.js b/src/core/markdown.js index c74471412b..7ecbcb1c7b 100644 --- a/src/core/markdown.js +++ b/src/core/markdown.js @@ -305,6 +305,8 @@ const processMDSections = convertElements("[data-format='markdown']:not(body)"); const blockLevelElements = "[data-format=markdown], section, div, address, article, aside, figure, header, main"; +export const hooks = ["before-markdown", "after-markdown"]; + export function run(conf) { const hasMDSections = !!document.querySelector( "[data-format=markdown]:not(body)" diff --git a/src/type-helper.d.ts b/src/type-helper.d.ts index a39b8fca9b..60af5ee938 100644 --- a/src/type-helper.d.ts +++ b/src/type-helper.d.ts @@ -115,12 +115,20 @@ interface BiblioData { status?: string; etAl?: boolean; } + +interface Extension { + on: string; + name: string; + run(conf: Conf, utils: Record): Promise; +} + interface Conf { informativeReferences: Set; normativeReferences: Set; localBiblio?: Record; biblio: Record; shortName: string; + extensions?: Extension[]; } type ResourceHintOption = {