@mzebley/mark-down-angular 1.0.0

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 ADDED
@@ -0,0 +1,119 @@
1
+ # mark↓ Angular Adapter
2
+ *(published as `@mzebley/mark-down-angular`)*
3
+
4
+ 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
+ ## Table of contents
7
+
8
+ 1. [Installation](#installation)
9
+ 2. [Bootstrap configuration](#bootstrap-configuration)
10
+ 3. [Consuming snippets](#consuming-snippets)
11
+ 4. [`<snippet-view>` component](#snippet-view-component)
12
+ 5. [Server-side rendering](#server-side-rendering)
13
+ 6. [Testing tips](#testing-tips)
14
+ 7. [Roadmap](#roadmap)
15
+ 8. [Related packages](#related-packages)
16
+
17
+ ## Installation
18
+
19
+ Install the Angular adapter alongside the core runtime:
20
+
21
+ ```bash
22
+ npm install @mzebley/mark-down-angular @mzebley/mark-down
23
+ ```
24
+
25
+ You will also need a manifest generated by the [CLI](../cli/README.md).
26
+
27
+ ## Bootstrap configuration
28
+
29
+ Provide a shared `SnippetClient` from your root bootstrap call or feature module:
30
+
31
+ ```ts
32
+ import { bootstrapApplication } from '@angular/platform-browser';
33
+ import { provideMarkDown } from '@mzebley/mark-down-angular';
34
+
35
+ bootstrapApplication(AppComponent, {
36
+ providers: [
37
+ ...provideMarkDown({
38
+ manifest: '/snippets-index.json',
39
+ }),
40
+ ],
41
+ });
42
+ ```
43
+
44
+ `provideMarkDown` wires up the client as an Angular provider using the options you pass through. The manifest can be a URL, a factory, or a pre-fetched array—identical to the [core runtime options](../core/README.md#client-options).
45
+
46
+ ## Consuming snippets
47
+
48
+ Inject `SnippetService` into components or services to access Observables for snippets:
49
+
50
+ ```ts
51
+ import { Component, inject } from '@angular/core';
52
+ import { SnippetService } from '@mzebley/mark-down-angular';
53
+
54
+ @Component({
55
+ selector: 'docs-hero',
56
+ template: `
57
+ <ng-container *ngIf="hero$ | async as hero">
58
+ <h1>{{ hero.title }}</h1>
59
+ <div [innerHTML]="hero.html"></div>
60
+ </ng-container>
61
+ `,
62
+ })
63
+ export class DocsHeroComponent {
64
+ private readonly snippets = inject(SnippetService);
65
+ readonly hero$ = this.snippets.get('getting-started-welcome');
66
+ }
67
+ ```
68
+
69
+ The service mirrors the core client APIs (`get`, `list`, `listByType`, `listByGroup`) but returns cold Observables that share cached results across subscribers.
70
+
71
+ ## `<snippet-view>` component
72
+
73
+ Render snippets declaratively with the bundled standalone component:
74
+
75
+ ```html
76
+ <snippet-view [slug]="'components-button'" (loaded)="onSnippetLoaded($event)"></snippet-view>
77
+ ```
78
+
79
+ Features:
80
+
81
+ - Uses Angular's `DomSanitizer` to render HTML safely.
82
+ - Emits a `loaded` event once the snippet resolves so parent components can react.
83
+ - Provides built-in "Loading snippet…" and "Snippet not found" fallbacks (customize by wrapping the component or using the service directly).
84
+ - Supports `class`/`ngClass` bindings for styling since it renders a standard `<div>`.
85
+
86
+ ## Server-side rendering
87
+
88
+ When running in Angular Universal, supply a server-compatible fetch implementation:
89
+
90
+ ```ts
91
+ import fetch from 'node-fetch';
92
+
93
+ provideMarkDown({
94
+ manifest: () => import('../snippets-index.json'),
95
+ fetcher: (input, init) => fetch(input as string, init),
96
+ });
97
+ ```
98
+
99
+ The provider forwards all options to the underlying `SnippetClient`, so SSR and preloaded manifests work exactly like the core package.
100
+
101
+ ## Testing tips
102
+
103
+ - Provide a mock manifest array during tests: `provideMarkDown({ manifest: mockManifest })`.
104
+ - Use `SnippetService` with the Angular `TestBed` to assert filtering behaviour.
105
+ - Pair with the [example application](../../examples/basic/README.md) to see how snippets integrate with routing and feature modules.
106
+
107
+ ## Roadmap
108
+
109
+ - **Template primitives** – structural directives for rendering snippet lists via `*markDownSnippets`.
110
+ - **AsyncPipe optimisations** – helpers that automatically unsubscribe / reuse cached Observables when using standalone components.
111
+ - **Schematics** – Angular CLI generator for wiring `provideMarkDown` + `SnippetViewComponent`.
112
+ - **SSR hydration helpers** – share manifest snapshots between server and browser to avoid double fetching.
113
+
114
+ ## Related packages
115
+
116
+ - [Core runtime](../core/README.md)
117
+ - [CLI](../cli/README.md)
118
+ - [React adapter](../react/README.md)
119
+ - [Project overview](../../README.md)
package/dist/index.cjs ADDED
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from2, except, desc) => {
13
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
14
+ for (let key of __getOwnPropNames(from2))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var __decorateClass = (decorators, target, key, kind) => {
30
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
31
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
32
+ if (decorator = decorators[i])
33
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
34
+ if (kind && result) __defProp(target, key, result);
35
+ return result;
36
+ };
37
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
38
+
39
+ // src/index.ts
40
+ var index_exports = {};
41
+ __export(index_exports, {
42
+ MARK_DOWN_CLIENT: () => MARK_DOWN_CLIENT,
43
+ MARK_DOWN_OPTIONS: () => MARK_DOWN_OPTIONS,
44
+ SnippetService: () => SnippetService,
45
+ SnippetViewComponent: () => SnippetViewComponent,
46
+ provideMarkDown: () => provideMarkDown
47
+ });
48
+ module.exports = __toCommonJS(index_exports);
49
+
50
+ // src/token.ts
51
+ var import_core = require("@angular/core");
52
+ var import_mark_down = require("@mzebley/mark-down");
53
+ var MARK_DOWN_CLIENT = new import_core.InjectionToken("mark-down-client");
54
+ var MARK_DOWN_OPTIONS = new import_core.InjectionToken("mark-down-options");
55
+ function provideMarkDown(options, client) {
56
+ return [
57
+ { provide: MARK_DOWN_OPTIONS, useValue: options },
58
+ {
59
+ provide: MARK_DOWN_CLIENT,
60
+ useFactory: (opts) => client ?? new import_mark_down.SnippetClient(opts),
61
+ deps: [MARK_DOWN_OPTIONS]
62
+ }
63
+ ];
64
+ }
65
+
66
+ // src/snippet.service.ts
67
+ var import_core2 = require("@angular/core");
68
+ var import_rxjs = require("rxjs");
69
+ var SnippetService = class {
70
+ constructor(client) {
71
+ this.client = client;
72
+ }
73
+ get(slug) {
74
+ return (0, import_rxjs.from)(this.client.get(slug)).pipe((0, import_rxjs.shareReplay)(1));
75
+ }
76
+ list(options) {
77
+ return (0, import_rxjs.from)(this.client.list(options)).pipe((0, import_rxjs.shareReplay)(1));
78
+ }
79
+ listByGroup(group, options) {
80
+ return (0, import_rxjs.from)(this.client.listByGroup(group, options)).pipe((0, import_rxjs.shareReplay)(1));
81
+ }
82
+ listByType(type, options) {
83
+ return (0, import_rxjs.from)(this.client.listByType(type, options)).pipe((0, import_rxjs.shareReplay)(1));
84
+ }
85
+ html(slug) {
86
+ return this.get(slug).pipe((0, import_rxjs.map)((snippet) => snippet?.html ?? null));
87
+ }
88
+ };
89
+ SnippetService = __decorateClass([
90
+ (0, import_core2.Injectable)({ providedIn: "root" }),
91
+ __decorateParam(0, (0, import_core2.Inject)(MARK_DOWN_CLIENT))
92
+ ], SnippetService);
93
+
94
+ // src/snippet-view.component.ts
95
+ var import_common = require("@angular/common");
96
+ var import_core3 = require("@angular/core");
97
+ var import_platform_browser = require("@angular/platform-browser");
98
+ var import_rxjs2 = require("rxjs");
99
+ var import_operators = require("rxjs/operators");
100
+ var import_dompurify = __toESM(require("dompurify"), 1);
101
+ var SnippetViewComponent = class {
102
+ constructor() {
103
+ this.slug$ = new import_rxjs2.BehaviorSubject(null);
104
+ this.sanitizer = (0, import_core3.inject)(import_platform_browser.DomSanitizer);
105
+ this.snippets = (0, import_core3.inject)(SnippetService);
106
+ this.loaded = new import_core3.EventEmitter();
107
+ this.snippet$ = this.slug$.pipe(
108
+ (0, import_operators.switchMap)((slug) => slug ? this.snippets.get(slug) : (0, import_rxjs2.of)(void 0)),
109
+ (0, import_operators.tap)((snippet) => this.loaded.emit(snippet)),
110
+ (0, import_operators.shareReplay)(1)
111
+ );
112
+ this.content$ = this.snippet$.pipe(
113
+ (0, import_operators.map)((snippet) => {
114
+ if (!snippet) {
115
+ return null;
116
+ }
117
+ const sanitized = import_dompurify.default.sanitize(snippet.html);
118
+ return this.sanitizer.bypassSecurityTrustHtml(sanitized);
119
+ })
120
+ );
121
+ }
122
+ ngOnChanges() {
123
+ this.slug$.next(this.slug ?? null);
124
+ }
125
+ };
126
+ __decorateClass([
127
+ (0, import_core3.Input)()
128
+ ], SnippetViewComponent.prototype, "slug", 2);
129
+ __decorateClass([
130
+ (0, import_core3.Output)()
131
+ ], SnippetViewComponent.prototype, "loaded", 2);
132
+ SnippetViewComponent = __decorateClass([
133
+ (0, import_core3.Component)({
134
+ selector: "snippet-view",
135
+ standalone: true,
136
+ imports: [import_common.CommonModule],
137
+ template: `
138
+ <ng-container *ngIf="content$ | async as html; else loading">
139
+ <div class="mark-down-snippet" [innerHTML]="html"></div>
140
+ </ng-container>
141
+ <ng-template #loading>
142
+ <div class="mark-down-snippet--loading">Loading snippet\u2026</div>
143
+ </ng-template>
144
+ `,
145
+ changeDetection: import_core3.ChangeDetectionStrategy.OnPush
146
+ })
147
+ ], SnippetViewComponent);
148
+ // Annotate the CommonJS export names for ESM import in node:
149
+ 0 && (module.exports = {
150
+ MARK_DOWN_CLIENT,
151
+ MARK_DOWN_OPTIONS,
152
+ SnippetService,
153
+ SnippetViewComponent,
154
+ provideMarkDown
155
+ });
@@ -0,0 +1,31 @@
1
+ import { InjectionToken, Provider, OnChanges, EventEmitter } from '@angular/core';
2
+ import { SnippetClient, SnippetClientOptions, Snippet, ListOptions, SnippetMeta } from '@mzebley/mark-down';
3
+ import { Observable } from 'rxjs';
4
+ import { SafeHtml } from '@angular/platform-browser';
5
+
6
+ declare const MARK_DOWN_CLIENT: InjectionToken<SnippetClient>;
7
+ declare const MARK_DOWN_OPTIONS: InjectionToken<SnippetClientOptions>;
8
+ declare function provideMarkDown(options: SnippetClientOptions, client?: SnippetClient): Provider[];
9
+
10
+ declare class SnippetService {
11
+ private readonly client;
12
+ constructor(client: SnippetClient);
13
+ get(slug: string): Observable<Snippet | undefined>;
14
+ list(options?: ListOptions): Observable<SnippetMeta[]>;
15
+ listByGroup(group: string, options?: ListOptions): Observable<SnippetMeta[]>;
16
+ listByType(type: string, options?: ListOptions): Observable<SnippetMeta[]>;
17
+ html(slug: string): Observable<string | null>;
18
+ }
19
+
20
+ declare class SnippetViewComponent implements OnChanges {
21
+ private readonly slug$;
22
+ private readonly sanitizer;
23
+ private readonly snippets;
24
+ slug?: string;
25
+ readonly loaded: EventEmitter<Snippet | undefined>;
26
+ private readonly snippet$;
27
+ readonly content$: Observable<SafeHtml | null>;
28
+ ngOnChanges(): void;
29
+ }
30
+
31
+ export { MARK_DOWN_CLIENT, MARK_DOWN_OPTIONS, SnippetService, SnippetViewComponent, provideMarkDown };
@@ -0,0 +1,31 @@
1
+ import { InjectionToken, Provider, OnChanges, EventEmitter } from '@angular/core';
2
+ import { SnippetClient, SnippetClientOptions, Snippet, ListOptions, SnippetMeta } from '@mzebley/mark-down';
3
+ import { Observable } from 'rxjs';
4
+ import { SafeHtml } from '@angular/platform-browser';
5
+
6
+ declare const MARK_DOWN_CLIENT: InjectionToken<SnippetClient>;
7
+ declare const MARK_DOWN_OPTIONS: InjectionToken<SnippetClientOptions>;
8
+ declare function provideMarkDown(options: SnippetClientOptions, client?: SnippetClient): Provider[];
9
+
10
+ declare class SnippetService {
11
+ private readonly client;
12
+ constructor(client: SnippetClient);
13
+ get(slug: string): Observable<Snippet | undefined>;
14
+ list(options?: ListOptions): Observable<SnippetMeta[]>;
15
+ listByGroup(group: string, options?: ListOptions): Observable<SnippetMeta[]>;
16
+ listByType(type: string, options?: ListOptions): Observable<SnippetMeta[]>;
17
+ html(slug: string): Observable<string | null>;
18
+ }
19
+
20
+ declare class SnippetViewComponent implements OnChanges {
21
+ private readonly slug$;
22
+ private readonly sanitizer;
23
+ private readonly snippets;
24
+ slug?: string;
25
+ readonly loaded: EventEmitter<Snippet | undefined>;
26
+ private readonly snippet$;
27
+ readonly content$: Observable<SafeHtml | null>;
28
+ ngOnChanges(): void;
29
+ }
30
+
31
+ export { MARK_DOWN_CLIENT, MARK_DOWN_OPTIONS, SnippetService, SnippetViewComponent, provideMarkDown };
package/dist/index.js ADDED
@@ -0,0 +1,124 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
12
+
13
+ // src/token.ts
14
+ import { InjectionToken } from "@angular/core";
15
+ import { SnippetClient } from "@mzebley/mark-down";
16
+ var MARK_DOWN_CLIENT = new InjectionToken("mark-down-client");
17
+ var MARK_DOWN_OPTIONS = new InjectionToken("mark-down-options");
18
+ function provideMarkDown(options, client) {
19
+ return [
20
+ { provide: MARK_DOWN_OPTIONS, useValue: options },
21
+ {
22
+ provide: MARK_DOWN_CLIENT,
23
+ useFactory: (opts) => client ?? new SnippetClient(opts),
24
+ deps: [MARK_DOWN_OPTIONS]
25
+ }
26
+ ];
27
+ }
28
+
29
+ // src/snippet.service.ts
30
+ import { Inject, Injectable } from "@angular/core";
31
+ import { from, map, shareReplay } from "rxjs";
32
+ var SnippetService = class {
33
+ constructor(client) {
34
+ this.client = client;
35
+ }
36
+ get(slug) {
37
+ return from(this.client.get(slug)).pipe(shareReplay(1));
38
+ }
39
+ list(options) {
40
+ return from(this.client.list(options)).pipe(shareReplay(1));
41
+ }
42
+ listByGroup(group, options) {
43
+ return from(this.client.listByGroup(group, options)).pipe(shareReplay(1));
44
+ }
45
+ listByType(type, options) {
46
+ return from(this.client.listByType(type, options)).pipe(shareReplay(1));
47
+ }
48
+ html(slug) {
49
+ return this.get(slug).pipe(map((snippet) => snippet?.html ?? null));
50
+ }
51
+ };
52
+ SnippetService = __decorateClass([
53
+ Injectable({ providedIn: "root" }),
54
+ __decorateParam(0, Inject(MARK_DOWN_CLIENT))
55
+ ], SnippetService);
56
+
57
+ // src/snippet-view.component.ts
58
+ import { CommonModule } from "@angular/common";
59
+ import {
60
+ ChangeDetectionStrategy,
61
+ Component,
62
+ EventEmitter,
63
+ Input,
64
+ Output,
65
+ inject
66
+ } from "@angular/core";
67
+ import { DomSanitizer } from "@angular/platform-browser";
68
+ import { BehaviorSubject, of } from "rxjs";
69
+ import { map as map2, shareReplay as shareReplay2, switchMap, tap } from "rxjs/operators";
70
+ import DOMPurify from "dompurify";
71
+ var SnippetViewComponent = class {
72
+ constructor() {
73
+ this.slug$ = new BehaviorSubject(null);
74
+ this.sanitizer = inject(DomSanitizer);
75
+ this.snippets = inject(SnippetService);
76
+ this.loaded = new EventEmitter();
77
+ this.snippet$ = this.slug$.pipe(
78
+ switchMap((slug) => slug ? this.snippets.get(slug) : of(void 0)),
79
+ tap((snippet) => this.loaded.emit(snippet)),
80
+ shareReplay2(1)
81
+ );
82
+ this.content$ = this.snippet$.pipe(
83
+ map2((snippet) => {
84
+ if (!snippet) {
85
+ return null;
86
+ }
87
+ const sanitized = DOMPurify.sanitize(snippet.html);
88
+ return this.sanitizer.bypassSecurityTrustHtml(sanitized);
89
+ })
90
+ );
91
+ }
92
+ ngOnChanges() {
93
+ this.slug$.next(this.slug ?? null);
94
+ }
95
+ };
96
+ __decorateClass([
97
+ Input()
98
+ ], SnippetViewComponent.prototype, "slug", 2);
99
+ __decorateClass([
100
+ Output()
101
+ ], SnippetViewComponent.prototype, "loaded", 2);
102
+ SnippetViewComponent = __decorateClass([
103
+ Component({
104
+ selector: "snippet-view",
105
+ standalone: true,
106
+ imports: [CommonModule],
107
+ template: `
108
+ <ng-container *ngIf="content$ | async as html; else loading">
109
+ <div class="mark-down-snippet" [innerHTML]="html"></div>
110
+ </ng-container>
111
+ <ng-template #loading>
112
+ <div class="mark-down-snippet--loading">Loading snippet\u2026</div>
113
+ </ng-template>
114
+ `,
115
+ changeDetection: ChangeDetectionStrategy.OnPush
116
+ })
117
+ ], SnippetViewComponent);
118
+ export {
119
+ MARK_DOWN_CLIENT,
120
+ MARK_DOWN_OPTIONS,
121
+ SnippetService,
122
+ SnippetViewComponent,
123
+ provideMarkDown
124
+ };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@mzebley/mark-down-angular",
3
+ "version": "1.0.0",
4
+ "description": "mark↓ Angular Adapter",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "scripts": {
13
+ "build": "tsup src/index.ts --dts --format esm,cjs"
14
+ },
15
+ "dependencies": {
16
+ "@mzebley/mark-down": "file:../core",
17
+ "dompurify": "^3.0.9"
18
+ },
19
+ "peerDependencies": {
20
+ "@angular/common": "^17.0.0",
21
+ "@angular/core": "^17.0.0",
22
+ "@angular/platform-browser": "^17.0.0",
23
+ "rxjs": "^7.8.0"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./token";
2
+ export * from "./snippet.service";
3
+ export * from "./snippet-view.component";
@@ -0,0 +1,59 @@
1
+ import { CommonModule } from "@angular/common";
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ EventEmitter,
6
+ Input,
7
+ OnChanges,
8
+ Output,
9
+ inject
10
+ } from "@angular/core";
11
+ import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
12
+ import { BehaviorSubject, Observable, of } from "rxjs";
13
+ import { map, shareReplay, switchMap, tap } from "rxjs/operators";
14
+ import type { Snippet } from "@mzebley/mark-down";
15
+ import { SnippetService } from "./snippet.service";
16
+ import DOMPurify from "dompurify";
17
+
18
+ @Component({
19
+ selector: "snippet-view",
20
+ standalone: true,
21
+ imports: [CommonModule],
22
+ template: `
23
+ <ng-container *ngIf="content$ | async as html; else loading">
24
+ <div class="mark-down-snippet" [innerHTML]="html"></div>
25
+ </ng-container>
26
+ <ng-template #loading>
27
+ <div class="mark-down-snippet--loading">Loading snippet…</div>
28
+ </ng-template>
29
+ `,
30
+ changeDetection: ChangeDetectionStrategy.OnPush
31
+ })
32
+ export class SnippetViewComponent implements OnChanges {
33
+ private readonly slug$ = new BehaviorSubject<string | null>(null);
34
+ private readonly sanitizer = inject(DomSanitizer);
35
+ private readonly snippets = inject(SnippetService);
36
+
37
+ @Input() slug?: string;
38
+ @Output() readonly loaded = new EventEmitter<Snippet | undefined>();
39
+
40
+ private readonly snippet$: Observable<Snippet | undefined> = this.slug$.pipe(
41
+ switchMap((slug) => (slug ? this.snippets.get(slug) : of(undefined))),
42
+ tap((snippet) => this.loaded.emit(snippet)),
43
+ shareReplay(1)
44
+ );
45
+
46
+ readonly content$: Observable<SafeHtml | null> = this.snippet$.pipe(
47
+ map((snippet) => {
48
+ if (!snippet) {
49
+ return null;
50
+ }
51
+ const sanitized = DOMPurify.sanitize(snippet.html);
52
+ return this.sanitizer.bypassSecurityTrustHtml(sanitized);
53
+ })
54
+ );
55
+
56
+ ngOnChanges(): void {
57
+ this.slug$.next(this.slug ?? null);
58
+ }
59
+ }
@@ -0,0 +1,30 @@
1
+ import { Inject, Injectable } from "@angular/core";
2
+ import { from, map, Observable, shareReplay } from "rxjs";
3
+ import type { ListOptions, Snippet, SnippetMeta } from "@mzebley/mark-down";
4
+ import { SnippetClient } from "@mzebley/mark-down";
5
+ import { MARK_DOWN_CLIENT } from "./token";
6
+
7
+ @Injectable({ providedIn: "root" })
8
+ export class SnippetService {
9
+ constructor(@Inject(MARK_DOWN_CLIENT) private readonly client: SnippetClient) {}
10
+
11
+ get(slug: string): Observable<Snippet | undefined> {
12
+ return from(this.client.get(slug)).pipe(shareReplay(1));
13
+ }
14
+
15
+ list(options?: ListOptions): Observable<SnippetMeta[]> {
16
+ return from(this.client.list(options)).pipe(shareReplay(1));
17
+ }
18
+
19
+ listByGroup(group: string, options?: ListOptions): Observable<SnippetMeta[]> {
20
+ return from(this.client.listByGroup(group, options)).pipe(shareReplay(1));
21
+ }
22
+
23
+ listByType(type: string, options?: ListOptions): Observable<SnippetMeta[]> {
24
+ return from(this.client.listByType(type, options)).pipe(shareReplay(1));
25
+ }
26
+
27
+ html(slug: string): Observable<string | null> {
28
+ return this.get(slug).pipe(map((snippet) => snippet?.html ?? null));
29
+ }
30
+ }
package/src/token.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { InjectionToken, Provider } from "@angular/core";
2
+ import { SnippetClient, type SnippetClientOptions } from "@mzebley/mark-down";
3
+
4
+ export const MARK_DOWN_CLIENT = new InjectionToken<SnippetClient>("mark-down-client");
5
+ export const MARK_DOWN_OPTIONS = new InjectionToken<SnippetClientOptions>("mark-down-options");
6
+
7
+ export function provideMarkDown(options: SnippetClientOptions, client?: SnippetClient): Provider[] {
8
+ return [
9
+ { provide: MARK_DOWN_OPTIONS, useValue: options },
10
+ {
11
+ provide: MARK_DOWN_CLIENT,
12
+ useFactory: (opts: SnippetClientOptions) => client ?? new SnippetClient(opts),
13
+ deps: [MARK_DOWN_OPTIONS]
14
+ }
15
+ ];
16
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "experimentalDecorators": true,
6
+ "emitDecoratorMetadata": true
7
+ },
8
+ "include": ["src/**/*"]
9
+ }