diff --git a/.github/pr-badge.yml b/.github/pr-badge.yml new file mode 100644 index 0000000..96b10e5 --- /dev/null +++ b/.github/pr-badge.yml @@ -0,0 +1,17 @@ +- icon: visualstudio + label: "GitHub.dev" + message: "PR-$prNumber" + color: "blue" + url: "https://github.dev/$owner/$repo/pull/$prNumber" + +- icon: github + label: "GitHub codespaces" + message: "PR-$prNumber" + color: "black" + url: "https://codespaces.new/$owner/$repo/pull/$prNumber" + +- icon: git + label: "GitPod.io" + message: "PR-$prNumber" + color: "orange" + url: "https://gitpod.io/?autostart=true#https://github.com/$owner/$repo/pull/$prNumber" diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000..c5406a6 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,85 @@ +# These settings are synced to GitHub by https://probot.github.io/apps/settings/ + +repository: + allow_merge_commit: false + + delete_branch_on_merge: true + + enable_vulnerability_alerts: true + +labels: + - name: bug + color: "#d73a4a" + description: Something isn't working + + - name: documentation + color: "#0075ca" + description: Improvements or additions to documentation + + - name: duplicate + color: "#cfd3d7" + description: This issue or pull request already exists + + - name: enhancement + color: "#a2eeef" + description: Some improvements + + - name: feature + color: "#16b33f" + description: New feature or request + + - name: good first issue + color: "#7057ff" + description: Good for newcomers + + - name: help wanted + color: "#008672" + description: Extra attention is needed + + - name: invalid + color: "#e4e669" + description: This doesn't seem right + + - name: question + color: "#d876e3" + description: Further information is requested + + - name: wontfix + color: "#ffffff" + description: This will not be worked on + +branches: + - name: main + # https://docs.github.com/en/rest/reference/repos#update-branch-protection + protection: + # Required. Require at least one approving review on a pull request, before merging. Set to null to disable. + required_pull_request_reviews: + # The number of approvals required. (1-6) + required_approving_review_count: 1 + # Dismiss approved reviews automatically when a new commit is pushed. + dismiss_stale_reviews: true + # Blocks merge until code owners have reviewed. + require_code_owner_reviews: true + # Specify which users and teams can dismiss pull request reviews. + # Pass an empty dismissal_restrictions object to disable. + # User and team dismissal_restrictions are only available for organization-owned repositories. + # Omit this parameter for personal repositories. + dismissal_restrictions: + # users: [] + # teams: [] + # Required. Require status checks to pass before merging. Set to null to disable + required_status_checks: + # Required. Require branches to be up to date before merging. + strict: true + # Required. The list of status checks to require in order to merge into this branch + contexts: [] + # Required. Enforce all configured restrictions for administrators. + # Set to true to enforce required status checks for repository administrators. + # Set to null to disable. + enforce_admins: true + # Prevent merge commits from being pushed to matching branches + required_linear_history: true + # Required. Restrict who can push to this branch. + # Team and user restrictions are only available for organization-owned repositories. + # Set to null to disable. + restrictions: null diff --git a/package.json b/package.json index 94af902..e7610d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "declarative-shadow-dom-polyfill", - "version": "0.3.2", + "version": "0.4.0", "license": "LGPL-2.1-or-later", "author": "shiy2008@gmail.com", "description": "Web standard polyfill for Declarative Shadow DOM", @@ -22,17 +22,20 @@ }, "main": "dist/index.js", "types": "dist/index.d.ts", + "peerDependencies": { + "typescript": ">=5.5.3" + }, "devDependencies": { "@types/jsdom": "^21.1.7", - "@types/node": "^18.19.39", - "husky": "^9.0.11", - "jsdom": "^24.1.0", + "@types/node": "^18.19.42", + "husky": "^9.1.1", + "jsdom": "^24.1.1", "lint-staged": "^15.2.7", - "prettier": "^3.3.2", + "prettier": "^3.3.3", "tsx": "^4.16.2", - "typedoc": "^0.26.4", - "typedoc-plugin-mdn-links": "^3.2.3", - "typescript": "^5.5.3" + "typedoc": "^0.26.5", + "typedoc-plugin-mdn-links": "^3.2.5", + "typescript": "~5.5.4" }, "prettier": { "trailingComma": "none" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d0ddb7..a1d23c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,32 +12,32 @@ importers: specifier: ^21.1.7 version: 21.1.7 '@types/node': - specifier: ^18.19.39 - version: 18.19.39 + specifier: ^18.19.42 + version: 18.19.42 husky: - specifier: ^9.0.11 - version: 9.0.11 + specifier: ^9.1.1 + version: 9.1.1 jsdom: - specifier: ^24.1.0 - version: 24.1.0 + specifier: ^24.1.1 + version: 24.1.1 lint-staged: specifier: ^15.2.7 version: 15.2.7 prettier: - specifier: ^3.3.2 - version: 3.3.2 + specifier: ^3.3.3 + version: 3.3.3 tsx: specifier: ^4.16.2 version: 4.16.2 typedoc: - specifier: ^0.26.4 - version: 0.26.4(typescript@5.5.3) + specifier: ^0.26.5 + version: 0.26.5(typescript@5.5.4) typedoc-plugin-mdn-links: - specifier: ^3.2.3 - version: 3.2.3(typedoc@0.26.4(typescript@5.5.3)) + specifier: ^3.2.5 + version: 3.2.5(typedoc@0.26.5(typescript@5.5.4)) typescript: - specifier: ^5.5.3 - version: 5.5.3 + specifier: ~5.5.4 + version: 5.5.4 packages: @@ -185,8 +185,8 @@ packages: '@types/jsdom@21.1.7': resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} - '@types/node@18.19.39': - resolution: {integrity: sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==} + '@types/node@18.19.42': + resolution: {integrity: sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==} '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -333,8 +333,8 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - husky@9.0.11: - resolution: {integrity: sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==} + husky@9.1.1: + resolution: {integrity: sha512-fCqlqLXcBnXa/TJXmT93/A36tJsjdJkibQ1MuIiFyCCYUlpYpIaj2mv1w+3KR6Rzu1IC3slFTje5f6DUp2A2rg==} engines: {node: '>=18'} hasBin: true @@ -364,8 +364,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jsdom@24.1.0: - resolution: {integrity: sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==} + jsdom@24.1.1: + resolution: {integrity: sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==} engines: {node: '>=18'} peerDependencies: canvas: ^2.11.2 @@ -437,8 +437,8 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - nwsapi@2.2.10: - resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==} + nwsapi@2.2.12: + resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} @@ -468,8 +468,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - prettier@3.3.2: - resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} hasBin: true @@ -575,20 +575,20 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - typedoc-plugin-mdn-links@3.2.3: - resolution: {integrity: sha512-hz22UgFuzdtgpBXet2dxKf9d2HVixL2VcE9Ke1+X1Nu8W8jSGIB2awIA6HFVsD0vk1OLe3ehU0ibyD5sN7wh4w==} + typedoc-plugin-mdn-links@3.2.5: + resolution: {integrity: sha512-duQ0H7+bATNSWVQRt3HubspZ9gqgSZxiQkenlTJ8lGsUrldZwpjG56hJqLD6BspNJfEnElP9hIU7yY5+/vF1Eg==} peerDependencies: typedoc: '>= 0.23.14 || 0.24.x || 0.25.x || 0.26.x' - typedoc@0.26.4: - resolution: {integrity: sha512-FlW6HpvULDKgc3rK04V+nbFyXogPV88hurarDPOjuuB5HAwuAlrCMQ5NeH7Zt68a/ikOKu6Z/0hFXAeC9xPccQ==} + typedoc@0.26.5: + resolution: {integrity: sha512-Vn9YKdjKtDZqSk+by7beZ+xzkkr8T8CYoiasqyt4TTRFy5+UHzL/mF/o4wGBjRF+rlWQHDb0t6xCpA3JNL5phg==} engines: {node: '>= 18'} hasBin: true peerDependencies: typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x - typescript@5.5.3: - resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true @@ -733,11 +733,11 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 18.19.39 + '@types/node': 18.19.42 '@types/tough-cookie': 4.0.5 parse5: 7.1.2 - '@types/node@18.19.39': + '@types/node@18.19.42': dependencies: undici-types: 5.26.5 @@ -896,7 +896,7 @@ snapshots: human-signals@5.0.0: {} - husky@9.0.11: {} + husky@9.1.1: {} iconv-lite@0.6.3: dependencies: @@ -916,7 +916,7 @@ snapshots: isexe@2.0.0: {} - jsdom@24.1.0: + jsdom@24.1.1: dependencies: cssstyle: 4.0.1 data-urls: 5.0.0 @@ -926,7 +926,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.10 + nwsapi: 2.2.12 parse5: 7.1.2 rrweb-cssom: 0.7.1 saxes: 6.0.0 @@ -1022,7 +1022,7 @@ snapshots: dependencies: path-key: 4.0.0 - nwsapi@2.2.10: {} + nwsapi@2.2.12: {} onetime@5.1.2: dependencies: @@ -1044,7 +1044,7 @@ snapshots: pidtree@0.6.0: {} - prettier@3.3.2: {} + prettier@3.3.3: {} psl@1.9.0: {} @@ -1137,20 +1137,20 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - typedoc-plugin-mdn-links@3.2.3(typedoc@0.26.4(typescript@5.5.3)): + typedoc-plugin-mdn-links@3.2.5(typedoc@0.26.5(typescript@5.5.4)): dependencies: - typedoc: 0.26.4(typescript@5.5.3) + typedoc: 0.26.5(typescript@5.5.4) - typedoc@0.26.4(typescript@5.5.3): + typedoc@0.26.5(typescript@5.5.4): dependencies: lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 shiki: 1.10.1 - typescript: 5.5.3 + typescript: 5.5.4 yaml: 2.4.5 - typescript@5.5.3: {} + typescript@5.5.4: {} uc.micro@2.1.0: {} diff --git a/source/index.ts b/source/index.ts index ef39a61..b37cb47 100644 --- a/source/index.ts +++ b/source/index.ts @@ -15,38 +15,52 @@ HTMLElement.prototype.attachShadow = function (options: ShadowRootInit) { return shadowRoot; }; -/** - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getHTML} - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/getHTML} - */ -export function getHTML( - this: Element | ShadowRoot, +export function* findShadowRoots(root: Node): Generator { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { + acceptNode: (node: Element) => + node instanceof HTMLElement + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_SKIP + }); + var currentNode: HTMLElement | null = null; + + while ((currentNode = walker.nextNode() as HTMLElement)) { + const shadowRoot = shadowDOMs.get(currentNode); + + if (shadowRoot) { + yield shadowRoot; + yield* findShadowRoots(shadowRoot); + } + } +} + +export function* generateHTML( + root: Node, { serializableShadowRoots, shadowRoots }: HTMLSerializationOptions = {} -) { +): Generator { shadowRoots = shadowRoots?.filter(Boolean) || []; - if (!serializableShadowRoots || !shadowRoots[0]) - return (this as HTMLElement).innerHTML; - - const walker = document.createTreeWalker(this, NodeFilter.SHOW_ALL, { - acceptNode: (node) => - node === this || node instanceof SVGElement - ? NodeFilter.FILTER_SKIP - : NodeFilter.FILTER_ACCEPT - }), - markup: string[] = []; + if (!serializableShadowRoots || !shadowRoots[0]) { + yield (root as HTMLElement).innerHTML; + return; + } + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ALL, { + acceptNode: (node) => + node === root || node instanceof SVGElement + ? NodeFilter.FILTER_SKIP + : NodeFilter.FILTER_ACCEPT + }); var currentNode: Node | null = null; while ((currentNode = walker.nextNode())) { if (currentNode instanceof CDATASection) - markup.push(``); - else if (currentNode instanceof Text) - markup.push(currentNode.nodeValue || ""); + yield ``; + else if (currentNode instanceof Text) yield currentNode.nodeValue || ""; else if (currentNode instanceof Comment) - markup.push(``); + yield ``; else if (currentNode instanceof SVGElement) - markup.push(xmlSerializer.serializeToString(currentNode)); + yield xmlSerializer.serializeToString(currentNode); else if (currentNode instanceof Element) { const tagName = currentNode.tagName.toLowerCase(), attributes = [...currentNode.attributes].map( @@ -54,24 +68,33 @@ export function getHTML( ), shadowRoot = shadowDOMs.get(currentNode); - markup.push(`<${[tagName, ...attributes].join(" ")}>`); + yield `<${[tagName, ...attributes].join(" ")}>`; + + if (shadowRoot && shadowRoots.includes(shadowRoot)) { + const shadowRootHTML = [ + ...generateHTML(shadowRoot, { serializableShadowRoots, shadowRoots }) + ].join(""); - if (shadowRoots.includes(shadowRoot)) - markup.push( - `` - ); - if (!currentNode.childNodes[0]) markup.push(``); + yield ``; + } + if (!currentNode.childNodes[0]) yield ``; } const { nextSibling, parentElement } = currentNode; - if (!nextSibling && parentElement !== this) - markup.push(``); + if (!nextSibling && parentElement && parentElement !== root) + yield ``; } +} - return markup.join(""); +/** + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getHTML} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/getHTML} + */ +export function getHTML( + this: Element | ShadowRoot, + options: HTMLSerializationOptions = {} +) { + return [...generateHTML(this, options)].join(""); } export function attachDeclarativeShadowRoots(root: HTMLElement | ShadowRoot) { @@ -83,8 +106,7 @@ export function attachDeclarativeShadowRoots(root: HTMLElement | ShadowRoot) { const { parentElement, content } = template; const shadowRoot = parentElement!.attachShadow({ - // @ts-ignore - mode: template.getAttribute("shadowrootmode") + mode: template.getAttribute("shadowrootmode") as ShadowRootMode }); shadowRoot.append(content); @@ -121,7 +143,6 @@ export function parseHTMLUnsafe(html: string) { declare global { interface ShadowRootSerializable { getHTML: typeof getHTML; - setHTMLUnsafe: typeof setHTMLUnsafe; } interface Element extends ShadowRootSerializable {} interface ShadowRoot extends ShadowRootSerializable {} @@ -131,7 +152,7 @@ Element.prototype.getHTML ||= getHTML; Element.prototype.setHTMLUnsafe ||= setHTMLUnsafe; ShadowRoot.prototype.getHTML ||= getHTML; ShadowRoot.prototype.setHTMLUnsafe ||= setHTMLUnsafe; -Document["parseHTMLUnsafe"] ||= parseHTMLUnsafe; +Document.parseHTMLUnsafe ||= parseHTMLUnsafe; new Promise((resolve) => { if (document.readyState === "complete") resolve(); diff --git a/test/index.spec.ts b/test/index.spec.ts index 31f395b..ca2b7b5 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,8 +1,8 @@ -import { strictEqual } from "node:assert"; +import { deepStrictEqual, strictEqual } from "node:assert"; import { describe, it } from "node:test"; import "./global"; -import "../source"; +import { findShadowRoots, generateHTML } from "../source"; const innerHTML = `

