From 539f70e3a284944f07fca0d705d0f6eaf14373a7 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 10:23:08 -0800 Subject: [PATCH 01/39] Make the default markdown renderer noop --- .../lit/src/0.8/ui/directives/directives.ts | 2 +- .../lit/src/0.8/ui/directives/markdown.ts | 152 ------------------ .../src/0.8/ui/directives/noop_markdown.ts | 32 ++++ renderers/lit/src/0.8/ui/text.ts | 12 +- renderers/lit/src/0.8/ui/utils/markdown.ts | 32 ++++ 5 files changed, 72 insertions(+), 158 deletions(-) delete mode 100644 renderers/lit/src/0.8/ui/directives/markdown.ts create mode 100644 renderers/lit/src/0.8/ui/directives/noop_markdown.ts create mode 100644 renderers/lit/src/0.8/ui/utils/markdown.ts diff --git a/renderers/lit/src/0.8/ui/directives/directives.ts b/renderers/lit/src/0.8/ui/directives/directives.ts index 3c838da9e..e81f614d6 100644 --- a/renderers/lit/src/0.8/ui/directives/directives.ts +++ b/renderers/lit/src/0.8/ui/directives/directives.ts @@ -14,4 +14,4 @@ limitations under the License. */ -export { markdown } from "./markdown.js"; +export { noopMarkdown } from "./noop_markdown.js"; diff --git a/renderers/lit/src/0.8/ui/directives/markdown.ts b/renderers/lit/src/0.8/ui/directives/markdown.ts deleted file mode 100644 index faf1b56a8..000000000 --- a/renderers/lit/src/0.8/ui/directives/markdown.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - Copyright 2025 Google LLC - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import { noChange } from "lit"; -import { - Directive, - DirectiveParameters, - Part, - directive, -} from "lit/directive.js"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import MarkdownIt from "markdown-it"; -import { RenderRule } from "markdown-it/lib/renderer.mjs"; -import * as Sanitizer from "./sanitizer.js"; - -class MarkdownDirective extends Directive { - #markdownIt = MarkdownIt({ - highlight: (str, lang) => { - switch (lang) { - case "html": { - const iframe = document.createElement("iframe"); - iframe.classList.add("html-view"); - iframe.srcdoc = str; - iframe.sandbox = ""; - return iframe.innerHTML; - } - - default: - return Sanitizer.escapeNodeText(str); - } - }, - }); - #lastValue: string | null = null; - #lastTagClassMap: string | null = null; - - update(_part: Part, [value, tagClassMap]: DirectiveParameters) { - if ( - this.#lastValue === value && - JSON.stringify(tagClassMap) === this.#lastTagClassMap - ) { - return noChange; - } - - this.#lastValue = value; - this.#lastTagClassMap = JSON.stringify(tagClassMap); - return this.render(value, tagClassMap); - } - - #originalClassMap = new Map(); - #applyTagClassMap(tagClassMap: Record) { - Object.entries(tagClassMap).forEach(([tag]) => { - let tokenName; - switch (tag) { - case "p": - tokenName = "paragraph"; - break; - case "h1": - case "h2": - case "h3": - case "h4": - case "h5": - case "h6": - tokenName = "heading"; - break; - case "ul": - tokenName = "bullet_list"; - break; - case "ol": - tokenName = "ordered_list"; - break; - case "li": - tokenName = "list_item"; - break; - case "a": - tokenName = "link"; - break; - case "strong": - tokenName = "strong"; - break; - case "em": - tokenName = "em"; - break; - } - - if (!tokenName) { - return; - } - - const key = `${tokenName}_open`; - this.#markdownIt.renderer.rules[key] = ( - tokens, - idx, - options, - _env, - self - ) => { - const token = tokens[idx]; - const tokenClasses = tagClassMap[token.tag] ?? []; - for (const clazz of tokenClasses) { - token.attrJoin("class", clazz); - } - - return self.renderToken(tokens, idx, options); - }; - }); - } - - #unapplyTagClassMap() { - for (const [key] of this.#originalClassMap) { - delete this.#markdownIt.renderer.rules[key]; - } - - this.#originalClassMap.clear(); - } - - /** - * Renders the markdown string to HTML using MarkdownIt. - * - * Note: MarkdownIt doesn't enable HTML in its output, so we render the - * value directly without further sanitization. - * @see https://github.com/markdown-it/markdown-it/blob/master/docs/security.md - */ - render(value: string, tagClassMap?: Record) { - if (tagClassMap) { - this.#applyTagClassMap(tagClassMap); - } - const htmlString = this.#markdownIt.render(value); - this.#unapplyTagClassMap(); - - return unsafeHTML(htmlString); - } -} - -export const markdown = directive(MarkdownDirective); - -const markdownItStandalone = MarkdownIt(); -export function renderMarkdownToHtmlString(value: string): string { - return markdownItStandalone.render(value); -} diff --git a/renderers/lit/src/0.8/ui/directives/noop_markdown.ts b/renderers/lit/src/0.8/ui/directives/noop_markdown.ts new file mode 100644 index 000000000..da145fd83 --- /dev/null +++ b/renderers/lit/src/0.8/ui/directives/noop_markdown.ts @@ -0,0 +1,32 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { html, TemplateResult } from "lit"; +import { MarkdownRenderer } from "../utils/markdown.js"; + +/** + * "Handles" Markdown rendering by doing nothing. + * + * Configure @a2ui/lit-markdown, or your custom Markdown renderer + * to actually parse and render Markdown in your app. + */ +class NoopMarkdownRenderer implements MarkdownRenderer { + render(markdown: string) : TemplateResult { + return html`
${markdown}
`; + } +} + +export const noopMarkdown = new NoopMarkdownRenderer(); diff --git a/renderers/lit/src/0.8/ui/text.ts b/renderers/lit/src/0.8/ui/text.ts index 82ef4aea0..5c8a39296 100644 --- a/renderers/lit/src/0.8/ui/text.ts +++ b/renderers/lit/src/0.8/ui/text.ts @@ -16,7 +16,9 @@ import { html, css, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { markdown } from "./directives/directives.js"; +import { consume } from '@lit/context'; +import { noopMarkdown } from "./directives/noop_markdown.js"; +import { markdownContext, MarkdownRenderer } from "./utils/markdown.js"; import { Root } from "./root.js"; import { A2uiMessageProcessor } from "@a2ui/web_core/data/model-processor"; import * as Primitives from "@a2ui/web_core/types/primitives"; @@ -44,6 +46,9 @@ export class Text extends Root { @property({ reflect: true, attribute: "usage-hint" }) accessor usageHint: Types.ResolvedText["usageHint"] | null = null; + @consume({context: markdownContext}) + accessor markdownRenderer: MarkdownRenderer = noopMarkdown; + static styles = [ structuralStyles, css` @@ -116,10 +121,7 @@ export class Text extends Root { break; // Body. } - return html`${markdown( - markdownText, - Styles.appendToAll(this.theme.markdown, ["ol", "ul", "li"], {}) - )}`; + return html`${this.markdownRenderer?.render(markdownText)}`; } #areHintedStyles(styles: unknown): styles is HintedStyles { diff --git a/renderers/lit/src/0.8/ui/utils/markdown.ts b/renderers/lit/src/0.8/ui/utils/markdown.ts new file mode 100644 index 000000000..73a26d90b --- /dev/null +++ b/renderers/lit/src/0.8/ui/utils/markdown.ts @@ -0,0 +1,32 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { TemplateResult } from "lit"; +import { createContext } from "@lit/context"; + +/** + * The interface for the markdown renderer that can be injected into the + * Lit context. + */ +export interface MarkdownRenderer { + render(markdown: string) : TemplateResult; +} + +/** + * A Lit Context to override the default (noop) markdown renderer. + */ +export const markdownContext = createContext( + Symbol("a2ui-lit-markdown-renderer") +); From 818a8072cd6872f3262e2f2c60d2253fb69f1958 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 10:26:13 -0800 Subject: [PATCH 02/39] Introduce lit-markdown-it package --- renderers/lit-markdown-it/.npmrc | 2 + renderers/lit-markdown-it/README.md | 9 + renderers/lit-markdown-it/package-lock.json | 683 ++++++++++++++++++ renderers/lit-markdown-it/package.json | 43 ++ renderers/lit-markdown-it/prepare-publish.mjs | 71 ++ renderers/lit-markdown-it/src/markdown.ts | 158 ++++ renderers/lit-markdown-it/src/sanitizer.ts | 40 + renderers/lit-markdown-it/tsconfig.json | 35 + 8 files changed, 1041 insertions(+) create mode 100644 renderers/lit-markdown-it/.npmrc create mode 100644 renderers/lit-markdown-it/README.md create mode 100644 renderers/lit-markdown-it/package-lock.json create mode 100644 renderers/lit-markdown-it/package.json create mode 100644 renderers/lit-markdown-it/prepare-publish.mjs create mode 100644 renderers/lit-markdown-it/src/markdown.ts create mode 100644 renderers/lit-markdown-it/src/sanitizer.ts create mode 100644 renderers/lit-markdown-it/tsconfig.json diff --git a/renderers/lit-markdown-it/.npmrc b/renderers/lit-markdown-it/.npmrc new file mode 100644 index 000000000..06b0eef7e --- /dev/null +++ b/renderers/lit-markdown-it/.npmrc @@ -0,0 +1,2 @@ +@a2ui:registry=https://us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/ +//us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/:always-auth=true diff --git a/renderers/lit-markdown-it/README.md b/renderers/lit-markdown-it/README.md new file mode 100644 index 000000000..2e908410d --- /dev/null +++ b/renderers/lit-markdown-it/README.md @@ -0,0 +1,9 @@ +Lit implementation of A2UI. + +Important: The sample code provided is for demonstration purposes and illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity. + +All operational data received from an external agent—including its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide crafted data in its fields (e.g., name, skills.description) that, if used without sanitization to construct prompts for a Large Language Model (LLM), could expose your application to prompt injection attacks. + +Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites. + +Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users. \ No newline at end of file diff --git a/renderers/lit-markdown-it/package-lock.json b/renderers/lit-markdown-it/package-lock.json new file mode 100644 index 000000000..eda19056b --- /dev/null +++ b/renderers/lit-markdown-it/package-lock.json @@ -0,0 +1,683 @@ +{ + "name": "@a2ui/lit-markdown", + "version": "0.8.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@a2ui/lit-markdown", + "version": "0.8.1", + "license": "Apache-2.0", + "dependencies": { + "@a2ui/web_core": "file:../web_core", + "@lit-labs/signals": "^0.1.3", + "@lit/context": "^1.1.4", + "lit": "^3.3.1", + "markdown-it": "^14.1.0", + "signal-utils": "^0.21.1" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/node": "^24.10.1", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, + "../web_core": { + "name": "@a2ui/web_core", + "version": "0.8.0", + "license": "Apache-2.0", + "devDependencies": { + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, + "node_modules/@a2ui/web_core": { + "resolved": "../web_core", + "link": true + }, + "node_modules/@lit-labs/signals": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@lit-labs/signals/-/signals-0.1.3.tgz", + "integrity": "sha512-P0yWgH5blwVyEwBg+WFspLzeu1i0ypJP1QB0l1Omr9qZLIPsUu0p4Fy2jshOg7oQyha5n163K3GJGeUhQQ682Q==", + "license": "BSD-3-Clause", + "dependencies": { + "lit": "^2.0.0 || ^3.0.0", + "signal-polyfill": "^0.2.0" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/context": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.6.tgz", + "integrity": "sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.6.2 || ^2.1.0" + } + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", + "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.1.tgz", + "integrity": "sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^3.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lit": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/signal-polyfill/-/signal-polyfill-0.2.2.tgz", + "integrity": "sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==", + "license": "Apache-2.0" + }, + "node_modules/signal-utils": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/signal-utils/-/signal-utils-0.21.1.tgz", + "integrity": "sha512-i9cdLSvVH4j8ql8mz2lyrA93xL499P8wEbIev3ldSriXeUwqh+wM4Q5VPhIZ19gPtIS4BOopJuKB8l1+wH9LCg==", + "license": "MIT", + "peerDependencies": { + "signal-polyfill": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/wireit": { + "version": "0.15.0-pre.2", + "resolved": "https://registry.npmjs.org/wireit/-/wireit-0.15.0-pre.2.tgz", + "integrity": "sha512-pXOTR56btrL7STFOPQgtq8MjAFWagSqs188E2FflCgcxk5uc0Xbn8CuLIR9FbqK97U3Jw6AK8zDEu/M/9ENqgA==", + "dev": true, + "license": "Apache-2.0", + "workspaces": [ + "vscode-extension", + "website" + ], + "dependencies": { + "brace-expansion": "^4.0.0", + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "jsonc-parser": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "bin": { + "wireit": "bin/wireit.js" + }, + "engines": { + "node": ">=18.0.0" + } + } + } +} diff --git a/renderers/lit-markdown-it/package.json b/renderers/lit-markdown-it/package.json new file mode 100644 index 000000000..e1d860619 --- /dev/null +++ b/renderers/lit-markdown-it/package.json @@ -0,0 +1,43 @@ +{ + "name": "@a2ui/lit-markdown-it", + "version": "0.8.1", + "description": "A2UI Lit Markdown Renderer using markdown-it", + "main": "./dist/markdown.js", + "types": "./dist/markdown.d.ts", + "exports": { + ".": { + "types": "./dist/src/markdown.d.ts", + "default": "./dist/src/markdown.js" + } + }, + "type": "module", + "repository": { + "directory": "renderers/lit-markdown-it", + "type": "git", + "url": "git+https://github.com/google/A2UI.git" + }, + "files": [ + "dist/src" + ], + "keywords": [], + "author": "Google", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/google/A2UI/issues" + }, + "homepage": "https://github.com/google/A2UI/tree/main/web#readme", + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/node": "^24.10.1", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + }, + "dependencies": { + "@lit-labs/signals": "^0.1.3", + "@lit/context": "^1.1.4", + "lit": "^3.3.1", + "markdown-it": "^14.1.0", + "signal-utils": "^0.21.1", + "@a2ui/web_core": "file:../web_core" + } +} diff --git a/renderers/lit-markdown-it/prepare-publish.mjs b/renderers/lit-markdown-it/prepare-publish.mjs new file mode 100644 index 000000000..dc3941989 --- /dev/null +++ b/renderers/lit-markdown-it/prepare-publish.mjs @@ -0,0 +1,71 @@ +import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'fs'; +import { join } from 'path'; + +// This script prepares the Lit package for publishing by: +// 1. Copying package.json to dist/ +// 2. Updating @a2ui/web_core dependency from 'file:...' to the actual version +// 3. Adjusting paths in package.json (main, types, exports) to be relative to dist/ + +const dirname = import.meta.dirname; +const corePkgPath = join(dirname, '../core/package.json'); +const litPkgPath = join(dirname, './package.json'); +const distDir = join(dirname, './dist'); + +if (!existsSync(distDir)) { + mkdirSync(distDir, { recursive: true }); +} + +// 1. Get Core Version +const corePkg = JSON.parse(readFileSync(corePkgPath, 'utf8')); +const coreVersion = corePkg.version; +if (!coreVersion) throw new Error('Cannot determine @a2ui/web_core version'); + +// 2. Read Lit Package +const litPkg = JSON.parse(readFileSync(litPkgPath, 'utf8')); + +// 3. Update Dependency +if (litPkg.dependencies && litPkg.dependencies['@a2ui/web_core']) { + litPkg.dependencies['@a2ui/web_core'] = '^' + coreVersion; +} else { + console.warn('Warning: @a2ui/web_core not found in dependencies.'); +} + +// 4. Adjust Paths for Dist +litPkg.main = adjustPath(litPkg.main); +litPkg.types = adjustPath(litPkg.types); + +if (litPkg.exports) { + for (const key in litPkg.exports) { + const exp = litPkg.exports[key]; + if (typeof exp === 'string') { + litPkg.exports[key] = adjustPath(exp); + } else { + if (exp.types) exp.types = adjustPath(exp.types); + if (exp.default) exp.default = adjustPath(exp.default); + if (exp.import) exp.import = adjustPath(exp.import); + if (exp.require) exp.require = adjustPath(exp.require); + } + } +} + +// 5. Write to dist/package.json +writeFileSync(join(distDir, 'package.json'), JSON.stringify(litPkg, null, 2)); + +// 6. Copy README and LICENSE +['README.md', 'LICENSE'].forEach(file => { + const src = join(dirname, file); + if (!existsSync(src)) { + throw new Error(`Missing required file for publishing: ${file}`); + } + copyFileSync(src, join(distDir, file)); +}); + +console.log(`Prepared dist/package.json with @a2ui/web_core@${coreVersion}`); + +// Utility function to adjustthe paths of the built files (dist/src/*) to (src/*) +function adjustPath(p) { + if (p && p.startsWith('./dist/')) { + return './' + p.substring(7); // Remove ./dist/ + } + return p; +} \ No newline at end of file diff --git a/renderers/lit-markdown-it/src/markdown.ts b/renderers/lit-markdown-it/src/markdown.ts new file mode 100644 index 000000000..6d8fc28c1 --- /dev/null +++ b/renderers/lit-markdown-it/src/markdown.ts @@ -0,0 +1,158 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { noChange } from "lit"; +import { + Directive, + DirectiveParameters, + Part, + directive, +} from "lit/directive.js"; +import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import MarkdownIt from "markdown-it/index.js"; +import { RenderRule } from "markdown-it/lib/renderer.mjs"; +import * as Sanitizer from "./sanitizer.js"; + +/** + * A Lit directive that renders markdown to HTML. + * + * This directive is intended to be used by the A2UI Lit renderer to render + * markdown to HTML. + */ +class MarkdownDirective extends Directive { + private markdownIt = MarkdownIt({ + highlight: (str, lang) => { + switch (lang) { + case "html": { + const iframe = document.createElement("iframe"); + iframe.classList.add("html-view"); + iframe.srcdoc = str; + iframe.sandbox = ""; + return iframe.innerHTML; + } + + default: + return Sanitizer.escapeNodeText(str); + } + }, + }); + private lastValue: string | null = null; + private lastTagClassMap: string | null = null; + + update(_part: Part, [value, tagClassMap]: DirectiveParameters) { + if ( + this.lastValue === value && + JSON.stringify(tagClassMap) === this.lastTagClassMap + ) { + return noChange; + } + + this.lastValue = value; + this.lastTagClassMap = JSON.stringify(tagClassMap); + return this.render(value, tagClassMap); + } + + private originalClassMap = new Map(); + private applyTagClassMap(tagClassMap: Record) { + Object.entries(tagClassMap).forEach(([tag]) => { + let tokenName; + switch (tag) { + case "p": + tokenName = "paragraph"; + break; + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + tokenName = "heading"; + break; + case "ul": + tokenName = "bullet_list"; + break; + case "ol": + tokenName = "ordered_list"; + break; + case "li": + tokenName = "list_item"; + break; + case "a": + tokenName = "link"; + break; + case "strong": + tokenName = "strong"; + break; + case "em": + tokenName = "em"; + break; + } + + if (!tokenName) { + return; + } + + const key = `${tokenName}_open`; + this.markdownIt.renderer.rules[key] = ( + tokens, + idx, + options, + _env, + self + ) => { + const token = tokens[idx]; + const tokenClasses = tagClassMap[token.tag] ?? []; + for (const clazz of tokenClasses) { + token.attrJoin("class", clazz); + } + + return self.renderToken(tokens, idx, options); + }; + }); + } + + private unapplyTagClassMap() { + for (const [key] of this.originalClassMap) { + delete this.markdownIt.renderer.rules[key]; + } + + this.originalClassMap.clear(); + } + + /** + * Renders the markdown string to HTML using MarkdownIt. + * + * Note: MarkdownIt doesn't enable HTML in its output, so we render the + * value directly without further sanitization. + * @see https://github.com/markdown-it/markdown-it/blob/master/docs/security.md + */ + render(value: string, tagClassMap?: Record) { + if (tagClassMap) { + this.applyTagClassMap(tagClassMap); + } + const htmlString = this.markdownIt.render(value); + this.unapplyTagClassMap(); + + return unsafeHTML(htmlString); + } +} + +export const markdown = directive(MarkdownDirective); + +const markdownItStandalone = MarkdownIt(); +export function renderMarkdownToHtmlString(value: string): string { + return markdownItStandalone.render(value); +} diff --git a/renderers/lit-markdown-it/src/sanitizer.ts b/renderers/lit-markdown-it/src/sanitizer.ts new file mode 100644 index 000000000..f9b0bc116 --- /dev/null +++ b/renderers/lit-markdown-it/src/sanitizer.ts @@ -0,0 +1,40 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { html, render } from "lit"; + +/** + * This is only safe for (and intended to be used for) text node positions. If + * you are using attribute position, then this is only safe if the attribute + * value is surrounded by double-quotes, and is unsafe otherwise (because the + * value could break out of the attribute value and e.g. add another attribute). + */ +export function escapeNodeText(str: string | null | undefined) { + const frag = document.createElement("div"); + render(html`${str}`, frag); + + return frag.innerHTML.replaceAll(//gim, ""); +} + +export function unescapeNodeText(str: string | null | undefined) { + if (!str) { + return ""; + } + + const frag = document.createElement("textarea"); + frag.innerHTML = str; + return frag.value; +} diff --git a/renderers/lit-markdown-it/tsconfig.json b/renderers/lit-markdown-it/tsconfig.json new file mode 100644 index 000000000..f928e3840 --- /dev/null +++ b/renderers/lit-markdown-it/tsconfig.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "incremental": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + // "allowJs": true, + "preserveWatchOutput": true, + "sourceMap": true, + "target": "es2022", + "module": "esnext", + "lib": ["es2023", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "useDefineForClassFields": false, + "rootDir": ".", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo", + + /* Bundler mode */ + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.json"] +} From 4d6490bb3b7bef71625bf3adb6d875eec185cb75 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 14:50:33 -0800 Subject: [PATCH 03/39] Add shared markdown renderer for all JS web renderers. --- renderers/markdown/README.md | 10 + renderers/markdown/markdown-it-shared/.npmrc | 2 + .../markdown/markdown-it-shared/README.md | 6 + .../markdown-it-shared/package-lock.json | 589 ++++++++++++++++++ .../markdown/markdown-it-shared/package.json | 63 ++ .../markdown-it-shared/prepare-publish.mjs | 73 +++ .../markdown-it-shared/src/markdown.ts | 38 ++ .../markdown-it-shared/src/raw-markdown.ts | 137 ++++ .../markdown-it-shared/src/sanitizer.ts | 24 + .../markdown/markdown-it-shared/tsconfig.json | 35 ++ 10 files changed, 977 insertions(+) create mode 100644 renderers/markdown/README.md create mode 100644 renderers/markdown/markdown-it-shared/.npmrc create mode 100644 renderers/markdown/markdown-it-shared/README.md create mode 100644 renderers/markdown/markdown-it-shared/package-lock.json create mode 100644 renderers/markdown/markdown-it-shared/package.json create mode 100644 renderers/markdown/markdown-it-shared/prepare-publish.mjs create mode 100644 renderers/markdown/markdown-it-shared/src/markdown.ts create mode 100644 renderers/markdown/markdown-it-shared/src/raw-markdown.ts create mode 100644 renderers/markdown/markdown-it-shared/src/sanitizer.ts create mode 100644 renderers/markdown/markdown-it-shared/tsconfig.json diff --git a/renderers/markdown/README.md b/renderers/markdown/README.md new file mode 100644 index 000000000..5e26efd3d --- /dev/null +++ b/renderers/markdown/README.md @@ -0,0 +1,10 @@ +This directory contains the default markdown implementations for A2UI. + +* `markdown-it-shared` is a shared markdown renderer that uses markdown-it and + DOMPurify to render markdown content in general. +* `markdown-it-lit` is the Lit facade of the markdown renderer for Lit. +* `markdown-it-angular` is the Angular facade of the markdown renderer + for Angular. + +Users should use the facade packages in their apps. There's nothing of interest +in the shared renderer package. diff --git a/renderers/markdown/markdown-it-shared/.npmrc b/renderers/markdown/markdown-it-shared/.npmrc new file mode 100644 index 000000000..06b0eef7e --- /dev/null +++ b/renderers/markdown/markdown-it-shared/.npmrc @@ -0,0 +1,2 @@ +@a2ui:registry=https://us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/ +//us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/:always-auth=true diff --git a/renderers/markdown/markdown-it-shared/README.md b/renderers/markdown/markdown-it-shared/README.md new file mode 100644 index 000000000..0c3252526 --- /dev/null +++ b/renderers/markdown/markdown-it-shared/README.md @@ -0,0 +1,6 @@ +Markdown renderer for A2UI using markdown-it and dompurify. + +This is used across all JS renderers, so the configuration is consistent. Each +renderer has a specific facade package that uses this renderer as a dependency. + +End users should use the facade package for their renderer of choice. diff --git a/renderers/markdown/markdown-it-shared/package-lock.json b/renderers/markdown/markdown-it-shared/package-lock.json new file mode 100644 index 000000000..085d5f884 --- /dev/null +++ b/renderers/markdown/markdown-it-shared/package-lock.json @@ -0,0 +1,589 @@ +{ + "name": "@a2ui/markdown-it-core", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@a2ui/markdown-it-core", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "markdown-it": "^14.1.0" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/node": "^24.10.1", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, + "../web_core": { + "name": "@a2ui/web_core", + "version": "0.8.0", + "extraneous": true, + "license": "Apache-2.0", + "devDependencies": { + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", + "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.1.tgz", + "integrity": "sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^3.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/wireit": { + "version": "0.15.0-pre.2", + "resolved": "https://registry.npmjs.org/wireit/-/wireit-0.15.0-pre.2.tgz", + "integrity": "sha512-pXOTR56btrL7STFOPQgtq8MjAFWagSqs188E2FflCgcxk5uc0Xbn8CuLIR9FbqK97U3Jw6AK8zDEu/M/9ENqgA==", + "dev": true, + "license": "Apache-2.0", + "workspaces": [ + "vscode-extension", + "website" + ], + "dependencies": { + "brace-expansion": "^4.0.0", + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "jsonc-parser": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "bin": { + "wireit": "bin/wireit.js" + }, + "engines": { + "node": ">=18.0.0" + } + } + } +} diff --git a/renderers/markdown/markdown-it-shared/package.json b/renderers/markdown/markdown-it-shared/package.json new file mode 100644 index 000000000..3d58a0bd4 --- /dev/null +++ b/renderers/markdown/markdown-it-shared/package.json @@ -0,0 +1,63 @@ +{ + "name": "@a2ui/markdown-it-shared", + "version": "0.0.1", + "description": "A Markdown renderer using markdown-it and dompurify.", + "main": "./dist/src/markdown.js", + "types": "./dist/src/markdown.d.ts", + "exports": { + ".": { + "types": "./dist/src/markdown.d.ts", + "default": "./dist/src/markdown.js" + } + }, + "type": "module", + "repository": { + "directory": "renderers/markdown/markdown-it-shared", + "type": "git", + "url": "git+https://github.com/google/A2UI.git" + }, + "files": [ + "dist/src" + ], + "scripts": { + "prepack": "npm run build", + "build": "wireit", + "build:tsc": "wireit" + }, + "wireit": { + "build": { + "dependencies": [ + "build:tsc" + ] + }, + "build:tsc": { + "command": "tsc -b --pretty", + "files": [ + "src/**/*.ts", + "src/**/*.json", + "tsconfig.json" + ], + "output": [ + "dist/", + "!dist/**/*.min.js{,.map}" + ], + "clean": "if-file-deleted" + } + }, + "keywords": [], + "author": "Google", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/google/A2UI/issues" + }, + "homepage": "https://github.com/google/A2UI/tree/main/web#readme", + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/node": "^24.10.1", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + }, + "dependencies": { + "markdown-it": "^14.1.0" + } +} diff --git a/renderers/markdown/markdown-it-shared/prepare-publish.mjs b/renderers/markdown/markdown-it-shared/prepare-publish.mjs new file mode 100644 index 000000000..73185139a --- /dev/null +++ b/renderers/markdown/markdown-it-shared/prepare-publish.mjs @@ -0,0 +1,73 @@ +import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'fs'; +import { join } from 'path'; + +// TODO: Review this script, it's copied wholesale from the lit package. + +// This script prepares the Lit package for publishing by: +// 1. Copying package.json to dist/ +// 2. Updating @a2ui/web_core dependency from 'file:...' to the actual version +// 3. Adjusting paths in package.json (main, types, exports) to be relative to dist/ + +const dirname = import.meta.dirname; +const corePkgPath = join(dirname, '../core/package.json'); +const litPkgPath = join(dirname, './package.json'); +const distDir = join(dirname, './dist'); + +if (!existsSync(distDir)) { + mkdirSync(distDir, { recursive: true }); +} + +// 1. Get Core Version +const corePkg = JSON.parse(readFileSync(corePkgPath, 'utf8')); +const coreVersion = corePkg.version; +if (!coreVersion) throw new Error('Cannot determine @a2ui/web_core version'); + +// 2. Read Lit Package +const litPkg = JSON.parse(readFileSync(litPkgPath, 'utf8')); + +// 3. Update Dependency +if (litPkg.dependencies && litPkg.dependencies['@a2ui/web_core']) { + litPkg.dependencies['@a2ui/web_core'] = '^' + coreVersion; +} else { + console.warn('Warning: @a2ui/web_core not found in dependencies.'); +} + +// 4. Adjust Paths for Dist +litPkg.main = adjustPath(litPkg.main); +litPkg.types = adjustPath(litPkg.types); + +if (litPkg.exports) { + for (const key in litPkg.exports) { + const exp = litPkg.exports[key]; + if (typeof exp === 'string') { + litPkg.exports[key] = adjustPath(exp); + } else { + if (exp.types) exp.types = adjustPath(exp.types); + if (exp.default) exp.default = adjustPath(exp.default); + if (exp.import) exp.import = adjustPath(exp.import); + if (exp.require) exp.require = adjustPath(exp.require); + } + } +} + +// 5. Write to dist/package.json +writeFileSync(join(distDir, 'package.json'), JSON.stringify(litPkg, null, 2)); + +// 6. Copy README and LICENSE +['README.md', 'LICENSE'].forEach(file => { + const src = join(dirname, file); + if (!existsSync(src)) { + throw new Error(`Missing required file for publishing: ${file}`); + } + copyFileSync(src, join(distDir, file)); +}); + +console.log(`Prepared dist/package.json with @a2ui/web_core@${coreVersion}`); + +// Utility function to adjustthe paths of the built files (dist/src/*) to (src/*) +function adjustPath(p) { + if (p && p.startsWith('./dist/')) { + return './' + p.substring(7); // Remove ./dist/ + } + return p; +} \ No newline at end of file diff --git a/renderers/markdown/markdown-it-shared/src/markdown.ts b/renderers/markdown/markdown-it-shared/src/markdown.ts new file mode 100644 index 000000000..d1a5dac23 --- /dev/null +++ b/renderers/markdown/markdown-it-shared/src/markdown.ts @@ -0,0 +1,38 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { rawMarkdownRenderer, TagClassMap } from "./raw-markdown.js"; +import { sanitizer } from "./sanitizer.js"; + +// TODO: Do we need to export the TagClassMap type? + +/** + * A Markdown renderer using markdown-it and dompurify. + */ +export const markdownRenderer = { + /** + * Renders markdown to HTML. + * @param value The markdown code to render. + * @param tagClassMap A map of tag names to classes. + * @returns The rendered HTML as a string. + */ + render: (value: string, tagClassMap?: TagClassMap) => { + const htmlString = rawMarkdownRenderer.render(value, tagClassMap); + return sanitizer.sanitize(htmlString); + }, + + // TODO: Do we need an unsanitized renderer? +}; diff --git a/renderers/markdown/markdown-it-shared/src/raw-markdown.ts b/renderers/markdown/markdown-it-shared/src/raw-markdown.ts new file mode 100644 index 000000000..7a328de2a --- /dev/null +++ b/renderers/markdown/markdown-it-shared/src/raw-markdown.ts @@ -0,0 +1,137 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import MarkdownIt from "markdown-it/index.js"; +import { sanitizer } from "./sanitizer"; + +/** + * A map of tag names to classes to apply when rendering a tag. + * + * For example, the following TagClassMap would apply the `a2ui-paragraph` class + * to all `

` tags: + * + * `{ "p": ["a2ui-paragraph"] }` + */ +export type TagClassMap = Record; + +/** + * A pre-configured instance of markdown-it to render markdown in A2UI web. + * + * This renderer does not perform any sanitization of the outgoing HTML. + */ +class MarkdownItCore { + private markdownIt = MarkdownIt({ + highlight: (str, lang) => { + switch (lang) { + case "html": { + const iframe = document.createElement("iframe"); + iframe.classList.add("html-view"); + iframe.srcdoc = str; + iframe.sandbox = ""; + return iframe.innerHTML; + } + + default: + return sanitizer.sanitize(str); + } + }, + }); + + /** + * Applies a tag class map to the markdown-it renderer. + * + * @param tagClassMap The tag class map to apply. + */ + private applyTagClassMap(tagClassMap: TagClassMap) { + Object.entries(tagClassMap).forEach(([tag]) => { + let tokenName; + switch (tag) { + case "p": + tokenName = "paragraph"; + break; + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + tokenName = "heading"; + break; + case "ul": + tokenName = "bullet_list"; + break; + case "ol": + tokenName = "ordered_list"; + break; + case "li": + tokenName = "list_item"; + break; + case "a": + tokenName = "link"; + break; + case "strong": + tokenName = "strong"; + break; + case "em": + tokenName = "em"; + break; + } + + if (!tokenName) { + return; + } + + const key = `${tokenName}_open`; + this.markdownIt.renderer.rules[key] = ( + tokens, + idx, + options, + _env, + self + ) => { + const token = tokens[idx]; + const tokenClasses = tagClassMap[token.tag] ?? []; + for (const clazz of tokenClasses) { + token.attrJoin("class", clazz); + } + + return self.renderToken(tokens, idx, options); + }; + }); + } + + /** + * Renders the markdown string to HTML using the internal MarkdownIt instance. + * + * @param tagClassMap A map of tag names to classes to apply when rendering a tag. + * + * This method does not perform any sanitization of the outgoing HTML. + */ + render(value: string, tagClassMap?: TagClassMap) { + if (tagClassMap) { + this.applyTagClassMap(tagClassMap); + } + const htmlString = this.markdownIt.render(value); + return htmlString; + } +} + +/** + * A pre-configured instance of markdown-it to render markdown in A2UI web. + * + * This renderer does not perform any sanitization of the outgoing HTML. + */ +export const rawMarkdownRenderer = new MarkdownItCore(); diff --git a/renderers/markdown/markdown-it-shared/src/sanitizer.ts b/renderers/markdown/markdown-it-shared/src/sanitizer.ts new file mode 100644 index 000000000..fcd13bcfe --- /dev/null +++ b/renderers/markdown/markdown-it-shared/src/sanitizer.ts @@ -0,0 +1,24 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +// TODO: Implement the DOMPurify sanitizer. + +/** + * A sanitizer that sanitizes HTML. + */ +export const sanitizer = { + sanitize: (html: string) => html, +} diff --git a/renderers/markdown/markdown-it-shared/tsconfig.json b/renderers/markdown/markdown-it-shared/tsconfig.json new file mode 100644 index 000000000..f928e3840 --- /dev/null +++ b/renderers/markdown/markdown-it-shared/tsconfig.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "incremental": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + // "allowJs": true, + "preserveWatchOutput": true, + "sourceMap": true, + "target": "es2022", + "module": "esnext", + "lib": ["es2023", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "useDefineForClassFields": false, + "rootDir": ".", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo", + + /* Bundler mode */ + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.json"] +} From 13862630ad24b2263b9cabfe09f8da659618ad2d Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 14:51:19 -0800 Subject: [PATCH 04/39] Add the markdown-lit facade package --- renderers/lit-markdown-it/README.md | 9 - renderers/lit-markdown-it/package.json | 43 ----- renderers/lit-markdown-it/src/markdown.ts | 158 ---------------- renderers/lit-markdown-it/src/sanitizer.ts | 40 ---- .../markdown-it-lit}/.npmrc | 0 renderers/markdown/markdown-it-lit/README.md | 1 + .../markdown-it-lit}/package-lock.json | 171 ++++++------------ .../markdown/markdown-it-lit/package.json | 66 +++++++ .../markdown-it-lit}/prepare-publish.mjs | 2 + .../markdown/markdown-it-lit/src/markdown.ts | 59 ++++++ .../markdown-it-lit}/tsconfig.json | 0 11 files changed, 181 insertions(+), 368 deletions(-) delete mode 100644 renderers/lit-markdown-it/README.md delete mode 100644 renderers/lit-markdown-it/package.json delete mode 100644 renderers/lit-markdown-it/src/markdown.ts delete mode 100644 renderers/lit-markdown-it/src/sanitizer.ts rename renderers/{lit-markdown-it => markdown/markdown-it-lit}/.npmrc (100%) create mode 100644 renderers/markdown/markdown-it-lit/README.md rename renderers/{lit-markdown-it => markdown/markdown-it-lit}/package-lock.json (80%) create mode 100644 renderers/markdown/markdown-it-lit/package.json rename renderers/{lit-markdown-it => markdown/markdown-it-lit}/prepare-publish.mjs (96%) create mode 100644 renderers/markdown/markdown-it-lit/src/markdown.ts rename renderers/{lit-markdown-it => markdown/markdown-it-lit}/tsconfig.json (100%) diff --git a/renderers/lit-markdown-it/README.md b/renderers/lit-markdown-it/README.md deleted file mode 100644 index 2e908410d..000000000 --- a/renderers/lit-markdown-it/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Lit implementation of A2UI. - -Important: The sample code provided is for demonstration purposes and illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity. - -All operational data received from an external agent—including its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide crafted data in its fields (e.g., name, skills.description) that, if used without sanitization to construct prompts for a Large Language Model (LLM), could expose your application to prompt injection attacks. - -Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites. - -Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users. \ No newline at end of file diff --git a/renderers/lit-markdown-it/package.json b/renderers/lit-markdown-it/package.json deleted file mode 100644 index e1d860619..000000000 --- a/renderers/lit-markdown-it/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@a2ui/lit-markdown-it", - "version": "0.8.1", - "description": "A2UI Lit Markdown Renderer using markdown-it", - "main": "./dist/markdown.js", - "types": "./dist/markdown.d.ts", - "exports": { - ".": { - "types": "./dist/src/markdown.d.ts", - "default": "./dist/src/markdown.js" - } - }, - "type": "module", - "repository": { - "directory": "renderers/lit-markdown-it", - "type": "git", - "url": "git+https://github.com/google/A2UI.git" - }, - "files": [ - "dist/src" - ], - "keywords": [], - "author": "Google", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/google/A2UI/issues" - }, - "homepage": "https://github.com/google/A2UI/tree/main/web#readme", - "devDependencies": { - "@types/markdown-it": "^14.1.2", - "@types/node": "^24.10.1", - "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2" - }, - "dependencies": { - "@lit-labs/signals": "^0.1.3", - "@lit/context": "^1.1.4", - "lit": "^3.3.1", - "markdown-it": "^14.1.0", - "signal-utils": "^0.21.1", - "@a2ui/web_core": "file:../web_core" - } -} diff --git a/renderers/lit-markdown-it/src/markdown.ts b/renderers/lit-markdown-it/src/markdown.ts deleted file mode 100644 index 6d8fc28c1..000000000 --- a/renderers/lit-markdown-it/src/markdown.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - Copyright 2025 Google LLC - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import { noChange } from "lit"; -import { - Directive, - DirectiveParameters, - Part, - directive, -} from "lit/directive.js"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import MarkdownIt from "markdown-it/index.js"; -import { RenderRule } from "markdown-it/lib/renderer.mjs"; -import * as Sanitizer from "./sanitizer.js"; - -/** - * A Lit directive that renders markdown to HTML. - * - * This directive is intended to be used by the A2UI Lit renderer to render - * markdown to HTML. - */ -class MarkdownDirective extends Directive { - private markdownIt = MarkdownIt({ - highlight: (str, lang) => { - switch (lang) { - case "html": { - const iframe = document.createElement("iframe"); - iframe.classList.add("html-view"); - iframe.srcdoc = str; - iframe.sandbox = ""; - return iframe.innerHTML; - } - - default: - return Sanitizer.escapeNodeText(str); - } - }, - }); - private lastValue: string | null = null; - private lastTagClassMap: string | null = null; - - update(_part: Part, [value, tagClassMap]: DirectiveParameters) { - if ( - this.lastValue === value && - JSON.stringify(tagClassMap) === this.lastTagClassMap - ) { - return noChange; - } - - this.lastValue = value; - this.lastTagClassMap = JSON.stringify(tagClassMap); - return this.render(value, tagClassMap); - } - - private originalClassMap = new Map(); - private applyTagClassMap(tagClassMap: Record) { - Object.entries(tagClassMap).forEach(([tag]) => { - let tokenName; - switch (tag) { - case "p": - tokenName = "paragraph"; - break; - case "h1": - case "h2": - case "h3": - case "h4": - case "h5": - case "h6": - tokenName = "heading"; - break; - case "ul": - tokenName = "bullet_list"; - break; - case "ol": - tokenName = "ordered_list"; - break; - case "li": - tokenName = "list_item"; - break; - case "a": - tokenName = "link"; - break; - case "strong": - tokenName = "strong"; - break; - case "em": - tokenName = "em"; - break; - } - - if (!tokenName) { - return; - } - - const key = `${tokenName}_open`; - this.markdownIt.renderer.rules[key] = ( - tokens, - idx, - options, - _env, - self - ) => { - const token = tokens[idx]; - const tokenClasses = tagClassMap[token.tag] ?? []; - for (const clazz of tokenClasses) { - token.attrJoin("class", clazz); - } - - return self.renderToken(tokens, idx, options); - }; - }); - } - - private unapplyTagClassMap() { - for (const [key] of this.originalClassMap) { - delete this.markdownIt.renderer.rules[key]; - } - - this.originalClassMap.clear(); - } - - /** - * Renders the markdown string to HTML using MarkdownIt. - * - * Note: MarkdownIt doesn't enable HTML in its output, so we render the - * value directly without further sanitization. - * @see https://github.com/markdown-it/markdown-it/blob/master/docs/security.md - */ - render(value: string, tagClassMap?: Record) { - if (tagClassMap) { - this.applyTagClassMap(tagClassMap); - } - const htmlString = this.markdownIt.render(value); - this.unapplyTagClassMap(); - - return unsafeHTML(htmlString); - } -} - -export const markdown = directive(MarkdownDirective); - -const markdownItStandalone = MarkdownIt(); -export function renderMarkdownToHtmlString(value: string): string { - return markdownItStandalone.render(value); -} diff --git a/renderers/lit-markdown-it/src/sanitizer.ts b/renderers/lit-markdown-it/src/sanitizer.ts deleted file mode 100644 index f9b0bc116..000000000 --- a/renderers/lit-markdown-it/src/sanitizer.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - Copyright 2025 Google LLC - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import { html, render } from "lit"; - -/** - * This is only safe for (and intended to be used for) text node positions. If - * you are using attribute position, then this is only safe if the attribute - * value is surrounded by double-quotes, and is unsafe otherwise (because the - * value could break out of the attribute value and e.g. add another attribute). - */ -export function escapeNodeText(str: string | null | undefined) { - const frag = document.createElement("div"); - render(html`${str}`, frag); - - return frag.innerHTML.replaceAll(//gim, ""); -} - -export function unescapeNodeText(str: string | null | undefined) { - if (!str) { - return ""; - } - - const frag = document.createElement("textarea"); - frag.innerHTML = str; - return frag.value; -} diff --git a/renderers/lit-markdown-it/.npmrc b/renderers/markdown/markdown-it-lit/.npmrc similarity index 100% rename from renderers/lit-markdown-it/.npmrc rename to renderers/markdown/markdown-it-lit/.npmrc diff --git a/renderers/markdown/markdown-it-lit/README.md b/renderers/markdown/markdown-it-lit/README.md new file mode 100644 index 000000000..53e88e951 --- /dev/null +++ b/renderers/markdown/markdown-it-lit/README.md @@ -0,0 +1 @@ +A2UI markdown renderer for Lit. diff --git a/renderers/lit-markdown-it/package-lock.json b/renderers/markdown/markdown-it-lit/package-lock.json similarity index 80% rename from renderers/lit-markdown-it/package-lock.json rename to renderers/markdown/markdown-it-lit/package-lock.json index eda19056b..2d1a22bb0 100644 --- a/renderers/lit-markdown-it/package-lock.json +++ b/renderers/markdown/markdown-it-lit/package-lock.json @@ -1,11 +1,28 @@ { - "name": "@a2ui/lit-markdown", + "name": "@a2ui/markdown-it-lit", "version": "0.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@a2ui/lit-markdown", + "name": "@a2ui/markdown-it-lit", + "version": "0.8.1", + "license": "Apache-2.0", + "dependencies": { + "@a2ui/lit": "file:../../lit", + "@a2ui/markdown-it-shared": "file:../markdown-it-shared", + "@a2ui/web_core": "file:../../web_core", + "@lit/context": "^1.1.4", + "lit": "^3.3.1" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, + "../../lit": { + "name": "@a2ui/lit", "version": "0.8.1", "license": "Apache-2.0", "dependencies": { @@ -16,6 +33,30 @@ "markdown-it": "^14.1.0", "signal-utils": "^0.21.1" }, + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/node": "^24.10.1", + "google-artifactregistry-auth": "^3.5.0", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, + "../../web_core": { + "name": "@a2ui/web_core", + "version": "0.8.0", + "license": "Apache-2.0", + "devDependencies": { + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, + "../markdown-it-shared": { + "name": "@a2ui/markdown-it-shared", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "markdown-it": "^14.1.0" + }, "devDependencies": { "@types/markdown-it": "^14.1.2", "@types/node": "^24.10.1", @@ -26,25 +67,24 @@ "../web_core": { "name": "@a2ui/web_core", "version": "0.8.0", + "extraneous": true, "license": "Apache-2.0", "devDependencies": { "typescript": "^5.8.3", "wireit": "^0.15.0-pre.2" } }, - "node_modules/@a2ui/web_core": { - "resolved": "../web_core", + "node_modules/@a2ui/lit": { + "resolved": "../../lit", "link": true }, - "node_modules/@lit-labs/signals": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@lit-labs/signals/-/signals-0.1.3.tgz", - "integrity": "sha512-P0yWgH5blwVyEwBg+WFspLzeu1i0ypJP1QB0l1Omr9qZLIPsUu0p4Fy2jshOg7oQyha5n163K3GJGeUhQQ682Q==", - "license": "BSD-3-Clause", - "dependencies": { - "lit": "^2.0.0 || ^3.0.0", - "signal-polyfill": "^0.2.0" - } + "node_modules/@a2ui/markdown-it-shared": { + "resolved": "../markdown-it-shared", + "link": true + }, + "node_modules/@a2ui/web_core": { + "resolved": "../../web_core", + "link": true }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.5.1", @@ -108,31 +148,6 @@ "node": ">= 8" } }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "24.10.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", @@ -163,12 +178,6 @@ "node": ">= 8" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/balanced-match": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", @@ -243,18 +252,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -383,15 +380,6 @@ "dev": true, "license": "MIT" }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, "node_modules/lit": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", @@ -423,29 +411,6 @@ "@types/trusted-types": "^2.0.2" } }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -505,15 +470,6 @@ "signal-exit": "^3.0.2" } }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -600,21 +556,6 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-polyfill": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/signal-polyfill/-/signal-polyfill-0.2.2.tgz", - "integrity": "sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==", - "license": "Apache-2.0" - }, - "node_modules/signal-utils": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/signal-utils/-/signal-utils-0.21.1.tgz", - "integrity": "sha512-i9cdLSvVH4j8ql8mz2lyrA93xL499P8wEbIev3ldSriXeUwqh+wM4Q5VPhIZ19gPtIS4BOopJuKB8l1+wH9LCg==", - "license": "MIT", - "peerDependencies": { - "signal-polyfill": "^0.2.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -642,12 +583,6 @@ "node": ">=14.17" } }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", diff --git a/renderers/markdown/markdown-it-lit/package.json b/renderers/markdown/markdown-it-lit/package.json new file mode 100644 index 000000000..59cf91d54 --- /dev/null +++ b/renderers/markdown/markdown-it-lit/package.json @@ -0,0 +1,66 @@ +{ + "name": "@a2ui/markdown-it-lit", + "version": "0.8.1", + "description": "A2UI Lit markdown renderer.", + "main": "./dist/src/markdown.js", + "types": "./dist/src/markdown.d.ts", + "exports": { + ".": { + "types": "./dist/src/markdown.d.ts", + "default": "./dist/src/markdown.js" + } + }, + "type": "module", + "repository": { + "directory": "renderers/markdown-it-lit", + "type": "git", + "url": "git+https://github.com/google/A2UI.git" + }, + "files": [ + "dist/src" + ], + "scripts": { + "prepack": "npm run build", + "build": "wireit", + "build:tsc": "wireit" + }, + "wireit": { + "build": { + "dependencies": [ + "build:tsc" + ] + }, + "build:tsc": { + "command": "tsc -b --pretty", + "files": [ + "src/**/*.ts", + "src/**/*.json", + "tsconfig.json" + ], + "output": [ + "dist/", + "!dist/**/*.min.js{,.map}" + ], + "clean": "if-file-deleted" + } + }, + "keywords": [], + "author": "Google", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/google/A2UI/issues" + }, + "homepage": "https://github.com/google/A2UI/tree/main/web#readme", + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + }, + "dependencies": { + "@lit/context": "^1.1.4", + "lit": "^3.3.1", + "@a2ui/lit": "file:../../lit", + "@a2ui/markdown-it-shared": "file:../markdown-it-shared", + "@a2ui/web_core": "file:../../web_core" + } +} diff --git a/renderers/lit-markdown-it/prepare-publish.mjs b/renderers/markdown/markdown-it-lit/prepare-publish.mjs similarity index 96% rename from renderers/lit-markdown-it/prepare-publish.mjs rename to renderers/markdown/markdown-it-lit/prepare-publish.mjs index dc3941989..73185139a 100644 --- a/renderers/lit-markdown-it/prepare-publish.mjs +++ b/renderers/markdown/markdown-it-lit/prepare-publish.mjs @@ -1,6 +1,8 @@ import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'fs'; import { join } from 'path'; +// TODO: Review this script, it's copied wholesale from the lit package. + // This script prepares the Lit package for publishing by: // 1. Copying package.json to dist/ // 2. Updating @a2ui/web_core dependency from 'file:...' to the actual version diff --git a/renderers/markdown/markdown-it-lit/src/markdown.ts b/renderers/markdown/markdown-it-lit/src/markdown.ts new file mode 100644 index 000000000..0341bfef2 --- /dev/null +++ b/renderers/markdown/markdown-it-lit/src/markdown.ts @@ -0,0 +1,59 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { noChange } from "lit"; +import { + Directive, + DirectiveParameters, + Part, + directive, +} from "lit/directive.js"; +import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import { markdownRenderer } from "@a2ui/markdown-it-shared"; + +/** + * A Lit directive that renders markdown to HTML. + * + * This directive is intended to be used by the A2UI Lit renderer to render + * markdown to HTML. + */ +class MarkdownDirective extends Directive { + private lastValue: string | null = null; + private lastTagClassMap: string | null = null; + + update(_part: Part, [value, tagClassMap]: DirectiveParameters) { + if ( + this.lastValue === value && + JSON.stringify(tagClassMap) === this.lastTagClassMap + ) { + return noChange; + } + + this.lastValue = value; + this.lastTagClassMap = JSON.stringify(tagClassMap); + return this.render(value, tagClassMap); + } + + /** + * Renders the markdown string to HTML. + */ + render(value: string, tagClassMap?: Record) { + const htmlString = markdownRenderer.render(value, tagClassMap); + return unsafeHTML(htmlString); + } +} + +export const markdown = directive(MarkdownDirective); diff --git a/renderers/lit-markdown-it/tsconfig.json b/renderers/markdown/markdown-it-lit/tsconfig.json similarity index 100% rename from renderers/lit-markdown-it/tsconfig.json rename to renderers/markdown/markdown-it-lit/tsconfig.json From 362317c6ade49a6281330879062e5914eb70fd7d Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:30:32 -0800 Subject: [PATCH 05/39] Import markdownit from module name, instead of index.js --- renderers/markdown/markdown-it-shared/package-lock.json | 4 ++-- renderers/markdown/markdown-it-shared/src/raw-markdown.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/renderers/markdown/markdown-it-shared/package-lock.json b/renderers/markdown/markdown-it-shared/package-lock.json index 085d5f884..5a52d0bf8 100644 --- a/renderers/markdown/markdown-it-shared/package-lock.json +++ b/renderers/markdown/markdown-it-shared/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@a2ui/markdown-it-core", + "name": "@a2ui/markdown-it-shared", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@a2ui/markdown-it-core", + "name": "@a2ui/markdown-it-shared", "version": "0.0.1", "license": "Apache-2.0", "dependencies": { diff --git a/renderers/markdown/markdown-it-shared/src/raw-markdown.ts b/renderers/markdown/markdown-it-shared/src/raw-markdown.ts index 7a328de2a..a17e5bf07 100644 --- a/renderers/markdown/markdown-it-shared/src/raw-markdown.ts +++ b/renderers/markdown/markdown-it-shared/src/raw-markdown.ts @@ -14,7 +14,7 @@ limitations under the License. */ -import MarkdownIt from "markdown-it/index.js"; +import markdownit from 'markdown-it'; import { sanitizer } from "./sanitizer"; /** @@ -33,7 +33,7 @@ export type TagClassMap = Record; * This renderer does not perform any sanitization of the outgoing HTML. */ class MarkdownItCore { - private markdownIt = MarkdownIt({ + private markdownIt = markdownit({ highlight: (str, lang) => { switch (lang) { case "html": { From 5530802b1e9c9133cdcb9db00c3d4ac990f16dc6 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:32:47 -0800 Subject: [PATCH 06/39] Move the markdown context to the UI library, next to the theme. --- renderers/lit/src/0.8/ui/context/context.ts | 24 ++++++++++++++++++ renderers/lit/src/0.8/ui/context/markdown.ts | 26 ++++++++++++++++++++ renderers/lit/src/0.8/ui/ui.ts | 2 +- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 renderers/lit/src/0.8/ui/context/context.ts create mode 100644 renderers/lit/src/0.8/ui/context/markdown.ts diff --git a/renderers/lit/src/0.8/ui/context/context.ts b/renderers/lit/src/0.8/ui/context/context.ts new file mode 100644 index 000000000..0e5b7acf4 --- /dev/null +++ b/renderers/lit/src/0.8/ui/context/context.ts @@ -0,0 +1,24 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import {markdownContext} from "./markdown.js"; +import {themeContext} from "./theme.js"; + +export { + markdownContext as markdown, + themeContext as theme, + themeContext, // Preserved for backwards compatibility. Prefer using `theme`. +}; diff --git a/renderers/lit/src/0.8/ui/context/markdown.ts b/renderers/lit/src/0.8/ui/context/markdown.ts new file mode 100644 index 000000000..0ad79d447 --- /dev/null +++ b/renderers/lit/src/0.8/ui/context/markdown.ts @@ -0,0 +1,26 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import { + DirectiveResult, +} from "lit/directive.js"; +import { createContext } from "@lit/context"; + +/** + * A Lit Context to override the default (noop) markdown renderer. + */ +export const markdownContext = createContext( + Symbol("a2ui-lit-markdown-renderer") +); diff --git a/renderers/lit/src/0.8/ui/ui.ts b/renderers/lit/src/0.8/ui/ui.ts index 476d4d567..7038d90a5 100644 --- a/renderers/lit/src/0.8/ui/ui.ts +++ b/renderers/lit/src/0.8/ui/ui.ts @@ -43,7 +43,7 @@ import { TextField } from "./text-field.js"; import { Text } from "./text.js"; import { Video } from "./video.js"; -export * as Context from "./context/theme.js"; +export * as Context from "./context/context.js"; export * as Utils from "./utils/utils.js"; export { ComponentRegistry, componentRegistry } from "./component-registry.js"; export { registerCustomComponents } from "./custom-components/index.js"; From b5632289e09369a996495a95d78cf638db329221 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:35:05 -0800 Subject: [PATCH 07/39] Inject the Context.markdown Directive into the text widget. Default to the noop version. --- .../src/0.8/ui/directives/noop_markdown.ts | 11 ++++--- renderers/lit/src/0.8/ui/text.ts | 11 ++++--- renderers/lit/src/0.8/ui/utils/markdown.ts | 32 ------------------- 3 files changed, 14 insertions(+), 40 deletions(-) delete mode 100644 renderers/lit/src/0.8/ui/utils/markdown.ts diff --git a/renderers/lit/src/0.8/ui/directives/noop_markdown.ts b/renderers/lit/src/0.8/ui/directives/noop_markdown.ts index da145fd83..4b343db4f 100644 --- a/renderers/lit/src/0.8/ui/directives/noop_markdown.ts +++ b/renderers/lit/src/0.8/ui/directives/noop_markdown.ts @@ -15,7 +15,10 @@ */ import { html, TemplateResult } from "lit"; -import { MarkdownRenderer } from "../utils/markdown.js"; +import { + directive, + Directive, +} from "lit/directive.js"; /** * "Handles" Markdown rendering by doing nothing. @@ -23,10 +26,10 @@ import { MarkdownRenderer } from "../utils/markdown.js"; * Configure @a2ui/lit-markdown, or your custom Markdown renderer * to actually parse and render Markdown in your app. */ -class NoopMarkdownRenderer implements MarkdownRenderer { - render(markdown: string) : TemplateResult { +class NoopMarkdownRendererDirective extends Directive { + render(markdown: string, _tagClassMap?: Record) : TemplateResult { return html`

${markdown}
`; } } -export const noopMarkdown = new NoopMarkdownRenderer(); +export const noopMarkdown = directive(NoopMarkdownRendererDirective); diff --git a/renderers/lit/src/0.8/ui/text.ts b/renderers/lit/src/0.8/ui/text.ts index 5c8a39296..85e1aebcb 100644 --- a/renderers/lit/src/0.8/ui/text.ts +++ b/renderers/lit/src/0.8/ui/text.ts @@ -18,7 +18,7 @@ import { html, css, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; import { consume } from '@lit/context'; import { noopMarkdown } from "./directives/noop_markdown.js"; -import { markdownContext, MarkdownRenderer } from "./utils/markdown.js"; +import * as Context from "./context/context.js"; import { Root } from "./root.js"; import { A2uiMessageProcessor } from "@a2ui/web_core/data/model-processor"; import * as Primitives from "@a2ui/web_core/types/primitives"; @@ -46,8 +46,8 @@ export class Text extends Root { @property({ reflect: true, attribute: "usage-hint" }) accessor usageHint: Types.ResolvedText["usageHint"] | null = null; - @consume({context: markdownContext}) - accessor markdownRenderer: MarkdownRenderer = noopMarkdown; + @consume({context: Context.markdown}) + accessor markdownRenderer = noopMarkdown; static styles = [ structuralStyles, @@ -121,7 +121,10 @@ export class Text extends Root { break; // Body. } - return html`${this.markdownRenderer?.render(markdownText)}`; + return html`${this.markdownRenderer( + markdownText, + Styles.appendToAll(this.theme.markdown, ["ol", "ul", "li"], {}) + )}`; } #areHintedStyles(styles: unknown): styles is HintedStyles { diff --git a/renderers/lit/src/0.8/ui/utils/markdown.ts b/renderers/lit/src/0.8/ui/utils/markdown.ts deleted file mode 100644 index 73a26d90b..000000000 --- a/renderers/lit/src/0.8/ui/utils/markdown.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - Copyright 2025 Google LLC - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import { TemplateResult } from "lit"; -import { createContext } from "@lit/context"; - -/** - * The interface for the markdown renderer that can be injected into the - * Lit context. - */ -export interface MarkdownRenderer { - render(markdown: string) : TemplateResult; -} - -/** - * A Lit Context to override the default (noop) markdown renderer. - */ -export const markdownContext = createContext( - Symbol("a2ui-lit-markdown-renderer") -); From e11a28eb91875ae6ec94f8b8ba5f75f39e647326 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:36:56 -0800 Subject: [PATCH 08/39] Inject the markdown-it-lit renderer on shell app --- samples/client/lit/shell/app.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/samples/client/lit/shell/app.ts b/samples/client/lit/shell/app.ts index 33a16cf57..705f87510 100644 --- a/samples/client/lit/shell/app.ts +++ b/samples/client/lit/shell/app.ts @@ -46,6 +46,7 @@ import { AppConfig } from "./configs/types.js"; import { config as restaurantConfig } from "./configs/restaurant.js"; import { config as contactsConfig } from "./configs/contacts.js"; import { styleMap } from "lit/directives/style-map.js"; +import { markdown } from "@a2ui/markdown-it-lit"; const configs: Record = { restaurant: restaurantConfig, @@ -54,8 +55,11 @@ const configs: Record = { @customElement("a2ui-shell") export class A2UILayoutEditor extends SignalWatcher(LitElement) { - @provide({ context: UI.Context.themeContext }) - accessor theme: v0_8.Types.Theme = uiTheme; + @provide({ context: UI.Context.theme }) + accessor #theme = uiTheme; + + @provide({ context: UI.Context.markdown }) + accessor #markdownRenderer = markdown; @state() accessor #requesting = false; @@ -290,7 +294,7 @@ export class A2UILayoutEditor extends SignalWatcher(LitElement) { // Apply the theme directly, which will use the Lit context. if (this.config.theme) { - this.theme = this.config.theme; + this.#theme = this.config.theme; } window.document.title = this.config.title; From 3545acd4665441aa4ee55df730b7c4b8bce9d000 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:37:17 -0800 Subject: [PATCH 09/39] Update miscellaneous lockfiles --- renderers/lit/package-lock.json | 3 +-- samples/client/lit/package-lock.json | 23 ++++++++++++++++++++++- samples/client/lit/shell/package.json | 3 ++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/renderers/lit/package-lock.json b/renderers/lit/package-lock.json index ced356e5f..437310760 100644 --- a/renderers/lit/package-lock.json +++ b/renderers/lit/package-lock.json @@ -1007,8 +1007,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/signal-polyfill/-/signal-polyfill-0.2.2.tgz", "integrity": "sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/signal-utils": { "version": "0.21.1", diff --git a/samples/client/lit/package-lock.json b/samples/client/lit/package-lock.json index 94aeccddf..b8f941bbc 100644 --- a/samples/client/lit/package-lock.json +++ b/samples/client/lit/package-lock.json @@ -36,6 +36,23 @@ "wireit": "^0.15.0-pre.2" } }, + "../../../renderers/markdown/markdown-it-lit": { + "name": "@a2ui/markdown-it-lit", + "version": "0.8.1", + "license": "Apache-2.0", + "dependencies": { + "@a2ui/lit": "file:../../lit", + "@a2ui/markdown-it-shared": "file:../markdown-it-shared", + "@a2ui/web_core": "file:../../web_core", + "@lit/context": "^1.1.4", + "lit": "^3.3.1" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.8.3", + "wireit": "^0.15.0-pre.2" + } + }, "contact": { "name": "@a2ui/contact", "version": "0.8.1", @@ -96,6 +113,10 @@ "resolved": "../../../renderers/lit", "link": true }, + "node_modules/@a2ui/markdown-it-lit": { + "resolved": "../../../renderers/markdown/markdown-it-lit", + "link": true + }, "node_modules/@a2ui/shell": { "resolved": "shell", "link": true @@ -2159,7 +2180,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2893,6 +2913,7 @@ "dependencies": { "@a2a-js/sdk": "^0.3.4", "@a2ui/lit": "file:../../../../renderers/lit", + "@a2ui/markdown-it-lit": "file:../../../../renderers/markdown/markdown-it-lit", "@google/genai": "^1.22.0", "@lit-labs/signals": "^0.1.3", "@lit/context": "^1.1.4", diff --git a/samples/client/lit/shell/package.json b/samples/client/lit/shell/package.json index 5a8b4d0da..68d55501e 100644 --- a/samples/client/lit/shell/package.json +++ b/samples/client/lit/shell/package.json @@ -79,10 +79,11 @@ "dependencies": { "@a2a-js/sdk": "^0.3.4", "@a2ui/lit": "file:../../../../renderers/lit", + "@a2ui/markdown-it-lit": "file:../../../../renderers/markdown/markdown-it-lit", "@google/genai": "^1.22.0", "@lit-labs/signals": "^0.1.3", "@lit/context": "^1.1.4", "@types/node": "^24.7.1", "lit": "^3.3.1" } -} \ No newline at end of file +} From ede18f15524e20279375e3bff1cad0f50f850e30 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 18:53:04 -0800 Subject: [PATCH 10/39] Fix typo in shell a2ui-surface closing tag --- samples/client/lit/shell/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/client/lit/shell/app.ts b/samples/client/lit/shell/app.ts index 705f87510..8960f12af 100644 --- a/samples/client/lit/shell/app.ts +++ b/samples/client/lit/shell/app.ts @@ -504,7 +504,7 @@ export class A2UILayoutEditor extends SignalWatcher(LitElement) { .surfaceId=${surfaceId} .surface=${surface} .processor=${this.#processor} - >`; + >`; } )} `; From 376ed917fe3959c568fd7f0386cd7e9564b35299 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 18:53:53 -0800 Subject: [PATCH 11/39] Wire the markdown renderer to the contact app. --- samples/client/lit/contact/contact.ts | 4 ++++ samples/client/lit/contact/package.json | 1 + samples/client/lit/package.json | 1 + 3 files changed, 6 insertions(+) diff --git a/samples/client/lit/contact/contact.ts b/samples/client/lit/contact/contact.ts index 8300abe16..796a2555b 100644 --- a/samples/client/lit/contact/contact.ts +++ b/samples/client/lit/contact/contact.ts @@ -37,6 +37,7 @@ import { type Snackbar } from "./ui/snackbar.js"; import { repeat } from "lit/directives/repeat.js"; import { v0_8 } from "@a2ui/lit"; import * as UI from "@a2ui/lit/ui"; +import { markdown } from "@a2ui/markdown-it-lit"; // Demo elements. import "./ui/ui.js"; @@ -54,6 +55,9 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) { @provide({ context: UI.Context.themeContext }) accessor theme: v0_8.Types.Theme = uiTheme; + @provide({ context: UI.Context.markdown }) + accessor #markdown = markdown; + @state() accessor #requesting = false; diff --git a/samples/client/lit/contact/package.json b/samples/client/lit/contact/package.json index 58da6a441..fcfd6e017 100644 --- a/samples/client/lit/contact/package.json +++ b/samples/client/lit/contact/package.json @@ -77,6 +77,7 @@ "dependencies": { "@a2a-js/sdk": "^0.3.4", "@a2ui/lit": "file:../../../../renderers/lit", + "@a2ui/markdown-it-lit": "file:../../../../renderers/markdown-it-lit", "@lit-labs/signals": "^0.1.3", "@lit/context": "^1.1.4", "lit": "^3.3.1" diff --git a/samples/client/lit/package.json b/samples/client/lit/package.json index fc5534f1b..f2e55e63c 100644 --- a/samples/client/lit/package.json +++ b/samples/client/lit/package.json @@ -14,6 +14,7 @@ "serve:agent:rizzcharts": "cd ../../agent/adk/rizzcharts && uv run .", "serve:agent:orchestrator": "cd ../../agent/adk/orchestrator && uv run .", "serve:shell": "cd shell && npm run dev", + "serve:contact": "cd contact && npm run dev", "build:renderer": "cd ../../../renderers/web_core && npm install && npm run build && cd ../lit && npm install && npm run build", "demo:all": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,REST,CONT1\" -c \"magenta,blue,green\" \"npm run serve:shell\" \"npm run serve:agent:restaurant\" \"npm run serve:agent:contact_lookup\"", "demo:restaurant": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,REST\" -c \"magenta,blue\" \"npm run serve:shell\" \"npm run serve:agent:restaurant\"", From decd9318e70d55fa83be1bd7231af7ff36c4a430 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 18:54:09 -0800 Subject: [PATCH 12/39] Miscellaneous package-lock.json update --- samples/client/lit/package-lock.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/samples/client/lit/package-lock.json b/samples/client/lit/package-lock.json index b8f941bbc..2c60e5d90 100644 --- a/samples/client/lit/package-lock.json +++ b/samples/client/lit/package-lock.json @@ -36,6 +36,7 @@ "wireit": "^0.15.0-pre.2" } }, + "../../../renderers/markdown-it-lit": {}, "../../../renderers/markdown/markdown-it-lit": { "name": "@a2ui/markdown-it-lit", "version": "0.8.1", @@ -60,6 +61,7 @@ "dependencies": { "@a2a-js/sdk": "^0.3.4", "@a2ui/lit": "file:../../../../renderers/lit", + "@a2ui/markdown-it-lit": "file:../../../../renderers/markdown-it-lit", "@lit-labs/signals": "^0.1.3", "@lit/context": "^1.1.4", "lit": "^3.3.1" @@ -72,6 +74,10 @@ "wireit": "^0.15.0-pre.2" } }, + "contact/node_modules/@a2ui/markdown-it-lit": { + "resolved": "../../../renderers/markdown-it-lit", + "link": true + }, "node_modules/@a2a-js/sdk": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.8.tgz", From 3396e8492cf50f77c5309c95a66df46549e3d662 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 19:28:21 -0800 Subject: [PATCH 13/39] Tweak the contact theme to align the h5 a little bit better --- samples/client/lit/contact/theme/theme.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/samples/client/lit/contact/theme/theme.ts b/samples/client/lit/contact/theme/theme.ts index e9aef950b..67f8a41b0 100644 --- a/samples/client/lit/contact/theme/theme.ts +++ b/samples/client/lit/contact/theme/theme.ts @@ -83,6 +83,12 @@ const h3 = { "typography-sz-ts": true, }; +const h5 = { + ...heading, + "layout-mt-2": true, // To align with the icons + "typography-sz-bm": true, +}; + const iframe = { "behavior-sw-n": true, }; @@ -166,6 +172,7 @@ const buttonLight = v0_8.Styles.merge(button, { "color-c-n100": true }); const h1Light = v0_8.Styles.merge(h1, { "color-c-n5": true }); const h2Light = v0_8.Styles.merge(h2, { "color-c-n5": true }); const h3Light = v0_8.Styles.merge(h3, { "color-c-n5": true }); +const h5Light = v0_8.Styles.merge(h5, { "color-c-n5": true }); const bodyLight = v0_8.Styles.merge(body, { "color-c-n5": true }); const pLight = v0_8.Styles.merge(p, { "color-c-n60": true }); const preLight = v0_8.Styles.merge(pre, { "color-c-n35": true }); @@ -438,7 +445,7 @@ export const theme: v0_8.Types.Theme = { h2: h2Light, h3: h3Light, h4: {}, - h5: {}, + h5: h5Light, iframe, input: inputLight, p: pLight, @@ -452,7 +459,7 @@ export const theme: v0_8.Types.Theme = { h2: [...Object.keys(h2Light)], h3: [...Object.keys(h3Light)], h4: [], - h5: [], + h5: [...Object.keys(h5Light)], ul: [...Object.keys(unorderedListLight)], ol: [...Object.keys(orderedListLight)], li: [...Object.keys(listItemLight)], From 6b7a4775658e89249953b516e18fb9277dae0782 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 4 Feb 2026 15:38:41 -0800 Subject: [PATCH 14/39] Refactor build:renderer as a bash for loop so it builds all the lit renderer dependencies. --- samples/client/lit/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/client/lit/package.json b/samples/client/lit/package.json index f2e55e63c..31703cfd1 100644 --- a/samples/client/lit/package.json +++ b/samples/client/lit/package.json @@ -15,7 +15,7 @@ "serve:agent:orchestrator": "cd ../../agent/adk/orchestrator && uv run .", "serve:shell": "cd shell && npm run dev", "serve:contact": "cd contact && npm run dev", - "build:renderer": "cd ../../../renderers/web_core && npm install && npm run build && cd ../lit && npm install && npm run build", + "build:renderer": "cd ../../../renderers && for dir in 'web_core' 'markdown/markdown-it-shared' 'markdown/markdown-it-lit' 'lit'; do (cd \"$dir\" && npm install && npm run build); done", "demo:all": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,REST,CONT1\" -c \"magenta,blue,green\" \"npm run serve:shell\" \"npm run serve:agent:restaurant\" \"npm run serve:agent:contact_lookup\"", "demo:restaurant": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,REST\" -c \"magenta,blue\" \"npm run serve:shell\" \"npm run serve:agent:restaurant\"", "demo:contact": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,CONT1\" -c \"magenta,green\" \"npm run serve:shell\" \"npm run serve:agent:contact_lookup\"", From 27426161268eb09506d7ab896f2565b1e502b785 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 4 Feb 2026 15:49:43 -0800 Subject: [PATCH 15/39] Update lit_samples_build.yml to use the existing build:renderer script. --- .github/workflows/lit_samples_build.yml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/lit_samples_build.yml b/.github/workflows/lit_samples_build.yml index e3679e82d..4dc55bd23 100644 --- a/.github/workflows/lit_samples_build.yml +++ b/.github/workflows/lit_samples_build.yml @@ -35,21 +35,9 @@ jobs: with: node-version: '20' - - name: Install web_core deps - working-directory: ./renderers/web_core - run: npm ci - - - name: Build web_core - working-directory: ./renderers/web_core - run: npm run build - - - name: Install lib's deps - working-directory: ./renderers/lit - run: npm i - - - name: Build lib - working-directory: ./renderers/lit - run: npm run build + - name: Build lit renderer and its dependencies + working-directory: ./samples/client/lit + run: npm run build:renderer - name: Install all lit samples workspaces' dependencies working-directory: ./samples/client/lit From 0bbcc7ebb706f1d70a456c58362b237188e3c689 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 9 Feb 2026 17:51:29 -0800 Subject: [PATCH 16/39] Make the NoopMarkdownRenderer a MinimalMarkdownRenderer instead. --- .../lit/src/0.8/ui/directives/directives.ts | 2 +- .../src/0.8/ui/directives/minimal_markdown.ts | 92 +++++++++++++++++++ .../src/0.8/ui/directives/noop_markdown.ts | 35 ------- renderers/lit/src/0.8/ui/text.ts | 6 +- 4 files changed, 96 insertions(+), 39 deletions(-) create mode 100644 renderers/lit/src/0.8/ui/directives/minimal_markdown.ts delete mode 100644 renderers/lit/src/0.8/ui/directives/noop_markdown.ts diff --git a/renderers/lit/src/0.8/ui/directives/directives.ts b/renderers/lit/src/0.8/ui/directives/directives.ts index e81f614d6..4078c76bc 100644 --- a/renderers/lit/src/0.8/ui/directives/directives.ts +++ b/renderers/lit/src/0.8/ui/directives/directives.ts @@ -14,4 +14,4 @@ limitations under the License. */ -export { noopMarkdown } from "./noop_markdown.js"; +export { minimalMarkdown } from "./minimal_markdown.js"; diff --git a/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts b/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts new file mode 100644 index 000000000..aa857688a --- /dev/null +++ b/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts @@ -0,0 +1,92 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { html, TemplateResult } from "lit"; +import { + directive, + Directive, + PartInfo, +} from "lit/directive.js"; + +/** + * A minimal Markdown renderer that only supports a few tags. + * + * Configure @a2ui/lit-markdown, or your custom Markdown renderer + * to actually parse and render Markdown in your app. + */ +class MinimalMarkdownRendererDirective extends Directive { + static _warned = false; + + constructor(partInfo: PartInfo) { + super(partInfo); + /** + * Warn the user in case they need actual Markdown rendering. + */ + if (!MinimalMarkdownRendererDirective._warned) { + console.warn("[MinimalMarkdownRendererDirective]", + "is a placeholder Markdown renderer. Most features are not supported.\n", + "Install `@a2ui/lit-markdown` for a fully-featured Markdown renderer."); + MinimalMarkdownRendererDirective._warned = true; + } + } + + /** + * Attempts to render some Markdown. + * + * Resist the urge to "improve" this method. Instead, use a more proper + * Markdown renderer. This is just a placeholder. + */ + render(markdown: string, _tagClassMap?: Record) : TemplateResult { + if (markdown.startsWith('# ')) { + let classes = this.getClasses("h1", _tagClassMap); + return html`

${markdown.substring(2)}

`; + } + if (markdown.startsWith('## ')) { + let classes = this.getClasses("h2", _tagClassMap); + return html`

${markdown.substring(3)}

`; + } + if (markdown.startsWith('### ')) { + let classes = this.getClasses("h3", _tagClassMap); + return html`

${markdown.substring(4)}

`; + } + if (markdown.startsWith('#### ')) { + let classes = this.getClasses("h4", _tagClassMap); + return html`

${markdown.substring(5)}

`; + } + if (markdown.startsWith('##### ')) { + let classes = this.getClasses("h5", _tagClassMap); + return html`
${markdown.substring(6)}
`; + } + if (markdown.startsWith('*') && markdown.endsWith('*')) { + let classes = this.getClasses("strong", _tagClassMap); + return html`${markdown.substring(1, markdown.length - 1)}`; + } + let classes = this.getClasses("p", _tagClassMap); + return html`

${markdown}

`; + } + + /** + * Returns a space-separated lists of clases for `tag` from `_tagClassMap`. + * @param tag The name of the tag ("h1", "p", etc.) + * @param _tagClassMap The class map to use. + * @returns A space-separated list of class names. + */ + getClasses(tag: string, _tagClassMap?: Record) { + return _tagClassMap?.[tag]?.join(' ') ?? ''; + } +} + +export const minimalMarkdown = directive(MinimalMarkdownRendererDirective); diff --git a/renderers/lit/src/0.8/ui/directives/noop_markdown.ts b/renderers/lit/src/0.8/ui/directives/noop_markdown.ts deleted file mode 100644 index 4b343db4f..000000000 --- a/renderers/lit/src/0.8/ui/directives/noop_markdown.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - Copyright 2025 Google LLC - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import { html, TemplateResult } from "lit"; -import { - directive, - Directive, -} from "lit/directive.js"; - -/** - * "Handles" Markdown rendering by doing nothing. - * - * Configure @a2ui/lit-markdown, or your custom Markdown renderer - * to actually parse and render Markdown in your app. - */ -class NoopMarkdownRendererDirective extends Directive { - render(markdown: string, _tagClassMap?: Record) : TemplateResult { - return html`
${markdown}
`; - } -} - -export const noopMarkdown = directive(NoopMarkdownRendererDirective); diff --git a/renderers/lit/src/0.8/ui/text.ts b/renderers/lit/src/0.8/ui/text.ts index 85e1aebcb..42a3d9d7c 100644 --- a/renderers/lit/src/0.8/ui/text.ts +++ b/renderers/lit/src/0.8/ui/text.ts @@ -17,7 +17,7 @@ import { html, css, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; import { consume } from '@lit/context'; -import { noopMarkdown } from "./directives/noop_markdown.js"; +import { minimalMarkdown } from "./directives/directives.js"; import * as Context from "./context/context.js"; import { Root } from "./root.js"; import { A2uiMessageProcessor } from "@a2ui/web_core/data/model-processor"; @@ -47,7 +47,7 @@ export class Text extends Root { accessor usageHint: Types.ResolvedText["usageHint"] | null = null; @consume({context: Context.markdown}) - accessor markdownRenderer = noopMarkdown; + accessor markdownRenderer = minimalMarkdown; static styles = [ structuralStyles, @@ -122,7 +122,7 @@ export class Text extends Root { } return html`${this.markdownRenderer( - markdownText, + markdownText, Styles.appendToAll(this.theme.markdown, ["ol", "ul", "li"], {}) )}`; } From cffb5575f0f8e0e6e83d62d4c5bdbff8c11d6212 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 11 Feb 2026 12:03:05 -0800 Subject: [PATCH 17/39] Remove markdown-it dependency from lit renderer --- renderers/lit/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/renderers/lit/package.json b/renderers/lit/package.json index 252709906..301275ffd 100644 --- a/renderers/lit/package.json +++ b/renderers/lit/package.json @@ -79,7 +79,6 @@ }, "homepage": "https://github.com/google/A2UI/tree/main/web#readme", "devDependencies": { - "@types/markdown-it": "^14.1.2", "@types/node": "^24.10.1", "google-artifactregistry-auth": "^3.5.0", "typescript": "^5.8.3", @@ -89,7 +88,6 @@ "@lit-labs/signals": "^0.1.3", "@lit/context": "^1.1.4", "lit": "^3.3.1", - "markdown-it": "^14.1.0", "signal-utils": "^0.21.1", "@a2ui/web_core": "file:../web_core" } From a58b1799cb238592600392b564901d12763a6767 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 12 Feb 2026 18:53:09 -0800 Subject: [PATCH 18/39] Add a base interface for Markdown Renderers in web_core --- renderers/web_core/src/v0_8/types/types.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/renderers/web_core/src/v0_8/types/types.ts b/renderers/web_core/src/v0_8/types/types.ts index 1e1f6686c..00f56948c 100644 --- a/renderers/web_core/src/v0_8/types/types.ts +++ b/renderers/web_core/src/v0_8/types/types.ts @@ -530,3 +530,23 @@ export interface Surface { components: Map; styles: Record; } + +/** + * A list of classnames to be applied to a tag when rendering Markdown. + */ +export type MarkdownRendererTagClassMap = Record; + +/** + * An object that can render Markdown with a given tag class map. + * + * The generic type parameter `T` is the type of the rendered output. + * + * This type is shared so it can be implemented and injected per renderer. + * + * TODO: replace `tagClassMap` by a more general MarkdownRendererConfig type, + * so it can be easily extended in the future without needing to update all + * renderers at the same time. + */ +export interface MarkdownRenderer { + render(value: string, tagClassMap?: MarkdownRendererTagClassMap): T; +} From 887d5c46c0ae0c8738b0426dab09e05da37bafa8 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 12 Feb 2026 18:54:17 -0800 Subject: [PATCH 19/39] Make the Lit default markdown renderer implement the new MarkdownRenderer interface --- .../lit/src/0.8/ui/directives/minimal_markdown.ts | 10 ++++++---- renderers/lit/src/0.8/ui/text.ts | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts b/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts index aa857688a..c49db55a8 100644 --- a/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts +++ b/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts @@ -14,12 +14,14 @@ limitations under the License. */ -import { html, TemplateResult } from "lit"; +import { html } from "lit"; import { directive, Directive, + DirectiveResult, PartInfo, } from "lit/directive.js"; +import * as Types from "@a2ui/web_core/types/types"; /** * A minimal Markdown renderer that only supports a few tags. @@ -27,7 +29,7 @@ import { * Configure @a2ui/lit-markdown, or your custom Markdown renderer * to actually parse and render Markdown in your app. */ -class MinimalMarkdownRendererDirective extends Directive { +class MinimalMarkdownRendererDirective extends Directive implements Types.MarkdownRenderer { static _warned = false; constructor(partInfo: PartInfo) { @@ -49,7 +51,7 @@ class MinimalMarkdownRendererDirective extends Directive { * Resist the urge to "improve" this method. Instead, use a more proper * Markdown renderer. This is just a placeholder. */ - render(markdown: string, _tagClassMap?: Record) : TemplateResult { + render(markdown: string, _tagClassMap?: Types.MarkdownRendererTagClassMap) : DirectiveResult { if (markdown.startsWith('# ')) { let classes = this.getClasses("h1", _tagClassMap); return html`

${markdown.substring(2)}

`; @@ -84,7 +86,7 @@ class MinimalMarkdownRendererDirective extends Directive { * @param _tagClassMap The class map to use. * @returns A space-separated list of class names. */ - getClasses(tag: string, _tagClassMap?: Record) { + getClasses(tag: string, _tagClassMap?: Types.MarkdownRendererTagClassMap) { return _tagClassMap?.[tag]?.join(' ') ?? ''; } } diff --git a/renderers/lit/src/0.8/ui/text.ts b/renderers/lit/src/0.8/ui/text.ts index 42a3d9d7c..12236bf5f 100644 --- a/renderers/lit/src/0.8/ui/text.ts +++ b/renderers/lit/src/0.8/ui/text.ts @@ -46,6 +46,7 @@ export class Text extends Root { @property({ reflect: true, attribute: "usage-hint" }) accessor usageHint: Types.ResolvedText["usageHint"] | null = null; + // This is a lit directive, that internally implements the Types.MarkdownRenderer interface. @consume({context: Context.markdown}) accessor markdownRenderer = minimalMarkdown; From b12bc69caeb991db652e4db18c285aef1f31a4f6 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 12 Feb 2026 18:55:26 -0800 Subject: [PATCH 20/39] Make the markdown-it-lit markdown renderer implement the interface --- renderers/markdown/markdown-it-lit/src/markdown.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/renderers/markdown/markdown-it-lit/src/markdown.ts b/renderers/markdown/markdown-it-lit/src/markdown.ts index 0341bfef2..106b59eed 100644 --- a/renderers/markdown/markdown-it-lit/src/markdown.ts +++ b/renderers/markdown/markdown-it-lit/src/markdown.ts @@ -18,11 +18,13 @@ import { noChange } from "lit"; import { Directive, DirectiveParameters, + DirectiveResult, Part, directive, } from "lit/directive.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { markdownRenderer } from "@a2ui/markdown-it-shared"; +import * as Types from "@a2ui/web_core/types/types"; /** * A Lit directive that renders markdown to HTML. @@ -30,7 +32,7 @@ import { markdownRenderer } from "@a2ui/markdown-it-shared"; * This directive is intended to be used by the A2UI Lit renderer to render * markdown to HTML. */ -class MarkdownDirective extends Directive { +class MarkdownItDirective extends Directive implements Types.MarkdownRenderer { private lastValue: string | null = null; private lastTagClassMap: string | null = null; @@ -50,10 +52,10 @@ class MarkdownDirective extends Directive { /** * Renders the markdown string to HTML. */ - render(value: string, tagClassMap?: Record) { + render(value: string, tagClassMap?: Types.MarkdownRendererTagClassMap) { const htmlString = markdownRenderer.render(value, tagClassMap); return unsafeHTML(htmlString); } } -export const markdown = directive(MarkdownDirective); +export const markdown = directive(MarkdownItDirective); From d83c3ffb85047964c6b473dfb676290302411ba3 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 12 Feb 2026 18:59:18 -0800 Subject: [PATCH 21/39] Remove and gitignore package-lock.json files from the new markdown packages. --- renderers/markdown/.gitignore | 2 + .../markdown-it-lit/package-lock.json | 618 ------------------ .../markdown-it-shared/package-lock.json | 589 ----------------- 3 files changed, 2 insertions(+), 1207 deletions(-) create mode 100644 renderers/markdown/.gitignore delete mode 100644 renderers/markdown/markdown-it-lit/package-lock.json delete mode 100644 renderers/markdown/markdown-it-shared/package-lock.json diff --git a/renderers/markdown/.gitignore b/renderers/markdown/.gitignore new file mode 100644 index 000000000..66ae216ef --- /dev/null +++ b/renderers/markdown/.gitignore @@ -0,0 +1,2 @@ +package-lock.json + diff --git a/renderers/markdown/markdown-it-lit/package-lock.json b/renderers/markdown/markdown-it-lit/package-lock.json deleted file mode 100644 index 2d1a22bb0..000000000 --- a/renderers/markdown/markdown-it-lit/package-lock.json +++ /dev/null @@ -1,618 +0,0 @@ -{ - "name": "@a2ui/markdown-it-lit", - "version": "0.8.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@a2ui/markdown-it-lit", - "version": "0.8.1", - "license": "Apache-2.0", - "dependencies": { - "@a2ui/lit": "file:../../lit", - "@a2ui/markdown-it-shared": "file:../markdown-it-shared", - "@a2ui/web_core": "file:../../web_core", - "@lit/context": "^1.1.4", - "lit": "^3.3.1" - }, - "devDependencies": { - "@types/node": "^24.10.1", - "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2" - } - }, - "../../lit": { - "name": "@a2ui/lit", - "version": "0.8.1", - "license": "Apache-2.0", - "dependencies": { - "@a2ui/web_core": "file:../web_core", - "@lit-labs/signals": "^0.1.3", - "@lit/context": "^1.1.4", - "lit": "^3.3.1", - "markdown-it": "^14.1.0", - "signal-utils": "^0.21.1" - }, - "devDependencies": { - "@types/markdown-it": "^14.1.2", - "@types/node": "^24.10.1", - "google-artifactregistry-auth": "^3.5.0", - "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2" - } - }, - "../../web_core": { - "name": "@a2ui/web_core", - "version": "0.8.0", - "license": "Apache-2.0", - "devDependencies": { - "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2" - } - }, - "../markdown-it-shared": { - "name": "@a2ui/markdown-it-shared", - "version": "0.0.1", - "license": "Apache-2.0", - "dependencies": { - "markdown-it": "^14.1.0" - }, - "devDependencies": { - "@types/markdown-it": "^14.1.2", - "@types/node": "^24.10.1", - "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2" - } - }, - "../web_core": { - "name": "@a2ui/web_core", - "version": "0.8.0", - "extraneous": true, - "license": "Apache-2.0", - "devDependencies": { - "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2" - } - }, - "node_modules/@a2ui/lit": { - "resolved": "../../lit", - "link": true - }, - "node_modules/@a2ui/markdown-it-shared": { - "resolved": "../markdown-it-shared", - "link": true - }, - "node_modules/@a2ui/web_core": { - "resolved": "../../web_core", - "link": true - }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", - "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/context": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.6.tgz", - "integrity": "sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^1.6.2 || ^2.1.0" - } - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", - "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/node": { - "version": "24.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", - "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/balanced-match": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", - "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.1.tgz", - "integrity": "sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^3.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lit": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", - "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", - "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", - "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/wireit": { - "version": "0.15.0-pre.2", - "resolved": "https://registry.npmjs.org/wireit/-/wireit-0.15.0-pre.2.tgz", - "integrity": "sha512-pXOTR56btrL7STFOPQgtq8MjAFWagSqs188E2FflCgcxk5uc0Xbn8CuLIR9FbqK97U3Jw6AK8zDEu/M/9ENqgA==", - "dev": true, - "license": "Apache-2.0", - "workspaces": [ - "vscode-extension", - "website" - ], - "dependencies": { - "brace-expansion": "^4.0.0", - "chokidar": "^3.5.3", - "fast-glob": "^3.2.11", - "jsonc-parser": "^3.0.0", - "proper-lockfile": "^4.1.2" - }, - "bin": { - "wireit": "bin/wireit.js" - }, - "engines": { - "node": ">=18.0.0" - } - } - } -} diff --git a/renderers/markdown/markdown-it-shared/package-lock.json b/renderers/markdown/markdown-it-shared/package-lock.json deleted file mode 100644 index 5a52d0bf8..000000000 --- a/renderers/markdown/markdown-it-shared/package-lock.json +++ /dev/null @@ -1,589 +0,0 @@ -{ - "name": "@a2ui/markdown-it-shared", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@a2ui/markdown-it-shared", - "version": "0.0.1", - "license": "Apache-2.0", - "dependencies": { - "markdown-it": "^14.1.0" - }, - "devDependencies": { - "@types/markdown-it": "^14.1.2", - "@types/node": "^24.10.1", - "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2" - } - }, - "../web_core": { - "name": "@a2ui/web_core", - "version": "0.8.0", - "extraneous": true, - "license": "Apache-2.0", - "devDependencies": { - "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", - "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", - "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.1.tgz", - "integrity": "sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^3.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/wireit": { - "version": "0.15.0-pre.2", - "resolved": "https://registry.npmjs.org/wireit/-/wireit-0.15.0-pre.2.tgz", - "integrity": "sha512-pXOTR56btrL7STFOPQgtq8MjAFWagSqs188E2FflCgcxk5uc0Xbn8CuLIR9FbqK97U3Jw6AK8zDEu/M/9ENqgA==", - "dev": true, - "license": "Apache-2.0", - "workspaces": [ - "vscode-extension", - "website" - ], - "dependencies": { - "brace-expansion": "^4.0.0", - "chokidar": "^3.5.3", - "fast-glob": "^3.2.11", - "jsonc-parser": "^3.0.0", - "proper-lockfile": "^4.1.2" - }, - "bin": { - "wireit": "bin/wireit.js" - }, - "engines": { - "node": ">=18.0.0" - } - } - } -} From e8be402d64571e91876ba27a1c1c39f486bf25c5 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 12 Feb 2026 18:59:48 -0800 Subject: [PATCH 22/39] Add an angular bridge for the markdown-it-shared package --- renderers/markdown/markdown-it-angular/.npmrc | 2 + .../markdown/markdown-it-angular/README.md | 29 +++++++++ .../markdown/markdown-it-angular/angular.json | 35 +++++++++++ .../markdown-it-angular/ng-package.json | 8 +++ .../markdown/markdown-it-angular/package.json | 59 +++++++++++++++++++ .../markdown-it-angular/postprocess-build.mjs | 48 +++++++++++++++ .../src/lib/markdown.spec.ts | 29 +++++++++ .../markdown-it-angular/src/lib/markdown.ts | 37 ++++++++++++ .../markdown-it-angular/src/public-api.ts | 17 ++++++ .../markdown-it-angular/tsconfig.json | 24 ++++++++ .../markdown-it-angular/tsconfig.lib.json | 16 +++++ .../markdown-it-angular/tsconfig.spec.json | 8 +++ 12 files changed, 312 insertions(+) create mode 100644 renderers/markdown/markdown-it-angular/.npmrc create mode 100644 renderers/markdown/markdown-it-angular/README.md create mode 100644 renderers/markdown/markdown-it-angular/angular.json create mode 100644 renderers/markdown/markdown-it-angular/ng-package.json create mode 100644 renderers/markdown/markdown-it-angular/package.json create mode 100644 renderers/markdown/markdown-it-angular/postprocess-build.mjs create mode 100644 renderers/markdown/markdown-it-angular/src/lib/markdown.spec.ts create mode 100644 renderers/markdown/markdown-it-angular/src/lib/markdown.ts create mode 100644 renderers/markdown/markdown-it-angular/src/public-api.ts create mode 100644 renderers/markdown/markdown-it-angular/tsconfig.json create mode 100644 renderers/markdown/markdown-it-angular/tsconfig.lib.json create mode 100644 renderers/markdown/markdown-it-angular/tsconfig.spec.json diff --git a/renderers/markdown/markdown-it-angular/.npmrc b/renderers/markdown/markdown-it-angular/.npmrc new file mode 100644 index 000000000..06b0eef7e --- /dev/null +++ b/renderers/markdown/markdown-it-angular/.npmrc @@ -0,0 +1,2 @@ +@a2ui:registry=https://us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/ +//us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/:always-auth=true diff --git a/renderers/markdown/markdown-it-angular/README.md b/renderers/markdown/markdown-it-angular/README.md new file mode 100644 index 000000000..583532796 --- /dev/null +++ b/renderers/markdown/markdown-it-angular/README.md @@ -0,0 +1,29 @@ +A2UI markdown renderer for Angular. + +## Development + +### Build + +Run `npm run build` to build the project. The build artifacts will be stored in the `dist/` directory. + +### Running unit tests + +Run `npm test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +To run tests in watch mode (browser stays open): + +```bash +npm run test:watch +``` + +### Code Formatting + +This project uses [Prettier](https://prettier.io/) for code formatting. The configuration is defined in `package.json`. + +To format all files in the project: + +```bash +npm run format +``` + +Most IDEs (like VS Code) can be configured to **Format On Save** using the local Prettier version and configuration. This is the recommended workflow. diff --git a/renderers/markdown/markdown-it-angular/angular.json b/renderers/markdown/markdown-it-angular/angular.json new file mode 100644 index 000000000..a5b166d22 --- /dev/null +++ b/renderers/markdown/markdown-it-angular/angular.json @@ -0,0 +1,35 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "projects": { + "lib": { + "projectType": "library", + "root": ".", + "sourceRoot": "./src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular/build:ng-packagr", + "configurations": { + "production": { + "tsConfig": "./tsconfig.lib.json" + }, + "development": { + "tsConfig": "./tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular/build:karma", + "options": { + "tsConfig": "./tsconfig.spec.json" + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/renderers/markdown/markdown-it-angular/ng-package.json b/renderers/markdown/markdown-it-angular/ng-package.json new file mode 100644 index 000000000..a1bf88f4b --- /dev/null +++ b/renderers/markdown/markdown-it-angular/ng-package.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "./dist", + "lib": { + "entryFile": "src/public-api.ts" + }, + "allowedNonPeerDependencies": ["markdown-it", "@a2ui/web_core"] +} diff --git a/renderers/markdown/markdown-it-angular/package.json b/renderers/markdown/markdown-it-angular/package.json new file mode 100644 index 000000000..5ee84c193 --- /dev/null +++ b/renderers/markdown/markdown-it-angular/package.json @@ -0,0 +1,59 @@ +{ + "name": "@a2ui/markdown-it-angular", + "version": "0.8.3", + "description": "A2UI Core Library", + "main": "./dist/fesm2022/a2ui-markdown-it-angular.mjs", + "types": "./dist/types/a2ui-markdown-it-angular.d.ts", + "type": "module", + "sideEffects": false, + "scripts": { + "build": "ng build", + "test": "ng test --watch=false --browsers=ChromeHeadless", + "test:watch": "ng test --browsers=ChromeHeadless", + "format": "prettier --write ." + }, + "dependencies": { + "@a2ui/markdown-it-shared": "file:../markdown-it-shared", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@a2ui/web_core": "file:../../web_core", + "@angular/core": "^21.0.0" + }, + "devDependencies": { + "@a2ui/angular": "file:../../angular", + "@a2ui/web_core": "file:../../web_core", + "@angular/build": "21.1.4", + "@angular/common": "21.1.4", + "@angular/compiler": "21.1.4", + "@angular/compiler-cli": "21.1.4", + "@angular/core": "21.1.4", + "@angular/platform-browser": "21.1.4", + "@angular/platform-browser-dynamic": "21.1.4", + "@types/jasmine": "~5.1.0", + "@types/markdown-it": "^14.1.2", + "@types/node": "^20.17.19", + "jasmine-core": "~5.9.0", + "karma": "^6.4.4", + "karma-chrome-launcher": "^3.2.0", + "karma-coverage": "^2.2.1", + "karma-jasmine": "^5.1.0", + "karma-jasmine-html-reporter": "^2.1.0", + "ng-packagr": "^21.0.0", + "prettier": "^3.8.1", + "tslib": "^2.8.1", + "typescript": "~5.9.2" + }, + "prettier": { + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] + } +} diff --git a/renderers/markdown/markdown-it-angular/postprocess-build.mjs b/renderers/markdown/markdown-it-angular/postprocess-build.mjs new file mode 100644 index 000000000..29d06582c --- /dev/null +++ b/renderers/markdown/markdown-it-angular/postprocess-build.mjs @@ -0,0 +1,48 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +// TODO: Update this script to do handle "markdown-it-shared" + +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +// Locally we depend on the Core package via a relative path so we can test it from source. +// This breaks when published to npm so the following script updates the version to the npm one. + +const dirname = import.meta.dirname; +const coreVersion = parsePackageJson(join(dirname, '../../web_core/package.json')).version; + +if (!coreVersion) { + throw new Error('Cannot determine version of Core package'); +} + +const packageJsonPath = join(dirname, './dist/package.json'); +const packageJson = parsePackageJson(packageJsonPath); + +if (!packageJson.dependencies['@a2ui/web_core']) { + throw new Error( + 'Angular package does not depend on the Core library. ' + + 'Either update the package.json or remove this script.', + ); +} + +packageJson.dependencies['@a2ui/web_core'] = '^' + coreVersion; +writeFileSync(packageJsonPath, JSON.stringify(packageJson, undefined, 2)); + +function parsePackageJson(path) { + const content = readFileSync(path, 'utf8'); + return JSON.parse(content); +} diff --git a/renderers/markdown/markdown-it-angular/src/lib/markdown.spec.ts b/renderers/markdown/markdown-it-angular/src/lib/markdown.spec.ts new file mode 100644 index 000000000..9a014274b --- /dev/null +++ b/renderers/markdown/markdown-it-angular/src/lib/markdown.spec.ts @@ -0,0 +1,29 @@ +import { TestBed } from '@angular/core/testing'; +import { MarkdownItMarkdownRenderer } from './markdown'; +import { markdownRenderer } from '@a2ui/markdown-it-shared'; + +describe('MarkdownItMarkdownRenderer', () => { + let service: MarkdownItMarkdownRenderer; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [MarkdownItMarkdownRenderer], + }); + service = TestBed.inject(MarkdownItMarkdownRenderer); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should delegate render to markdownRenderer', () => { + const renderSpy = spyOn(markdownRenderer, 'render').and.returnValue('

test

'); + const input = 'test'; + const tagClassMap = { p: ['my-class'] }; + + const result = service.render(input, tagClassMap); + + expect(renderSpy).toHaveBeenCalledWith(input, tagClassMap); + expect(result).toBe('

test

'); + }); +}); diff --git a/renderers/markdown/markdown-it-angular/src/lib/markdown.ts b/renderers/markdown/markdown-it-angular/src/lib/markdown.ts new file mode 100644 index 000000000..2f5f6f6e9 --- /dev/null +++ b/renderers/markdown/markdown-it-angular/src/lib/markdown.ts @@ -0,0 +1,37 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { markdownRenderer } from '@a2ui/markdown-it-shared'; +import * as Types from '@a2ui/web_core/types/types'; + +/** + * Wraps the `markdownRenderer` from `@a2ui/markdown-it-shared` to provide + * Angular integration. + */ +@Injectable({ providedIn: 'root' }) +export class MarkdownItMarkdownRenderer implements Types.MarkdownRenderer { + render(value: string, tagClassMap?: Types.MarkdownRendererTagClassMap) { + return markdownRenderer.render(value, tagClassMap); + } +} + +// TODO: Import @a2ui/angular and provide a ready-made provider so users can +// override the default renderer more easily? +// export const markdownRendererProvider = { +// provide: MarkdownRenderer, +// useClass: MarkdownItMarkdownRenderer +// }; diff --git a/renderers/markdown/markdown-it-angular/src/public-api.ts b/renderers/markdown/markdown-it-angular/src/public-api.ts new file mode 100644 index 000000000..360ba63b9 --- /dev/null +++ b/renderers/markdown/markdown-it-angular/src/public-api.ts @@ -0,0 +1,17 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +export * from './lib/markdown'; diff --git a/renderers/markdown/markdown-it-angular/tsconfig.json b/renderers/markdown/markdown-it-angular/tsconfig.json new file mode 100644 index 000000000..fcc745064 --- /dev/null +++ b/renderers/markdown/markdown-it-angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve", + "moduleResolution": "bundler" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "typeCheckHostBindings": true, + "strictTemplates": true + } +} diff --git a/renderers/markdown/markdown-it-angular/tsconfig.lib.json b/renderers/markdown/markdown-it-angular/tsconfig.lib.json new file mode 100644 index 000000000..b4b335aed --- /dev/null +++ b/renderers/markdown/markdown-it-angular/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/lib", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "inlineSources": true, + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["**/*.spec.ts"], + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/renderers/markdown/markdown-it-angular/tsconfig.spec.json b/renderers/markdown/markdown-it-angular/tsconfig.spec.json new file mode 100644 index 000000000..d427f4c3a --- /dev/null +++ b/renderers/markdown/markdown-it-angular/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.ts"] +} From 1539d7dbbfd07cf63fd1866a41d5e9d92c54e642 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 12 Feb 2026 19:00:42 -0800 Subject: [PATCH 23/39] Export a noop MarkdownRenderer so it can be overridden from apps --- renderers/angular/src/lib/data/index.ts | 1 + renderers/angular/src/lib/data/markdown.ts | 96 ++-------------------- 2 files changed, 6 insertions(+), 91 deletions(-) diff --git a/renderers/angular/src/lib/data/index.ts b/renderers/angular/src/lib/data/index.ts index 59c24ba49..9dfcd336c 100644 --- a/renderers/angular/src/lib/data/index.ts +++ b/renderers/angular/src/lib/data/index.ts @@ -14,5 +14,6 @@ limitations under the License. */ +export * from './markdown'; export * from './processor'; export * from './types'; diff --git a/renderers/angular/src/lib/data/markdown.ts b/renderers/angular/src/lib/data/markdown.ts index dac3a77d2..f31c6055c 100644 --- a/renderers/angular/src/lib/data/markdown.ts +++ b/renderers/angular/src/lib/data/markdown.ts @@ -14,101 +14,15 @@ limitations under the License. */ -import { inject, Injectable, SecurityContext } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import MarkdownIt from 'markdown-it'; +import { /*inject,*/ Injectable } from '@angular/core'; +// import { DomSanitizer } from '@angular/platform-browser'; +// TODO: Make this a noop/minimal Markdown renderer @Injectable({ providedIn: 'root' }) export class MarkdownRenderer { - private originalClassMap = new Map(); - private sanitizer = inject(DomSanitizer); - - private markdownIt = MarkdownIt({ - highlight: (str, lang) => { - if (lang === 'html') { - const iframe = document.createElement('iframe'); - iframe.classList.add('html-view'); - iframe.srcdoc = str; - iframe.sandbox = ''; - return iframe.innerHTML; - } - - return str; - }, - }); + // private sanitizer = inject(DomSanitizer); render(value: string, tagClassMap?: Record) { - if (tagClassMap) { - this.applyTagClassMap(tagClassMap); - } - const htmlString = this.markdownIt.render(value); - this.unapplyTagClassMap(); - return this.sanitizer.sanitize(SecurityContext.HTML, htmlString); - } - - private applyTagClassMap(tagClassMap: Record) { - Object.entries(tagClassMap).forEach(([tag, classes]) => { - let tokenName; - switch (tag) { - case 'p': - tokenName = 'paragraph'; - break; - case 'h1': - case 'h2': - case 'h3': - case 'h4': - case 'h5': - case 'h6': - tokenName = 'heading'; - break; - case 'ul': - tokenName = 'bullet_list'; - break; - case 'ol': - tokenName = 'ordered_list'; - break; - case 'li': - tokenName = 'list_item'; - break; - case 'a': - tokenName = 'link'; - break; - case 'strong': - tokenName = 'strong'; - break; - case 'em': - tokenName = 'em'; - break; - } - - if (!tokenName) { - return; - } - - const key = `${tokenName}_open`; - const original = this.markdownIt.renderer.rules[key]; - this.originalClassMap.set(key, original); - - this.markdownIt.renderer.rules[key] = (tokens, idx, options, env, self) => { - const token = tokens[idx]; - for (const clazz of classes) { - token.attrJoin('class', clazz); - } - - if (original) { - return original.call(this, tokens, idx, options, env, self); - } else { - return self.renderToken(tokens, idx, options); - } - }; - }); - } - - private unapplyTagClassMap() { - for (const [key, original] of this.originalClassMap) { - this.markdownIt.renderer.rules[key] = original; - } - - this.originalClassMap.clear(); + return `
${value}
`; } } From aa8f5be4cc79460f446f59134eff7a39fb202cfd Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 12 Feb 2026 19:02:22 -0800 Subject: [PATCH 24/39] Inject the new markdown-it-angular renderer in the restaurants sample app. --- samples/client/angular/package.json | 8 +++++--- samples/client/angular/projects/restaurant/src/app/app.ts | 6 +++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/samples/client/angular/package.json b/samples/client/angular/package.json index af5c513be..11a0c8913 100644 --- a/samples/client/angular/package.json +++ b/samples/client/angular/package.json @@ -11,9 +11,9 @@ "serve:ssr:restaurant": "node dist/restaurant/server/server.mjs", "serve:ssr:rizzcharts": "node dist/rizzcharts/server/server.mjs", "serve:ssr:contact": "node dist/contact/server/server.mjs", - "build:web_core": "cd ../../../renderers/web_core && npm install && npm run build", + "build:renderer": "cd ../../../renderers && for dir in 'web_core' 'markdown/markdown-it-shared' 'markdown/markdown-it-angular'; do (cd \"$dir\" && npm install && npm run build); done", "serve:agent:restaurant": "cd ../../agent/adk/restaurant_finder && uv run .", - "demo:restaurant": "npm run build:web_core && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:restaurant\" \"npm start -- restaurant\"" + "demo:restaurant": "npm run build:renderer && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:restaurant\" \"npm start -- restaurant\"" }, "prettier": { "printWidth": 100, @@ -31,6 +31,7 @@ "dependencies": { "@a2a-js/sdk": "^0.3.4", "@a2ui/web_core": "file:../../../renderers/web_core", + "@a2ui/markdown-it-angular": "file:../../../renderers/markdown/markdown-it-angular", "@angular/cdk": "^20.2.10", "@angular/common": "^21.0.3", "@angular/compiler": "^21.0.3", @@ -82,6 +83,7 @@ }, "workspaces": [ "projects/*", - "../../../renderers/web_core" + "../../../renderers/web_core", + "../../../renderers/markdown/markdown-it-angular" ] } diff --git a/samples/client/angular/projects/restaurant/src/app/app.ts b/samples/client/angular/projects/restaurant/src/app/app.ts index 01ab562fe..d1d8b658a 100644 --- a/samples/client/angular/projects/restaurant/src/app/app.ts +++ b/samples/client/angular/projects/restaurant/src/app/app.ts @@ -14,16 +14,20 @@ limitations under the License. */ -import { MessageProcessor, Surface } from '@a2ui/angular'; +import { MessageProcessor, Surface, MarkdownRenderer } from '@a2ui/angular'; import * as Types from '@a2ui/web_core/types/types'; import { Component, DOCUMENT, inject, signal } from '@angular/core'; import { Client } from './client'; +import { MarkdownItMarkdownRenderer } from '@a2ui/markdown-it-angular'; @Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: 'app.css', imports: [Surface], + providers: [ + { provide: MarkdownRenderer, useClass: MarkdownItMarkdownRenderer }, + ] }) export class App { protected client = inject(Client); From 858201394527bce98f8a35cb1e163b18c94184d4 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 15:27:46 -0800 Subject: [PATCH 25/39] Share the prettier configuration across all subpackages --- renderers/markdown/.prettierrc | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 renderers/markdown/.prettierrc diff --git a/renderers/markdown/.prettierrc b/renderers/markdown/.prettierrc new file mode 100644 index 000000000..d6c16d7ee --- /dev/null +++ b/renderers/markdown/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] +} From c40fbb5f0adb1a4047252ae097de7f64ba52bad9 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:32:20 -0800 Subject: [PATCH 26/39] Use the Type from web_core for the shared markdown renderer. --- .../markdown-it-shared/src/markdown.ts | 12 +- .../markdown-it-shared/src/raw-markdown.ts | 112 ++++++------------ 2 files changed, 42 insertions(+), 82 deletions(-) diff --git a/renderers/markdown/markdown-it-shared/src/markdown.ts b/renderers/markdown/markdown-it-shared/src/markdown.ts index d1a5dac23..7041c05e2 100644 --- a/renderers/markdown/markdown-it-shared/src/markdown.ts +++ b/renderers/markdown/markdown-it-shared/src/markdown.ts @@ -14,10 +14,12 @@ limitations under the License. */ -import { rawMarkdownRenderer, TagClassMap } from "./raw-markdown.js"; -import { sanitizer } from "./sanitizer.js"; +import { rawMarkdownRenderer } from './raw-markdown.js'; +import { sanitizer } from './sanitizer.js'; +import * as Types from '@a2ui/web_core'; -// TODO: Do we need to export the TagClassMap type? +// Export the class and type for consumers +export { MarkdownItRenderer } from './raw-markdown.js'; /** * A Markdown renderer using markdown-it and dompurify. @@ -29,10 +31,8 @@ export const markdownRenderer = { * @param tagClassMap A map of tag names to classes. * @returns The rendered HTML as a string. */ - render: (value: string, tagClassMap?: TagClassMap) => { + render: (value: string, tagClassMap?: Types.MarkdownRendererTagClassMap) => { const htmlString = rawMarkdownRenderer.render(value, tagClassMap); return sanitizer.sanitize(htmlString); }, - - // TODO: Do we need an unsanitized renderer? }; diff --git a/renderers/markdown/markdown-it-shared/src/raw-markdown.ts b/renderers/markdown/markdown-it-shared/src/raw-markdown.ts index a17e5bf07..9d4fd1959 100644 --- a/renderers/markdown/markdown-it-shared/src/raw-markdown.ts +++ b/renderers/markdown/markdown-it-shared/src/raw-markdown.ts @@ -15,32 +15,23 @@ */ import markdownit from 'markdown-it'; -import { sanitizer } from "./sanitizer"; - -/** - * A map of tag names to classes to apply when rendering a tag. - * - * For example, the following TagClassMap would apply the `a2ui-paragraph` class - * to all `

` tags: - * - * `{ "p": ["a2ui-paragraph"] }` - */ -export type TagClassMap = Record; +import { sanitizer } from './sanitizer.js'; +import * as Types from '@a2ui/web_core'; /** * A pre-configured instance of markdown-it to render markdown in A2UI web. * * This renderer does not perform any sanitization of the outgoing HTML. */ -class MarkdownItCore { +export class MarkdownItRenderer { private markdownIt = markdownit({ highlight: (str, lang) => { switch (lang) { - case "html": { - const iframe = document.createElement("iframe"); - iframe.classList.add("html-view"); + case 'html': { + const iframe = document.createElement('iframe'); + iframe.classList.add('html-view'); iframe.srcdoc = str; - iframe.sandbox = ""; + iframe.sandbox.add(''); return iframe.innerHTML; } @@ -50,67 +41,40 @@ class MarkdownItCore { }, }); + constructor() { + this.registerTagClassMapRules(); + } + /** - * Applies a tag class map to the markdown-it renderer. - * - * @param tagClassMap The tag class map to apply. + * Registers rules to apply tag class maps from the environment. */ - private applyTagClassMap(tagClassMap: TagClassMap) { - Object.entries(tagClassMap).forEach(([tag]) => { - let tokenName; - switch (tag) { - case "p": - tokenName = "paragraph"; - break; - case "h1": - case "h2": - case "h3": - case "h4": - case "h5": - case "h6": - tokenName = "heading"; - break; - case "ul": - tokenName = "bullet_list"; - break; - case "ol": - tokenName = "ordered_list"; - break; - case "li": - tokenName = "list_item"; - break; - case "a": - tokenName = "link"; - break; - case "strong": - tokenName = "strong"; - break; - case "em": - tokenName = "em"; - break; - } - - if (!tokenName) { - return; - } + private registerTagClassMapRules() { + const rulesToProxy = [ + 'paragraph_open', + 'heading_open', + 'bullet_list_open', + 'ordered_list_open', + 'list_item_open', + 'link_open', + 'strong_open', + 'em_open', + ]; - const key = `${tokenName}_open`; - this.markdownIt.renderer.rules[key] = ( - tokens, - idx, - options, - _env, - self - ) => { + for (const ruleName of rulesToProxy) { + this.markdownIt.renderer.rules[ruleName] = (tokens, idx, options, env, self) => { const token = tokens[idx]; - const tokenClasses = tagClassMap[token.tag] ?? []; - for (const clazz of tokenClasses) { - token.attrJoin("class", clazz); + const tagClassMap = env?.tagClassMap as Types.MarkdownRendererTagClassMap | undefined; + + if (tagClassMap) { + const tokenClasses = tagClassMap[token.tag] ?? []; + for (const clazz of tokenClasses) { + token.attrJoin('class', clazz); + } } return self.renderToken(tokens, idx, options); }; - }); + } } /** @@ -120,12 +84,8 @@ class MarkdownItCore { * * This method does not perform any sanitization of the outgoing HTML. */ - render(value: string, tagClassMap?: TagClassMap) { - if (tagClassMap) { - this.applyTagClassMap(tagClassMap); - } - const htmlString = this.markdownIt.render(value); - return htmlString; + render(value: string, tagClassMap?: Types.MarkdownRendererTagClassMap) { + return this.markdownIt.render(value, { tagClassMap }); } } @@ -134,4 +94,4 @@ class MarkdownItCore { * * This renderer does not perform any sanitization of the outgoing HTML. */ -export const rawMarkdownRenderer = new MarkdownItCore(); +export const rawMarkdownRenderer = new MarkdownItRenderer(); From aa8ac063da34da13be1a1407c5b4c5a00e2b01d2 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:32:43 -0800 Subject: [PATCH 27/39] Add a test to markdown-it-shared --- .../markdown-it-shared/src/markdown.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 renderers/markdown/markdown-it-shared/src/markdown.test.ts diff --git a/renderers/markdown/markdown-it-shared/src/markdown.test.ts b/renderers/markdown/markdown-it-shared/src/markdown.test.ts new file mode 100644 index 000000000..9a0d70105 --- /dev/null +++ b/renderers/markdown/markdown-it-shared/src/markdown.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { MarkdownItRenderer } from './raw-markdown.js'; + +describe('MarkdownItRenderer', () => { + it('renders basic markdown', () => { + const renderer = new MarkdownItRenderer(); + const result = renderer.render('# Hello'); + assert.match(result, /

Hello<\/h1>/); + }); + + it('applies tag classes via tagClassMap', () => { + const renderer = new MarkdownItRenderer(); + const result = renderer.render('# Hello', { h1: ['custom-class'] }); + assert.match(result, /

Hello<\/h1>/); + }); + + it('applies multiple classes', () => { + const renderer = new MarkdownItRenderer(); + const result = renderer.render('para', { p: ['class1', 'class2'] }); + assert.match(result, /

para<\/p>/); + }); + + it('is stateless (tagClassMap does not persist)', () => { + const renderer = new MarkdownItRenderer(); + + // First render with class + const result1 = renderer.render('# Hello', { h1: ['persistent?'] }); + assert.match(result1, /class="persistent\?"/); + + // Second render without class + const result2 = renderer.render('# Hello'); + assert.doesNotMatch(result2, /class="persistent\?"/); + assert.match(result2, /

Hello<\/h1>/); + }); + + it('handles empty tagClassMap', () => { + const renderer = new MarkdownItRenderer(); + const result = renderer.render('# Hello', {}); + assert.match(result, /

Hello<\/h1>/); + }); +}); From 8430847c2bd0e483128fc45b6ccaf32a99af4737 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:33:24 -0800 Subject: [PATCH 28/39] Setup tests and formatting for package. Apply formatting and update README. --- .../markdown/markdown-it-shared/README.md | 30 ++++++++++++++++++- .../markdown/markdown-it-shared/package.json | 16 +++++++++- .../markdown-it-shared/prepare-publish.mjs | 4 +-- .../markdown-it-shared/src/sanitizer.ts | 2 +- .../markdown/markdown-it-shared/tsconfig.json | 5 +++- 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/renderers/markdown/markdown-it-shared/README.md b/renderers/markdown/markdown-it-shared/README.md index 0c3252526..97b9fe2d5 100644 --- a/renderers/markdown/markdown-it-shared/README.md +++ b/renderers/markdown/markdown-it-shared/README.md @@ -3,4 +3,32 @@ Markdown renderer for A2UI using markdown-it and dompurify. This is used across all JS renderers, so the configuration is consistent. Each renderer has a specific facade package that uses this renderer as a dependency. -End users should use the facade package for their renderer of choice. +End users should use the facade package for their renderer of choice. + +## Development + +### Build + +Run `npm run build` to build the project. The build artifacts will be stored in the `dist/` directory. + +### Running unit tests + +Run `npm test` to execute the unit tests. + +### Code Formatting + +This project uses [Prettier](https://prettier.io/) for code formatting. The configuration is defined in `../.prettierrc`. + +To format all files in the project: + +```bash +npm run format +``` + +To check the format, run: + +```bash +npx prettier --check . +``` + +Most IDEs (like VS Code) can be configured to **Format On Save** using the local Prettier version and configuration. This is the recommended workflow. diff --git a/renderers/markdown/markdown-it-shared/package.json b/renderers/markdown/markdown-it-shared/package.json index 3d58a0bd4..b115c9e30 100644 --- a/renderers/markdown/markdown-it-shared/package.json +++ b/renderers/markdown/markdown-it-shared/package.json @@ -22,7 +22,10 @@ "scripts": { "prepack": "npm run build", "build": "wireit", - "build:tsc": "wireit" + "build:tsc": "wireit", + "test": "wireit", + "format": "prettier --ignore-path ../.gitignore --write .", + "format:check": "prettier --ignore-path ../.gitignore --check ." }, "wireit": { "build": { @@ -42,6 +45,12 @@ "!dist/**/*.min.js{,.map}" ], "clean": "if-file-deleted" + }, + "test": { + "command": "node --test --enable-source-maps --test-reporter spec dist/src/*.test.js", + "dependencies": [ + "build" + ] } }, "keywords": [], @@ -51,9 +60,14 @@ "url": "https://github.com/google/A2UI/issues" }, "homepage": "https://github.com/google/A2UI/tree/main/web#readme", + "peerDependencies": { + "@a2ui/web_core": "file:../../web_core" + }, "devDependencies": { + "@a2ui/web_core": "file:../../web_core", "@types/markdown-it": "^14.1.2", "@types/node": "^24.10.1", + "prettier": "^3.4.2", "typescript": "^5.8.3", "wireit": "^0.15.0-pre.2" }, diff --git a/renderers/markdown/markdown-it-shared/prepare-publish.mjs b/renderers/markdown/markdown-it-shared/prepare-publish.mjs index 73185139a..4635c3229 100644 --- a/renderers/markdown/markdown-it-shared/prepare-publish.mjs +++ b/renderers/markdown/markdown-it-shared/prepare-publish.mjs @@ -54,7 +54,7 @@ if (litPkg.exports) { writeFileSync(join(distDir, 'package.json'), JSON.stringify(litPkg, null, 2)); // 6. Copy README and LICENSE -['README.md', 'LICENSE'].forEach(file => { +['README.md', 'LICENSE'].forEach((file) => { const src = join(dirname, file); if (!existsSync(src)) { throw new Error(`Missing required file for publishing: ${file}`); @@ -70,4 +70,4 @@ function adjustPath(p) { return './' + p.substring(7); // Remove ./dist/ } return p; -} \ No newline at end of file +} diff --git a/renderers/markdown/markdown-it-shared/src/sanitizer.ts b/renderers/markdown/markdown-it-shared/src/sanitizer.ts index fcd13bcfe..2573d0e6a 100644 --- a/renderers/markdown/markdown-it-shared/src/sanitizer.ts +++ b/renderers/markdown/markdown-it-shared/src/sanitizer.ts @@ -21,4 +21,4 @@ */ export const sanitizer = { sanitize: (html: string) => html, -} +}; diff --git a/renderers/markdown/markdown-it-shared/tsconfig.json b/renderers/markdown/markdown-it-shared/tsconfig.json index f928e3840..5348e6967 100644 --- a/renderers/markdown/markdown-it-shared/tsconfig.json +++ b/renderers/markdown/markdown-it-shared/tsconfig.json @@ -29,7 +29,10 @@ "strict": true, "noUnusedLocals": false, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "paths": { + "@a2ui/web_core": ["../../web_core"] + } }, "include": ["src/**/*.ts", "src/**/*.json"] } From 8ac9baec4fc2301ecb5b7dd24981d7fba777e8cc Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:34:16 -0800 Subject: [PATCH 29/39] Add test for the markdown integration. --- .../markdown-it-lit/src/markdown.test.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 renderers/markdown/markdown-it-lit/src/markdown.test.ts diff --git a/renderers/markdown/markdown-it-lit/src/markdown.test.ts b/renderers/markdown/markdown-it-lit/src/markdown.test.ts new file mode 100644 index 000000000..7ac0d2023 --- /dev/null +++ b/renderers/markdown/markdown-it-lit/src/markdown.test.ts @@ -0,0 +1,78 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import assert from 'node:assert'; +import { describe, it, mock } from 'node:test'; +import { MarkdownItDirective } from './markdown.js'; +import { markdownRenderer } from '@a2ui/markdown-it-shared'; +import { noChange } from 'lit'; +import { PartInfo, PartType } from 'lit/directive.js'; + +describe('MarkdownItDirective', () => { + const partInfo: PartInfo = { + type: PartType.CHILD, + }; + + it('should render markdown using markdownRenderer', () => { + const directive = new MarkdownItDirective(partInfo); + const renderSpy = mock.method(markdownRenderer, 'render', () => '

test

'); + + const result = directive.render('test', { p: ['class1'] }); + + assert.strictEqual(renderSpy.mock.callCount(), 1); + assert.deepStrictEqual(renderSpy.mock.calls[0].arguments, ['test', { p: ['class1'] }]); + + // unsafeHTML returns a symbol-like object, checking strict equality might be tricky + // but we can check if it looks like what we expect or just rely on it being the result of unsafeHTML + // For now, let's assume if it doesn't throw and calls render, it's working. + // We can check if the result is not null/undefined. + assert.ok(result); + }); + + it('should not re-render if value and tagClassMap are effectively the same', () => { + const directive = new MarkdownItDirective(partInfo); + const renderSpy = mock.method(directive, 'render', () => 'rendered'); + + // First render + directive.update({} as any, ['test', { p: ['class1'] }]); + assert.strictEqual(renderSpy.mock.callCount(), 1); + + // Second render with same values + const result = directive.update({} as any, ['test', { p: ['class1'] }]); + assert.strictEqual(renderSpy.mock.callCount(), 1); + assert.strictEqual(result, noChange); + }); + + it('should re-render if value changes', () => { + const directive = new MarkdownItDirective(partInfo); + const renderSpy = mock.method(directive, 'render', () => 'rendered'); + + directive.update({} as any, ['test1', {}]); + directive.update({} as any, ['test2', {}]); + + assert.strictEqual(renderSpy.mock.callCount(), 2); + }); + + it('should re-render if tagClassMap changes', () => { + const directive = new MarkdownItDirective(partInfo); + const renderSpy = mock.method(directive, 'render', () => 'rendered'); + + directive.update({} as any, ['test', { p: ['c1'] }]); + directive.update({} as any, ['test', { p: ['c2'] }]); + + assert.strictEqual(renderSpy.mock.callCount(), 2); + }); +}); From e1832c04a59697d3150c4b43e57c884648bdc99f Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:34:33 -0800 Subject: [PATCH 30/39] Setup tests and formatting for package. Apply formatting and update README. --- renderers/markdown/markdown-it-lit/README.md | 28 +++++++++++++++++++ .../markdown/markdown-it-lit/package.json | 27 ++++++++++++++---- .../markdown-it-lit/prepare-publish.mjs | 4 +-- .../markdown/markdown-it-lit/src/markdown.ts | 28 ++++++++----------- 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/renderers/markdown/markdown-it-lit/README.md b/renderers/markdown/markdown-it-lit/README.md index 53e88e951..3bcd473b7 100644 --- a/renderers/markdown/markdown-it-lit/README.md +++ b/renderers/markdown/markdown-it-lit/README.md @@ -1 +1,29 @@ A2UI markdown renderer for Lit. + +## Development + +### Build + +Run `npm run build` to build the project. The build artifacts will be stored in the `dist/` directory. + +### Running unit tests + +Run `npm test` to execute the unit tests. + +### Code Formatting + +This project uses [Prettier](https://prettier.io/) for code formatting. The configuration is defined in `../.prettierrc`. + +To format all files in the project: + +```bash +npm run format +``` + +To check the format, run: + +```bash +npx prettier --check . +``` + +Most IDEs (like VS Code) can be configured to **Format On Save** using the local Prettier version and configuration. This is the recommended workflow. diff --git a/renderers/markdown/markdown-it-lit/package.json b/renderers/markdown/markdown-it-lit/package.json index 59cf91d54..8802479f5 100644 --- a/renderers/markdown/markdown-it-lit/package.json +++ b/renderers/markdown/markdown-it-lit/package.json @@ -17,12 +17,18 @@ "url": "git+https://github.com/google/A2UI.git" }, "files": [ - "dist/src" + "dist/src/**/*.js", + "dist/src/**/*.d.ts", + "dist/src/**/*.map", + "!dist/src/**/*.test.*" ], "scripts": { "prepack": "npm run build", "build": "wireit", - "build:tsc": "wireit" + "build:tsc": "wireit", + "test": "wireit", + "format": "prettier --ignore-path ../.gitignore --write .", + "format:check": "prettier --ignore-path ../.gitignore --check ." }, "wireit": { "build": { @@ -42,6 +48,12 @@ "!dist/**/*.min.js{,.map}" ], "clean": "if-file-deleted" + }, + "test": { + "command": "node --test --enable-source-maps --test-reporter spec dist/src/*.test.js", + "dependencies": [ + "build" + ] } }, "keywords": [], @@ -51,16 +63,19 @@ "url": "https://github.com/google/A2UI/issues" }, "homepage": "https://github.com/google/A2UI/tree/main/web#readme", + "peerDependencies": { + "@a2ui/web_core": "file:../../web_core" + }, "devDependencies": { + "@a2ui/web_core": "file:../../web_core", "@types/node": "^24.10.1", + "prettier": "^3.8.1", "typescript": "^5.8.3", "wireit": "^0.15.0-pre.2" }, "dependencies": { - "@lit/context": "^1.1.4", - "lit": "^3.3.1", - "@a2ui/lit": "file:../../lit", "@a2ui/markdown-it-shared": "file:../markdown-it-shared", - "@a2ui/web_core": "file:../../web_core" + "@lit/context": "^1.1.4", + "lit": "^3.3.1" } } diff --git a/renderers/markdown/markdown-it-lit/prepare-publish.mjs b/renderers/markdown/markdown-it-lit/prepare-publish.mjs index 73185139a..4635c3229 100644 --- a/renderers/markdown/markdown-it-lit/prepare-publish.mjs +++ b/renderers/markdown/markdown-it-lit/prepare-publish.mjs @@ -54,7 +54,7 @@ if (litPkg.exports) { writeFileSync(join(distDir, 'package.json'), JSON.stringify(litPkg, null, 2)); // 6. Copy README and LICENSE -['README.md', 'LICENSE'].forEach(file => { +['README.md', 'LICENSE'].forEach((file) => { const src = join(dirname, file); if (!existsSync(src)) { throw new Error(`Missing required file for publishing: ${file}`); @@ -70,4 +70,4 @@ function adjustPath(p) { return './' + p.substring(7); // Remove ./dist/ } return p; -} \ No newline at end of file +} diff --git a/renderers/markdown/markdown-it-lit/src/markdown.ts b/renderers/markdown/markdown-it-lit/src/markdown.ts index 106b59eed..ab8bc23e3 100644 --- a/renderers/markdown/markdown-it-lit/src/markdown.ts +++ b/renderers/markdown/markdown-it-lit/src/markdown.ts @@ -14,17 +14,11 @@ limitations under the License. */ -import { noChange } from "lit"; -import { - Directive, - DirectiveParameters, - DirectiveResult, - Part, - directive, -} from "lit/directive.js"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import { markdownRenderer } from "@a2ui/markdown-it-shared"; -import * as Types from "@a2ui/web_core/types/types"; +import { noChange } from 'lit'; +import { Directive, DirectiveParameters, DirectiveResult, Part, directive } from 'lit/directive.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { markdownRenderer } from '@a2ui/markdown-it-shared'; +import * as Types from '@a2ui/web_core/types/types'; /** * A Lit directive that renders markdown to HTML. @@ -32,15 +26,17 @@ import * as Types from "@a2ui/web_core/types/types"; * This directive is intended to be used by the A2UI Lit renderer to render * markdown to HTML. */ -class MarkdownItDirective extends Directive implements Types.MarkdownRenderer { +export class MarkdownItDirective + extends Directive + implements Types.MarkdownRenderer +{ private lastValue: string | null = null; private lastTagClassMap: string | null = null; update(_part: Part, [value, tagClassMap]: DirectiveParameters) { - if ( - this.lastValue === value && - JSON.stringify(tagClassMap) === this.lastTagClassMap - ) { + // Check if the value and tagClassMap are the same as the last time. + // If they are, return noChange to avoid re-rendering. + if (this.lastValue === value && JSON.stringify(tagClassMap) === this.lastTagClassMap) { return noChange; } From 613fdd8a44d55dade68875426332da1fbeb687cb Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:35:14 -0800 Subject: [PATCH 31/39] Update prettier configuration and README --- renderers/markdown/markdown-it-angular/README.md | 8 +++++++- .../markdown/markdown-it-angular/package.json | 16 ++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/renderers/markdown/markdown-it-angular/README.md b/renderers/markdown/markdown-it-angular/README.md index 583532796..f51dc89e1 100644 --- a/renderers/markdown/markdown-it-angular/README.md +++ b/renderers/markdown/markdown-it-angular/README.md @@ -18,7 +18,7 @@ npm run test:watch ### Code Formatting -This project uses [Prettier](https://prettier.io/) for code formatting. The configuration is defined in `package.json`. +This project uses [Prettier](https://prettier.io/) for code formatting. The configuration is defined in `../.prettierrc`. To format all files in the project: @@ -26,4 +26,10 @@ To format all files in the project: npm run format ``` +To check the format, run: + +```bash +npx prettier --check . +``` + Most IDEs (like VS Code) can be configured to **Format On Save** using the local Prettier version and configuration. This is the recommended workflow. diff --git a/renderers/markdown/markdown-it-angular/package.json b/renderers/markdown/markdown-it-angular/package.json index 5ee84c193..9a17acdc0 100644 --- a/renderers/markdown/markdown-it-angular/package.json +++ b/renderers/markdown/markdown-it-angular/package.json @@ -10,7 +10,8 @@ "build": "ng build", "test": "ng test --watch=false --browsers=ChromeHeadless", "test:watch": "ng test --browsers=ChromeHeadless", - "format": "prettier --write ." + "format": "prettier --ignore-path ../.gitignore --write .", + "format:check": "prettier --ignore-path ../.gitignore --check ." }, "dependencies": { "@a2ui/markdown-it-shared": "file:../markdown-it-shared", @@ -41,19 +42,6 @@ "karma-jasmine-html-reporter": "^2.1.0", "ng-packagr": "^21.0.0", "prettier": "^3.8.1", - "tslib": "^2.8.1", "typescript": "~5.9.2" - }, - "prettier": { - "printWidth": 100, - "singleQuote": true, - "overrides": [ - { - "files": "*.html", - "options": { - "parser": "angular" - } - } - ] } } From 1a5d9edaf74a814f6490f04edbfb1aa3bdc04ce6 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:36:15 -0800 Subject: [PATCH 32/39] Add dist and .wireit directories to gitignore --- renderers/markdown/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/renderers/markdown/.gitignore b/renderers/markdown/.gitignore index 66ae216ef..acd8f16db 100644 --- a/renderers/markdown/.gitignore +++ b/renderers/markdown/.gitignore @@ -1,2 +1,3 @@ package-lock.json - +.wireit +dist From 79e9809322e4c99e8d94609a5988c05523e3f02a Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:37:02 -0800 Subject: [PATCH 33/39] Improve jsdoc of the MarkdownRendererTagClassMap type --- renderers/web_core/src/v0_8/types/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/renderers/web_core/src/v0_8/types/types.ts b/renderers/web_core/src/v0_8/types/types.ts index 00f56948c..ce4a1d74e 100644 --- a/renderers/web_core/src/v0_8/types/types.ts +++ b/renderers/web_core/src/v0_8/types/types.ts @@ -533,6 +533,11 @@ export interface Surface { /** * A list of classnames to be applied to a tag when rendering Markdown. + * + * For example, the following TagClassMap would apply the `a2ui-paragraph` class + * to all `

` tags: + * + * `{ "p": ["a2ui-paragraph"] }` */ export type MarkdownRendererTagClassMap = Record; From 68b54bdd19af187589e0d01b389e8bc79bf8f04a Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:37:52 -0800 Subject: [PATCH 34/39] Update error message of the Minimal Markdown renderer --- renderers/lit/src/0.8/ui/directives/minimal_markdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts b/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts index c49db55a8..cd1f06091 100644 --- a/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts +++ b/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts @@ -40,7 +40,7 @@ class MinimalMarkdownRendererDirective extends Directive implements Types.Markdo if (!MinimalMarkdownRendererDirective._warned) { console.warn("[MinimalMarkdownRendererDirective]", "is a placeholder Markdown renderer. Most features are not supported.\n", - "Install `@a2ui/lit-markdown` for a fully-featured Markdown renderer."); + "Use `@a2ui/markdown-it-lit` for a fully-featured Markdown renderer."); MinimalMarkdownRendererDirective._warned = true; } } From 4d0d44e166918ecdad985b9380672ff7378c2ffa Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:49:57 -0800 Subject: [PATCH 35/39] Make the markdown renderer in angular noop. --- renderers/angular/src/lib/data/markdown.ts | 71 ++++++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/renderers/angular/src/lib/data/markdown.ts b/renderers/angular/src/lib/data/markdown.ts index f31c6055c..c6aa7007b 100644 --- a/renderers/angular/src/lib/data/markdown.ts +++ b/renderers/angular/src/lib/data/markdown.ts @@ -14,15 +14,74 @@ limitations under the License. */ -import { /*inject,*/ Injectable } from '@angular/core'; -// import { DomSanitizer } from '@angular/platform-browser'; +import { Injectable } from '@angular/core'; +import * as Types from "@a2ui/web_core/types/types"; -// TODO: Make this a noop/minimal Markdown renderer +/** + * A minimal Markdown renderer that only supports a few tags. + * + * Configure @a2ui/markdown-it-angular, or build a custom Markdown renderer + * to actually parse and render Markdown in your app. + */ @Injectable({ providedIn: 'root' }) export class MarkdownRenderer { - // private sanitizer = inject(DomSanitizer); - render(value: string, tagClassMap?: Record) { - return `

