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 = {