Skip to content
Merged
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: 4 additions & 2 deletions renderers/angular/src/lib/catalog/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -45,10 +46,11 @@ interface HintedStyles {
<section
[class]="classes()"
[style]="additionalStyles()"
[innerHTML]="resolvedText()"
[innerHTML]="resolvedText() | async"
></section>
`,
encapsulation: ViewEncapsulation.None,
imports: [AsyncPipe],
styles: `
a2ui-text {
display: block;
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion renderers/angular/src/lib/data/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
if (this.markdownRenderer) {
// The markdownRenderer should return a sanitized string.
return this.markdownRenderer(value, markdownOptions);
Expand Down
14 changes: 10 additions & 4 deletions renderers/lit/src/0.8/ui/directives/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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`<span class="no-markdown-renderer">${value}</span>`);
}

if (!MarkdownDirective.defaultMarkdownWarningLogged) {
Expand Down
7 changes: 7 additions & 0 deletions renderers/markdown/markdown-it/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## 0.0.2

- Made the markdown renderer async to support async markdown renderers.

## 0.0.1

- Initial release
2 changes: 1 addition & 1 deletion renderers/markdown/markdown-it/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
20 changes: 10 additions & 10 deletions renderers/markdown/markdown-it/src/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, /<h1>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 <script>alert("XSS")</script>';
const html = renderMarkdown(input);
const html = await renderMarkdown(input);

// Markdown-it will escape it to &lt;script&gt;
assert.match(html, /&lt;script&gt;alert\("XSS"\)&lt;\/script&gt;/);
assert.doesNotMatch(html, /<script>/);
});

it('preserves safe HTML output', () => {
it('preserves safe HTML output', async () => {
const input = 'This is **bold** and *italic*.';
const html = renderMarkdown(input);
const html = await renderMarkdown(input);

assert.match(html, /<strong>bold<\/strong>/);
assert.match(html, /<em>italic<\/em>/);
});

it('preserves classnames applied via tagClassMap', () => {
it('preserves classnames applied via tagClassMap', async () => {
const input = '# Heading\n\nParagraph text';
const html = renderMarkdown(input, {
const html = await renderMarkdown(input, {
tagClassMap: {
h1: ['text-h1', 'bold'],
p: ['body-text'],
Expand Down
7 changes: 5 additions & 2 deletions renderers/markdown/markdown-it/src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ import * as Types from '@a2ui/web_core';
* A Markdown to HTML renderer using markdown-it and dompurify.
* @param value The markdown code to render.
* @param options Options for the markdown renderer.
* @returns The rendered HTML as a string.
* @returns A promise that resolves to the rendered HTML as a string.
*/
export function renderMarkdown(value: string, options?: Types.MarkdownRendererOptions): string {
export async function renderMarkdown(
value: string,
options?: Types.MarkdownRendererOptions,
): Promise<string> {
const htmlString = rawMarkdownRenderer.render(value, options?.tagClassMap);
return sanitize(htmlString);
}
9 changes: 9 additions & 0 deletions renderers/web_core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 0.8.3

* The `MarkdownRenderer` type is now async and returns a `Promise<string>`.

## 0.8.2

## 0.8.1

## 0.8.0
2 changes: 1 addition & 1 deletion renderers/web_core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@a2ui/web_core",
"version": "0.8.2",
"version": "0.8.3",
"description": "A2UI Core Library",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

``

"main": "./dist/src/v0_8/index.js",
"types": "./dist/src/v0_8/index.d.ts",
Expand Down
4 changes: 2 additions & 2 deletions renderers/web_core/src/v0_8/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,12 +514,12 @@ export declare interface Surface {
// Markdown rendering
/**
* Renders `markdown` using `options`.
* @returns The rendered HTML as a string.
* @returns A promise that resolves to the rendered HTML as a string.
*/
export declare type MarkdownRenderer = (
markdown: string,
options?: MarkdownRendererOptions,
) => string;
) => Promise<string>;

/**
* A map of tag names to a list of classnames to be applied to a tag.
Expand Down
Loading