diff --git a/src/core/algorithms.js b/src/core/algorithms.js index 9a46608be5..2867b13cd0 100644 --- a/src/core/algorithms.js +++ b/src/core/algorithms.js @@ -4,6 +4,7 @@ Currently used only for adding 'assert' class to algorithm lists */ import css from "../styles/algorithms.css.js"; import { html } from "./import-maps.js"; +import { insertStyle } from "./insert-style.js"; export const name = "core/algorithms"; @@ -32,7 +33,5 @@ export function run() { } } - const style = document.createElement("style"); - style.textContent = css; - document.head.appendChild(style); + insertStyle(css); } diff --git a/src/core/caniuse.js b/src/core/caniuse.js index 4b7b357f9a..aa796b325c 100644 --- a/src/core/caniuse.js +++ b/src/core/caniuse.js @@ -11,6 +11,7 @@ import { codedJoinAnd, docLink, showError, showWarning } from "./utils.js"; import { pub, sub } from "./pubsubhub.js"; import css from "../styles/caniuse.css.js"; import { html } from "./import-maps.js"; +import { insertStyle } from "./insert-style.js"; export const name = "core/caniuse"; @@ -59,14 +60,10 @@ export function prepare(conf) { return; // no feature to show } - document.head.appendChild( - html`` - ); + insertStyle(css, { + id: "caniuse-stylesheet", + className: options.removeOnSave ? "removeOnSave" : "", + }); } /** * @param {string} browser diff --git a/src/core/cddl.js b/src/core/cddl.js index 9aa929d6af..4d6e242dff 100644 --- a/src/core/cddl.js +++ b/src/core/cddl.js @@ -14,6 +14,7 @@ import { import { addHashId, showError, showWarning, xmlEscape } from "./utils.js"; import { createCopyButton, injectCopyScript } from "./clipboard.js"; import css from "../styles/cddl.css.js"; +import { insertStyle } from "./insert-style.js"; import { registerDefinition } from "./dfn-map.js"; import { sub } from "./pubsubhub.js"; @@ -703,14 +704,9 @@ export async function run() { if (!cddls.length) return; // Inject CSS - const style = document.createElement("style"); - style.textContent = css; - const styleAnchor = document.querySelector("head link, head > *:last-child"); - if (styleAnchor) { - styleAnchor.before(style); - } else { - document.head.append(style); - } + insertStyle(css, { + before: document.querySelector("head link, head > *:last-child"), + }); // Import cddlparser via import-maps (avoids fs/path imports in main entry) const parse = (/** @type {string} */ text) => { diff --git a/src/core/data-type.js b/src/core/data-type.js index 0e530297fc..c9abe92c33 100644 --- a/src/core/data-type.js +++ b/src/core/data-type.js @@ -6,6 +6,7 @@ * Set `conf.highlightVars = true` to enable. */ import css from "../styles/datatype.css.js"; +import { insertStyle } from "./insert-style.js"; export const name = "core/data-type"; @@ -17,9 +18,7 @@ export function run(conf) { return; } - const style = document.createElement("style"); - style.textContent = css; - document.head.appendChild(style); + insertStyle(css); let section = null; const varMap = new Map(); diff --git a/src/core/dfn-index.js b/src/core/dfn-index.js index 2a00414ce0..2a631f1cab 100644 --- a/src/core/dfn-index.js +++ b/src/core/dfn-index.js @@ -10,6 +10,7 @@ import { biblio } from "./biblio.js"; import css from "../styles/dfn-index.css.js"; import { getTermFromElement } from "./xref.js"; import { html } from "./import-maps.js"; +import { insertStyle } from "./insert-style.js"; import { renderInlineCitation } from "./render-biblio.js"; import { sub } from "./pubsubhub.js"; import { toCiteDetails } from "./data-cite.js"; @@ -103,9 +104,7 @@ export function run() { return; } - const styleEl = document.createElement("style"); - styleEl.textContent = css; - document.head.appendChild(styleEl); + insertStyle(css); index.classList.add("appendix"); if (!index.querySelector("h2, h1")) { diff --git a/src/core/dfn-panel.js b/src/core/dfn-panel.js index 358d89ccdd..402c5d5fda 100644 --- a/src/core/dfn-panel.js +++ b/src/core/dfn-panel.js @@ -5,17 +5,13 @@ import css from "../styles/dfn-panel.css.js"; import { fetchBase } from "./text-loader.js"; import { html } from "./import-maps.js"; +import { insertStyle } from "./insert-style.js"; import { norm } from "./utils.js"; export const name = "core/dfn-panel"; export async function run() { - document.head.insertBefore( - html``, - document.querySelector("link") - ); + insertStyle(css, { before: document.querySelector("link") }); /** @type {NodeListOf} */ const elems = document.querySelectorAll( diff --git a/src/core/examples.js b/src/core/examples.js index 9b48cdc3c7..8aa205fece 100644 --- a/src/core/examples.js +++ b/src/core/examples.js @@ -9,6 +9,7 @@ import { addId, getIntlData } from "./utils.js"; import css from "../styles/examples.css.js"; import { html } from "./import-maps.js"; +import { insertStyle } from "./insert-style.js"; export const name = "core/examples"; @@ -75,12 +76,7 @@ export function run() { ); if (!examples.length) return; - document.head.insertBefore( - html``, - document.querySelector("link") - ); + insertStyle(css, { before: document.querySelector("link") }); let number = 0; examples.forEach(example => { diff --git a/src/core/highlight-vars.js b/src/core/highlight-vars.js index 537a1328a7..3ece135a23 100644 --- a/src/core/highlight-vars.js +++ b/src/core/highlight-vars.js @@ -9,6 +9,7 @@ */ import css from "../styles/var.css.js"; import { fetchBase } from "./text-loader.js"; +import { insertStyle } from "./insert-style.js"; import { sub } from "./pubsubhub.js"; export const name = "core/highlight-vars"; @@ -21,9 +22,7 @@ export async function run(conf) { return; } - const styleElement = document.createElement("style"); - styleElement.textContent = css; - document.head.appendChild(styleElement); + insertStyle(css); const script = document.createElement("script"); script.id = "respec-highlight-vars"; diff --git a/src/core/highlight.js b/src/core/highlight.js index 176fda085a..0ff368e5cd 100644 --- a/src/core/highlight.js +++ b/src/core/highlight.js @@ -5,7 +5,7 @@ * Performs syntax highlighting to all pre and code elements. */ import css from "../styles/highlight.css.js"; -import { html } from "./import-maps.js"; +import { insertStyle } from "./insert-style.js"; import { msgIdGenerator } from "./utils.js"; import { workerPromise } from "./worker.js"; export const name = "core/highlight"; @@ -107,10 +107,6 @@ export async function run(conf) { const promisesToHighlight = highlightables .filter(elem => elem.textContent.trim()) .map(highlightElement); - document.head.appendChild( - html`` - ); + insertStyle(css); await Promise.all(promisesToHighlight); } diff --git a/src/core/implementation-status.js b/src/core/implementation-status.js index e56d3a200d..2a06100a4f 100644 --- a/src/core/implementation-status.js +++ b/src/core/implementation-status.js @@ -12,6 +12,7 @@ import { docLink, fetchAndCache, showWarning } from "./utils.js"; import { pub, sub } from "./pubsubhub.js"; import css from "../styles/implementation-status.css.js"; import { html } from "./import-maps.js"; +import { insertStyle } from "./insert-style.js"; export const name = "core/implementation-status"; @@ -165,14 +166,10 @@ export function prepare(conf) { const options = /** @type {ImplementationStatusOptions} */ ( conf.implementationStatus ); - document.head.appendChild( - html`` - ); + insertStyle(css, { + id: "baseline-stylesheet", + className: options.removeOnSave ? "removeOnSave" : "", + }); } /** @param {RespecConfig} conf */ diff --git a/src/core/insert-style.js b/src/core/insert-style.js new file mode 100644 index 0000000000..20ba2870bf --- /dev/null +++ b/src/core/insert-style.js @@ -0,0 +1,28 @@ +// @ts-check +// Module core/insert-style +// One place where ReSpec adds its own ``, - headElem.querySelector("link") - ); + insertStyle(css, { before: document.head.querySelector("link") }); handleIssues(issuesAndNotes, ghIssues, conf); const ednotes = document.querySelectorAll(".ednote"); ednotes.forEach(ednote => { diff --git a/src/core/mdn-annotation.js b/src/core/mdn-annotation.js index cfecc784f5..35f7f32592 100644 --- a/src/core/mdn-annotation.js +++ b/src/core/mdn-annotation.js @@ -2,6 +2,7 @@ import { docLink, fetchAndCache, getIntlData, showError } from "./utils.js"; import css from "../styles/mdn-annotation.css.js"; import { html } from "./import-maps.js"; +import { insertStyle } from "./insert-style.js"; export const name = "core/mdn-annotation"; @@ -152,9 +153,7 @@ export async function run(conf) { return; } - const style = document.createElement("style"); - style.textContent = css; - document.head.append(style); + insertStyle(css); for (const elem of findElements(mdnSpecJson)) { const mdnSpecArray = mdnSpecJson[elem.id]; diff --git a/src/core/style.js b/src/core/style.js index 8f3c4c2029..dd710b1bc7 100644 --- a/src/core/style.js +++ b/src/core/style.js @@ -12,22 +12,10 @@ export const name = "core/style"; import css from "../styles/respec.css.js"; +import { insertStyle } from "./insert-style.js"; // Opportunistically inserts the style to reduce some FOUC. -/** @type {HTMLStyleElement} */ -const styleElement = insertStyle(); - -/** - * Inserts the ReSpec CSS as a `style` element into the document's `head`. - * @return {HTMLStyleElement} The `style` element that was inserted. - */ -function insertStyle() { - const styleElement = document.createElement("style"); - styleElement.id = "respec-mainstyle"; - styleElement.textContent = css; - document.head.appendChild(styleElement); - return styleElement; -} +const styleElement = insertStyle(css, { id: "respec-mainstyle" }); /** * Removes the ReSpec CSS if the `noReSpecCSS` configuration option is `true`. diff --git a/src/core/ui.js b/src/core/ui.js index 7a6ebaac9b..b56545cfa9 100644 --- a/src/core/ui.js +++ b/src/core/ui.js @@ -11,21 +11,13 @@ import { html, pluralize } from "./import-maps.js"; import { reindent, xmlEscape } from "./utils.js"; import css from "../styles/ui.css.js"; +import { insertStyle } from "./insert-style.js"; import { markdownToHtml } from "./markdown.js"; import { sub } from "./pubsubhub.js"; export const name = "core/ui"; // Opportunistically inserts the style, with the chance to reduce some FOUC -insertStyle(); - -function insertStyle() { - const styleElement = document.createElement("style"); - styleElement.id = "respec-ui-styles"; - styleElement.textContent = css; - styleElement.classList.add("removeOnSave"); - document.head.appendChild(styleElement); - return styleElement; -} +insertStyle(css, { id: "respec-ui-styles", className: "removeOnSave" }); /** * @param {Element | null | undefined} elem diff --git a/src/core/webidl.js b/src/core/webidl.js index fdc1f8ec2e..ed21eb83f1 100644 --- a/src/core/webidl.js +++ b/src/core/webidl.js @@ -16,6 +16,7 @@ import { decorateDfn, findDfn } from "./dfn-finder.js"; import { html, webidl2 } from "./import-maps.js"; import { addCopyIDLButton } from "./webidl-clipboard.js"; import css from "../styles/webidl.css.js"; +import { insertStyle } from "./insert-style.js"; import { registerDefinition } from "./dfn-map.js"; export const name = "core/webidl"; @@ -384,9 +385,9 @@ export async function run() { if (!idls.length) { return; } - const style = document.createElement("style"); - style.textContent = css; - document.querySelector("head link, head > *:last-child").before(style); + insertStyle(css, { + before: document.querySelector("head link, head > *:last-child"), + }); const astArray = [...idls].map(renderWebIDL); diff --git a/tests/unit/core/insert-style-spec.js b/tests/unit/core/insert-style-spec.js new file mode 100644 index 0000000000..aeda45ca4d --- /dev/null +++ b/tests/unit/core/insert-style-spec.js @@ -0,0 +1,77 @@ +"use strict"; + +import { insertStyle } from "/src/core/insert-style.js"; + +describe("Core - insertStyle", () => { + /** @type {Element[]} */ + let added; + + beforeEach(() => { + added = []; + }); + + afterEach(() => { + for (const el of added) el.remove(); + }); + + /** @param {Parameters} args */ + function insert(...args) { + const style = insertStyle(...args); + added.push(style); + return style; + } + + it("appends to the head when given no anchor", () => { + const style = insert("a{color:red}"); + expect(style.parentNode).toBe(document.head); + expect(document.head.lastElementChild).toBe(style); + }); + + it("inserts ahead of an anchor in the head", () => { + const anchor = document.createElement("meta"); + document.head.appendChild(anchor); + added.push(anchor); + const style = insert("a{color:red}", { before: anchor }); + expect(style.nextElementSibling).toBe(anchor); + expect(style.parentNode).toBe(document.head); + }); + + it("appends to the head when the anchor is null", () => { + const style = insert("a{color:red}", { before: null }); + expect(style.parentNode).toBe(document.head); + }); + + // Callers pass an unscoped `document.querySelector("link")`, which can return a node + // outside the head. Inserting relative to the anchor rather than through `head` put + // ReSpec's CSS in the body. + it("never inserts outside the head, whatever the anchor's parent is", () => { + const stray = document.createElement("link"); + document.body.appendChild(stray); + try { + expect(() => insert("a{color:red}", { before: stray })).toThrowError(); + expect(document.body.querySelector("style")).toBeNull(); + } finally { + stray.remove(); + } + }); + + it("sets the css as text, without escaping", () => { + const css = 'a[data-x="1"] > b::after{content:"&"}'; + const style = insert(css); + expect(style.textContent).toBe(css); + }); + + it("omits the class attribute when no className is given", () => { + const style = insert("a{color:red}"); + expect(style.hasAttribute("class")).toBeFalse(); + }); + + it("sets id and className when given", () => { + const style = insert("a{color:red}", { + id: "probe-id", + className: "probe", + }); + expect(style.id).toBe("probe-id"); + expect(style.className).toBe("probe"); + }); +});