bearminion 1.5.0 → 1.6.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.
@@ -271,10 +271,10 @@ declare const textMarkSchema: z.ZodObject<{
271
271
  start: z.ZodNumber;
272
272
  end: z.ZodNumber;
273
273
  weight: z.ZodOptional<z.ZodEnum<{
274
+ bold: "bold";
274
275
  regular: "regular";
275
276
  medium: "medium";
276
277
  semibold: "semibold";
277
- bold: "bold";
278
278
  }>>;
279
279
  italic: z.ZodOptional<z.ZodBoolean>;
280
280
  font: z.ZodOptional<z.ZodEnum<{
@@ -315,10 +315,10 @@ declare function withEmphasis<T extends z.ZodRawShape>(shape: T): z.ZodPipe<z.Zo
315
315
  start: z.ZodNumber;
316
316
  end: z.ZodNumber;
317
317
  weight: z.ZodOptional<z.ZodEnum<{
318
+ bold: "bold";
318
319
  regular: "regular";
319
320
  medium: "medium";
320
321
  semibold: "semibold";
321
- bold: "bold";
322
322
  }>>;
323
323
  italic: z.ZodOptional<z.ZodBoolean>;
324
324
  font: z.ZodOptional<z.ZodEnum<{
@@ -333,10 +333,10 @@ declare function withEmphasis<T extends z.ZodRawShape>(shape: T): z.ZodPipe<z.Zo
333
333
  start: z.ZodNumber;
334
334
  end: z.ZodNumber;
335
335
  weight: z.ZodOptional<z.ZodEnum<{
336
+ bold: "bold";
336
337
  regular: "regular";
337
338
  medium: "medium";
338
339
  semibold: "semibold";
339
- bold: "bold";
340
340
  }>>;
341
341
  italic: z.ZodOptional<z.ZodBoolean>;
342
342
  font: z.ZodOptional<z.ZodEnum<{
@@ -353,10 +353,10 @@ declare function withEmphasis<T extends z.ZodRawShape>(shape: T): z.ZodPipe<z.Zo
353
353
  start: z.ZodNumber;
354
354
  end: z.ZodNumber;
355
355
  weight: z.ZodOptional<z.ZodEnum<{
356
+ bold: "bold";
356
357
  regular: "regular";
357
358
  medium: "medium";
358
359
  semibold: "semibold";
359
- bold: "bold";
360
360
  }>>;
361
361
  italic: z.ZodOptional<z.ZodBoolean>;
362
362
  font: z.ZodOptional<z.ZodEnum<{
@@ -613,10 +613,10 @@ declare function createSiteSchema(header: HeaderParts, footer: FooterParts): z.Z
613
613
  start: z.ZodNumber;
614
614
  end: z.ZodNumber;
615
615
  weight: z.ZodOptional<z.ZodEnum<{
616
+ bold: "bold";
616
617
  regular: "regular";
617
618
  medium: "medium";
618
619
  semibold: "semibold";
619
- bold: "bold";
620
620
  }>>;
621
621
  italic: z.ZodOptional<z.ZodBoolean>;
622
622
  font: z.ZodOptional<z.ZodEnum<{
@@ -848,7 +848,7 @@ declare function createSiteSchema(header: HeaderParts, footer: FooterParts): z.Z
848
848
  nameMarks: {
849
849
  start: number;
850
850
  end: number;
851
- weight?: "regular" | "medium" | "semibold" | "bold" | undefined;
851
+ weight?: "bold" | "regular" | "medium" | "semibold" | undefined;
852
852
  italic?: boolean | undefined;
853
853
  font?: "sans" | "serif" | "mono" | undefined;
854
854
  }[] | undefined;
@@ -1106,7 +1106,7 @@ declare function createSiteSchema(header: HeaderParts, footer: FooterParts): z.Z
1106
1106
  nameMarks?: {
1107
1107
  start: number;
1108
1108
  end: number;
1109
- weight?: "regular" | "medium" | "semibold" | "bold" | undefined;
1109
+ weight?: "bold" | "regular" | "medium" | "semibold" | undefined;
1110
1110
  italic?: boolean | undefined;
1111
1111
  font?: "sans" | "serif" | "mono" | undefined;
1112
1112
  }[] | undefined;
@@ -1,21 +1,30 @@
1
1
  //#region src/rich-text.d.ts
2
2
  /**
3
3
  * The subset the editor toolbar writes: paragraphs, line breaks, bold, italic, inline code (backticks),
4
- * links, and bulleted or numbered lists. Headings and images are not supported.
4
+ * links, and bulleted or numbered lists. Images are not supported. Headings are not either, unless a field asks
5
+ * for them (`{ headings: true }`): then `## ` and `### ` lines are h2 and h3, and nothing else is a heading.
5
6
  * Stored as markdown. Rendered as a fixed set of tags, with text escaped.
6
7
  *
7
- * The output is always block content (`<p>`, `<ul>`, `<ol>`), so the element that holds it must accept blocks:
8
- * a `div` or a `blockquote`, never a `<p>` or a heading.
8
+ * The output is always block content (`<p>`, `<ul>`, `<ol>`, with headings `<h2>`, `<h3>`), so the element that
9
+ * holds it must accept blocks: a `div` or a `blockquote`, never a `<p>` or a heading.
9
10
  */
10
- export declare function renderRichText(markdown: string): string;
11
+ /** What a rich field allows beyond the base subset. */
12
+ export interface RichOptions {
13
+ /** Allow h2 and h3 as `## ` and `### ` lines. Page titles are h1, so nothing above h2 exists, and nothing below h3. */
14
+ headings?: boolean;
15
+ }
16
+ export declare function renderRichText(markdown: string, options?: RichOptions): string;
11
17
  /**
12
18
  * Pasted HTML, reduced to what a rich field can hold. The HTML is parsed (never inserted), read back as the
13
19
  * markdown subset, and rendered again, so only the renderer's own tags survive: no attributes, handlers or
14
20
  * elements from the clipboard reach the page. Headings and images are dropped. Browser only.
15
21
  */
16
- export declare function sanitizeRichHtml(html: string): string;
17
- /** Drop markdown headings and images from pasted plain text. */
18
- export declare function stripPlainRich(text: string): string;
22
+ export declare function sanitizeRichHtml(html: string, options?: RichOptions): string;
23
+ /**
24
+ * Drop markdown images from pasted plain text, and headings too unless the field allows them. Then a heading of any
25
+ * level becomes the nearest one the field has: `#` is `##`, and `####` and deeper are `###`.
26
+ */
27
+ export declare function stripPlainRich(text: string, options?: RichOptions): string;
19
28
  /** True when a link URL is safe to put in an href. */
20
29
  export declare function isSafeHref(url: string): boolean;
21
30
  export declare function escapeHtml(value: string): string;
@@ -23,7 +32,7 @@ export declare function escapeHtml(value: string): string;
23
32
  * Turn the formatted DOM of a rich field back into the stored markdown subset. The HTML is parsed with
24
33
  * `DOMParser`, which never runs scripts or loads resources. Browser only (the editor overlay).
25
34
  */
26
- export declare function markdownFromHtml(html: string): string;
35
+ export declare function markdownFromHtml(html: string, options?: RichOptions): string;
27
36
  export interface RichLinkSpan {
28
37
  label: string;
29
38
  href: string;
package/dist/rich-text.js CHANGED
@@ -1,27 +1,35 @@
1
1
  //#region src/rich-text.ts
2
- /**
3
- * The subset the editor toolbar writes: paragraphs, line breaks, bold, italic, inline code (backticks),
4
- * links, and bulleted or numbered lists. Headings and images are not supported.
5
- * Stored as markdown. Rendered as a fixed set of tags, with text escaped.
6
- *
7
- * The output is always block content (`<p>`, `<ul>`, `<ol>`), so the element that holds it must accept blocks:
8
- * a `div` or a `blockquote`, never a `<p>` or a heading.
9
- */
2
+ const HEADING_LINE = /^(#{2,3})[ \t]+(\S.*)$/;
10
3
  const BLOCKED_PROTOCOL = /^(?:javascript|data|vbscript):/i;
11
- function renderRichText(markdown) {
12
- return markdown.replace(/\r\n/g, "\n").split(/\n{2,}/).filter((block) => block.trim() !== "").map((block) => renderBlock(block)).map((html) => isListHtml(html) ? html : `<p>${html}</p>`).join("");
4
+ function renderRichText(markdown, options = {}) {
5
+ const headings = options.headings === true;
6
+ const text = markdown.replace(/\r\n/g, "\n");
7
+ return (headings ? text.split("\n").map((line) => HEADING_LINE.test(line.trim()) ? `\n\n${line.trim()}\n\n` : line).join("\n") : text).split(/\n{2,}/).filter((block) => block.trim() !== "").map((block) => {
8
+ const heading = headings ? HEADING_LINE.exec(block.trim()) : null;
9
+ if (heading) {
10
+ const tag = heading[1]?.length === 2 ? "h2" : "h3";
11
+ return `<${tag}>${renderInline((heading[2] ?? "").trim())}</${tag}>`;
12
+ }
13
+ const html = renderBlock(block);
14
+ return isListHtml(html) ? html : `<p>${html}</p>`;
15
+ }).join("");
13
16
  }
14
17
  /**
15
18
  * Pasted HTML, reduced to what a rich field can hold. The HTML is parsed (never inserted), read back as the
16
19
  * markdown subset, and rendered again, so only the renderer's own tags survive: no attributes, handlers or
17
20
  * elements from the clipboard reach the page. Headings and images are dropped. Browser only.
18
21
  */
19
- function sanitizeRichHtml(html) {
20
- return renderRichText(markdownFromHtml(html));
22
+ function sanitizeRichHtml(html, options = {}) {
23
+ return renderRichText(markdownFromHtml(html, options), options);
21
24
  }
22
- /** Drop markdown headings and images from pasted plain text. */
23
- function stripPlainRich(text) {
24
- return text.replace(/!\[[^\]]*\]\([^)\n]*\)/g, "").replace(/^#{1,6}[ \t]+.*$/gm, "");
25
+ /**
26
+ * Drop markdown images from pasted plain text, and headings too unless the field allows them. Then a heading of any
27
+ * level becomes the nearest one the field has: `#` is `##`, and `####` and deeper are `###`.
28
+ */
29
+ function stripPlainRich(text, options = {}) {
30
+ const withoutImages = text.replace(/!\[[^\]]*\]\([^)\n]*\)/g, "");
31
+ if (options.headings !== true) return withoutImages.replace(/^#{1,6}[ \t]+.*$/gm, "");
32
+ return withoutImages.replace(/^#{4,6}([ \t]+)/gm, "###$1").replace(/^#([ \t]+)/gm, "##$1");
25
33
  }
26
34
  function renderBlock(block) {
27
35
  const lines = block.split("\n");
@@ -155,8 +163,8 @@ function escapeAttr(value) {
155
163
  * Turn the formatted DOM of a rich field back into the stored markdown subset. The HTML is parsed with
156
164
  * `DOMParser`, which never runs scripts or loads resources. Browser only (the editor overlay).
157
165
  */
158
- function markdownFromHtml(html) {
159
- return cleanup(serialize(parseHtml(html)));
166
+ function markdownFromHtml(html, options = {}) {
167
+ return cleanup(serialize(parseHtml(html, options.headings === true)));
160
168
  }
161
169
  /** Elements whose content is dropped with them. */
162
170
  const DROPPED = /* @__PURE__ */ new Set([
@@ -171,7 +179,10 @@ const DROPPED = /* @__PURE__ */ new Set([
171
179
  "img",
172
180
  "picture",
173
181
  "video",
174
- "audio",
182
+ "audio"
183
+ ]);
184
+ /** Headings a field without them drops, with their text. */
185
+ const HEADING_TAGS = /* @__PURE__ */ new Set([
175
186
  "h1",
176
187
  "h2",
177
188
  "h3",
@@ -179,16 +190,16 @@ const DROPPED = /* @__PURE__ */ new Set([
179
190
  "h5",
180
191
  "h6"
181
192
  ]);
182
- function parseHtml(html) {
193
+ function parseHtml(html, headings) {
183
194
  const root = {
184
195
  mark: "root",
185
196
  children: []
186
197
  };
187
198
  const body = new DOMParser().parseFromString(`<!doctype html><body>${html}`, "text/html").body;
188
- appendChildren(body, root);
199
+ appendChildren(body, root, headings);
189
200
  return root;
190
201
  }
191
- function appendChildren(parent, into) {
202
+ function appendChildren(parent, into, headings) {
192
203
  for (const child of Array.from(parent.childNodes)) {
193
204
  if (child.nodeType === TEXT_NODE) {
194
205
  into.children.push({
@@ -202,6 +213,7 @@ function appendChildren(parent, into) {
202
213
  const element = child;
203
214
  const name = element.tagName.toLowerCase();
204
215
  if (DROPPED.has(name)) continue;
216
+ if (HEADING_TAGS.has(name) && !headings) continue;
205
217
  if (name === "br") {
206
218
  into.children.push({
207
219
  mark: "text",
@@ -212,7 +224,7 @@ function appendChildren(parent, into) {
212
224
  }
213
225
  const mark = blockOf(name) ?? markOf(name, element.getAttribute("style") ?? "");
214
226
  if (!mark) {
215
- appendChildren(element, into);
227
+ appendChildren(element, into, headings);
216
228
  continue;
217
229
  }
218
230
  const tab = mark === "link" ? element.getAttribute("data-new-tab") : null;
@@ -223,12 +235,14 @@ function appendChildren(parent, into) {
223
235
  newTab: tab === "true" ? "new" : tab === "false" ? "same" : void 0
224
236
  };
225
237
  into.children.push(node);
226
- appendChildren(element, node);
238
+ appendChildren(element, node, headings);
227
239
  }
228
240
  }
229
241
  const ELEMENT_NODE = 1;
230
242
  const TEXT_NODE = 3;
231
243
  function blockOf(name) {
244
+ if (name === "h1" || name === "h2") return "h2";
245
+ if (name === "h3" || name === "h4" || name === "h5" || name === "h6") return "h3";
232
246
  if (name === "ul") return "ul";
233
247
  if (name === "ol") return "ol";
234
248
  if (name === "li") return "li";
@@ -260,6 +274,13 @@ function serializeFlow(nodes) {
260
274
  out += `${list}\n\n`;
261
275
  continue;
262
276
  }
277
+ if (node.mark === "h2" || node.mark === "h3") {
278
+ const text = node.children.map((child) => serializeInline(child)).join("").replace(/\s*\n+\s*/g, " ").trim();
279
+ if (text === "") continue;
280
+ if (out.trim() !== "" && !out.endsWith("\n\n")) out = out.replace(/\n*$/, "\n\n");
281
+ out += `${node.mark === "h2" ? "##" : "###"} ${text}\n\n`;
282
+ continue;
283
+ }
263
284
  if (node.mark === "block") {
264
285
  const inner = serializeFlow(node.children).replace(/\n+$/, "");
265
286
  if (inner === "") continue;
@@ -291,7 +312,7 @@ function serializeInline(node) {
291
312
  return text.trim() === "" ? text : `\`${text}\``;
292
313
  }
293
314
  if (node.mark === "ul" || node.mark === "ol") return serializeList(node);
294
- if (node.mark === "li" || node.mark === "block" || node.mark === "root") return serializeFlow(node.children);
315
+ if (node.mark === "li" || node.mark === "block" || node.mark === "root" || node.mark === "h2" || node.mark === "h3") return serializeFlow(node.mark === "h2" || node.mark === "h3" ? [node] : node.children);
295
316
  const inner = node.children.map((child) => serializeInline(child)).join("");
296
317
  if (!inner) return "";
297
318
  if (node.mark === "strong") return `**${inner}**`;
@@ -1,3 +1,33 @@
1
+ //#region src/media-source.ts
2
+ const YOUTUBE = /^https?:\/\/(?:(?:www|m)\.)?(?:youtube\.com\/(?:watch\?(?:[^#\s]*&)?v=|embed\/|shorts\/|live\/)|youtube-nocookie\.com\/embed\/|youtu\.be\/)([\w-]{6,})(?:[?&#][^\s]*)?$/i;
3
+ const VIMEO = /^https?:\/\/(?:(?:www|player)\.)?vimeo\.com\/(?:video\/)?(\d+)(?:[/?#][^\s]*)?$/i;
4
+ /** A path in `public/` (`/videos/demo.webm`) or an `https://` link to a video file. */
5
+ const VIDEO_FILE = /^(?:\/(?!\/)|https?:\/\/)\S+\.(?:mp4|webm|m4v|mov|ogv)(?:[?#]\S*)?$/i;
6
+ function parseMediaSource(value) {
7
+ const src = value.trim();
8
+ const youtube = YOUTUBE.exec(src);
9
+ if (youtube?.[1]) {
10
+ const id = youtube[1];
11
+ return {
12
+ kind: "youtube",
13
+ id,
14
+ embed: `https://www.youtube-nocookie.com/embed/${id}?autoplay=1&rel=0`,
15
+ thumbnail: `https://i.ytimg.com/vi/${id}/hqdefault.jpg`
16
+ };
17
+ }
18
+ const vimeo = VIMEO.exec(src);
19
+ if (vimeo?.[1]) return {
20
+ kind: "vimeo",
21
+ id: vimeo[1],
22
+ embed: `https://player.vimeo.com/video/${vimeo[1]}?autoplay=1&dnt=1`
23
+ };
24
+ if (VIDEO_FILE.test(src)) return {
25
+ kind: "file",
26
+ src
27
+ };
28
+ return null;
29
+ }
30
+ //#endregion
1
31
  //#region src/consent.ts
2
32
  /** Longest copy the consent banner takes, in characters. The editor shows the same limits. */
3
33
  const CONSENT_LIMITS = {
@@ -478,4 +508,4 @@ function contrastChecks(colors) {
478
508
  return checks;
479
509
  }
480
510
  //#endregion
481
- export { consentCopy as _, contrastRatio as a, hasThemeColors as c, tokenToHex as d, CONSENT_COPY as f, CONSENT_REOPEN as g, CONSENT_LIMITS as h, contrastChecks as i, hexToToken as l, CONSENT_HREF_MESSAGE as m, HEX_COLOR as n, derivePalette as o, CONSENT_HREF as p, OVERRIDE_TOKENS as r, hasOverrides as s, DEFAULT_BRAND as t, themeCss as u, consentTextIssues as v };
511
+ export { consentCopy as _, contrastRatio as a, parseMediaSource as b, hasThemeColors as c, tokenToHex as d, CONSENT_COPY as f, CONSENT_REOPEN as g, CONSENT_LIMITS as h, contrastChecks as i, hexToToken as l, CONSENT_HREF_MESSAGE as m, HEX_COLOR as n, derivePalette as o, CONSENT_HREF as p, OVERRIDE_TOKENS as r, hasOverrides as s, DEFAULT_BRAND as t, themeCss as u, consentTextIssues as v, VIDEO_FILE as y };