diff --git a/renderers/angular/src/lib/catalog/text.ts b/renderers/angular/src/lib/catalog/text.ts
index 631849760..eba0228c0 100644
--- a/renderers/angular/src/lib/catalog/text.ts
+++ b/renderers/angular/src/lib/catalog/text.ts
@@ -22,6 +22,7 @@ import {
input,
ViewEncapsulation,
} from '@angular/core';
+import { AsyncPipe } from '@angular/common';
import { DynamicComponent } from '../rendering/dynamic-component';
import * as Primitives from '@a2ui/web_core/types/primitives';
import * as Styles from '@a2ui/web_core/styles/index';
@@ -45,10 +46,11 @@ interface HintedStyles {
`,
encapsulation: ViewEncapsulation.None,
+ imports: [AsyncPipe],
styles: `
a2ui-text {
display: block;
@@ -75,7 +77,7 @@ export class Text extends DynamicComponent {
let value = super.resolvePrimitive(this.text());
if (value == null) {
- return '(empty)';
+ return Promise.resolve('(empty)');
}
switch (usageHint) {
diff --git a/renderers/angular/src/lib/data/markdown.ts b/renderers/angular/src/lib/data/markdown.ts
index 4250b9733..a4914ee29 100644
--- a/renderers/angular/src/lib/data/markdown.ts
+++ b/renderers/angular/src/lib/data/markdown.ts
@@ -28,7 +28,7 @@ export class MarkdownRenderer {
private sanitizer = inject(DomSanitizer);
private static defaultMarkdownWarningLogged = false;
- render(value: string, markdownOptions?: Types.MarkdownRendererOptions) {
+ async render(value: string, markdownOptions?: Types.MarkdownRendererOptions): Promise {
if (this.markdownRenderer) {
// The markdownRenderer should return a sanitized string.
return this.markdownRenderer(value, markdownOptions);
diff --git a/renderers/lit/src/0.8/ui/directives/markdown.ts b/renderers/lit/src/0.8/ui/directives/markdown.ts
index 20ee6b0be..4536fa7f4 100644
--- a/renderers/lit/src/0.8/ui/directives/markdown.ts
+++ b/renderers/lit/src/0.8/ui/directives/markdown.ts
@@ -22,6 +22,7 @@ import {
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 {
@@ -49,10 +50,15 @@ class MarkdownDirective extends Directive {
*/
render(value: string, markdownRenderer?: Types.MarkdownRenderer, markdownOptions?: Types.MarkdownRendererOptions) {
if (markdownRenderer) {
- // The markdown renderer returns a string, which we need to convert to a
- // template result using unsafeHTML.
- // It is the responsibilty of the markdown renderer to sanitize the HTML.
- return unsafeHTML(markdownRenderer(value, markdownOptions));
+ 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`${value}`);
}
if (!MarkdownDirective.defaultMarkdownWarningLogged) {
diff --git a/renderers/markdown/markdown-it/CHANGELOG.md b/renderers/markdown/markdown-it/CHANGELOG.md
new file mode 100644
index 000000000..34ed72139
--- /dev/null
+++ b/renderers/markdown/markdown-it/CHANGELOG.md
@@ -0,0 +1,7 @@
+## 0.0.2
+
+- Made the markdown renderer async to support async markdown renderers.
+
+## 0.0.1
+
+- Initial release
diff --git a/renderers/markdown/markdown-it/package.json b/renderers/markdown/markdown-it/package.json
index 69de3d150..03580ba74 100644
--- a/renderers/markdown/markdown-it/package.json
+++ b/renderers/markdown/markdown-it/package.json
@@ -1,6 +1,6 @@
{
"name": "@a2ui/markdown-it",
- "version": "0.0.1",
+ "version": "0.0.2",
"description": "A Markdown renderer using markdown-it and dompurify.",
"keywords": [],
"homepage": "https://github.com/google/A2UI/tree/main/web#readme",
diff --git a/renderers/markdown/markdown-it/src/markdown.test.ts b/renderers/markdown/markdown-it/src/markdown.test.ts
index 4378f8f95..e3ca65674 100644
--- a/renderers/markdown/markdown-it/src/markdown.test.ts
+++ b/renderers/markdown/markdown-it/src/markdown.test.ts
@@ -50,42 +50,42 @@ describe('MarkdownItRenderer', () => {
});
describe('renderMarkdown', () => {
- it('renders markdown successfully', () => {
- const html = renderMarkdown('# Hello World');
+ it('renders markdown successfully', async () => {
+ const html = await renderMarkdown('# Hello World');
assert.match(html, /Hello World<\/h1>/);
});
- it('sanitizes malicious markdown links', () => {
+ it('sanitizes malicious markdown links', async () => {
// Markdown-it strips javascript links by default, emitting the raw markdown string.
// DOMPurify acts as a secondary layer of defense.
const input = 'This is a test [link](javascript:alert("XSS"))';
- const html = renderMarkdown(input);
+ const html = await renderMarkdown(input);
// Ensure the javascript protocol link is neutralized completely
assert.doesNotMatch(html, /href="javascript:alert/);
assert.match(html, /\[link\]\(javascript:alert\("XSS"\)\)/); // It remains raw text
});
- it('safely escapes HTML input without enabling raw HTML', () => {
+ it('safely escapes HTML input without enabling raw HTML', async () => {
const input = 'This is a test ';
- const html = renderMarkdown(input);
+ const html = await renderMarkdown(input);
// Markdown-it will escape it to <script>
assert.match(html, /<script>alert\("XSS"\)<\/script>/);
assert.doesNotMatch(html, /