@mzebley/mark-down-angular 1.2.3 → 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 CHANGED
@@ -94,11 +94,18 @@ Render snippets declaratively with the bundled standalone component:
94
94
 
95
95
  Features:
96
96
 
97
- - Uses Angular's `DomSanitizer` to render HTML safely.
97
+ - Uses Angular's `DomSanitizer` for trusted HTML binding and DOMPurify in browser rendering.
98
98
  - Emits a `loaded` event once the snippet resolves so parent components can react.
99
99
  - Provides a loading placeholder and gracefully emits `undefined` when the slug cannot be resolved.
100
100
  - Supports `class`/`ngClass` bindings for styling since it renders a standard `<div>`.
101
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
+
102
109
  ## Server-side rendering
103
110
 
104
111
  When running in Angular Universal, supply a server-compatible fetch implementation:
package/dist/index.cjs CHANGED
@@ -72,29 +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.snippet$ = this.slug$.pipe(
75
+ this.state$ = this.slug$.pipe(
76
76
  (0, import_operators.switchMap)(
77
- (slug) => slug ? this.snippets.get(slug).pipe((0, import_operators.catchError)(() => (0, import_rxjs.of)(null))) : (0, import_rxjs.of)(null)
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)((snippet) => this.loaded.emit(snippet ?? void 0)),
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.snippet$.pipe(
83
- (0, import_operators.map)((snippet) => {
84
- if (!snippet) {
85
- return null;
86
- }
87
- if (typeof window === "undefined") {
88
- return this.sanitizer.bypassSecurityTrustHtml(snippet.html);
89
- }
90
- const sanitized = import_dompurify.default.sanitize(snippet.html);
91
- return this.sanitizer.bypassSecurityTrustHtml(sanitized);
92
- })
97
+ this.content$ = this.state$.pipe(
98
+ (0, import_operators.map)((state) => state.html)
93
99
  );
94
100
  }
95
101
  ngOnChanges() {
96
102
  this.slug$.next(this.slug ?? null);
97
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
+ }
98
114
  };
99
115
  __decorateClass([
100
116
  (0, import_core.Input)()
@@ -108,12 +124,25 @@ SnippetViewComponent = __decorateClass([
108
124
  standalone: true,
109
125
  imports: [import_common.CommonModule],
110
126
  template: `
111
- <ng-container *ngIf="content$ | async as html; else loading">
112
- <div class="mark-down-snippet" [innerHTML]="html"></div>
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>
113
145
  </ng-container>
114
- <ng-template #loading>
115
- <div class="mark-down-snippet--loading">Loading snippet\u2026</div>
116
- </ng-template>
117
146
  `,
118
147
  changeDetection: import_core.ChangeDetectionStrategy.OnPush
119
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
- private readonly snippet$;
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
- private readonly snippet$;
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 { catchError, map, shareReplay, switchMap, tap } from "rxjs/operators";
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,29 +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.snippet$ = this.slug$.pipe(
57
+ this.state$ = this.slug$.pipe(
51
58
  switchMap(
52
- (slug) => slug ? this.snippets.get(slug).pipe(catchError(() => of(null))) : of(null)
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((snippet) => this.loaded.emit(snippet ?? void 0)),
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.snippet$.pipe(
58
- map((snippet) => {
59
- if (!snippet) {
60
- return null;
61
- }
62
- if (typeof window === "undefined") {
63
- return this.sanitizer.bypassSecurityTrustHtml(snippet.html);
64
- }
65
- const sanitized = DOMPurify.sanitize(snippet.html);
66
- return this.sanitizer.bypassSecurityTrustHtml(sanitized);
67
- })
79
+ this.content$ = this.state$.pipe(
80
+ map((state) => state.html)
68
81
  );
69
82
  }
70
83
  ngOnChanges() {
71
84
  this.slug$.next(this.slug ?? null);
72
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
+ }
73
96
  };
74
97
  __decorateClass([
75
98
  Input()
@@ -83,12 +106,25 @@ SnippetViewComponent = __decorateClass([
83
106
  standalone: true,
84
107
  imports: [CommonModule],
85
108
  template: `
86
- <ng-container *ngIf="content$ | async as html; else loading">
87
- <div class="mark-down-snippet" [innerHTML]="html"></div>
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>
88
127
  </ng-container>
89
- <ng-template #loading>
90
- <div class="mark-down-snippet--loading">Loading snippet\u2026</div>
91
- </ng-template>
92
128
  `,
93
129
  changeDetection: ChangeDetectionStrategy.OnPush
94
130
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mzebley/mark-down-angular",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "mark↓ Angular Adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",