Skip to content

Commit

Permalink
feat(core/beforeSave): support modifications before save (#3909)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoscaceres authored Dec 20, 2021
1 parent cee0f04 commit 56552de
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 0 deletions.
1 change: 1 addition & 0 deletions profiles/w3c.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const modules = [
import("../src/core/anchor-expander.js"),
import("../src/core/custom-elements/index.js"),
import("../src/core/web-monetization.js"),
import("../src/core/before-save.js"),
/* Linters must be the last thing to run */
import("../src/core/linter-rules/check-charset.js"),
import("../src/core/linter-rules/check-punctuation.js"),
Expand Down
47 changes: 47 additions & 0 deletions src/core/before-save.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { docLink, showError } from "./utils.js";
import { sub } from "./pubsubhub.js";

export const name = "core/before-save";

export function run(conf) {
if (!conf.beforeSave) return;

if (
!Array.isArray(conf.beforeSave) ||
conf.beforeSave.some(
el => typeof el !== "function" || el.constructor.name === "AsyncFunction"
)
) {
const msg = docLink`${"[beforeSave]"} configuration option must be an array of synchronous JS functions.`;
showError(msg, name);
return;
}

sub(
"beforesave",
documentElement => {
performTransformations(conf.beforeSave, documentElement.ownerDocument);
},
{ once: true }
);
}
/**
* @param {Array<Function>} transforms
* @param {Document}
*/
function performTransformations(transforms, doc) {
let pos = 0;
for (const fn of transforms) {
try {
fn(doc);
} catch (err) {
const nameOrPosition = `\`${fn.name}\`` || `at position ${pos}`;
const msg = docLink`Function ${nameOrPosition}\` threw an error during processing of ${"[beforeSave]"}.`;
const hint = "See developer console.";
showError(msg, name, { hint });
console.error(err);
} finally {
pos++;
}
}
}
28 changes: 28 additions & 0 deletions tests/spec/SpecHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,34 @@ export function makeRSDoc(opts, src, style = "") {
iframes.push(ifr);
});
}
/**
* Used to get errors and warnings from a spec.
*/
class UIMessageFilters {
/**
* @param {"warnings" | "errors"} type
*/
constructor(type) {
this.cache = new Map();
this.type = type;
}
/**
* @param {string} pluginName
* @returns (Document) => Array<RespecError>
*/
filter(pluginName) {
if (this.cache.has(pluginName)) {
return this.cache.get(pluginName);
}
const filter = doc => {
return doc.respec[this.type].filter(err => err.plugin === pluginName);
};
this.cache.set(pluginName, filter);
return filter;
}
}
export const errorFilters = new UIMessageFilters("errors");
export const warningFilters = new UIMessageFilters("warnings");

/**
* @param {Document} doc
Expand Down
29 changes: 29 additions & 0 deletions tests/spec/core/before-save-spec.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<script class="remove">
const modifier1 = exportedDoc => {
const p = exportedDoc.createElement("p");
p.id = "p1";
p.textContent = "This is paragraph 1";
exportedDoc.body.appendChild(p);
};
const modifier2 = exportedDoc => {
const p = exportedDoc.createElement("p");
p.id = "p2";
p.textContent = "This is a sync paragraph";
exportedDoc.body.appendChild(p);
};
const respecConfig = {
specStatus: "unofficial",
shortName: "i",
editors: [
{ name: "Foo", url: "https://foo.com/" },
],
beforeSave: [modifier1, modifier2],
};
</script>
<section id="abstract">
<p>abstract.</p>
</section>
<section id="sotd">
<p>CUSTOM PARAGRAPH</p>
</section>
50 changes: 50 additions & 0 deletions tests/spec/core/before-save-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
errorFilters,
flushIframes,
getExportedDoc,
makeRSDoc,
makeStandardOps,
} from "../SpecHelper.js";

describe("Core - beforeSave config option", () => {
afterAll(flushIframes);
const beforeSaveErrors = errorFilters.filter("core/before-save");
it("allows modification before saving", async () => {
const ops = makeStandardOps();
ops.config = null; // use src doc's config
const doc = await makeRSDoc(ops, "spec/core/before-save-spec.html");
expect(doc.getElementById("p1")).toBeNull();
expect(doc.getElementById("p2")).toBeNull();
const exportedDoc = await getExportedDoc(doc);
expect(exportedDoc.querySelectorAll("#p1").length).toBe(1);
expect(exportedDoc.querySelectorAll("#p2").length).toBe(1);
// make sure that the functions are run in order...
const p1 = exportedDoc.querySelector("#p1");
const p2 = exportedDoc.querySelector("#p2");
expect(p1.nextElementSibling).toBe(p2);
});

it("complains if it's not passed an array", async () => {
const ops = makeStandardOps({ beforeSave: "not a array" });
const doc = await makeRSDoc(ops);
const errors = beforeSaveErrors(doc);
expect(errors.length).toBe(1);
expect(errors[0].message).toContain("array of synchronous JS functions");
});

it("complains if it's not passed a function in the array", async () => {
const ops = makeStandardOps({ beforeSave: ["not a function", () => {}] });
const doc = await makeRSDoc(ops);
const errors = beforeSaveErrors(doc);
expect(errors.length).toBe(1);
expect(errors[0].message).toContain("array of synchronous JS functions");
});

it("complains if passed an async function", async () => {
const ops = makeStandardOps({ beforeSave: [() => {}, async () => {}] });
const doc = await makeRSDoc(ops);
const errors = beforeSaveErrors(doc);
expect(errors.length).toBe(1);
expect(errors[0].message).toContain("array of synchronous JS functions");
});
});

0 comments on commit 56552de

Please sign in to comment.