diff --git a/.github/workflows/docs-code-ci.yml b/.github/workflows/docs-code-ci.yml index d43f35d4fc..41e15ba503 100644 --- a/.github/workflows/docs-code-ci.yml +++ b/.github/workflows/docs-code-ci.yml @@ -85,5 +85,8 @@ jobs: - name: Visual smoke run: npm run docs:visual + - name: Mermaid in WebKit + run: npm run docs:visual -- --mermaid-only --webkit + - name: Community invitation in Chromium and WebKit run: npm run docs:visual:community diff --git a/package-lock.json b/package-lock.json index 3f1bada290..e4264a457c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "lucide": "1.56.0", "markdown-it": "15.0.2", "markdown-it-anchor": "10.0.0", - "mermaid": "11.17.2", + "mermaid": "12.1.0", "pagefind": "1.5.2", "playwright": "^1.64.0", "tsx": "4.23.15", @@ -47,10 +47,45 @@ "dev": true, "license": "MIT" }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-13.2.0.tgz", + "integrity": "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "13.2.0", + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/gast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-13.2.0.tgz", + "integrity": "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-13.2.0.tgz", + "integrity": "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@chevrotain/types": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", - "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-13.2.0.tgz", + "integrity": "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==", "dev": true, "license": "Apache-2.0" }, @@ -554,13 +589,16 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", - "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-2.0.1.tgz", + "integrity": "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==", "dev": true, "license": "MIT", "dependencies": { - "@chevrotain/types": "~11.1.2" + "@chevrotain/types": "~13.2.0" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@openclaw/carapace": { @@ -1468,6 +1506,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chevrotain": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz", + "integrity": "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "13.2.0", + "@chevrotain/gast": "13.2.0", + "@chevrotain/regexp-to-ast": "13.2.0", + "@chevrotain/types": "13.2.0", + "@chevrotain/utils": "13.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/collapse-white-space": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", @@ -2205,6 +2260,13 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "dev": true, + "license": "EPL-2.0" + }, "node_modules/entities": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", @@ -2425,16 +2487,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastdom": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastdom/-/fastdom-1.0.12.tgz", - "integrity": "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg==", - "dev": true, - "license": "MIT", - "dependencies": { - "strictdom": "^1.0.1" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3022,17 +3074,18 @@ "license": "MIT" }, "node_modules/mermaid": { - "version": "11.17.2", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.17.2.tgz", - "integrity": "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.1.0.tgz", + "integrity": "sha512-wlVCp+8eTupfCeeFvoZNNiTuHrvag0P2jz/ILgb/f/6jkVokUefOcujefi8qUe/j2asHiSePncVsz/xzzA80LQ==", "dev": true, "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.2.1", + "@mermaid-js/parser": "^2.0.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", + "chevrotain": "~13.2.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", @@ -3040,9 +3093,9 @@ "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", - "dompurify": "^3.3.3", + "dompurify": "^3.4.12", + "elkjs": "^0.9.3", "es-toolkit": "^1.45.1", - "fastdom": "1.0.12", "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", @@ -3050,6 +3103,9 @@ "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/micromark": { @@ -4003,13 +4059,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strictdom": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strictdom/-/strictdom-1.0.1.tgz", - "integrity": "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==", - "dev": true, - "license": "MIT" - }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", diff --git a/package.json b/package.json index 08bd13fabb..ff9e7ebe87 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lucide": "1.56.0", "markdown-it": "15.0.2", "markdown-it-anchor": "10.0.0", - "mermaid": "11.17.2", + "mermaid": "12.1.0", "pagefind": "1.5.2", "playwright": "^1.64.0", "tsx": "4.23.15", diff --git a/scripts/docs-site/elements-fixture.mjs b/scripts/docs-site/elements-fixture.mjs index 87b7e0f715..32226ff982 100644 --- a/scripts/docs-site/elements-fixture.mjs +++ b/scripts/docs-site/elements-fixture.mjs @@ -412,6 +412,13 @@ flowchart LR A["Ready
state"] --> B["Rendered"] \`\`\` + +stateDiagram-v2 + [*] --> Ready + Ready --> Complete + Complete --> [*] + + flowchart LR Broken --> diff --git a/scripts/docs-site/site-js.mjs b/scripts/docs-site/site-js.mjs index ef7382611f..2947abf692 100644 --- a/scripts/docs-site/site-js.mjs +++ b/scripts/docs-site/site-js.mjs @@ -219,7 +219,8 @@ function mermaidOptions(){ const node=color("--docs-diagram-node")||color("--paper"); const border=color("--docs-diagram-border")||color("--line-strong"); const line=color("--docs-diagram-line")||color("--muted"); - return {startOnLoad:false,securityLevel:"strict",theme:"base",fontFamily, + // Keep the docs appearance stable when Mermaid changes its diagram defaults. + return {startOnLoad:false,securityLevel:"strict",theme:"base",layout:"dagre",look:"classic",fontFamily, sequence:{width:128,height:48,actorMargin:64,messageMargin:44,actorFontSize:14,actorFontWeight:600,messageFontSize:13,noteFontSize:13}, themeVariables:{darkMode:root.dataset.theme!=="light",fontFamily,fontSize:"14px",background:color("--paper"),primaryColor:node,primaryTextColor:color("--ink"),primaryBorderColor:border,secondaryColor:node,tertiaryColor:color("--paper"),mainBkg:node,lineColor:line,textColor:color("--text"),nodeBorder:border,clusterBkg:color("--paper"),clusterBorder:border,edgeLabelBackground:color("--paper"),actorBorder:border,actorBkg:node,actorTextColor:color("--ink"),actorLineColor:border,signalColor:color("--brand"),signalTextColor:color("--text"),labelBoxBkgColor:node,labelBoxBorderColor:border,labelTextColor:color("--ink"),noteBkgColor:node,noteBorderColor:border,noteTextColor:color("--text")}} } diff --git a/scripts/docs-site/visual-smoke.mjs b/scripts/docs-site/visual-smoke.mjs index 0f075cacd6..fb15aa873c 100644 --- a/scripts/docs-site/visual-smoke.mjs +++ b/scripts/docs-site/visual-smoke.mjs @@ -2,11 +2,14 @@ import fs from "node:fs"; import http from "node:http"; import path from "node:path"; -import { chromium } from "playwright"; +import { chromium, webkit } from "playwright"; const accordionOnly = process.argv.includes("--accordion-only"); -if (process.argv.slice(2).some((arg) => arg !== "--accordion-only")) { - throw new Error("usage: visual-smoke.mjs [--accordion-only]"); +const mermaidOnly = process.argv.includes("--mermaid-only"); +const useWebkit = process.argv.includes("--webkit"); +if (process.argv.slice(2).some((arg) => !["--accordion-only", "--mermaid-only", "--webkit"].includes(arg)) + || (accordionOnly && mermaidOnly) || (useWebkit && !mermaidOnly)) { + throw new Error("usage: visual-smoke.mjs [--accordion-only | --mermaid-only [--webkit]]"); } const root = process.cwd(); @@ -36,10 +39,10 @@ const server = http.createServer((req, res) => { await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const base = `http://127.0.0.1:${server.address().port}`; -const browser = await chromium.launch({ headless: true }); +const browser = await (useWebkit ? webkit : chromium).launch({ headless: true }); try { - const checks = accordionOnly ? [checkAccordionNavigation] : [ + const checks = accordionOnly ? [checkAccordionNavigation] : mermaidOnly ? [checkMermaid] : [ checkLanguageSelection, checkAccordionNavigation, checkLandingNavigation, @@ -48,6 +51,7 @@ try { checkHeaderSurface, checkFooterSocials, checkDesktop, + checkMermaid, checkTocScrollspy, checkTocRailScrolling, checkCompactToc, @@ -57,7 +61,7 @@ try { checkLightMode, ]; for (const check of checks) await check(); - console.log(`docs ${accordionOnly ? "accordion" : "visual"} smoke ok: screenshots in ${path.relative(root, artifacts)}`); + console.log(`docs ${accordionOnly ? "accordion" : mermaidOnly ? "mermaid" : "visual"} smoke ok: screenshots in ${path.relative(root, artifacts)}`); } finally { await browser.close(); server.close(); @@ -529,42 +533,81 @@ async function checkFooterSocials() { } finally { await page.close(); } } -async function checkDesktop() { - const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); - await page.goto(`${base}/__elements`, { waitUntil: "networkidle" }); - await page.screenshot({ path: path.join(artifacts, "elements-desktop-dark.png"), fullPage: true }); - await page.evaluate(() => scrollTo(0, 760)); - await page.screenshot({ path: path.join(artifacts, "elements-desktop-scrolled.png"), fullPage: false }); +async function expectMermaidDiagrams(page) { + for (const [source, label] of [ + ["sequenceDiagram", "healthy"], + ["flowchart LR", "Rendered"], + ["stateDiagram-v2", "Complete"], + ]) { + // Expand controls contain SVG icons too; the direct child is the rendered diagram. + const svg = page.locator('.oc-mermaid.is-rendered[data-mermaid*="' + source + '"] > svg').first(); + await svg.waitFor({ state: "visible", timeout: 10000 }); + if (!(await svg.textContent()).includes(label)) { + throw new Error(source + " rendered without its expected labels"); + } + } +} - const overlap = await page.evaluate(() => { - const header = document.querySelector(".site-header")?.getBoundingClientRect(); - const sidebar = document.querySelector(".sidebar")?.getBoundingClientRect(); - const toc = document.querySelector(".toc")?.getBoundingClientRect(); - return { - headerTop: header?.top, - sidebarClear: header && sidebar ? sidebar.top >= header.bottom - 1 : false, - tocClear: header && toc ? toc.top >= header.bottom - 1 : false, - }; - }); - if (overlap.headerTop !== 0 || !overlap.sidebarClear || !overlap.tocClear) { - throw new Error(`desktop sticky shell overlap: ${JSON.stringify(overlap)}`); +async function navigateMermaidPage(page, route) { + await page.evaluate((href) => { + const link = document.createElement("a"); + link.href = href; + link.textContent = "Mermaid navigation target"; + document.querySelector(".doc").prepend(link); + }, route); + await page.getByRole("link", { name: "Mermaid navigation target", exact: true }).click(); + await page.waitForURL((url) => url.pathname.replace(/\/$/, "") === route.replace(/\/$/, "")); + if (!await page.evaluate(() => window.__mermaidNavigationProbe === true)) { + throw new Error("Mermaid navigation fell back to a full page load"); } +} - await expectVisible(page, ".oc-code [data-code-copy]", "code copy button"); - await expectVisible(page, ".page-status .page-status-beta", "page status badge"); - await expectVisible(page, ".oc-code.has-line-numbers .code-line.is-highlighted", "line-highlighted code"); - await expectVisible(page, ".oc-badge.oc-badge-orange", "badge"); - await expectVisible(page, ".oc-panel", "panel"); - await expectVisible(page, ".oc-prompt [data-prompt-copy]", "prompt copy"); - await expectVisible(page, ".oc-tile-group .oc-tile", "tiles"); +async function checkMermaid() { + const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); + try { + await page.goto(base + "/__elements", { waitUntil: "networkidle" }); + await checkMermaidContent(page); + const before = await page.locator(".oc-mermaid.is-rendered > svg").evaluateAll((svgs) => svgs.map((svg) => svg.id)); + const theme = await page.locator("html").getAttribute("data-theme"); + await page.locator(".header-row [data-theme-toggle]").click(); + await page.waitForFunction(({ ids, previousTheme }) => { + const svgs = [...document.querySelectorAll(".oc-mermaid.is-rendered > svg")]; + return document.documentElement.dataset.theme !== previousTheme + && svgs.length === ids.length && svgs.every((svg, i) => svg.id !== ids[i]); + }, { ids: before, previousTheme: theme }); + await expectMermaidDiagrams(page); + await page.locator(".oc-mermaid.is-error pre code").waitFor({ state: "visible", timeout: 10000 }); + await page.evaluate(() => { window.__mermaidNavigationProbe = true; }); + await page.locator("[data-mermaid-expand]").first().click(); + await page.locator("[data-mermaid-overlay-canvas] svg").waitFor({ state: "visible", timeout: 10000 }); + // Use the normal link handler so the cached module and overlay lifecycle survive PJAX. + await page.keyboard.press("Escape"); + await navigateMermaidPage(page, "/"); + if (await page.locator("[data-mermaid-overlay].open").count() + || await page.locator("body.has-mermaid-overlay").count()) { + throw new Error("Mermaid overlay state leaked across page navigation"); + } + await navigateMermaidPage(page, "/__elements"); + await expectMermaidDiagrams(page); + await page.locator(".oc-mermaid.is-error pre code").waitFor({ state: "visible", timeout: 10000 }); + await page.locator("[data-mermaid-expand]").first().click(); + await page.locator("[data-mermaid-overlay-canvas] svg").waitFor({ state: "visible", timeout: 10000 }); + await page.keyboard.press("Escape"); + } finally { + await page.close(); + } +} + +async function checkMermaidContent(page) { await expectVisible(page, ".oc-mermaid", "mermaid block"); - await page.locator(".oc-mermaid.is-rendered svg").first().waitFor({ state: "visible", timeout: 10000 }); + await expectMermaidDiagrams(page); await page.waitForFunction(() => [...document.querySelectorAll(".oc-mermaid.is-rendered")] .some((block) => block.getAttribute("data-mermaid")?.includes("
") && block.querySelector("svg")) ); await page.locator("[data-mermaid-expand]").first().click(); await page.locator("[data-mermaid-overlay].open").waitFor({ state: "visible" }); + await page.locator("[data-mermaid-overlay-canvas] svg").waitFor({ state: "visible", timeout: 10000 }); await page.keyboard.press("Tab"); const mermaidOverlayFocus = await page.evaluate(() => ({ activeInOverlay: Boolean(document.querySelector("[data-mermaid-overlay].open")?.contains(document.activeElement)), @@ -585,6 +628,7 @@ async function checkDesktop() { } await page.locator("[data-mermaid-expand]").first().click(); await page.locator("[data-mermaid-overlay].open").waitFor({ state: "visible" }); + await page.locator("[data-mermaid-overlay-canvas] svg").waitFor({ state: "visible", timeout: 10000 }); await page.evaluate(() => history.pushState({ docs: true }, "", `${location.pathname}#fixture-components`)); await page.goBack(); await page.locator("[data-mermaid-overlay].open").waitFor({ state: "hidden" }); @@ -600,6 +644,7 @@ async function checkDesktop() { } await page.locator("[data-mermaid-expand]").first().click(); await page.locator("[data-mermaid-overlay].open").waitFor({ state: "visible" }); + await page.locator("[data-mermaid-overlay-canvas] svg").waitFor({ state: "visible", timeout: 10000 }); await page.keyboard.press("Meta+K"); if (await page.locator(".search-modal.open").count()) { throw new Error("mermaid overlay allowed the search shortcut"); @@ -613,14 +658,46 @@ async function checkDesktop() { throw new Error(`mermaid overlay did not close on Escape: ${JSON.stringify(mermaidOverlayClose)}`); } await page.locator(".oc-mermaid.is-error pre code").first().waitFor({ state: "visible", timeout: 10000 }); + // Mermaid 12 reports the parser error; render IDs detect artifacts independent of that wording. const mermaidErrorLeak = await page.evaluate(() => ({ bodyText: document.body.innerText.includes("Syntax error in text"), leakedSvg: [...document.querySelectorAll("svg")] - .some((svg) => !svg.closest("[data-mermaid]") && svg.textContent?.includes("Syntax error in text")), + .some((svg) => !svg.closest("[data-mermaid]") + && (svg.id.startsWith("oc_mermaid_") || svg.textContent?.includes("Syntax error in text"))), })); if (mermaidErrorLeak.bodyText || mermaidErrorLeak.leakedSvg) { throw new Error(`mermaid error artifact leaked into shell: ${JSON.stringify(mermaidErrorLeak)}`); } +} + +async function checkDesktop() { + const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); + await page.goto(`${base}/__elements`, { waitUntil: "networkidle" }); + await page.screenshot({ path: path.join(artifacts, "elements-desktop-dark.png"), fullPage: true }); + await page.evaluate(() => scrollTo(0, 760)); + await page.screenshot({ path: path.join(artifacts, "elements-desktop-scrolled.png"), fullPage: false }); + + const overlap = await page.evaluate(() => { + const header = document.querySelector(".site-header")?.getBoundingClientRect(); + const sidebar = document.querySelector(".sidebar")?.getBoundingClientRect(); + const toc = document.querySelector(".toc")?.getBoundingClientRect(); + return { + headerTop: header?.top, + sidebarClear: header && sidebar ? sidebar.top >= header.bottom - 1 : false, + tocClear: header && toc ? toc.top >= header.bottom - 1 : false, + }; + }); + if (overlap.headerTop !== 0 || !overlap.sidebarClear || !overlap.tocClear) { + throw new Error(`desktop sticky shell overlap: ${JSON.stringify(overlap)}`); + } + + await expectVisible(page, ".oc-code [data-code-copy]", "code copy button"); + await expectVisible(page, ".page-status .page-status-beta", "page status badge"); + await expectVisible(page, ".oc-code.has-line-numbers .code-line.is-highlighted", "line-highlighted code"); + await expectVisible(page, ".oc-badge.oc-badge-orange", "badge"); + await expectVisible(page, ".oc-panel", "panel"); + await expectVisible(page, ".oc-prompt [data-prompt-copy]", "prompt copy"); + await expectVisible(page, ".oc-tile-group .oc-tile", "tiles"); const componentSkin = await page.evaluate(() => { const code = getComputedStyle(document.querySelector(".oc-code")); const pre = getComputedStyle(document.querySelector(".oc-code pre"));