@mzebley/mark-down-angular 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -25
- package/dist/index.cjs +48 -16
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +56 -17
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# mark↓ Angular Adapter
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
_(published as `@mzebley/mark-down-angular`)_
|
|
3
4
|
|
|
4
5
|
Angular bindings for the [mark↓ core runtime](../core/README.md). This package wraps `SnippetClient` with Angular-friendly providers, services, and components so you can render Markdown snippets safely inside your application. For background on the monorepo, see the [root README](../../README.md).
|
|
5
6
|
|
|
@@ -29,14 +30,14 @@ You will also need a manifest generated by the [CLI](../cli/README.md).
|
|
|
29
30
|
Provide a shared `SnippetClient` from your root bootstrap call or feature module:
|
|
30
31
|
|
|
31
32
|
```ts
|
|
32
|
-
import { bootstrapApplication } from
|
|
33
|
-
import { provideSnippetClient } from
|
|
33
|
+
import { bootstrapApplication } from "@angular/platform-browser";
|
|
34
|
+
import { provideSnippetClient } from "@mzebley/mark-down/angular";
|
|
34
35
|
|
|
35
36
|
bootstrapApplication(AppComponent, {
|
|
36
37
|
providers: [
|
|
37
38
|
...provideSnippetClient({
|
|
38
|
-
manifest:
|
|
39
|
-
base:
|
|
39
|
+
manifest: "/assets/content/snippets/manifest.json",
|
|
40
|
+
base: "/assets/content/snippets",
|
|
40
41
|
}),
|
|
41
42
|
],
|
|
42
43
|
});
|
|
@@ -46,12 +47,12 @@ bootstrapApplication(AppComponent, {
|
|
|
46
47
|
|
|
47
48
|
### Angular compatibility
|
|
48
49
|
|
|
49
|
-
| Angular version | Status
|
|
50
|
-
|
|
|
51
|
-
| 17.x
|
|
52
|
-
| 18.x
|
|
53
|
-
| 19.x
|
|
54
|
-
| 20.x
|
|
50
|
+
| Angular version | Status |
|
|
51
|
+
| --------------- | ------------ |
|
|
52
|
+
| 17.x | ✅ Supported |
|
|
53
|
+
| 18.x | ✅ Supported |
|
|
54
|
+
| 19.x | ✅ Supported |
|
|
55
|
+
| 20.x | ✅ Supported |
|
|
55
56
|
|
|
56
57
|
The package declares peer dependency ranges `>=17 <21` so projects running any currently supported Angular major release can install without `--legacy-peer-deps`.
|
|
57
58
|
|
|
@@ -60,11 +61,11 @@ The package declares peer dependency ranges `>=17 <21` so projects running any c
|
|
|
60
61
|
Inject `MarkdownSnippetService` into components or services to access Observables for snippets:
|
|
61
62
|
|
|
62
63
|
```ts
|
|
63
|
-
import { Component, inject } from
|
|
64
|
-
import { MarkdownSnippetService } from
|
|
64
|
+
import { Component, inject } from "@angular/core";
|
|
65
|
+
import { MarkdownSnippetService } from "@mzebley/mark-down/angular";
|
|
65
66
|
|
|
66
67
|
@Component({
|
|
67
|
-
selector:
|
|
68
|
+
selector: "docs-hero",
|
|
68
69
|
template: `
|
|
69
70
|
<ng-container *ngIf="hero$ | async as hero">
|
|
70
71
|
<h1>{{ hero.title }}</h1>
|
|
@@ -74,7 +75,7 @@ import { MarkdownSnippetService } from '@mzebley/mark-down/angular';
|
|
|
74
75
|
})
|
|
75
76
|
export class DocsHeroComponent {
|
|
76
77
|
private readonly snippets = inject(MarkdownSnippetService);
|
|
77
|
-
readonly hero$ = this.snippets.get(
|
|
78
|
+
readonly hero$ = this.snippets.get("getting-started-welcome");
|
|
78
79
|
}
|
|
79
80
|
```
|
|
80
81
|
|
|
@@ -85,31 +86,42 @@ The service mirrors the core client APIs (`get`, `listAll`, `listByType`, `listB
|
|
|
85
86
|
Render snippets declaratively with the bundled standalone component:
|
|
86
87
|
|
|
87
88
|
```html
|
|
88
|
-
<snippet-view
|
|
89
|
+
<snippet-view
|
|
90
|
+
[slug]="'components-button'"
|
|
91
|
+
(loaded)="onSnippetLoaded($event)"
|
|
92
|
+
></snippet-view>
|
|
89
93
|
```
|
|
90
94
|
|
|
91
95
|
Features:
|
|
92
96
|
|
|
93
|
-
- Uses Angular's `DomSanitizer`
|
|
97
|
+
- Uses Angular's `DomSanitizer` for trusted HTML binding and DOMPurify in browser rendering.
|
|
94
98
|
- Emits a `loaded` event once the snippet resolves so parent components can react.
|
|
95
99
|
- Provides a loading placeholder and gracefully emits `undefined` when the slug cannot be resolved.
|
|
96
100
|
- Supports `class`/`ngClass` bindings for styling since it renders a standard `<div>`.
|
|
97
101
|
|
|
102
|
+
### Security warning
|
|
103
|
+
|
|
104
|
+
The Angular adapter is intentionally compatible with untrusted/unsanitized snippet pipelines. That flexibility is a feature, but it means you must decide where sanitization happens:
|
|
105
|
+
|
|
106
|
+
- For untrusted content, sanitize in `SnippetClient` (`sanitize` option) and/or during build with `mark-down compile-page --sanitize`.
|
|
107
|
+
- If sanitization is disabled, snippet HTML is treated as trusted and may include dangerous markup.
|
|
108
|
+
|
|
98
109
|
## Server-side rendering
|
|
99
110
|
|
|
100
111
|
When running in Angular Universal, supply a server-compatible fetch implementation:
|
|
101
112
|
|
|
102
113
|
```ts
|
|
103
|
-
import fetch from
|
|
114
|
+
import fetch from "node-fetch";
|
|
104
115
|
|
|
105
116
|
provideSnippetClient({
|
|
106
|
-
manifest: () => import(
|
|
107
|
-
fetch: (url) =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
manifest: () => import("../snippets-index.json"),
|
|
118
|
+
fetch: (url) =>
|
|
119
|
+
fetch(url).then((response) => {
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(`Request failed with status ${response.status}`);
|
|
122
|
+
}
|
|
123
|
+
return response;
|
|
124
|
+
}),
|
|
113
125
|
});
|
|
114
126
|
```
|
|
115
127
|
|
package/dist/index.cjs
CHANGED
|
@@ -72,26 +72,45 @@ var SnippetViewComponent = class {
|
|
|
72
72
|
this.sanitizer = (0, import_core.inject)(import_platform_browser.DomSanitizer);
|
|
73
73
|
this.snippets = (0, import_core.inject)(import_angular2.MarkdownSnippetService);
|
|
74
74
|
this.loaded = new import_core.EventEmitter();
|
|
75
|
-
this.
|
|
75
|
+
this.state$ = this.slug$.pipe(
|
|
76
76
|
(0, import_operators.switchMap)(
|
|
77
|
-
(slug) => slug ? this.snippets.get(slug).pipe(
|
|
77
|
+
(slug) => slug ? this.snippets.get(slug).pipe(
|
|
78
|
+
(0, import_operators.map)((snippet) => ({ loading: false, error: false, snippet })),
|
|
79
|
+
(0, import_operators.catchError)(
|
|
80
|
+
() => (0, import_rxjs.of)({ loading: false, error: true, snippet: null })
|
|
81
|
+
),
|
|
82
|
+
(0, import_operators.startWith)({ loading: true, error: false, snippet: null })
|
|
83
|
+
) : (0, import_rxjs.of)({ loading: false, error: false, snippet: null })
|
|
78
84
|
),
|
|
79
|
-
(0, import_operators.tap)((
|
|
85
|
+
(0, import_operators.tap)((state) => {
|
|
86
|
+
if (!state.loading) {
|
|
87
|
+
this.loaded.emit(state.snippet ?? void 0);
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
(0, import_operators.map)((state) => ({
|
|
91
|
+
loading: state.loading,
|
|
92
|
+
error: state.error,
|
|
93
|
+
html: this.toSafeHtml(state.snippet)
|
|
94
|
+
})),
|
|
80
95
|
(0, import_operators.shareReplay)({ bufferSize: 1, refCount: true })
|
|
81
96
|
);
|
|
82
|
-
this.content$ = this.
|
|
83
|
-
(0, import_operators.map)((
|
|
84
|
-
if (!snippet) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const sanitized = import_dompurify.default.sanitize(snippet.html);
|
|
88
|
-
return this.sanitizer.bypassSecurityTrustHtml(sanitized);
|
|
89
|
-
})
|
|
97
|
+
this.content$ = this.state$.pipe(
|
|
98
|
+
(0, import_operators.map)((state) => state.html)
|
|
90
99
|
);
|
|
91
100
|
}
|
|
92
101
|
ngOnChanges() {
|
|
93
102
|
this.slug$.next(this.slug ?? null);
|
|
94
103
|
}
|
|
104
|
+
toSafeHtml(snippet) {
|
|
105
|
+
if (!snippet) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (typeof window === "undefined") {
|
|
109
|
+
return this.sanitizer.bypassSecurityTrustHtml(snippet.html);
|
|
110
|
+
}
|
|
111
|
+
const sanitized = import_dompurify.default.sanitize(snippet.html);
|
|
112
|
+
return this.sanitizer.bypassSecurityTrustHtml(sanitized);
|
|
113
|
+
}
|
|
95
114
|
};
|
|
96
115
|
__decorateClass([
|
|
97
116
|
(0, import_core.Input)()
|
|
@@ -105,12 +124,25 @@ SnippetViewComponent = __decorateClass([
|
|
|
105
124
|
standalone: true,
|
|
106
125
|
imports: [import_common.CommonModule],
|
|
107
126
|
template: `
|
|
108
|
-
<ng-container *ngIf="
|
|
109
|
-
<div class="mark-down-snippet"
|
|
127
|
+
<ng-container *ngIf="state$ | async as state">
|
|
128
|
+
<div *ngIf="state.loading" class="mark-down-snippet--loading">
|
|
129
|
+
Loading snippet\u2026
|
|
130
|
+
</div>
|
|
131
|
+
<div *ngIf="!state.loading && state.error" class="mark-down-snippet--error">
|
|
132
|
+
Unable to load snippet.
|
|
133
|
+
</div>
|
|
134
|
+
<div
|
|
135
|
+
*ngIf="!state.loading && !state.error && state.html !== null"
|
|
136
|
+
class="mark-down-snippet"
|
|
137
|
+
[innerHTML]="state.html"
|
|
138
|
+
></div>
|
|
139
|
+
<div
|
|
140
|
+
*ngIf="!state.loading && !state.error && state.html === null"
|
|
141
|
+
class="mark-down-snippet--empty"
|
|
142
|
+
>
|
|
143
|
+
Snippet not found.
|
|
144
|
+
</div>
|
|
110
145
|
</ng-container>
|
|
111
|
-
<ng-template #loading>
|
|
112
|
-
<div class="mark-down-snippet--loading">Loading snippet\u2026</div>
|
|
113
|
-
</ng-template>
|
|
114
146
|
`,
|
|
115
147
|
changeDetection: import_core.ChangeDetectionStrategy.OnPush
|
|
116
148
|
})
|
package/dist/index.d.cts
CHANGED
|
@@ -18,9 +18,14 @@ declare class SnippetViewComponent implements OnChanges {
|
|
|
18
18
|
private readonly snippets;
|
|
19
19
|
slug?: string;
|
|
20
20
|
readonly loaded: EventEmitter<Snippet | undefined>;
|
|
21
|
-
|
|
21
|
+
readonly state$: Observable<{
|
|
22
|
+
loading: boolean;
|
|
23
|
+
error: boolean;
|
|
24
|
+
html: SafeHtml | null;
|
|
25
|
+
}>;
|
|
22
26
|
readonly content$: Observable<SafeHtml | null>;
|
|
23
27
|
ngOnChanges(): void;
|
|
28
|
+
private toSafeHtml;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export { MARK_DOWN_CLIENT, MARK_DOWN_OPTIONS, SnippetViewComponent, provideMarkDown };
|
package/dist/index.d.ts
CHANGED
|
@@ -18,9 +18,14 @@ declare class SnippetViewComponent implements OnChanges {
|
|
|
18
18
|
private readonly snippets;
|
|
19
19
|
slug?: string;
|
|
20
20
|
readonly loaded: EventEmitter<Snippet | undefined>;
|
|
21
|
-
|
|
21
|
+
readonly state$: Observable<{
|
|
22
|
+
loading: boolean;
|
|
23
|
+
error: boolean;
|
|
24
|
+
html: SafeHtml | null;
|
|
25
|
+
}>;
|
|
22
26
|
readonly content$: Observable<SafeHtml | null>;
|
|
23
27
|
ngOnChanges(): void;
|
|
28
|
+
private toSafeHtml;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export { MARK_DOWN_CLIENT, MARK_DOWN_OPTIONS, SnippetViewComponent, provideMarkDown };
|
package/dist/index.js
CHANGED
|
@@ -39,7 +39,14 @@ import {
|
|
|
39
39
|
} from "@angular/core";
|
|
40
40
|
import { DomSanitizer } from "@angular/platform-browser";
|
|
41
41
|
import { BehaviorSubject, of } from "rxjs";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
catchError,
|
|
44
|
+
map,
|
|
45
|
+
shareReplay,
|
|
46
|
+
startWith,
|
|
47
|
+
switchMap,
|
|
48
|
+
tap
|
|
49
|
+
} from "rxjs/operators";
|
|
43
50
|
import DOMPurify from "dompurify";
|
|
44
51
|
var SnippetViewComponent = class {
|
|
45
52
|
constructor() {
|
|
@@ -47,26 +54,45 @@ var SnippetViewComponent = class {
|
|
|
47
54
|
this.sanitizer = inject(DomSanitizer);
|
|
48
55
|
this.snippets = inject(MarkdownSnippetService);
|
|
49
56
|
this.loaded = new EventEmitter();
|
|
50
|
-
this.
|
|
57
|
+
this.state$ = this.slug$.pipe(
|
|
51
58
|
switchMap(
|
|
52
|
-
(slug) => slug ? this.snippets.get(slug).pipe(
|
|
59
|
+
(slug) => slug ? this.snippets.get(slug).pipe(
|
|
60
|
+
map((snippet) => ({ loading: false, error: false, snippet })),
|
|
61
|
+
catchError(
|
|
62
|
+
() => of({ loading: false, error: true, snippet: null })
|
|
63
|
+
),
|
|
64
|
+
startWith({ loading: true, error: false, snippet: null })
|
|
65
|
+
) : of({ loading: false, error: false, snippet: null })
|
|
53
66
|
),
|
|
54
|
-
tap((
|
|
67
|
+
tap((state) => {
|
|
68
|
+
if (!state.loading) {
|
|
69
|
+
this.loaded.emit(state.snippet ?? void 0);
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
map((state) => ({
|
|
73
|
+
loading: state.loading,
|
|
74
|
+
error: state.error,
|
|
75
|
+
html: this.toSafeHtml(state.snippet)
|
|
76
|
+
})),
|
|
55
77
|
shareReplay({ bufferSize: 1, refCount: true })
|
|
56
78
|
);
|
|
57
|
-
this.content$ = this.
|
|
58
|
-
map((
|
|
59
|
-
if (!snippet) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
const sanitized = DOMPurify.sanitize(snippet.html);
|
|
63
|
-
return this.sanitizer.bypassSecurityTrustHtml(sanitized);
|
|
64
|
-
})
|
|
79
|
+
this.content$ = this.state$.pipe(
|
|
80
|
+
map((state) => state.html)
|
|
65
81
|
);
|
|
66
82
|
}
|
|
67
83
|
ngOnChanges() {
|
|
68
84
|
this.slug$.next(this.slug ?? null);
|
|
69
85
|
}
|
|
86
|
+
toSafeHtml(snippet) {
|
|
87
|
+
if (!snippet) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (typeof window === "undefined") {
|
|
91
|
+
return this.sanitizer.bypassSecurityTrustHtml(snippet.html);
|
|
92
|
+
}
|
|
93
|
+
const sanitized = DOMPurify.sanitize(snippet.html);
|
|
94
|
+
return this.sanitizer.bypassSecurityTrustHtml(sanitized);
|
|
95
|
+
}
|
|
70
96
|
};
|
|
71
97
|
__decorateClass([
|
|
72
98
|
Input()
|
|
@@ -80,12 +106,25 @@ SnippetViewComponent = __decorateClass([
|
|
|
80
106
|
standalone: true,
|
|
81
107
|
imports: [CommonModule],
|
|
82
108
|
template: `
|
|
83
|
-
<ng-container *ngIf="
|
|
84
|
-
<div class="mark-down-snippet"
|
|
109
|
+
<ng-container *ngIf="state$ | async as state">
|
|
110
|
+
<div *ngIf="state.loading" class="mark-down-snippet--loading">
|
|
111
|
+
Loading snippet\u2026
|
|
112
|
+
</div>
|
|
113
|
+
<div *ngIf="!state.loading && state.error" class="mark-down-snippet--error">
|
|
114
|
+
Unable to load snippet.
|
|
115
|
+
</div>
|
|
116
|
+
<div
|
|
117
|
+
*ngIf="!state.loading && !state.error && state.html !== null"
|
|
118
|
+
class="mark-down-snippet"
|
|
119
|
+
[innerHTML]="state.html"
|
|
120
|
+
></div>
|
|
121
|
+
<div
|
|
122
|
+
*ngIf="!state.loading && !state.error && state.html === null"
|
|
123
|
+
class="mark-down-snippet--empty"
|
|
124
|
+
>
|
|
125
|
+
Snippet not found.
|
|
126
|
+
</div>
|
|
85
127
|
</ng-container>
|
|
86
|
-
<ng-template #loading>
|
|
87
|
-
<div class="mark-down-snippet--loading">Loading snippet\u2026</div>
|
|
88
|
-
</ng-template>
|
|
89
128
|
`,
|
|
90
129
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
91
130
|
})
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mzebley/mark-down-angular",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "mark↓ Angular Adapter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
|
-
"files": [
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
10
12
|
"exports": {
|
|
11
13
|
".": {
|
|
12
14
|
"types": "./dist/index.d.ts",
|
|
@@ -24,6 +26,9 @@
|
|
|
24
26
|
"@mzebley/mark-down": "^1.2.2",
|
|
25
27
|
"dompurify": "^3.0.9"
|
|
26
28
|
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@angular/platform-browser": ">=17 <21"
|
|
31
|
+
},
|
|
27
32
|
"peerDependencies": {
|
|
28
33
|
"@angular/common": ">=17 <21",
|
|
29
34
|
"@angular/core": ">=17 <21",
|