Skip to content

Commit 72d9894

Browse files
authored
Split markdown rendering to a separate package. (#647)
* Introduce the @a2ui/markdown-it package to render markdown across web renderers * Add some basic types for the markdown renderer in web_core. * Update the Lit and Angular restaurants sample injecting the new shared renderer.
1 parent dffc178 commit 72d9894

File tree

36 files changed

+2121
-336
lines changed

36 files changed

+2121
-336
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
/renderers/angular/ @ditman @ava-cassiopeia @crisbeto
2222
/renderers/lit/ @ditman @ava-cassiopeia @paullewis
2323
/renderers/web_core/ @ditman @ava-cassiopeia @jacobsimionato
24+
/renderers/markdown/ @ditman
2425

2526
# Specifications
2627
/specification/ @gspencergoog @jacobsimionato

.github/workflows/lit_samples_build.yml

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,9 @@ jobs:
3535
with:
3636
node-version: '20'
3737

38-
- name: Install web_core deps
39-
working-directory: ./renderers/web_core
40-
run: npm ci
41-
42-
- name: Build web_core
43-
working-directory: ./renderers/web_core
44-
run: npm run build
45-
46-
- name: Install lib's deps
47-
working-directory: ./renderers/lit
48-
run: npm i
49-
50-
- name: Build lib
51-
working-directory: ./renderers/lit
52-
run: npm run build
38+
- name: Build lit renderer and its dependencies
39+
working-directory: ./samples/client/lit
40+
run: npm run build:renderer
5341

5442
- name: Install all lit samples workspaces' dependencies
5543
working-directory: ./samples/client/lit

.github/workflows/ng_build_and_test.yml

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,34 +35,14 @@ jobs:
3535
with:
3636
node-version: '20'
3737

38-
- name: Install web_core dependencies
39-
working-directory: ./renderers/web_core
40-
run: npm i
41-
42-
- name: Build web_core
43-
working-directory: ./renderers/web_core
44-
run: npm run build
45-
46-
- name: Install web lib deps
47-
working-directory: ./renderers/lit
48-
run: npm i
49-
50-
- name: Build web lib
51-
working-directory: ./renderers/lit
52-
run: npm run build
53-
54-
- name: Install renderer deps
55-
working-directory: ./renderers/angular
56-
run: npm i
57-
58-
- name: Build Angular renderer
59-
working-directory: ./renderers/angular
60-
run: npm run build
61-
6238
- name: Install top-level deps
6339
working-directory: ./samples/client/angular
6440
run: npm i
6541

42+
- name: Build Angular renderer and its dependencies
43+
working-directory: ./samples/client/angular
44+
run: npm run build:renderer
45+
6646
- name: Build contact sample
6747
working-directory: ./samples/client/angular
6848
run: npm run build contact

renderers/angular/ng-package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
"lib": {
55
"entryFile": "src/public-api.ts"
66
},
7-
"allowedNonPeerDependencies": ["markdown-it", "@a2ui/web_core"]
7+
"allowedNonPeerDependencies": ["@a2ui/web_core"]
88
}

renderers/angular/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
},
88
"dependencies": {
99
"@a2ui/web_core": "file:../web_core",
10-
"markdown-it": "^14.1.0",
1110
"tslib": "^2.3.0"
1211
},
1312
"peerDependencies": {
@@ -23,7 +22,6 @@
2322
"@angular/core": "^21.2.0",
2423
"@types/express": "^5.0.1",
2524
"@types/jasmine": "~5.1.0",
26-
"@types/markdown-it": "^14.1.2",
2725
"@types/node": "^20.17.19",
2826
"@types/uuid": "^10.0.0",
2927
"@vitest/browser": "^4.0.15",

renderers/angular/src/lib/catalog/text.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ export class Text extends DynamicComponent {
103103
}
104104

105105
return this.markdownRenderer.render(
106-
value,
107-
Styles.appendToAll(this.theme.markdown, ['ol', 'ul', 'li'], {}),
106+
value, {
107+
tagClassMap: Styles.appendToAll(this.theme.markdown, ['ol', 'ul', 'li'], {}),
108+
},
108109
);
109110
});
110111

renderers/angular/src/lib/data/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
export * from './processor';
1818
export * from './types';
19+
export { provideMarkdownRenderer } from './markdown';

renderers/angular/src/lib/data/markdown.ts

Lines changed: 32 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -14,101 +14,47 @@
1414
limitations under the License.
1515
*/
1616

17-
import { inject, Injectable, SecurityContext } from '@angular/core';
17+
import { inject, Injectable, InjectionToken, SecurityContext } from '@angular/core';
1818
import { DomSanitizer } from '@angular/platform-browser';
19-
import MarkdownIt from 'markdown-it';
19+
import * as Types from '@a2ui/web_core/types/types';
20+
21+
// We need this because Types.MarkdownRenderer is a raw TS type, and can't be used as a token directly.
22+
const MARKDOWN_RENDERER_TOKEN = new InjectionToken<Types.MarkdownRenderer>('MARKDOWN_RENDERER');
2023

