From 30b8e7a7448145db14ca4bf4c6447299bc18d014 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 10:23:08 -0800 Subject: [PATCH 01/15] 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 36f417684e0484bb4c35ceca224761db06499435 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 10:26:13 -0800 Subject: [PATCH 02/15] 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 130702915842273941ee2e777b1ceaecab2088e1 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 14:50:33 -0800 Subject: [PATCH 03/15] 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 94276925667008b4c26f82fa28a84287c0a6b1c2 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 14:51:19 -0800 Subject: [PATCH 04/15] 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 9de5c17e78d8525f21b74f68bc6de18918f7f07f Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:30:32 -0800 Subject: [PATCH 05/15] 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 be3d98418da697cefda046501b720d3b210bcd27 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:32:47 -0800 Subject: [PATCH 06/15] 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 92f4dbcbafd4670f78c5fdf7880687e478d72b52 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:35:05 -0800 Subject: [PATCH 07/15] 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 269094ad9010691d7c5256a7aaf007117cb4a763 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:36:56 -0800 Subject: [PATCH 08/15] 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 f1d673756c991e5a43a8531bfefde6f235e5e221 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 15:37:17 -0800 Subject: [PATCH 09/15] Update miscellaneous lockfiles --- renderers/lit/package-lock.json | 3 +-- samples/agent/adk/uv.lock | 4 +--- samples/client/lit/package-lock.json | 23 ++++++++++++++++++++++- samples/client/lit/shell/package.json | 3 ++- 4 files changed, 26 insertions(+), 7 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/agent/adk/uv.lock b/samples/agent/adk/uv.lock index 48cf6be77..bbba9aba0 100644 --- a/samples/agent/adk/uv.lock +++ b/samples/agent/adk/uv.lock @@ -52,6 +52,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "pyink", specifier = ">=24.10.0" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, ] @@ -1145,7 +1146,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, @@ -1153,7 +1153,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, @@ -1161,7 +1160,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, 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 0264f5866..9ffefb92a 100644 --- a/samples/client/lit/shell/package.json +++ b/samples/client/lit/shell/package.json @@ -77,10 +77,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 b46e5f6f1361b9dfdb1f47b2f4ca4bfff9dea1f7 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 18:53:04 -0800 Subject: [PATCH 10/15] 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 89de7ede42974d9fb255691980ff3e5291317d38 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 18:53:53 -0800 Subject: [PATCH 11/15] 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 8a8e527d5a603c6dbce12ca48e7a492245c040ad Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 18:54:09 -0800 Subject: [PATCH 12/15] 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 ecf80b14bae83e17c5067c71b3f18e3bfa403782 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Feb 2026 19:28:21 -0800 Subject: [PATCH 13/15] 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 731bd50fb884547395897fcfd65b223d2a42f321 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 4 Feb 2026 15:38:41 -0800 Subject: [PATCH 14/15] 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 63cdbaa92e3c363f6db670a1d664991326c6fb4c Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 4 Feb 2026 15:49:43 -0800 Subject: [PATCH 15/15] 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