@mzebley/mark-down 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 +18 -22
- package/dist/angular/index.d.ts +3 -1
- package/dist/angular/index.js +18 -8
- package/dist/angular/index.js.map +1 -1
- package/dist/browser.js +222 -31
- package/dist/browser.js.map +1 -1
- package/dist/{chunk-MWZFQXNW.js → chunk-WZCXKUXV.js} +128 -3
- package/dist/chunk-WZCXKUXV.js.map +1 -0
- package/dist/{chunk-35YHML5Z.js → chunk-X5L6GGFF.js} +69 -25
- package/dist/chunk-X5L6GGFF.js.map +1 -0
- package/dist/{chunk-GWLMADTU.js → chunk-ZEQXN4ZD.js} +4 -2
- package/dist/{chunk-GWLMADTU.js.map → chunk-ZEQXN4ZD.js.map} +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +7 -5
- package/dist/inline.d.ts +4 -0
- package/dist/inline.js +20 -6
- package/dist/inline.js.map +1 -1
- package/dist/mark-down-inline.umd.js +8951 -14
- package/dist/mark-down-inline.umd.js.map +1 -1
- package/dist/sanitize-DI2uKnlG.d.ts +10 -0
- package/dist/slug.js +1 -1
- package/dist/{snippet-client-CiQX2Zcn.d.ts → snippet-client-S6E_j24g.d.ts} +3 -0
- package/package.json +5 -2
- package/dist/chunk-35YHML5Z.js.map +0 -1
- package/dist/chunk-MWZFQXNW.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# mark↓ Core Runtime
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
_(published as `@mzebley/mark-down`)_
|
|
3
4
|
|
|
4
5
|
This package provides the framework-agnostic `SnippetClient` and supporting types used to fetch, cache, and render Markdown snippets at runtime. For a monorepo overview visit the [root README](../../README.md).
|
|
5
6
|
|
|
@@ -54,9 +55,14 @@ The client lazily loads the manifest when first needed, then fetches Markdown fi
|
|
|
54
55
|
- **`cache`** (`boolean`, default `true`) – enable or disable per-snippet and manifest memoisation.
|
|
55
56
|
- **`verbose`** (`boolean`) – log helpful warnings (for example, slug mismatches) during development.
|
|
56
57
|
- **`render`** (`(markdown: string) => string | Promise<string>`) – override the default `marked` renderer when you need custom HTML output.
|
|
58
|
+
- **`sanitize`** (`{ policy?: "default" | "strict" | "permissive"; config?: SanitizeHtmlOptions }`) – optionally sanitize rendered HTML before it is returned.
|
|
57
59
|
|
|
58
60
|
All options are optional except `manifest`. Results are rendered with `marked` by default; override at the application level if you need a different Markdown pipeline.
|
|
59
61
|
|
|
62
|
+
`sanitize` is opt-in and uses [`sanitize-html`](https://github.com/apostrophecms/sanitize-html) under the hood. Use the `policy` presets for common Markdown output or override the `config` to tweak allowed tags/attributes.
|
|
63
|
+
|
|
64
|
+
Security note: mark↓ intentionally allows unsanitized output when `sanitize` is omitted. This is a supported feature for trusted pipelines, but it is unsafe for untrusted content. Enable `sanitize` whenever snippets can be user-generated or externally sourced.
|
|
65
|
+
|
|
60
66
|
## Working with snippets
|
|
61
67
|
|
|
62
68
|
Commonly used APIs:
|
|
@@ -80,12 +86,13 @@ import { SnippetClient } from "@mzebley/mark-down";
|
|
|
80
86
|
|
|
81
87
|
const client = new SnippetClient({
|
|
82
88
|
manifest: () => import("./snippets-index.json"),
|
|
83
|
-
fetch: (url) =>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
fetch: (url) =>
|
|
90
|
+
fetch(url).then((response) => {
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`Request failed with status ${response.status}`);
|
|
93
|
+
}
|
|
94
|
+
return response;
|
|
95
|
+
}),
|
|
89
96
|
});
|
|
90
97
|
```
|
|
91
98
|
|
|
@@ -145,11 +152,7 @@ Host the UMD file yourself or load it from a CDN—no build tooling required.
|
|
|
145
152
|
Write plain Markdown directly in your HTML. The helper finds `[data-markdown]` blocks by default.
|
|
146
153
|
|
|
147
154
|
```html
|
|
148
|
-
<div data-markdown>
|
|
149
|
-
# Hello
|
|
150
|
-
|
|
151
|
-
This is inline markdown with no build step.
|
|
152
|
-
</div>
|
|
155
|
+
<div data-markdown># Hello This is inline markdown with no build step.</div>
|
|
153
156
|
|
|
154
157
|
<script src="path/to/mark-down-inline.umd.js"></script>
|
|
155
158
|
<script>
|
|
@@ -167,16 +170,8 @@ You can optionally prepend YAML front matter to provide metadata for each block.
|
|
|
167
170
|
|
|
168
171
|
```html
|
|
169
172
|
<div data-markdown>
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
title: Introduction
|
|
173
|
-
tags: [hero]
|
|
174
|
-
variant: lead
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
# Introduction
|
|
178
|
-
|
|
179
|
-
This block has metadata.
|
|
173
|
+
--- slug: intro title: Introduction tags: [hero] variant: lead --- #
|
|
174
|
+
Introduction This block has metadata.
|
|
180
175
|
</div>
|
|
181
176
|
```
|
|
182
177
|
|
|
@@ -196,6 +191,7 @@ enhanceInlineMarkdown({
|
|
|
196
191
|
selector?: string; // defaults to "[data-markdown]"
|
|
197
192
|
processFrontMatter?: boolean; // defaults to true
|
|
198
193
|
applyMetaToDom?: boolean; // defaults to true
|
|
194
|
+
sanitize?: { policy?: "default" | "strict" | "permissive"; config?: SanitizeHtmlOptions };
|
|
199
195
|
});
|
|
200
196
|
```
|
|
201
197
|
|
package/dist/angular/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { InjectionToken, Provider } from '@angular/core';
|
|
2
2
|
import { Observable } from 'rxjs';
|
|
3
|
-
import { f as SnippetClient, e as SnippetClientOptions, a as Snippet, S as SnippetMeta } from '../snippet-client-
|
|
3
|
+
import { f as SnippetClient, e as SnippetClientOptions, a as Snippet, S as SnippetMeta } from '../snippet-client-S6E_j24g.js';
|
|
4
|
+
import '../sanitize-DI2uKnlG.js';
|
|
5
|
+
import 'sanitize-html';
|
|
4
6
|
|
|
5
7
|
declare const SNIPPET_CLIENT: InjectionToken<SnippetClient>;
|
|
6
8
|
declare const SNIPPET_CLIENT_OPTIONS: InjectionToken<SnippetClientOptions>;
|
package/dist/angular/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SnippetClient
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-X5L6GGFF.js";
|
|
4
|
+
import "../chunk-ZEQXN4ZD.js";
|
|
5
|
+
import "../chunk-WZCXKUXV.js";
|
|
6
6
|
import {
|
|
7
7
|
__decorateClass,
|
|
8
8
|
__decorateParam
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
// src/angular/index.ts
|
|
12
12
|
import { Inject, Injectable, InjectionToken } from "@angular/core";
|
|
13
13
|
import { from, map, shareReplay } from "rxjs";
|
|
14
|
-
var SNIPPET_CLIENT = new InjectionToken(
|
|
14
|
+
var SNIPPET_CLIENT = new InjectionToken(
|
|
15
|
+
"@mzebley/mark-down/SNIPPET_CLIENT"
|
|
16
|
+
);
|
|
15
17
|
var SNIPPET_CLIENT_OPTIONS = new InjectionToken(
|
|
16
18
|
"@mzebley/mark-down/SNIPPET_CLIENT_OPTIONS"
|
|
17
19
|
);
|
|
@@ -30,16 +32,24 @@ var MarkdownSnippetService = class {
|
|
|
30
32
|
this.client = client;
|
|
31
33
|
}
|
|
32
34
|
get(slug) {
|
|
33
|
-
return from(this.client.get(slug)).pipe(
|
|
35
|
+
return from(this.client.get(slug)).pipe(
|
|
36
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
37
|
+
);
|
|
34
38
|
}
|
|
35
39
|
listAll() {
|
|
36
|
-
return from(this.client.listAll()).pipe(
|
|
40
|
+
return from(this.client.listAll()).pipe(
|
|
41
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
42
|
+
);
|
|
37
43
|
}
|
|
38
44
|
listByGroup(group) {
|
|
39
|
-
return from(this.client.listByGroup(group)).pipe(
|
|
45
|
+
return from(this.client.listByGroup(group)).pipe(
|
|
46
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
47
|
+
);
|
|
40
48
|
}
|
|
41
49
|
listByType(type) {
|
|
42
|
-
return from(this.client.listByType(type)).pipe(
|
|
50
|
+
return from(this.client.listByType(type)).pipe(
|
|
51
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
52
|
+
);
|
|
43
53
|
}
|
|
44
54
|
html(slug) {
|
|
45
55
|
return this.get(slug).pipe(map((snippet) => snippet.html));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/angular/index.ts"],"sourcesContent":["import { Inject, Injectable, InjectionToken, Provider } from \"@angular/core\";\nimport { from, map, Observable, shareReplay } from \"rxjs\";\nimport { SnippetClient } from \"../snippet-client\";\nimport type { Snippet, SnippetClientOptions, SnippetMeta } from \"../types\";\n\nexport const SNIPPET_CLIENT = new InjectionToken<SnippetClient>(\"@mzebley/mark-down/SNIPPET_CLIENT\");\nexport const SNIPPET_CLIENT_OPTIONS = new InjectionToken<SnippetClientOptions>(\n \"@mzebley/mark-down/SNIPPET_CLIENT_OPTIONS\"
|
|
1
|
+
{"version":3,"sources":["../../src/angular/index.ts"],"sourcesContent":["import { Inject, Injectable, InjectionToken, Provider } from \"@angular/core\";\nimport { from, map, Observable, shareReplay } from \"rxjs\";\nimport { SnippetClient } from \"../snippet-client\";\nimport type { Snippet, SnippetClientOptions, SnippetMeta } from \"../types\";\n\nexport const SNIPPET_CLIENT = new InjectionToken<SnippetClient>(\n \"@mzebley/mark-down/SNIPPET_CLIENT\",\n);\nexport const SNIPPET_CLIENT_OPTIONS = new InjectionToken<SnippetClientOptions>(\n \"@mzebley/mark-down/SNIPPET_CLIENT_OPTIONS\",\n);\n\nexport function provideSnippetClient(\n options: SnippetClientOptions,\n): Provider[] {\n return [\n { provide: SNIPPET_CLIENT_OPTIONS, useValue: options },\n {\n provide: SNIPPET_CLIENT,\n useFactory: (opts: SnippetClientOptions) => new SnippetClient(opts),\n deps: [SNIPPET_CLIENT_OPTIONS],\n },\n ];\n}\n\n@Injectable({ providedIn: \"root\" })\nexport class MarkdownSnippetService {\n constructor(@Inject(SNIPPET_CLIENT) private readonly client: SnippetClient) {}\n\n get(slug: string): Observable<Snippet> {\n return from(this.client.get(slug)).pipe(\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n }\n\n listAll(): Observable<SnippetMeta[]> {\n return from(this.client.listAll()).pipe(\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n }\n\n listByGroup(group: string): Observable<SnippetMeta[]> {\n return from(this.client.listByGroup(group)).pipe(\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n }\n\n listByType(type: string): Observable<SnippetMeta[]> {\n return from(this.client.listByType(type)).pipe(\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n }\n\n html(slug: string): Observable<string> {\n return this.get(slug).pipe(map((snippet) => snippet.html));\n }\n}\n\nexport type { Snippet, SnippetClientOptions, SnippetMeta } from \"../types\";\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,QAAQ,YAAY,sBAAgC;AAC7D,SAAS,MAAM,KAAiB,mBAAmB;AAI5C,IAAM,iBAAiB,IAAI;AAAA,EAChC;AACF;AACO,IAAM,yBAAyB,IAAI;AAAA,EACxC;AACF;AAEO,SAAS,qBACd,SACY;AACZ,SAAO;AAAA,IACL,EAAE,SAAS,wBAAwB,UAAU,QAAQ;AAAA,IACrD;AAAA,MACE,SAAS;AAAA,MACT,YAAY,CAAC,SAA+B,IAAI,cAAc,IAAI;AAAA,MAClE,MAAM,CAAC,sBAAsB;AAAA,IAC/B;AAAA,EACF;AACF;AAGO,IAAM,yBAAN,MAA6B;AAAA,EAClC,YAAqD,QAAuB;AAAvB;AAAA,EAAwB;AAAA,EAE7E,IAAI,MAAmC;AACrC,WAAO,KAAK,KAAK,OAAO,IAAI,IAAI,CAAC,EAAE;AAAA,MACjC,YAAY,EAAE,YAAY,GAAG,UAAU,KAAK,CAAC;AAAA,IAC/C;AAAA,EACF;AAAA,EAEA,UAAqC;AACnC,WAAO,KAAK,KAAK,OAAO,QAAQ,CAAC,EAAE;AAAA,MACjC,YAAY,EAAE,YAAY,GAAG,UAAU,KAAK,CAAC;AAAA,IAC/C;AAAA,EACF;AAAA,EAEA,YAAY,OAA0C;AACpD,WAAO,KAAK,KAAK,OAAO,YAAY,KAAK,CAAC,EAAE;AAAA,MAC1C,YAAY,EAAE,YAAY,GAAG,UAAU,KAAK,CAAC;AAAA,IAC/C;AAAA,EACF;AAAA,EAEA,WAAW,MAAyC;AAClD,WAAO,KAAK,KAAK,OAAO,WAAW,IAAI,CAAC,EAAE;AAAA,MACxC,YAAY,EAAE,YAAY,GAAG,UAAU,KAAK,CAAC;AAAA,IAC/C;AAAA,EACF;AAAA,EAEA,KAAK,MAAkC;AACrC,WAAO,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,CAAC,YAAY,QAAQ,IAAI,CAAC;AAAA,EAC3D;AACF;AA9Ba,yBAAN;AAAA,EADN,WAAW,EAAE,YAAY,OAAO,CAAC;AAAA,EAEnB,0BAAO,cAAc;AAAA,GADvB;","names":[]}
|
package/dist/browser.js
CHANGED
|
@@ -8,7 +8,9 @@ function normalizeSlug(input) {
|
|
|
8
8
|
}
|
|
9
9
|
const normalized = value.toLowerCase().replace(NON_ALPHANUMERIC, "-").replace(/-{2,}/g, "-").replace(LEADING_TRAILING_DASH, "");
|
|
10
10
|
if (!normalized) {
|
|
11
|
-
throw new Error(
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Slug '${input}' does not contain any alphanumeric characters`
|
|
13
|
+
);
|
|
12
14
|
}
|
|
13
15
|
return normalized;
|
|
14
16
|
}
|
|
@@ -47,7 +49,12 @@ function parseFrontMatter(raw) {
|
|
|
47
49
|
throw new ManifestLoadError("Failed to parse snippet front-matter.", error);
|
|
48
50
|
}
|
|
49
51
|
if (!isRecord(data)) {
|
|
50
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
content: raw.slice(match[0].length),
|
|
54
|
+
meta: {},
|
|
55
|
+
extra: {},
|
|
56
|
+
hasFrontMatter: true
|
|
57
|
+
};
|
|
51
58
|
}
|
|
52
59
|
const { known, extra } = splitFrontMatter(data);
|
|
53
60
|
return {
|
|
@@ -128,13 +135,134 @@ function renderMarkdown(markdown) {
|
|
|
128
135
|
throw new Error("renderMarkdown unexpectedly returned a Promise");
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
// src/sanitize.ts
|
|
139
|
+
import sanitizeHtml from "sanitize-html";
|
|
140
|
+
var DEFAULT_ALLOWED_TAGS = [
|
|
141
|
+
"a",
|
|
142
|
+
"b",
|
|
143
|
+
"blockquote",
|
|
144
|
+
"br",
|
|
145
|
+
"code",
|
|
146
|
+
"del",
|
|
147
|
+
"em",
|
|
148
|
+
"h1",
|
|
149
|
+
"h2",
|
|
150
|
+
"h3",
|
|
151
|
+
"h4",
|
|
152
|
+
"h5",
|
|
153
|
+
"h6",
|
|
154
|
+
"hr",
|
|
155
|
+
"i",
|
|
156
|
+
"img",
|
|
157
|
+
"li",
|
|
158
|
+
"ol",
|
|
159
|
+
"p",
|
|
160
|
+
"pre",
|
|
161
|
+
"span",
|
|
162
|
+
"strong",
|
|
163
|
+
"sub",
|
|
164
|
+
"sup",
|
|
165
|
+
"table",
|
|
166
|
+
"tbody",
|
|
167
|
+
"td",
|
|
168
|
+
"th",
|
|
169
|
+
"thead",
|
|
170
|
+
"tr",
|
|
171
|
+
"ul"
|
|
172
|
+
];
|
|
173
|
+
var STRICT_ALLOWED_TAGS = [
|
|
174
|
+
"a",
|
|
175
|
+
"blockquote",
|
|
176
|
+
"br",
|
|
177
|
+
"code",
|
|
178
|
+
"em",
|
|
179
|
+
"li",
|
|
180
|
+
"ol",
|
|
181
|
+
"p",
|
|
182
|
+
"pre",
|
|
183
|
+
"strong",
|
|
184
|
+
"ul"
|
|
185
|
+
];
|
|
186
|
+
var DEFAULT_ALLOWED_ATTRIBUTES = {
|
|
187
|
+
a: ["href", "name", "target", "rel"],
|
|
188
|
+
img: ["src", "alt", "title", "width", "height"],
|
|
189
|
+
code: ["class"],
|
|
190
|
+
pre: ["class"],
|
|
191
|
+
span: ["class"],
|
|
192
|
+
"*": ["id", "title", "aria-*", "data-*"]
|
|
193
|
+
};
|
|
194
|
+
var STRICT_ALLOWED_ATTRIBUTES = {
|
|
195
|
+
a: ["href", "name", "target", "rel"]
|
|
196
|
+
};
|
|
197
|
+
var POLICY_CONFIG = {
|
|
198
|
+
default: {
|
|
199
|
+
allowedTags: DEFAULT_ALLOWED_TAGS,
|
|
200
|
+
allowedAttributes: DEFAULT_ALLOWED_ATTRIBUTES,
|
|
201
|
+
allowedSchemes: ["http", "https", "mailto"],
|
|
202
|
+
allowedSchemesByTag: {
|
|
203
|
+
img: ["http", "https", "data"]
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
strict: {
|
|
207
|
+
allowedTags: STRICT_ALLOWED_TAGS,
|
|
208
|
+
allowedAttributes: STRICT_ALLOWED_ATTRIBUTES,
|
|
209
|
+
allowedSchemes: ["http", "https", "mailto"]
|
|
210
|
+
},
|
|
211
|
+
permissive: {
|
|
212
|
+
...sanitizeHtml.defaults,
|
|
213
|
+
allowedTags: sanitizeHtml.defaults.allowedTags ? [...sanitizeHtml.defaults.allowedTags] : void 0,
|
|
214
|
+
allowedAttributes: sanitizeHtml.defaults.allowedAttributes ? { ...sanitizeHtml.defaults.allowedAttributes } : void 0
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
function sanitizeMarkup(html, options = {}) {
|
|
218
|
+
const policy = options.policy ?? "default";
|
|
219
|
+
const base = POLICY_CONFIG[policy];
|
|
220
|
+
const merged = mergeOptions(base, options.config);
|
|
221
|
+
return sanitizeHtml(html, merged);
|
|
222
|
+
}
|
|
223
|
+
function mergeOptions(base, overrides) {
|
|
224
|
+
if (!overrides) {
|
|
225
|
+
return base;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
...base,
|
|
229
|
+
...overrides,
|
|
230
|
+
allowedAttributes: mergeAllowedAttributes(
|
|
231
|
+
base.allowedAttributes,
|
|
232
|
+
overrides.allowedAttributes
|
|
233
|
+
),
|
|
234
|
+
allowedClasses: mergeRecord(base.allowedClasses, overrides.allowedClasses),
|
|
235
|
+
allowedStyles: mergeRecord(base.allowedStyles, overrides.allowedStyles)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function mergeAllowedAttributes(base, overrides) {
|
|
239
|
+
if (overrides === false) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (base === false) {
|
|
243
|
+
return overrides ?? false;
|
|
244
|
+
}
|
|
245
|
+
if (!base && !overrides) {
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
return { ...base ?? {}, ...overrides ?? {} };
|
|
249
|
+
}
|
|
250
|
+
function mergeRecord(base, overrides) {
|
|
251
|
+
if (!base && !overrides) {
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
return { ...base ?? {}, ...overrides ?? {} };
|
|
255
|
+
}
|
|
256
|
+
|
|
131
257
|
// src/snippet-client.ts
|
|
132
258
|
var HTTP_PATTERN = /^https?:\/\//i;
|
|
133
259
|
var SnippetClient = class {
|
|
134
260
|
constructor(options) {
|
|
135
261
|
this.snippetCache = /* @__PURE__ */ new Map();
|
|
136
262
|
if (!options || !options.manifest) {
|
|
137
|
-
throw new ManifestLoadError(
|
|
263
|
+
throw new ManifestLoadError(
|
|
264
|
+
"A manifest source must be provided to SnippetClient."
|
|
265
|
+
);
|
|
138
266
|
}
|
|
139
267
|
this.manifestUrl = typeof options.manifest === "string" ? options.manifest : void 0;
|
|
140
268
|
this.inferredBase = this.manifestUrl ? deriveBaseFromManifest(this.manifestUrl) : void 0;
|
|
@@ -154,7 +282,8 @@ var SnippetClient = class {
|
|
|
154
282
|
frontMatter: options.frontMatter !== false,
|
|
155
283
|
cache: options.cache !== false,
|
|
156
284
|
verbose: options.verbose === true,
|
|
157
|
-
render: renderer
|
|
285
|
+
render: renderer,
|
|
286
|
+
sanitize: options.sanitize
|
|
158
287
|
};
|
|
159
288
|
}
|
|
160
289
|
async get(slug) {
|
|
@@ -231,7 +360,9 @@ var SnippetClient = class {
|
|
|
231
360
|
} else if (typeof source === "function") {
|
|
232
361
|
const result = await source();
|
|
233
362
|
if (!Array.isArray(result)) {
|
|
234
|
-
throw new ManifestLoadError(
|
|
363
|
+
throw new ManifestLoadError(
|
|
364
|
+
"Manifest loader must resolve to an array of snippet metadata."
|
|
365
|
+
);
|
|
235
366
|
}
|
|
236
367
|
entries = result.map(normalizeManifestEntry);
|
|
237
368
|
} else {
|
|
@@ -263,11 +394,17 @@ var SnippetClient = class {
|
|
|
263
394
|
try {
|
|
264
395
|
raw = await this.options.fetch(url);
|
|
265
396
|
} catch (error) {
|
|
266
|
-
throw new ManifestLoadError(
|
|
397
|
+
throw new ManifestLoadError(
|
|
398
|
+
`Failed to fetch snippet at '${url}'.`,
|
|
399
|
+
error
|
|
400
|
+
);
|
|
267
401
|
}
|
|
268
402
|
const frontMatter = this.options.frontMatter ? parseFrontMatter(raw) : void 0;
|
|
269
403
|
const body = frontMatter?.content ?? raw;
|
|
270
|
-
|
|
404
|
+
let html = await this.options.render(body);
|
|
405
|
+
if (this.options.sanitize) {
|
|
406
|
+
html = sanitizeMarkup(html, this.options.sanitize);
|
|
407
|
+
}
|
|
271
408
|
const merged = {
|
|
272
409
|
...meta,
|
|
273
410
|
...pickMeta(frontMatter?.meta),
|
|
@@ -283,7 +420,10 @@ var SnippetClient = class {
|
|
|
283
420
|
}
|
|
284
421
|
} catch (error) {
|
|
285
422
|
if (this.options.verbose) {
|
|
286
|
-
console.warn(
|
|
423
|
+
console.warn(
|
|
424
|
+
`Failed to normalize front-matter slug '${frontMatter.slug}':`,
|
|
425
|
+
error
|
|
426
|
+
);
|
|
287
427
|
}
|
|
288
428
|
}
|
|
289
429
|
}
|
|
@@ -315,26 +455,38 @@ function parseManifest(raw, source) {
|
|
|
315
455
|
try {
|
|
316
456
|
parsed = JSON.parse(raw);
|
|
317
457
|
} catch (error) {
|
|
318
|
-
throw new ManifestLoadError(
|
|
458
|
+
throw new ManifestLoadError(
|
|
459
|
+
`Manifest at '${source}' is not valid JSON.`,
|
|
460
|
+
error
|
|
461
|
+
);
|
|
319
462
|
}
|
|
320
463
|
if (!Array.isArray(parsed)) {
|
|
321
|
-
throw new ManifestLoadError(
|
|
464
|
+
throw new ManifestLoadError(
|
|
465
|
+
`Manifest at '${source}' must be a JSON array.`
|
|
466
|
+
);
|
|
322
467
|
}
|
|
323
468
|
return parsed;
|
|
324
469
|
}
|
|
325
470
|
function normalizeManifestEntry(entry) {
|
|
471
|
+
if (!entry || typeof entry !== "object") {
|
|
472
|
+
throw new ManifestLoadError("Manifest entry must be an object.");
|
|
473
|
+
}
|
|
326
474
|
if (!entry.slug) {
|
|
327
|
-
throw new ManifestLoadError(
|
|
475
|
+
throw new ManifestLoadError(
|
|
476
|
+
"Manifest entry is missing required 'slug' property."
|
|
477
|
+
);
|
|
328
478
|
}
|
|
329
479
|
if (!entry.path) {
|
|
330
|
-
throw new ManifestLoadError(
|
|
480
|
+
throw new ManifestLoadError(
|
|
481
|
+
`Manifest entry for '${entry.slug}' is missing required 'path'.`
|
|
482
|
+
);
|
|
331
483
|
}
|
|
332
484
|
const normalized = {
|
|
333
485
|
slug: entry.slug,
|
|
334
486
|
title: entry.title,
|
|
335
487
|
type: entry.type,
|
|
336
488
|
order: entry.order,
|
|
337
|
-
tags: entry.tags
|
|
489
|
+
tags: normalizeTags2(entry.tags),
|
|
338
490
|
path: normalizeForwardSlashes(entry.path),
|
|
339
491
|
group: entry.group ?? null,
|
|
340
492
|
draft: entry.draft,
|
|
@@ -372,21 +524,22 @@ function joinPaths(base, relative) {
|
|
|
372
524
|
return normalizeForwardSlashes(`${leading}${trimmed}`);
|
|
373
525
|
}
|
|
374
526
|
function normalizeForwardSlashes(value) {
|
|
375
|
-
|
|
527
|
+
const sanitized = value.replace(/\\/g, "/");
|
|
528
|
+
if (HTTP_PATTERN.test(sanitized)) {
|
|
376
529
|
try {
|
|
377
|
-
const url = new URL(
|
|
530
|
+
const url = new URL(sanitized);
|
|
378
531
|
url.pathname = url.pathname.replace(/\/{2,}/g, "/");
|
|
379
532
|
return url.toString();
|
|
380
533
|
} catch {
|
|
381
534
|
}
|
|
382
535
|
}
|
|
383
|
-
if (
|
|
384
|
-
return `//${
|
|
536
|
+
if (sanitized.startsWith("//")) {
|
|
537
|
+
return `//${sanitized.slice(2).replace(/\/{2,}/g, "/")}`;
|
|
385
538
|
}
|
|
386
|
-
if (
|
|
387
|
-
return `/${
|
|
539
|
+
if (sanitized.startsWith("/")) {
|
|
540
|
+
return `/${sanitized.slice(1).replace(/\/{2,}/g, "/")}`;
|
|
388
541
|
}
|
|
389
|
-
return
|
|
542
|
+
return sanitized.replace(/\/{2,}/g, "/");
|
|
390
543
|
}
|
|
391
544
|
function mergeExtra(base, overrides) {
|
|
392
545
|
if (!base && !overrides) {
|
|
@@ -403,20 +556,36 @@ function cloneRecord(value) {
|
|
|
403
556
|
function cloneMeta(meta) {
|
|
404
557
|
return {
|
|
405
558
|
...meta,
|
|
406
|
-
tags: meta.tags
|
|
559
|
+
tags: normalizeTags2(meta.tags),
|
|
407
560
|
extra: cloneRecord(meta.extra)
|
|
408
561
|
};
|
|
409
562
|
}
|
|
563
|
+
function normalizeTags2(value) {
|
|
564
|
+
if (!value) {
|
|
565
|
+
return void 0;
|
|
566
|
+
}
|
|
567
|
+
if (Array.isArray(value)) {
|
|
568
|
+
return value.map((entry) => String(entry));
|
|
569
|
+
}
|
|
570
|
+
if (typeof value === "string") {
|
|
571
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
572
|
+
}
|
|
573
|
+
return void 0;
|
|
574
|
+
}
|
|
410
575
|
async function defaultFetch(url) {
|
|
411
576
|
const runtimeFetch = globalThis.fetch;
|
|
412
577
|
if (!runtimeFetch) {
|
|
413
|
-
throw new ManifestLoadError(
|
|
578
|
+
throw new ManifestLoadError(
|
|
579
|
+
"No global fetch implementation is available. Provide a custom fetch function."
|
|
580
|
+
);
|
|
414
581
|
}
|
|
415
582
|
return runtimeFetch(url);
|
|
416
583
|
}
|
|
417
584
|
async function resolveResponseText(response, url) {
|
|
418
585
|
if (!response.ok) {
|
|
419
|
-
throw new ManifestLoadError(
|
|
586
|
+
throw new ManifestLoadError(
|
|
587
|
+
`Request to '${url}' failed with status ${response.status}.`
|
|
588
|
+
);
|
|
420
589
|
}
|
|
421
590
|
return response.text();
|
|
422
591
|
}
|
|
@@ -470,7 +639,12 @@ function installBufferShim() {
|
|
|
470
639
|
}
|
|
471
640
|
if (ArrayBuffer.isView(value)) {
|
|
472
641
|
super(
|
|
473
|
-
new Uint8Array(
|
|
642
|
+
new Uint8Array(
|
|
643
|
+
value.buffer.slice(
|
|
644
|
+
value.byteOffset,
|
|
645
|
+
value.byteOffset + value.byteLength
|
|
646
|
+
)
|
|
647
|
+
)
|
|
474
648
|
);
|
|
475
649
|
return;
|
|
476
650
|
}
|
|
@@ -478,7 +652,9 @@ function installBufferShim() {
|
|
|
478
652
|
}
|
|
479
653
|
toString(encoding = "utf-8") {
|
|
480
654
|
if (encoding !== "utf-8" && encoding !== "utf8") {
|
|
481
|
-
throw new Error(
|
|
655
|
+
throw new Error(
|
|
656
|
+
`Unsupported encoding '${encoding}' in browser Buffer shim`
|
|
657
|
+
);
|
|
482
658
|
}
|
|
483
659
|
return textDecoder.decode(this);
|
|
484
660
|
}
|
|
@@ -486,7 +662,9 @@ function installBufferShim() {
|
|
|
486
662
|
const from = (value, encoding = "utf-8") => {
|
|
487
663
|
if (typeof value === "string") {
|
|
488
664
|
if (encoding !== "utf-8" && encoding !== "utf8") {
|
|
489
|
-
throw new Error(
|
|
665
|
+
throw new Error(
|
|
666
|
+
`Unsupported encoding '${encoding}' in browser Buffer shim`
|
|
667
|
+
);
|
|
490
668
|
}
|
|
491
669
|
return new BrowserBuffer(textEncoder.encode(value));
|
|
492
670
|
}
|
|
@@ -499,7 +677,9 @@ function installBufferShim() {
|
|
|
499
677
|
if (typeof value.length === "number") {
|
|
500
678
|
return new BrowserBuffer(Array.from(value));
|
|
501
679
|
}
|
|
502
|
-
throw new TypeError(
|
|
680
|
+
throw new TypeError(
|
|
681
|
+
"Unsupported input passed to Buffer.from in browser shim"
|
|
682
|
+
);
|
|
503
683
|
};
|
|
504
684
|
const alloc = (size, fill) => {
|
|
505
685
|
if (size < 0) {
|
|
@@ -528,11 +708,19 @@ function installBufferShim() {
|
|
|
528
708
|
(buffer) => buffer instanceof BrowserBuffer ? buffer : new BrowserBuffer(buffer)
|
|
529
709
|
);
|
|
530
710
|
const length = totalLength ?? sanitized.reduce((acc, current) => acc + current.length, 0);
|
|
711
|
+
if (length < 0) {
|
|
712
|
+
throw new RangeError("Invalid Buffer length");
|
|
713
|
+
}
|
|
531
714
|
const result = new BrowserBuffer(length);
|
|
532
715
|
let offset = 0;
|
|
533
716
|
for (const buffer of sanitized) {
|
|
534
|
-
|
|
535
|
-
|
|
717
|
+
const remaining = length - offset;
|
|
718
|
+
if (remaining <= 0) {
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
const slice = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer;
|
|
722
|
+
result.set(slice, offset);
|
|
723
|
+
offset += slice.length;
|
|
536
724
|
}
|
|
537
725
|
return result;
|
|
538
726
|
};
|
|
@@ -550,7 +738,9 @@ function installBufferShim() {
|
|
|
550
738
|
};
|
|
551
739
|
Object.defineProperties(BrowserBuffer, {
|
|
552
740
|
from: { value: from },
|
|
553
|
-
isBuffer: {
|
|
741
|
+
isBuffer: {
|
|
742
|
+
value: (candidate) => candidate instanceof BrowserBuffer
|
|
743
|
+
},
|
|
554
744
|
alloc: { value: alloc },
|
|
555
745
|
concat: { value: concat },
|
|
556
746
|
byteLength: { value: byteLength }
|
|
@@ -575,6 +765,7 @@ export {
|
|
|
575
765
|
SnippetNotFoundError,
|
|
576
766
|
normalizeSlug,
|
|
577
767
|
parseFrontMatter,
|
|
578
|
-
renderMarkdown
|
|
768
|
+
renderMarkdown,
|
|
769
|
+
sanitizeMarkup
|
|
579
770
|
};
|
|
580
771
|
//# sourceMappingURL=browser.js.map
|