Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions renderers/lit/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.9.1

- \[v0_9\] Modify Text widget from the basic catalog to support markdown.
- \[v0_9\] Add `Context.markdown` to the public API
- \[CI\] Fix post-build script.

## 0.8.4

- Add a `v0_9` renderer. Import from `@a2ui/lit/v0_9`.
Expand Down
7 changes: 4 additions & 3 deletions renderers/lit/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion renderers/lit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@a2ui/lit",
"version": "0.9.0",
"version": "0.9.1",
"description": "A2UI Lit Library",
"author": "Google",
"license": "Apache-2.0",
Expand Down
43 changes: 32 additions & 11 deletions renderers/lit/src/v0_9/catalogs/basic/components/Text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@

import { html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { consume } from "@lit/context";
import { TextApi } from "@a2ui/web_core/v0_9/basic_catalog";
import { A2uiLitElement, A2uiController } from "@a2ui/lit/v0_9";
import { A2uiLitElement, A2uiController, Context } from "@a2ui/lit/v0_9";
import * as Types from "@a2ui/web_core/types/types";

import { markdown } from "../../../directives/directives.js";

@customElement("a2ui-basic-text")
export class A2uiBasicTextElement extends A2uiLitElement<typeof TextApi> {

// Retrieve a MarkdownRenderer provided by the application.
@consume({ context: Context.markdown, subscribe: true })
accessor markdownRenderer: Types.MarkdownRenderer | undefined;

protected createController() {
return new A2uiController(this, TextApi);
}
Expand All @@ -29,23 +38,35 @@ export class A2uiBasicTextElement extends A2uiLitElement<typeof TextApi> {
const props = this.controller.props;
if (!props) return nothing;

const variant = props.variant ?? "body";
switch (variant) {
// Use props.variant to convert props.text to markdown
let markdownText = props.text;
switch (props.variant) {
case "h1":
return html`<h1>${props.text}</h1>`;
markdownText = `# ${markdownText}`;
break;
case "h2":
return html`<h2>${props.text}</h2>`;
markdownText = `## ${markdownText}`;
break;
case "h3":
return html`<h3>${props.text}</h3>`;
markdownText = `### ${markdownText}`;
break;
case "h4":
return html`<h4>${props.text}</h4>`;
markdownText = `#### ${markdownText}`;
break;
case "h5":
return html`<h5>${props.text}</h5>`;
case "caption":
return html`<span class="a2ui-caption">${props.text}</span>`;
markdownText = `##### ${markdownText}`;
break;
default:
return html`<p>${props.text}</p>`;
break; // body and caption.
}

const renderedMarkdown = markdown(markdownText, this.markdownRenderer);
// There's not a good way to handle the caption variant in markdown, so we
// tag it with a class so it can be tweaked via CSS.
if (props.variant === "caption") {
return html`<span class="a2ui-caption">${renderedMarkdown}</span>`;
}
return html`${renderedMarkdown}`;
}
}

Expand Down
24 changes: 24 additions & 0 deletions renderers/lit/src/v0_9/context/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright 2026 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
*
* http://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 { markdown } from "./markdown.js";

/**
* Contexts used to inject dependencies into the Lit renderer.
*/
export const Context = {
markdown,
};
25 changes: 25 additions & 0 deletions renderers/lit/src/v0_9/context/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { createContext } from "@lit/context";
import * as Types from "@a2ui/web_core/types/types";

/**
* The markdown renderer context.
*
* This is used by the Text widget to render markdown content.
*/
export const markdown = createContext<Types.MarkdownRenderer | undefined>(Symbol("A2UIMarkdown"));
17 changes: 17 additions & 0 deletions renderers/lit/src/v0_9/directives/directives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export { markdown } from "./markdown.js";
74 changes: 74 additions & 0 deletions renderers/lit/src/v0_9/directives/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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, noChange } from "lit";
import {
Directive,
DirectiveParameters,
Part,
directive,
} from "lit/directive.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { until } from "lit/directives/until.js";
import * as Types from "@a2ui/web_core/types/types";

class MarkdownDirective extends Directive {
private lastValue: string | null = null;
private lastTagClassMap: string | null = null;

update(_part: Part, [value, markdownRenderer, markdownOptions]: DirectiveParameters<this>) {
const jsonTagClassMap = JSON.stringify(markdownOptions?.tagClassMap);
if (
this.lastValue === value &&
jsonTagClassMap === this.lastTagClassMap
) {
return noChange;
}

this.lastValue = value;
this.lastTagClassMap = jsonTagClassMap;
return this.render(value, markdownRenderer, markdownOptions);
}

private static defaultMarkdownWarningLogged = false;
/**
* Renders the markdown string to HTML using the injected markdown renderer,
* if present. Otherwise, it returns the value wrapped in a span.
*/
render(value: string, markdownRenderer?: Types.MarkdownRenderer, markdownOptions?: Types.MarkdownRendererOptions) {
if (markdownRenderer) {
const rendered = markdownRenderer(value, markdownOptions).then((value) => {
// `value` is a plain string, which we need to convert to a template
// with the `unsafeHTML` directive.
// It is the responsibility of the markdown renderer to sanitize the HTML.
return unsafeHTML(value);
})
// The until directive lets us render a placeholder *until* the rendered
// content resolves.
return until(rendered, html`<span class="no-markdown-renderer">${value}</span>`);
}

if (!MarkdownDirective.defaultMarkdownWarningLogged) {
console.warn("[MarkdownDirective]",
"can't render markdown because no markdown renderer is configured.\n",
"Use `@a2ui/markdown-it`, or your own markdown renderer.");
MarkdownDirective.defaultMarkdownWarningLogged = true;
}
return html`<span class="no-markdown-renderer">${value}</span>`;
}
}

export const markdown = directive(MarkdownDirective);
1 change: 1 addition & 0 deletions renderers/lit/src/v0_9/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export type { LitComponentApi } from "./types.js";
export { A2uiController } from "./a2ui-controller.js";
export { A2uiSurface } from "./surface/a2ui-surface.js";
export { A2uiLitElement } from "./a2ui-lit-element.js";
export { Context } from "./context/context.js";
export { minimalCatalog } from "./catalogs/minimal/index.js";
export { basicCatalog } from "./catalogs/basic/index.js";
91 changes: 91 additions & 0 deletions renderers/lit/src/v0_9/tests/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 { setupTestDom, teardownTestDom, asyncUpdate } from "./dom-setup.js";
import assert from "node:assert";
import { describe, it, before, after } from "node:test";
import * as Types from "@a2ui/web_core/types/types";

describe("Markdown Directive", () => {
before(() => {
// Set up the DOM before any lit imports in the tests
setupTestDom();
});

after(() => {
teardownTestDom();
});

it("should render fallback when no renderer is provided", async () => {
const { html, render } = await import("lit");
const { markdown } = await import("../directives/markdown.js");

const container = document.createElement("div");

// Render the directive directly into our container
render(html`<div>${markdown("Hello world")}</div>`, container);

const htmlContent = container.innerHTML;
assert.ok(
htmlContent.includes("no-markdown-renderer"),
"Should render fallback span class",
);
assert.ok(
container.textContent?.includes("Hello world"),
"Should render fallback text properly",
);
});

it("should render parsed markdown when renderer is provided", async () => {
const { html, render } = await import("lit");
const { markdown } = await import("../directives/markdown.js");

const container = document.createElement("div");

let resolveRenderer: (value: string) => void;
// Leak the `resolve` function of this promise to the `resolveRenderer`
// variable, so we can call it later in the test.
const renderPromise = new Promise<string>((resolve) => {
resolveRenderer = resolve;
});
// Mock a markdown renderer that resolves by calling `resolveRenderer`
const mockRenderer: Types.MarkdownRenderer = async () => renderPromise;

// Render the directive with our mock renderer
render(
html`<div>${markdown("Hello markdown", mockRenderer)}</div>`,
container,
);

// Before resolution, should show the placeholder (until directive)
assert.ok(container.innerHTML.includes("no-markdown-renderer"));

// Resolve the promise via asyncUpdate so it yields to the macro-task queue
await asyncUpdate(container, () => {
resolveRenderer!("<b>Rendered HTML</b>");
});

const htmlContent = container.innerHTML;
assert.ok(
htmlContent.includes("<b>Rendered HTML</b>"),
"Should render the HTML from the renderer",
);
assert.ok(
!htmlContent.includes("no-markdown-renderer"),
"Placeholder should be gone",
);
});
});
2 changes: 1 addition & 1 deletion renderers/markdown/markdown-it/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading