From c3a77646b40752765990c77cd8a6fe0b669083a7 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:37:40 +0800 Subject: [PATCH 1/6] feat: add x-markdown component for rendering markdown content with styling support --- .github/lynx-stack.instructions.md | 5 +- packages/web-platform/web-elements/index.css | 1 + .../web-platform/web-elements/package.json | 9 + .../src/elements/XMarkdown/XMarkdown.ts | 16 ++ .../elements/XMarkdown/XMarkdownAttributes.ts | 231 ++++++++++++++++++ .../src/elements/XMarkdown/index.ts | 11 + .../src/elements/XMarkdown/x-markdown.css | 15 ++ .../src/elements/htmlTemplates.ts | 81 +++++- .../web-elements/src/types/markdown-it.d.ts | 1 + .../tests/fixtures/x-markdown/basic.html | 39 +++ .../tests/fixtures/x-markdown/events.html | 45 ++++ .../tests/fixtures/x-markdown/image.html | 39 +++ .../tests/fixtures/x-markdown/style.html | 50 ++++ .../web-elements/tests/x-markdown.spec.ts | 90 +++++++ pnpm-lock.yaml | 61 +++++ 15 files changed, 681 insertions(+), 13 deletions(-) create mode 100644 packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdown.ts create mode 100644 packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts create mode 100644 packages/web-platform/web-elements/src/elements/XMarkdown/index.ts create mode 100644 packages/web-platform/web-elements/src/elements/XMarkdown/x-markdown.css create mode 100644 packages/web-platform/web-elements/src/types/markdown-it.d.ts create mode 100644 packages/web-platform/web-elements/tests/fixtures/x-markdown/basic.html create mode 100644 packages/web-platform/web-elements/tests/fixtures/x-markdown/events.html create mode 100644 packages/web-platform/web-elements/tests/fixtures/x-markdown/image.html create mode 100644 packages/web-platform/web-elements/tests/fixtures/x-markdown/style.html create mode 100644 packages/web-platform/web-elements/tests/x-markdown.spec.ts diff --git a/.github/lynx-stack.instructions.md b/.github/lynx-stack.instructions.md index 023e0c6ec5..7f78f96edc 100644 --- a/.github/lynx-stack.instructions.md +++ b/.github/lynx-stack.instructions.md @@ -2,7 +2,10 @@ applyTo: "packages/web-platform/web-elements/**/*" --- -When updating web element APIs, add targeted Playwright tests in packages/web-platform/web-elements/tests/web-elements.spec.ts and keep changes minimal. +When updating web element APIs, add targeted Playwright tests in a dedicated spec file under packages/web-platform/web-elements/tests and keep changes minimal. Ensure Playwright browsers are installed (pnpm exec playwright install --with-deps ) before running web-elements tests. + For x-input type="number" in web-elements, keep inner input type as text, set inputmode="decimal", and filter number input internally without setting input-filter explicitly. Add new web-elements UI fixtures under packages/web-platform/web-elements/tests/fixtures and commit matching snapshots in packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots. + +For x-markdown updates, keep markdown-style injected via a shadow-root style tag and render markdown only on attribute changes. diff --git a/packages/web-platform/web-elements/index.css b/packages/web-platform/web-elements/index.css index dcdf435448..cd2d2f04a2 100644 --- a/packages/web-platform/web-elements/index.css +++ b/packages/web-platform/web-elements/index.css @@ -13,6 +13,7 @@ @import url("./src/elements/XSvg/x-svg.css"); @import url("./src/elements/XImage/x-image.css"); @import url("./src/elements/XInput/x-input.css"); +@import url("./src/elements/XMarkdown/x-markdown.css"); @import url("./src/elements/XOverlayNg/x-overlay-ng.css"); @import url("./src/elements/XRefreshView/x-refresh-view.css"); @import url("./src/elements/XSwiper/x-swiper.css"); diff --git a/packages/web-platform/web-elements/package.json b/packages/web-platform/web-elements/package.json index bd0cb582d9..0b2c75e3af 100644 --- a/packages/web-platform/web-elements/package.json +++ b/packages/web-platform/web-elements/package.json @@ -58,6 +58,11 @@ "types": "./dist/elements/XInput/index.d.ts", "default": "./dist/elements/XInput/index.js" }, + "./XMarkdown": { + "@lynx-js/source-field": "./src/elements/XMarkdown/index.ts", + "types": "./dist/elements/XMarkdown/index.d.ts", + "default": "./dist/elements/XMarkdown/index.js" + }, "./XOverlayNg": { "@lynx-js/source-field": "./src/elements/XOverlayNg/index.ts", "types": "./dist/elements/XOverlayNg/index.d.ts", @@ -134,11 +139,15 @@ "test:install": "playwright install --with-deps chromium firefox webkit", "test:update": "playwright test --ui --update-snapshots" }, + "dependencies": { + "markdown-it": "^14.1.0" + }, "devDependencies": { "@lynx-js/playwright-fixtures": "workspace:*", "@playwright/test": "^1.58.2", "@rsbuild/core": "catalog:rsbuild", "@rsbuild/plugin-source-build": "1.0.4", + "@types/markdown-it": "^14.1.1", "nyc": "^17.1.0", "tslib": "^2.8.1" }, diff --git a/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdown.ts b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdown.ts new file mode 100644 index 0000000000..c24663389e --- /dev/null +++ b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdown.ts @@ -0,0 +1,16 @@ +/* +// Copyright 2024 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +*/ +import { Component } from '../../element-reactive/index.js'; +import { CommonEventsAndMethods } from '../common/CommonEventsAndMethods.js'; +import { templateXMarkdown } from '../htmlTemplates.js'; +import { XMarkdownAttributes } from './XMarkdownAttributes.js'; + +@Component( + 'x-markdown', + [CommonEventsAndMethods, XMarkdownAttributes], + templateXMarkdown, +) +export class XMarkdown extends HTMLElement {} diff --git a/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts new file mode 100644 index 0000000000..0cff2e6442 --- /dev/null +++ b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts @@ -0,0 +1,231 @@ +/* +// Copyright 2024 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +*/ +import MarkdownIt from 'markdown-it'; +import { + boostedQueueMicrotask, + genDomGetter, + registerAttributeHandler, +} from '../../element-reactive/index.js'; +import type { XMarkdown } from './XMarkdown.js'; + +const markdownParser = new MarkdownIt({ + html: false, + linkify: true, +}); + +const unitlessCssProperties = new Set([ + 'font-weight', + 'line-height', + 'opacity', + 'z-index', + 'flex', + 'flex-grow', + 'flex-shrink', + 'order', +]); + +const selectorMap: Record = { + normalText: '.markdown-body', + link: '.markdown-body a', + inlineCode: '.markdown-body code:not(pre code)', + codeBlock: ['.markdown-body pre', '.markdown-body pre code'], + h1: '.markdown-body h1', + h2: '.markdown-body h2', + h3: '.markdown-body h3', + h4: '.markdown-body h4', + h5: '.markdown-body h5', + h6: '.markdown-body h6', + quote: '.markdown-body blockquote', + orderedList: '.markdown-body ol', + unorderedList: '.markdown-body ul', + listItem: '.markdown-body li', + image: '.markdown-body img', + span: '.markdown-body span', + p: '.markdown-body p', +}; + +const normalizeColor = (value: string) => { + const hex = value.trim(); + if (/^[0-9a-fA-F]{6,8}$/.test(hex)) { + return `#${hex}`; + } + return value; +}; + +const camelToKebab = (value: string) => + value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); + +const toCssValue = (property: string, value: unknown) => { + if (value === null || value === undefined) return null; + if (typeof value === 'number') { + return unitlessCssProperties.has(property) ? `${value}` : `${value}px`; + } + if (typeof value === 'string') { + if (property.includes('color')) { + return normalizeColor(value); + } + return value; + } + return null; +}; + +const parseMarkdownStyle = (value: string | null) => { + if (!value) return {} as Record>; + try { + const parsed = JSON.parse(value); + if (parsed && typeof parsed === 'object') { + return parsed as Record>; + } + } catch { + return {} as Record>; + } + return {} as Record>; +}; + +const buildMarkdownStyleCss = ( + style: Record>, +) => { + const rules: string[] = []; + for (const [key, properties] of Object.entries(style)) { + if (!properties || typeof properties !== 'object') continue; + let selectors: string[] = []; + if (key.startsWith('.') || key.startsWith('#')) { + selectors = [`.markdown-body ${key}`]; + } else if (selectorMap[key]) { + selectors = Array.isArray(selectorMap[key]) + ? (selectorMap[key] as string[]) + : [selectorMap[key] as string]; + } else { + continue; + } + const declarations = Object.entries(properties) + .map(([property, value]) => { + const cssProperty = camelToKebab(property); + const cssValue = toCssValue(cssProperty, value); + return cssValue ? `${cssProperty}: ${cssValue};` : null; + }) + .filter(Boolean) + .join(''); + if (!declarations) continue; + for (const selector of selectors) { + rules.push(`${selector} {${declarations}}`); + } + } + return rules.join('\n'); +}; + +export class XMarkdownAttributes { + static observedAttributes = ['content', 'markdown-style', 'content-id']; + + readonly #dom: XMarkdown; + readonly #root = genDomGetter(() => this.#dom.shadowRoot!, '#markdown-root'); + readonly #style = genDomGetter( + () => this.#dom.shadowRoot!, + '#markdown-style', + ); + + #pendingRender = false; + #content = ''; + #contentId?: string; + #eventsAttached = false; + + constructor(dom: XMarkdown) { + this.#dom = dom; + } + + connectedCallback() { + this.#ensureEvents(); + } + + dispose() { + if (!this.#eventsAttached) return; + const root = this.#root(); + root.removeEventListener('click', this.#handleClick); + this.#eventsAttached = false; + } + + #ensureEvents() { + if (this.#eventsAttached) return; + const root = this.#root(); + root.addEventListener('click', this.#handleClick); + this.#eventsAttached = true; + } + + #handleClick = (event: MouseEvent) => { + const target = event.target as Element | null; + if (!target) return; + const root = this.#root(); + const anchor = target.closest('a'); + if (anchor && root.contains(anchor)) { + event.preventDefault(); + this.#dom.dispatchEvent( + new CustomEvent('bindlink', { + detail: { + url: anchor.getAttribute('href') ?? '', + content: anchor.textContent ?? '', + contentId: this.#contentId, + }, + bubbles: true, + composed: true, + }), + ); + return; + } + const image = target.closest('img'); + if (image && root.contains(image)) { + this.#dom.dispatchEvent( + new CustomEvent('bindimageTap', { + detail: { + url: image.getAttribute('src') ?? '', + contentId: this.#contentId, + }, + bubbles: true, + composed: true, + }), + ); + } + }; + + #scheduleRender() { + if (this.#pendingRender) return; + this.#pendingRender = true; + boostedQueueMicrotask(() => { + this.#pendingRender = false; + this.#render(); + }); + } + + #render() { + const root = this.#root(); + if (!this.#content) { + root.innerHTML = ''; + return; + } + root.innerHTML = markdownParser.render(this.#content); + } + + #applyMarkdownStyle(value: string | null) { + const styleTag = this.#style(); + const parsed = parseMarkdownStyle(value); + styleTag.textContent = buildMarkdownStyleCss(parsed); + } + + @registerAttributeHandler('content', true) + _handleContent(newVal: string | null) { + this.#content = newVal ?? ''; + this.#scheduleRender(); + } + + @registerAttributeHandler('markdown-style', true) + _handleMarkdownStyle(newVal: string | null) { + this.#applyMarkdownStyle(newVal); + } + + @registerAttributeHandler('content-id', true) + _handleContentId(newVal: string | null) { + this.#contentId = newVal ?? undefined; + } +} diff --git a/packages/web-platform/web-elements/src/elements/XMarkdown/index.ts b/packages/web-platform/web-elements/src/elements/XMarkdown/index.ts new file mode 100644 index 0000000000..36b3f5acd3 --- /dev/null +++ b/packages/web-platform/web-elements/src/elements/XMarkdown/index.ts @@ -0,0 +1,11 @@ +// Copyright 2024 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +/** + * @module elements/XMarkdown + * + * `x-markdown` renders markdown content with minimal styling. + * It supports the `content` and `markdown-style` attributes. + */ +export { XMarkdown } from './XMarkdown.js'; diff --git a/packages/web-platform/web-elements/src/elements/XMarkdown/x-markdown.css b/packages/web-platform/web-elements/src/elements/XMarkdown/x-markdown.css new file mode 100644 index 0000000000..47d6ea22d2 --- /dev/null +++ b/packages/web-platform/web-elements/src/elements/XMarkdown/x-markdown.css @@ -0,0 +1,15 @@ +/* +// Copyright 2024 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +*/ +x-markdown { + display: flex; + flex-direction: column; + align-items: stretch; + color: inherit; +} + +x-markdown::part(root) { + width: 100%; +} diff --git a/packages/web-platform/web-elements/src/elements/htmlTemplates.ts b/packages/web-platform/web-elements/src/elements/htmlTemplates.ts index 6f7a0f0163..dbc8a4583d 100644 --- a/packages/web-platform/web-elements/src/elements/htmlTemplates.ts +++ b/packages/web-platform/web-elements/src/elements/htmlTemplates.ts @@ -104,9 +104,9 @@ export const templateXImage = (attributes: { src?: string }) => { return ` `; }; -export const templateFilterImage = templateXImage; +export const templateFilterImage = /*#__PURE__*/ templateXImage; -export const templateXInput = ` + +
`; + +export const templateInlineImage = /*#__PURE__*/ templateXImage; -export const templateXTextarea = ` `; -export const templateXSvg = () => { +export const templateXSvg = /*#__PURE__*/ () => { return ` `; }; diff --git a/packages/web-platform/web-elements/src/types/markdown-it.d.ts b/packages/web-platform/web-elements/src/types/markdown-it.d.ts new file mode 100644 index 0000000000..ae4a4b412a --- /dev/null +++ b/packages/web-platform/web-elements/src/types/markdown-it.d.ts @@ -0,0 +1 @@ +declare module 'markdown-it'; diff --git a/packages/web-platform/web-elements/tests/fixtures/x-markdown/basic.html b/packages/web-platform/web-elements/tests/fixtures/x-markdown/basic.html new file mode 100644 index 0000000000..635e43caef --- /dev/null +++ b/packages/web-platform/web-elements/tests/fixtures/x-markdown/basic.html @@ -0,0 +1,39 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + + diff --git a/packages/web-platform/web-elements/tests/fixtures/x-markdown/events.html b/packages/web-platform/web-elements/tests/fixtures/x-markdown/events.html new file mode 100644 index 0000000000..157f4a1aef --- /dev/null +++ b/packages/web-platform/web-elements/tests/fixtures/x-markdown/events.html @@ -0,0 +1,45 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + + diff --git a/packages/web-platform/web-elements/tests/fixtures/x-markdown/image.html b/packages/web-platform/web-elements/tests/fixtures/x-markdown/image.html new file mode 100644 index 0000000000..f7c901fe91 --- /dev/null +++ b/packages/web-platform/web-elements/tests/fixtures/x-markdown/image.html @@ -0,0 +1,39 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + + diff --git a/packages/web-platform/web-elements/tests/fixtures/x-markdown/style.html b/packages/web-platform/web-elements/tests/fixtures/x-markdown/style.html new file mode 100644 index 0000000000..2013fa7eb8 --- /dev/null +++ b/packages/web-platform/web-elements/tests/fixtures/x-markdown/style.html @@ -0,0 +1,50 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + + diff --git a/packages/web-platform/web-elements/tests/x-markdown.spec.ts b/packages/web-platform/web-elements/tests/x-markdown.spec.ts new file mode 100644 index 0000000000..b93552a181 --- /dev/null +++ b/packages/web-platform/web-elements/tests/x-markdown.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from '@lynx-js/playwright-fixtures'; +import type { Page } from '@playwright/test'; + +const goto = async (page: Page, fixtureName: string) => { + await page.goto(`tests/fixtures/${fixtureName}.html`, { waitUntil: 'load' }); + await page.evaluate(() => document.fonts.ready); +}; + +test.describe('x-markdown', () => { + test('should render basic markdown', async ({ page }) => { + await goto(page, 'x-markdown/basic'); + const markdown = page.locator('x-markdown'); + + await expect(markdown.locator('h1')).toHaveText('Title'); + await expect(markdown.locator('strong')).toHaveText('bold'); + await expect(markdown.locator('em')).toHaveText('italic'); + await expect(markdown.locator('li')).toHaveCount(2); + await expect(markdown.locator('code').first()).toContainText('code'); + }); + + test('should apply markdown-style updates', async ({ page }) => { + await goto(page, 'x-markdown/style'); + const markdown = page.locator('x-markdown'); + + const link = markdown.locator('a'); + await expect(link).toHaveCSS('color', 'rgb(255, 0, 0)'); + + const inlineCode = markdown.locator('code').first(); + await expect(inlineCode).toHaveCSS('color', 'rgb(0, 0, 255)'); + await expect(inlineCode).toHaveCSS( + 'background-color', + 'rgb(238, 238, 238)', + ); + + await markdown.evaluate((el) => { + el.setAttribute( + 'markdown-style', + JSON.stringify({ link: { color: '0000ff' } }), + ); + }); + await expect(link).toHaveCSS('color', 'rgb(0, 0, 255)'); + }); + + test('should render image', async ({ page }) => { + await goto(page, 'x-markdown/image'); + const markdown = page.locator('x-markdown'); + + const image = markdown.locator('img'); + await expect(image).toHaveAttribute( + 'src', + '/tests/fixtures/resources/firefox-logo.png', + ); + }); + + test('should update content on attribute change', async ({ page }) => { + await goto(page, 'x-markdown/basic'); + const markdown = page.locator('x-markdown'); + + await markdown.evaluate((el) => { + el.setAttribute('content', '# Updated'); + }); + await expect(markdown.locator('h1')).toHaveText('Updated'); + }); + + test('should fire bindlink and bindimageTap events', async ({ page }) => { + await goto(page, 'x-markdown/events'); + const markdown = page.locator('x-markdown'); + + await markdown.locator('a').click(); + await page.waitForFunction(() => (window as any)._bindlink_detail !== null); + const linkDetail = await page.evaluate(() => + (window as any)._bindlink_detail + ); + expect(linkDetail.url).toBe('https://example.com'); + expect(linkDetail.content).toBe('link'); + expect(linkDetail.contentId).toBe('case-1'); + + await markdown.locator('img').click(); + await page.waitForFunction(() => + (window as any)._bindimage_detail !== null + ); + const imageDetail = await page.evaluate(() => + (window as any)._bindimage_detail + ); + expect(imageDetail.url).toContain( + '/tests/fixtures/resources/firefox-logo.png', + ); + expect(imageDetail.contentId).toBe('case-1'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cacbb74b81..d3bc546272 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1258,6 +1258,10 @@ importers: version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(@vitest/ui@3.2.4)(jsdom@27.4.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.31.6) packages/web-platform/web-elements: + dependencies: + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 devDependencies: '@lynx-js/playwright-fixtures': specifier: workspace:* @@ -1271,6 +1275,9 @@ importers: '@rsbuild/plugin-source-build': specifier: 1.0.4 version: 1.0.4(@rsbuild/core@1.7.3) + '@types/markdown-it': + specifier: ^14.1.1 + version: 14.1.1 nyc: specifier: ^17.1.0 version: 17.1.0 @@ -4320,12 +4327,21 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/loader-utils@2.0.6': resolution: {integrity: sha512-cgu0Xefgq9O5FjFR78jgI6X31aPjDWCaJ6LCfRtlj6BtyVVWiXagysSYlPACwGKAzRwsFLjKXcj4iGfcVt6cLw==} + '@types/markdown-it@14.1.1': + resolution: {integrity: sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -7177,6 +7193,9 @@ packages: resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + lint-staged@16.2.7: resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} engines: {node: '>=20.17'} @@ -7295,6 +7314,10 @@ packages: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -7359,6 +7382,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -8250,6 +8276,10 @@ packages: engines: {node: '>=18'} hasBin: true + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -9492,6 +9522,9 @@ packages: peerDependencies: typescript: '>=4.8.0 <5.10.0' + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -13256,15 +13289,24 @@ snapshots: dependencies: '@types/node': 24.10.13 + '@types/linkify-it@5.0.0': {} + '@types/loader-utils@2.0.6': dependencies: '@types/node': 24.10.13 '@types/webpack': 4.41.40 + '@types/markdown-it@14.1.1': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} + '@types/mdx@2.0.13': {} '@types/mime@1.3.5': {} @@ -16553,6 +16595,10 @@ snapshots: lines-and-columns@2.0.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + lint-staged@16.2.7: dependencies: commander: 14.0.3 @@ -16669,6 +16715,15 @@ snapshots: markdown-extensions@2.0.0: {} + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} math-intrinsics@1.1.0: {} @@ -16851,6 +16906,8 @@ snapshots: mdn-data@2.12.2: {} + mdurl@2.0.0: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -17856,6 +17913,8 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 + punycode.js@2.3.1: {} + punycode@2.3.1: {} q@1.5.1: {} @@ -19283,6 +19342,8 @@ snapshots: randexp: 0.5.3 typescript: 5.9.3 + uc.micro@2.1.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 From dddccc38137d090109fb3d8387ec59060814b9e1 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:33:49 +0800 Subject: [PATCH 2/6] feat(x-markdown): implement incremental content rendering and update tests --- .../elements/XMarkdown/XMarkdownAttributes.ts | 113 +++++++++++++++++- .../tests/fixtures/shell-project.ts | 1 + .../fixtures/x-markdown/incremental.html | 34 ++++++ .../web-elements/tests/x-markdown.spec.ts | 73 +++++++++++ 4 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 packages/web-platform/web-elements/tests/fixtures/x-markdown/incremental.html diff --git a/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts index 0cff2e6442..9a1a854650 100644 --- a/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts +++ b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts @@ -11,10 +11,23 @@ import { } from '../../element-reactive/index.js'; import type { XMarkdown } from './XMarkdown.js'; -const markdownParser = new MarkdownIt({ - html: false, - linkify: true, -}); +const MarkdownItCtor = + (MarkdownIt as unknown as { default?: typeof MarkdownIt }).default + ?? MarkdownIt; +let markdownParser: MarkdownIt | null = null; +let markdownParserError: unknown; +const getMarkdownParser = () => { + if (markdownParser || markdownParserError) return markdownParser; + try { + markdownParser = new MarkdownItCtor({ + html: false, + linkify: true, + }); + } catch (error) { + markdownParserError = error; + } + return markdownParser; +}; const unitlessCssProperties = new Set([ 'font-weight', @@ -129,8 +142,12 @@ export class XMarkdownAttributes { #pendingRender = false; #content = ''; + #renderedContent = ''; #contentId?: string; #eventsAttached = false; + #appendRemainder = ''; + #appendFlushTimer?: ReturnType; + #appendFlushDelay = 60; constructor(dom: XMarkdown) { this.#dom = dom; @@ -202,9 +219,95 @@ export class XMarkdownAttributes { const root = this.#root(); if (!this.#content) { root.innerHTML = ''; + this.#renderedContent = ''; + this.#appendRemainder = ''; + this.#clearAppendFlushTimer(); + return; + } + const parser = getMarkdownParser(); + if (!parser) { + root.textContent = this.#content; + this.#renderedContent = this.#content; + this.#appendRemainder = ''; + this.#clearAppendFlushTimer(); + return; + } + if (this.#canAppendIncrementally()) { + this.#appendIncrementally(root); + return; + } + root.innerHTML = parser.render(this.#content); + this.#renderedContent = this.#content; + this.#appendRemainder = ''; + this.#clearAppendFlushTimer(); + } + + #canAppendIncrementally() { + if (!this.#renderedContent) return false; + if (!this.#content.startsWith(this.#renderedContent)) return false; + if (this.#content.length === this.#renderedContent.length) return false; + return true; + } + + #appendIncrementally(root: HTMLElement) { + const delta = this.#content.slice(this.#renderedContent.length); + if (!delta) return; + const lastNewlineIndex = delta.lastIndexOf('\n'); + if (lastNewlineIndex === -1) { + this.#appendRemainder = delta; + this.#scheduleAppendFlush(); return; } - root.innerHTML = markdownParser.render(this.#content); + const chunk = delta.slice(0, lastNewlineIndex + 1); + if (chunk) { + const parser = getMarkdownParser(); + if (!parser) return; + const html = parser.render(chunk); + this.#appendHtml(root, html); + this.#renderedContent += chunk; + } + this.#appendRemainder = delta.slice(lastNewlineIndex + 1); + if (this.#appendRemainder) { + this.#scheduleAppendFlush(); + } else { + this.#clearAppendFlushTimer(); + } + } + + #scheduleAppendFlush() { + this.#clearAppendFlushTimer(); + this.#appendFlushTimer = setTimeout(() => { + this.#appendFlushTimer = undefined; + this.#flushAppendRemainder(); + }, this.#appendFlushDelay); + } + + #clearAppendFlushTimer() { + if (this.#appendFlushTimer) { + clearTimeout(this.#appendFlushTimer); + this.#appendFlushTimer = undefined; + } + } + + #flushAppendRemainder() { + if (!this.#appendRemainder) return; + if (!this.#content.startsWith(this.#renderedContent)) return; + const expectedLength = this.#renderedContent.length + + this.#appendRemainder.length; + if (this.#content.length !== expectedLength) return; + const root = this.#root(); + const parser = getMarkdownParser(); + if (!parser) return; + const html = parser.render(this.#appendRemainder); + this.#appendHtml(root, html); + this.#renderedContent += this.#appendRemainder; + this.#appendRemainder = ''; + } + + #appendHtml(root: HTMLElement, html: string) { + const template = document.createElement('template'); + template.innerHTML = html; + root.append(template.content); } #applyMarkdownStyle(value: string | null) { diff --git a/packages/web-platform/web-elements/tests/fixtures/shell-project.ts b/packages/web-platform/web-elements/tests/fixtures/shell-project.ts index a2384b3dfe..df60dfe46b 100644 --- a/packages/web-platform/web-elements/tests/fixtures/shell-project.ts +++ b/packages/web-platform/web-elements/tests/fixtures/shell-project.ts @@ -1,5 +1,6 @@ import '../../src/compat/LinearContainer/LinearContainer.js'; import '../../src/elements/all.js'; +import '../../src/elements/XMarkdown/index.js'; import '../../index.css'; import '@lynx-js/playwright-fixtures/common.css'; import { Component } from '../../src/element-reactive/component.js'; diff --git a/packages/web-platform/web-elements/tests/fixtures/x-markdown/incremental.html b/packages/web-platform/web-elements/tests/fixtures/x-markdown/incremental.html new file mode 100644 index 0000000000..5c1e32a7f7 --- /dev/null +++ b/packages/web-platform/web-elements/tests/fixtures/x-markdown/incremental.html @@ -0,0 +1,34 @@ + + + + + + web playground + + + + + + + + + + + + + + + + + + diff --git a/packages/web-platform/web-elements/tests/x-markdown.spec.ts b/packages/web-platform/web-elements/tests/x-markdown.spec.ts index b93552a181..65a103d80c 100644 --- a/packages/web-platform/web-elements/tests/x-markdown.spec.ts +++ b/packages/web-platform/web-elements/tests/x-markdown.spec.ts @@ -6,6 +6,41 @@ const goto = async (page: Page, fixtureName: string) => { await page.evaluate(() => document.fonts.ready); }; +const getShadowText = async (page: Page, selector: string) => + page.evaluate((value) => { + const element = document.querySelector('x-markdown'); + return element?.shadowRoot?.querySelector(value)?.textContent ?? ''; + }, selector); + +const getShadowCount = async (page: Page, selector: string) => + page.evaluate((value) => { + const element = document.querySelector('x-markdown'); + return element?.shadowRoot?.querySelectorAll(value).length ?? 0; + }, selector); + +const appendContent = async (page: Page, suffix: string) => { + await page.evaluate((value) => { + const element = document.querySelector('x-markdown'); + const content = element?.getAttribute('content') ?? ''; + element?.setAttribute('content', `${content}${value}`); + }, suffix); +}; + +const captureFirstChild = async (page: Page) => { + await page.evaluate(() => { + const element = document.querySelector('x-markdown'); + const root = element?.shadowRoot?.querySelector('#markdown-root'); + (window as any).__markdown_first_child = root?.firstElementChild ?? null; + }); +}; + +const isFirstChildSame = async (page: Page) => + page.evaluate(() => { + const element = document.querySelector('x-markdown'); + const root = element?.shadowRoot?.querySelector('#markdown-root'); + return root?.firstElementChild === (window as any).__markdown_first_child; + }); + test.describe('x-markdown', () => { test('should render basic markdown', async ({ page }) => { await goto(page, 'x-markdown/basic'); @@ -87,4 +122,42 @@ test.describe('x-markdown', () => { ); expect(imageDetail.contentId).toBe('case-1'); }); + + test('should append content incrementally', async ({ page }) => { + await goto(page, 'x-markdown/basic'); + expect(await getShadowText(page, 'h1')).toBe('Title'); + await captureFirstChild(page); + await appendContent(page, '\n\n## More'); + await page.waitForFunction(() => { + const element = document.querySelector('x-markdown'); + const root = element?.shadowRoot; + return root?.querySelector('h2')?.textContent === 'More'; + }); + expect(await isFirstChildSame(page)).toBe(true); + }); + + test('should re-render when content diverges', async ({ page }) => { + await goto(page, 'x-markdown/basic'); + await captureFirstChild(page); + await page.evaluate(() => { + const element = document.querySelector('x-markdown'); + element?.setAttribute('content', '# Title\n\nReplaced'); + }); + await page.waitForFunction(() => { + const element = document.querySelector('x-markdown'); + return element?.shadowRoot?.querySelector('p')?.textContent + === 'Replaced'; + }); + expect(await isFirstChildSame(page)).toBe(false); + }); + + test('should batch append by newline boundary', async ({ page }) => { + await goto(page, 'x-markdown/incremental'); + expect(await getShadowCount(page, 'p')).toBe(1); + await appendContent(page, 'Line 2'); + await page.waitForTimeout(20); + expect(await getShadowCount(page, 'p')).toBe(1); + await page.waitForTimeout(80); + expect(await getShadowCount(page, 'p')).toBe(2); + }); }); From 74d4e4995a3ab526e37e145be10cede9814ed8a3 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:54:19 +0800 Subject: [PATCH 3/6] feat(x-markdown): add DOMPurify for HTML sanitization and update dependencies --- .../web-platform/web-elements/package.json | 1 + .../elements/XMarkdown/XMarkdownAttributes.ts | 27 +++++++++++++++++-- .../web-elements/src/types/markdown-it.d.ts | 1 - pnpm-lock.yaml | 9 ++++++- 4 files changed, 34 insertions(+), 4 deletions(-) delete mode 100644 packages/web-platform/web-elements/src/types/markdown-it.d.ts diff --git a/packages/web-platform/web-elements/package.json b/packages/web-platform/web-elements/package.json index 0b2c75e3af..a8d0e2e297 100644 --- a/packages/web-platform/web-elements/package.json +++ b/packages/web-platform/web-elements/package.json @@ -140,6 +140,7 @@ "test:update": "playwright test --ui --update-snapshots" }, "dependencies": { + "dompurify": "^3.0.8", "markdown-it": "^14.1.0" }, "devDependencies": { diff --git a/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts index 9a1a854650..01a33fc558 100644 --- a/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts +++ b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts @@ -3,6 +3,7 @@ // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. */ +import createDOMPurify from 'dompurify'; import MarkdownIt from 'markdown-it'; import { boostedQueueMicrotask, @@ -14,8 +15,13 @@ import type { XMarkdown } from './XMarkdown.js'; const MarkdownItCtor = (MarkdownIt as unknown as { default?: typeof MarkdownIt }).default ?? MarkdownIt; +const DOMPurifyCtor = + (createDOMPurify as unknown as { default?: typeof createDOMPurify }).default + ?? createDOMPurify; let markdownParser: MarkdownIt | null = null; let markdownParserError: unknown; +let htmlSanitizer: ReturnType | null = null; +let htmlSanitizerError: unknown; const getMarkdownParser = () => { if (markdownParser || markdownParserError) return markdownParser; try { @@ -29,6 +35,22 @@ const getMarkdownParser = () => { return markdownParser; }; +const getHtmlSanitizer = () => { + if (htmlSanitizer || htmlSanitizerError) return htmlSanitizer; + try { + htmlSanitizer = DOMPurifyCtor(window); + } catch (error) { + htmlSanitizerError = error; + } + return htmlSanitizer; +}; + +const sanitizeHtml = (value: string) => { + const sanitizer = getHtmlSanitizer(); + if (!sanitizer) return value; + return sanitizer.sanitize(value, { USE_PROFILES: { html: true } }) as string; +}; + const unitlessCssProperties = new Set([ 'font-weight', 'line-height', @@ -236,7 +258,8 @@ export class XMarkdownAttributes { this.#appendIncrementally(root); return; } - root.innerHTML = parser.render(this.#content); + const rendered = parser.render(this.#content); + root.innerHTML = sanitizeHtml(rendered); this.#renderedContent = this.#content; this.#appendRemainder = ''; this.#clearAppendFlushTimer(); @@ -306,7 +329,7 @@ export class XMarkdownAttributes { #appendHtml(root: HTMLElement, html: string) { const template = document.createElement('template'); - template.innerHTML = html; + template.innerHTML = sanitizeHtml(html); root.append(template.content); } diff --git a/packages/web-platform/web-elements/src/types/markdown-it.d.ts b/packages/web-platform/web-elements/src/types/markdown-it.d.ts deleted file mode 100644 index ae4a4b412a..0000000000 --- a/packages/web-platform/web-elements/src/types/markdown-it.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'markdown-it'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3bc546272..d4dd81b94f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1259,6 +1259,9 @@ importers: packages/web-platform/web-elements: dependencies: + dompurify: + specifier: ^3.0.8 + version: 3.0.8 markdown-it: specifier: ^14.1.0 version: 14.1.0 @@ -5726,6 +5729,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.0.8: + resolution: {integrity: sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -8289,7 +8295,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qs@6.13.0: @@ -14736,6 +14741,8 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.0.8: {} + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 From 924aca0293672cb772c0b9ceb68cb9785326fe16 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:32:52 +0800 Subject: [PATCH 4/6] + fix build --- .../web-platform/web-elements/package.json | 2 +- pnpm-lock.yaml | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/web-platform/web-elements/package.json b/packages/web-platform/web-elements/package.json index a8d0e2e297..13cd672e04 100644 --- a/packages/web-platform/web-elements/package.json +++ b/packages/web-platform/web-elements/package.json @@ -140,7 +140,7 @@ "test:update": "playwright test --ui --update-snapshots" }, "dependencies": { - "dompurify": "^3.0.8", + "dompurify": "^3.3.1", "markdown-it": "^14.1.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4dd81b94f..b5ae0a4c4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1260,8 +1260,8 @@ importers: packages/web-platform/web-elements: dependencies: dompurify: - specifier: ^3.0.8 - version: 3.0.8 + specifier: ^3.3.1 + version: 3.3.1 markdown-it: specifier: ^14.1.0 version: 14.1.0 @@ -4422,6 +4422,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/uglify-js@3.17.5': resolution: {integrity: sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==} @@ -5729,8 +5732,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.0.8: - resolution: {integrity: sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -13386,6 +13389,9 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/uglify-js@3.17.5': dependencies: source-map: 0.6.1 @@ -14741,7 +14747,9 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.0.8: {} + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 domutils@3.2.2: dependencies: From 8b0165bbd92f260b9249f53ea655859409662016 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:12:43 +0800 Subject: [PATCH 5/6] refactor(x-markdown): simplify html sanitizer initialization by removing error handling --- .../src/elements/XMarkdown/XMarkdownAttributes.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts index 01a33fc558..0071f0eba3 100644 --- a/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts +++ b/packages/web-platform/web-elements/src/elements/XMarkdown/XMarkdownAttributes.ts @@ -21,7 +21,6 @@ const DOMPurifyCtor = let markdownParser: MarkdownIt | null = null; let markdownParserError: unknown; let htmlSanitizer: ReturnType | null = null; -let htmlSanitizerError: unknown; const getMarkdownParser = () => { if (markdownParser || markdownParserError) return markdownParser; try { @@ -36,12 +35,8 @@ const getMarkdownParser = () => { }; const getHtmlSanitizer = () => { - if (htmlSanitizer || htmlSanitizerError) return htmlSanitizer; - try { - htmlSanitizer = DOMPurifyCtor(window); - } catch (error) { - htmlSanitizerError = error; - } + if (htmlSanitizer) return htmlSanitizer; + htmlSanitizer = DOMPurifyCtor(window)!; return htmlSanitizer; }; From d9fb103311bd03ac9601d5a2dd1dff53a8eca1e5 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:49:10 +0800 Subject: [PATCH 6/6] + fix --- .changeset/nasty-lizards-refuse.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/nasty-lizards-refuse.md diff --git a/.changeset/nasty-lizards-refuse.md b/.changeset/nasty-lizards-refuse.md new file mode 100644 index 0000000000..0c340a2142 --- /dev/null +++ b/.changeset/nasty-lizards-refuse.md @@ -0,0 +1,11 @@ +--- +"@lynx-js/web-elements": patch +--- + +feat: add x-markdown support + +The x-markdonw is under opt-in importing pattern. + +```javascript +import '@lynx-js/web-elements/XMarkdown'; +```