2124
@Injectable({ providedIn: 'root' })
2225
export class MarkdownRenderer {
23-
private originalClassMap = new Map<string, any>();
24-
private sanitizer = inject(DomSanitizer);
2526

26-
private markdownIt = MarkdownIt({
27-
highlight: (str, lang) => {
28-
if (lang === 'html') {
29-
const iframe = document.createElement('iframe');
30-
iframe.classList.add('html-view');
31-
iframe.srcdoc = str;
32-
iframe.sandbox = '';
33-
return iframe.innerHTML;
34-
}
35-
36-
return str;
37-
},
38-
});
27+
private markdownRenderer = inject(MARKDOWN_RENDERER_TOKEN, { optional: true });
28+
private sanitizer = inject(DomSanitizer);
29+
private static defaultMarkdownWarningLogged = false;
3930

40-
render(value: string, tagClassMap?: Record<string, string[]>) {
41-
if (tagClassMap) {
42-
this.applyTagClassMap(tagClassMap);
31+
render(value: string, markdownOptions?: Types.MarkdownRendererOptions) {
32+
if (this.markdownRenderer) {
33+
// The markdownRenderer should return a sanitized string.
34+
return this.markdownRenderer(value, markdownOptions);
4335
}
44-
const htmlString = this.markdownIt.render(value);
45-
this.unapplyTagClassMap();
46-
return this.sanitizer.sanitize(SecurityContext.HTML, htmlString);
47-
}
48-
49-
private applyTagClassMap(tagClassMap: Record<string, string[]>) {
50-
Object.entries(tagClassMap).forEach(([tag, classes]) => {
51-
let tokenName;
52-
switch (tag) {
53-
case 'p':
54-
tokenName = 'paragraph';
55-
break;
56-
case 'h1':
57-
case 'h2':
58-
case 'h3':
59-
case 'h4':
60-
case 'h5':
61-
case 'h6':
62-
tokenName = 'heading';
63-
break;
64-
case 'ul':
65-
tokenName = 'bullet_list';
66-
break;
67-
case 'ol':
68-
tokenName = 'ordered_list';
69-
break;
70-
case 'li':
71-
tokenName = 'list_item';
72-
break;
73-
case 'a':
74-
tokenName = 'link';
75-
break;
76-
case 'strong':
77-
tokenName = 'strong';
78-
break;
79-
case 'em':
80-
tokenName = 'em';
81-
break;
82-
}
83-
84-
if (!tokenName) {
85-
return;
86-
}
8736

88-
const key = `${tokenName}_open`;
89-
const original = this.markdownIt.renderer.rules[key];
90-
this.originalClassMap.set(key, original);
91-
92-
this.markdownIt.renderer.rules[key] = (tokens, idx, options, env, self) => {
93-
const token = tokens[idx];
94-
for (const clazz of classes) {
95-
token.attrJoin('class', clazz);
96-
}
97-
98-
if (original) {
99-
return original.call(this, tokens, idx, options, env, self);
100-
} else {
101-
return self.renderToken(tokens, idx, options);
102-
}
103-
};
104-
});
105-
}
106-
107-
private unapplyTagClassMap() {
108-
for (const [key, original] of this.originalClassMap) {
109-
this.markdownIt.renderer.rules[key] = original;
37+
if (!MarkdownRenderer.defaultMarkdownWarningLogged) {
38+
console.warn("[MarkdownRenderer]",
39+
"can't render markdown because no markdown renderer is configured.\n",
40+
"Use `@a2ui/markdown-it`, or your own markdown renderer.");
41+
MarkdownRenderer.defaultMarkdownWarningLogged = true;
11042
}
11143

112-
this.originalClassMap.clear();
44+
// Return a span with a sanitized version of the input `value`.
45+
const sanitizedValue = this.sanitizer.sanitize(SecurityContext.HTML, value);
46+
return `<span class="no-markdown-renderer">${sanitizedValue}</span>`;
11347
}
11448
}
49+
50+
/**
51+
* Allows the user to provide a markdown renderer function.
52+
* @param {Types.MarkdownRenderer} markdownRenderer a markdown renderer function.
53+
* @returns an Angular provider for the markdown renderer.
54+
*/
55+
export function provideMarkdownRenderer(markdownRenderer: Types.MarkdownRenderer) {
56+
return {
57+
provide: MARKDOWN_RENDERER_TOKEN,
58+
useValue: markdownRenderer,
59+
};
60+
}

0 commit comments

Comments
 (0)