${value}
`; + static _warned = false; + + constructor() { + /** + * Warn the user in case they need actual Markdown rendering. + */ + if (!MarkdownRenderer._warned) { + console.warn("[MarkdownRenderer]", + "is a placeholder Markdown renderer. Most features are not supported.\n", + "Use `@a2ui/markdown-it-angular` for a fully-featured Markdown renderer."); + MarkdownRenderer._warned = true; + } + } + + /** + * Attempts to render some Markdown. + * + * Resist the urge to "improve" this method. Instead, use a more proper + * Markdown renderer. This is just a placeholder. + */ + render(markdown: string, _tagClassMap?: Types.MarkdownRendererTagClassMap) : string { + if (markdown.startsWith('# ')) { + let classes = this.getClasses("h1", _tagClassMap); + return `

${markdown.substring(2)}

`; + } + if (markdown.startsWith('## ')) { + let classes = this.getClasses("h2", _tagClassMap); + return `

${markdown.substring(3)}

`; + } + if (markdown.startsWith('### ')) { + let classes = this.getClasses("h3", _tagClassMap); + return `

${markdown.substring(4)}

`; + } + if (markdown.startsWith('#### ')) { + let classes = this.getClasses("h4", _tagClassMap); + return `

${markdown.substring(5)}

`; + } + if (markdown.startsWith('##### ')) { + let classes = this.getClasses("h5", _tagClassMap); + return `
${markdown.substring(6)}
`; + } + if (markdown.startsWith('*') && markdown.endsWith('*')) { + let classes = this.getClasses("strong", _tagClassMap); + return `${markdown.substring(1, markdown.length - 1)}`; + } + let classes = this.getClasses("p", _tagClassMap); + return `

