@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 CHANGED
@@ -1,5 +1,6 @@
1
1
  # mark↓ Core Runtime
2
- *(published as `@mzebley/mark-down`)*
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) => fetch(url).then((response) => {
84
- if (!response.ok) {
85
- throw new Error(`Request failed with status ${response.status}`);
86
- }
87
- return response;
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
- slug: intro
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
 
@@ -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-CiQX2Zcn.js';
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>;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  SnippetClient
3
- } from "../chunk-35YHML5Z.js";
4
- import "../chunk-GWLMADTU.js";
5
- import "../chunk-MWZFQXNW.js";
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("@mzebley/mark-down/SNIPPET_CLIENT");
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(shareReplay({ bufferSize: 1, refCount: true }));
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(shareReplay({ bufferSize: 1, refCount: true }));
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(shareReplay({ bufferSize: 1, refCount: true }));
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(shareReplay({ bufferSize: 1, refCount: true }));
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\"\n);\n\nexport function provideSnippetClient(options: SnippetClientOptions): 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(shareReplay({ bufferSize: 1, refCount: true }));\n }\n\n listAll(): Observable<SnippetMeta[]> {\n return from(this.client.listAll()).pipe(shareReplay({ bufferSize: 1, refCount: true }));\n }\n\n listByGroup(group: string): Observable<SnippetMeta[]> {\n return from(this.client.listByGroup(group)).pipe(shareReplay({ bufferSize: 1, refCount: true }));\n }\n\n listByType(type: string): Observable<SnippetMeta[]> {\n return from(this.client.listByType(type)).pipe(shareReplay({ bufferSize: 1, refCount: true }));\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,eAA8B,mCAAmC;AAC5F,IAAM,yBAAyB,IAAI;AAAA,EACxC;AACF;AAEO,SAAS,qBAAqB,SAA2C;AAC9E,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,KAAK,YAAY,EAAE,YAAY,GAAG,UAAU,KAAK,CAAC,CAAC;AAAA,EACxF;AAAA,EAEA,UAAqC;AACnC,WAAO,KAAK,KAAK,OAAO,QAAQ,CAAC,EAAE,KAAK,YAAY,EAAE,YAAY,GAAG,UAAU,KAAK,CAAC,CAAC;AAAA,EACxF;AAAA,EAEA,YAAY,OAA0C;AACpD,WAAO,KAAK,KAAK,OAAO,YAAY,KAAK,CAAC,EAAE,KAAK,YAAY,EAAE,YAAY,GAAG,UAAU,KAAK,CAAC,CAAC;AAAA,EACjG;AAAA,EAEA,WAAW,MAAyC;AAClD,WAAO,KAAK,KAAK,OAAO,WAAW,IAAI,CAAC,EAAE,KAAK,YAAY,EAAE,YAAY,GAAG,UAAU,KAAK,CAAC,CAAC;AAAA,EAC/F;AAAA,EAEA,KAAK,MAAkC;AACrC,WAAO,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,CAAC,YAAY,QAAQ,IAAI,CAAC;AAAA,EAC3D;AACF;AAtBa,yBAAN;AAAA,EADN,WAAW,EAAE,YAAY,OAAO,CAAC;AAAA,EAEnB,0BAAO,cAAc;AAAA,GADvB;","names":[]}
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(`Slug '${input}' does not contain any alphanumeric characters`);
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 { content: raw.slice(match[0].length), meta: {}, extra: {}, hasFrontMatter: true };
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("A manifest source must be provided to SnippetClient.");
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("Manifest loader must resolve to an array of snippet metadata.");
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(`Failed to fetch snippet at '${url}'.`, error);
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
- const html = await this.options.render(body);
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(`Failed to normalize front-matter slug '${frontMatter.slug}':`, error);
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(`Manifest at '${source}' is not valid JSON.`, error);
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(`Manifest at '${source}' must be a JSON array.`);
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("Manifest entry is missing required 'slug' property.");
475
+ throw new ManifestLoadError(
476
+ "Manifest entry is missing required 'slug' property."
477
+ );
328
478
  }
329
479
  if (!entry.path) {
330
- throw new ManifestLoadError(`Manifest entry for '${entry.slug}' is missing required 'path'.`);
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 ? [...entry.tags] : void 0,
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
- if (HTTP_PATTERN.test(value)) {
527
+ const sanitized = value.replace(/\\/g, "/");
528
+ if (HTTP_PATTERN.test(sanitized)) {
376
529
  try {
377
- const url = new URL(value);
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 (value.startsWith("//")) {
384
- return `//${value.slice(2).replace(/\/{2,}/g, "/")}`;
536
+ if (sanitized.startsWith("//")) {
537
+ return `//${sanitized.slice(2).replace(/\/{2,}/g, "/")}`;
385
538
  }
386
- if (value.startsWith("/")) {
387
- return `/${value.slice(1).replace(/\/{2,}/g, "/")}`;
539
+ if (sanitized.startsWith("/")) {
540
+ return `/${sanitized.slice(1).replace(/\/{2,}/g, "/")}`;
388
541
  }
389
- return value.replace(/\/{2,}/g, "/");
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 ? [...meta.tags] : void 0,
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("No global fetch implementation is available. Provide a custom fetch function.");
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(`Request to '${url}' failed with status ${response.status}.`);
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(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength))
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(`Unsupported encoding '${encoding}' in browser Buffer shim`);
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(`Unsupported encoding '${encoding}' in browser Buffer shim`);
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("Unsupported input passed to Buffer.from in browser shim");
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
- result.set(buffer, offset);
535
- offset += buffer.length;
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: { value: (candidate) => candidate instanceof BrowserBuffer },
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