Hello, Declarative Shadow DOM!

`; const shadowHTML = ``; @@ -10,7 +10,7 @@ const outerHTML = `${shadowHTML}`; describe("Document.parseHTMLUnsafe()", () => { it("should parse a Declarative Shadow DOM string into a Document", () => { - const { body } = Document["parseHTMLUnsafe"](outerHTML); + const { body } = Document.parseHTMLUnsafe(outerHTML); const outerElements = body.querySelectorAll("*"); @@ -32,6 +32,34 @@ describe(".setHTMLUnsafe()", () => { }); }); +describe("internal utility", () => { + it("should find all kinds of shadow roots in a DOM tree", () => { + const { body } = Document.parseHTMLUnsafe(` + + + + + `); + const closedShadowRoot = body.lastElementChild!.attachShadow({ + mode: "closed" + }), + { shadowRoot } = body.querySelector("open-tag")!, + shadowRoots = [...findShadowRoots(body)]; + + deepStrictEqual([shadowRoot, closedShadowRoot], shadowRoots); + }); + + it("should generate HTML strings of a DOM tree", () => { + const markups = [ + ...generateHTML(document.body, { + serializableShadowRoots: true, + shadowRoots: [...findShadowRoots(document.body)] + }) + ]; + strictEqual(markups.join(""), outerHTML); + }); +}); + describe(".getHTML()", () => { it("should return `.innerHTML` value with no parameter", () => { const { body } = document; diff --git a/tsconfig.json b/tsconfig.json index 17d0427..d349175 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "strict": true, "module": "UMD", "moduleResolution": "Node", "downlevelIteration": true,