${markdown}

`; + } + + /** + * Returns a space-separated lists of clases for `tag` from `_tagClassMap`. + * @param tag The name of the tag ("h1", "p", etc.) + * @param _tagClassMap The class map to use. + * @returns A space-separated list of class names. + */ + getClasses(tag: string, _tagClassMap?: Types.MarkdownRendererTagClassMap) { + return _tagClassMap?.[tag]?.join(' ') ?? ''; } } From 118a9a19f4166af8a0d8213b64ddaca78210ff09 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:50:11 -0800 Subject: [PATCH 36/39] Remove the markdown-it dependency from the angular renderer. --- renderers/angular/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/renderers/angular/package.json b/renderers/angular/package.json index 2be6aef48..da572bdfb 100644 --- a/renderers/angular/package.json +++ b/renderers/angular/package.json @@ -6,7 +6,6 @@ }, "dependencies": { "@a2ui/web_core": "file:../web_core", - "markdown-it": "^14.1.0", "tslib": "^2.3.0" }, "peerDependencies": { @@ -22,7 +21,6 @@ "@angular/core": "^21.0.0", "@types/express": "^5.0.1", "@types/jasmine": "~5.1.0", - "@types/markdown-it": "^14.1.2", "@types/node": "^20.17.19", "@types/uuid": "^10.0.0", "@vitest/browser": "^4.0.15", From 5702a9c238e887e32114a819f6310b78483c2f53 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 16:50:54 -0800 Subject: [PATCH 37/39] Update the jsdoc of Lit mininal markdown to point to markdown-it-lit --- renderers/lit/src/0.8/ui/directives/minimal_markdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts b/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts index cd1f06091..886bf1df7 100644 --- a/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts +++ b/renderers/lit/src/0.8/ui/directives/minimal_markdown.ts @@ -26,7 +26,7 @@ import * as Types from "@a2ui/web_core/types/types"; /** * A minimal Markdown renderer that only supports a few tags. * - * Configure @a2ui/lit-markdown, or your custom Markdown renderer + * Configure @a2ui/markdown-it-lit, or your custom Markdown renderer * to actually parse and render Markdown in your app. */ class MinimalMarkdownRendererDirective extends Directive implements Types.MarkdownRenderer { From 36d8e07f5e794aff53ef2c9609a01c2f13f695fa Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 17:05:44 -0800 Subject: [PATCH 38/39] Use the build:renderer angular sample target to prepare samples. --- .github/workflows/ng_build_and_test.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ng_build_and_test.yml b/.github/workflows/ng_build_and_test.yml index 1658b79c4..6b13c8b41 100644 --- a/.github/workflows/ng_build_and_test.yml +++ b/.github/workflows/ng_build_and_test.yml @@ -51,13 +51,9 @@ jobs: working-directory: ./renderers/lit run: npm run build - - name: Install renderer deps - working-directory: ./renderers/angular - run: npm i - - - name: Build Angular renderer - working-directory: ./renderers/angular - run: npm run build + - name: Build Angular renderer and its dependencies + working-directory: ./samples/client/angular + run: npm run build:renderer - name: Install top-level deps working-directory: ./samples/client/angular From 98cda53c265ad34d23e2beb07f7ec4eb0afd9af9 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 13 Feb 2026 17:10:49 -0800 Subject: [PATCH 39/39] Do not run ng before installing it --- .github/workflows/ng_build_and_test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ng_build_and_test.yml b/.github/workflows/ng_build_and_test.yml index 6b13c8b41..8153babdd 100644 --- a/.github/workflows/ng_build_and_test.yml +++ b/.github/workflows/ng_build_and_test.yml @@ -51,14 +51,14 @@ jobs: working-directory: ./renderers/lit run: npm run build - - name: Build Angular renderer and its dependencies - working-directory: ./samples/client/angular - run: npm run build:renderer - - name: Install top-level deps working-directory: ./samples/client/angular run: npm i + - name: Build Angular renderer and its dependencies + working-directory: ./samples/client/angular + run: npm run build:renderer + - name: Build contact sample working-directory: ./samples/client/angular run: npm run build contact