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");
+ });
+});