@franken-suite/franken-markdown 0.4.2 → 0.4.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/fmd-view.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `<fmd-view>` web component type surface (bead uito).
3
+ */
4
+
5
+ export interface FmdRenderedDetail {
6
+ bytes: number;
7
+ sourceLength: number;
8
+ diagnostics: Array<{
9
+ severity: "warning" | "error";
10
+ start: number;
11
+ end: number;
12
+ message: string;
13
+ }>;
14
+ }
15
+
16
+ export interface FmdViewEventMap {
17
+ "fmd-rendered": CustomEvent<FmdRenderedDetail>;
18
+ "fmd-error": CustomEvent<{ error: string }>;
19
+ }
20
+
21
+ export interface FmdView extends HTMLElement {
22
+ addEventListener<K extends keyof FmdViewEventMap>(
23
+ type: K,
24
+ listener: (this: FmdView, ev: FmdViewEventMap[K]) => void,
25
+ options?: boolean | AddEventListenerOptions
26
+ ): void;
27
+ addEventListener(
28
+ type: string,
29
+ listener: EventListenerOrEventListenerObject,
30
+ options?: boolean | AddEventListenerOptions
31
+ ): void;
32
+ }
33
+
34
+ /**
35
+ * Register `<fmd-view>` (idempotent). Auto-registered in browser globals on
36
+ * import; call this explicitly for a non-global registry.
37
+ */
38
+ export function registerFmdView(
39
+ registry?: Pick<CustomElementRegistry, "define" | "get">
40
+ ): "fmd-view";
package/fmd-view.js ADDED
@@ -0,0 +1,137 @@
1
+ // <fmd-view> — drop-in Markdown renderer web component (bead uito).
2
+ //
3
+ // Framework-free custom element over the package's createRenderer surface:
4
+ //
5
+ // <fmd-view src="./README.md"></fmd-view>
6
+ // <fmd-view>Six **bytes** of markdown</fmd-view>
7
+ //
8
+ // Attributes:
9
+ // src - fetch this URL as the Markdown source (slot content is the
10
+ // fallback when src is absent/unset)
11
+ // font - "sans" | "serif"
12
+ // dark-mode - "auto" | "disabled"
13
+ //
14
+ // The rendered document lands in a shadow root (style-isolated; the engine's
15
+ // self-contained HTML carries its own CSS). A "fmd-rendered" CustomEvent with
16
+ // { bytes, sourceLength, diagnostics } fires after each successful render;
17
+ // "fmd-error" fires on fetch/render failure. Bytes parity with renderHtml()
18
+ // is exact: the same call is used.
19
+ //
20
+ // Zero dependencies; the shared wasm payload is the package's.
21
+
22
+ import { createRenderer } from "./franken_markdown.js";
23
+
24
+ let rendererPromise = null;
25
+
26
+ function sharedRenderer(wasmInput) {
27
+ if (!rendererPromise) {
28
+ rendererPromise = createRenderer(wasmInput);
29
+ }
30
+ return rendererPromise;
31
+ }
32
+
33
+ const SLOT_RENDER = Symbol("slot source");
34
+
35
+ class FmdView extends HTMLElement {
36
+ static get observedAttributes() {
37
+ return ["src", "font", "dark-mode"];
38
+ }
39
+
40
+ #root = null;
41
+ #lastSrc = undefined;
42
+
43
+ constructor() {
44
+ super();
45
+ this.#root = this.attachShadow({ mode: "open" });
46
+ this.#root.innerHTML = "<style>:host{display:block}</style><slot></slot>";
47
+ }
48
+
49
+ connectedCallback() {
50
+ this.#render();
51
+ }
52
+
53
+ attributeChangedCallback(name, oldV, newV) {
54
+ if (oldV === newV || !this.#root) {
55
+ return;
56
+ }
57
+ if (name === "src" && newV !== this.#lastSrc) {
58
+ this.#render();
59
+ return;
60
+ }
61
+ if (name === "font" || name === "dark-mode") {
62
+ this.#render();
63
+ }
64
+ }
65
+
66
+ async #source() {
67
+ const src = this.getAttribute("src");
68
+ if (src) {
69
+ const res = await fetch(src);
70
+ if (!res.ok) {
71
+ throw new Error(`fmd-view: fetch ${src} -> HTTP ${res.status}`);
72
+ }
73
+ return await res.text();
74
+ }
75
+ // Slot content as inline Markdown. Preserve text exactly.
76
+ const slot = this.querySelector("script[type='text/markdown']");
77
+ if (slot) {
78
+ return slot.textContent ?? "";
79
+ }
80
+ return this.textContent ?? "";
81
+ }
82
+
83
+ async #render() {
84
+ const srcAttr = this.getAttribute("src");
85
+ this.#lastSrc = srcAttr;
86
+ let markdown;
87
+ try {
88
+ markdown = await this.#source();
89
+ } catch (err) {
90
+ this.dispatchEvent(new CustomEvent("fmd-error", { detail: { error: String(err) } }));
91
+ return;
92
+ }
93
+ let renderer;
94
+ try {
95
+ // If the host already initialized the engine with explicit wasm bytes,
96
+ // a package-level export would be needed; default path resolves the
97
+ // bundled artifact via the wrapper's own URL resolution.
98
+ renderer = await sharedRenderer(undefined);
99
+ } catch (err) {
100
+ this.dispatchEvent(new CustomEvent("fmd-error", { detail: { error: String(err) } }));
101
+ return;
102
+ }
103
+ const out = await renderer.renderHtml(markdown, {
104
+ font: this.getAttribute("font") === "serif" ? "serif" : "sans",
105
+ darkMode: this.getAttribute("dark-mode") === "disabled" ? "disabled" : "auto",
106
+ });
107
+ const html = out.text();
108
+ this.#root.innerHTML = `<style>:host{display:block}</style>${html}`;
109
+ this.dispatchEvent(
110
+ new CustomEvent("fmd-rendered", {
111
+ detail: {
112
+ bytes: out.bytes.byteLength,
113
+ sourceLength: out.sourceLength,
114
+ diagnostics: out.diagnostics,
115
+ },
116
+ })
117
+ );
118
+ }
119
+ }
120
+
121
+ const ELEMENT_NAME = "fmd-view";
122
+
123
+ /**
124
+ * Register <fmd-view> on a customElements registry (defaults to global).
125
+ * Idempotent; returns the element name.
126
+ */
127
+ export function registerFmdView(registry = globalThis.customElements) {
128
+ if (!registry.get(ELEMENT_NAME)) {
129
+ registry.define(ELEMENT_NAME, FmdView);
130
+ }
131
+ return ELEMENT_NAME;
132
+ }
133
+
134
+ // Auto-register in browser globals (no-op under Node/bundlers without it).
135
+ if (typeof globalThis.customElements !== "undefined") {
136
+ registerFmdView();
137
+ }
@@ -1,4 +1,12 @@
1
- export type FmdOutputFormat = "html" | "pdf";
1
+ export type FmdOutputFormat =
2
+ | "html"
3
+ | "pdf"
4
+ | "svg"
5
+ | "epub"
6
+ | "interactive-html"
7
+ | "diff-html"
8
+ | "book-site"
9
+ | "book-pdf";
2
10
  export type FmdFont = "sans" | "serif";
3
11
  export type FmdDarkMode = "auto" | "disabled";
4
12
 
@@ -58,6 +66,20 @@ export interface FmdRenderOptions {
58
66
  headingScale?: number;
59
67
  /** Nominal table cell size override in points; clamped to [5, base]. */
60
68
  tableFontSize?: number;
69
+ /** BCP-47 language tag used for HTML metadata and language-aware hyphenation. */
70
+ lang?: "en" | "de" | "fr" | "es" | "nl" | string;
71
+ /** Generate a document table of contents. */
72
+ toc?: boolean;
73
+ /** Maximum generated TOC heading depth (1...6). */
74
+ tocDepth?: number;
75
+ /** Adaptive PDF page-count target. */
76
+ fitToPages?: number;
77
+ /** Enable optical-margin punctuation protrusion for justified PDF text. */
78
+ microtype?: "disabled" | "protrusion";
79
+ /** Boolean alias for microtype: "protrusion". */
80
+ microtypeProtrusion?: boolean;
81
+ /** Standalone SVG poster width in points. */
82
+ maxWidthPt?: number;
61
83
  /** Host-supplied image bytes (HTML data URIs and PDF embedding); any number per render. */
62
84
  pdfImages?: FmdPdfImageAsset[];
63
85
  /** Host-supplied TrueType font bytes by renderer slot. */
@@ -110,14 +132,80 @@ export interface FmdCapabilities {
110
132
 
111
133
  export interface FmdRenderer {
112
134
  capabilities(): Promise<FmdCapabilities>;
135
+ accessibilityAudit(markdown: string): Promise<FmdAccessibilityReport>;
136
+ documentStats(markdown: string): Promise<FmdDocumentStats>;
137
+ renderBookPdf(files: FmdBookFile[], options?: FmdRenderOptions): Promise<FmdRenderOutput>;
138
+ renderBookSite(files: FmdBookFile[], options?: FmdRenderOptions): Promise<FmdRenderOutput>;
139
+ renderEpub(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
113
140
  renderHtml(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
141
+ renderInteractiveHtml(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
114
142
  renderPdf(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
143
+ renderSemanticDiff(oldMarkdown: string, newMarkdown: string, options?: FmdDiffOptions): Promise<FmdRenderOutput>;
144
+ renderSvg(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
145
+ searchIndex(markdown: string): Promise<FmdSearchIndex>;
146
+ semanticDiff(oldMarkdown: string, newMarkdown: string, options?: FmdDiffOptions): Promise<FmdSemanticDiff>;
147
+ }
148
+
149
+ export interface FmdBookFile { path: string; source: string; }
150
+ export interface FmdDiffOptions { oldName?: string; newName?: string; }
151
+ export interface FmdFinding { severity?: string; code: string; message?: string; detail?: string; }
152
+ export interface FmdDocumentStats {
153
+ schema: "fmd-document-stats-v1";
154
+ bytes: number;
155
+ lines: number;
156
+ words: number;
157
+ characters: number;
158
+ sentences: number;
159
+ syllables: number;
160
+ reading_time_secs: number;
161
+ speaking_time_secs: number;
162
+ flesch_reading_ease: number;
163
+ flesch_kincaid_grade: number;
164
+ reading_ease_label: string;
165
+ structure: Record<string, unknown>;
166
+ outline: Array<{ level: number; text: string; slug: string }>;
167
+ findings: FmdFinding[];
168
+ }
169
+ export interface FmdAccessibilityReport {
170
+ schema_version: "1";
171
+ target: "pdf";
172
+ findings: FmdFinding[];
173
+ [key: string]: unknown;
174
+ }
175
+ export interface FmdSearchIndex {
176
+ schema: "fmd-search-index-v1";
177
+ entries: Array<{ kind: "heading" | "paragraph"; level?: number; anchor: string; text: string }>;
178
+ }
179
+ export interface FmdSemanticDiff {
180
+ schema: "fmd-diff-v1";
181
+ old_name: string;
182
+ new_name: string;
183
+ stats: {
184
+ unchanged_blocks: number;
185
+ inserted_blocks: number;
186
+ deleted_blocks: number;
187
+ modified_blocks: number;
188
+ words_inserted: number;
189
+ words_deleted: number;
190
+ similarity_ratio: number;
191
+ };
192
+ [key: string]: unknown;
115
193
  }
116
194
 
117
195
  export function init(input?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<void>;
118
196
  export function capabilities(): Promise<FmdCapabilities>;
197
+ export function accessibilityAudit(markdown: string): Promise<FmdAccessibilityReport>;
198
+ export function documentStats(markdown: string): Promise<FmdDocumentStats>;
199
+ export function renderBookPdf(files: FmdBookFile[], options?: FmdRenderOptions): Promise<FmdRenderOutput>;
200
+ export function renderBookSite(files: FmdBookFile[], options?: FmdRenderOptions): Promise<FmdRenderOutput>;
201
+ export function renderEpub(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
119
202
  export function renderHtml(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
203
+ export function renderInteractiveHtml(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
120
204
  export function renderPdf(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
205
+ export function renderSemanticDiff(oldMarkdown: string, newMarkdown: string, options?: FmdDiffOptions): Promise<FmdRenderOutput>;
206
+ export function renderSvg(markdown: string, options?: FmdRenderOptions): Promise<FmdRenderOutput>;
207
+ export function searchIndex(markdown: string): Promise<FmdSearchIndex>;
208
+ export function semanticDiff(oldMarkdown: string, newMarkdown: string, options?: FmdDiffOptions): Promise<FmdSemanticDiff>;
121
209
  export function createRenderer(
122
210
  input?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module
123
211
  ): Promise<FmdRenderer>;
@@ -1,9 +1,17 @@
1
1
  import initWasm, {
2
+ accessibilityAudit as wasmAccessibilityAudit,
2
3
  capabilities as wasmCapabilities,
3
- renderHtmlConfigured,
4
- renderHtmlConfiguredWithFonts,
5
- renderHtmlConfiguredMulti,
6
- renderPdfConfiguredMulti
4
+ documentStats as wasmDocumentStats,
5
+ renderBookPdf as wasmRenderBookPdf,
6
+ renderBookSite as wasmRenderBookSite,
7
+ renderEpubConfigured,
8
+ renderHtmlConfiguredAdvanced,
9
+ renderInteractiveHtmlConfigured,
10
+ renderPdfConfiguredMulti,
11
+ renderSemanticDiffHtml as wasmRenderSemanticDiffHtml,
12
+ renderSvgConfigured,
13
+ searchIndex as wasmSearchIndex,
14
+ semanticDiff as wasmSemanticDiff
7
15
  } from "./pkg/franken_markdown.js";
8
16
 
9
17
  let initPromise = null;
@@ -29,62 +37,37 @@ export async function renderHtml(markdown, options = {}) {
29
37
  await init();
30
38
  const pdfImages = pdfImagesOption(options.pdfImages);
31
39
  const fontAssets = fontAssetsOption(options.fontAssets);
32
- if (pdfImages.length > 0) {
33
- const destinations = pdfImages.map((image) => image.destination);
34
- const lengths = new Uint32Array(pdfImages.map((image) => image.bytes.length));
35
- const totalBytes = pdfImages.reduce((sum, image) => sum + image.bytes.length, 0);
36
- const flatBytes = new Uint8Array(totalBytes);
37
- let offset = 0;
38
- for (const image of pdfImages) {
39
- flatBytes.set(image.bytes, offset);
40
- offset += image.bytes.length;
41
- }
42
- return normalizeResult(
43
- renderHtmlConfiguredMulti(
44
- String(markdown),
45
- stringOption(options.font),
46
- darkModeOption(options.darkMode),
47
- verbatimOption(options.title),
48
- verbatimOption(options.customCss),
49
- Boolean(options.allowRawHtml),
50
- fontBytesForSlot(fontAssets, "body-regular"),
51
- fontBytesForSlot(fontAssets, "body-bold"),
52
- fontBytesForSlot(fontAssets, "body-italic"),
53
- fontBytesForSlot(fontAssets, "body-bold-italic"),
54
- fontBytesForSlot(fontAssets, "mono-regular"),
55
- fontWeightsForSlots(fontAssets),
56
- destinations,
57
- flatBytes,
58
- lengths
59
- )
60
- );
61
- }
62
- if (fontAssets.length > 0) {
63
- return normalizeResult(
64
- renderHtmlConfiguredWithFonts(
65
- String(markdown),
66
- stringOption(options.font),
67
- darkModeOption(options.darkMode),
68
- verbatimOption(options.title),
69
- verbatimOption(options.customCss),
70
- Boolean(options.allowRawHtml),
71
- fontBytesForSlot(fontAssets, "body-regular"),
72
- fontBytesForSlot(fontAssets, "body-bold"),
73
- fontBytesForSlot(fontAssets, "body-italic"),
74
- fontBytesForSlot(fontAssets, "body-bold-italic"),
75
- fontBytesForSlot(fontAssets, "mono-regular"),
76
- fontWeightsForSlots(fontAssets)
77
- )
78
- );
40
+ const fontScale = fontScaleOption(options.fontScale ?? options.typeSize);
41
+ const destinations = pdfImages.map((image) => image.destination);
42
+ const lengths = new Uint32Array(pdfImages.map((image) => image.bytes.length));
43
+ const totalBytes = pdfImages.reduce((sum, image) => sum + image.bytes.length, 0);
44
+ const flatBytes = new Uint8Array(totalBytes);
45
+ let offset = 0;
46
+ for (const image of pdfImages) {
47
+ flatBytes.set(image.bytes, offset);
48
+ offset += image.bytes.length;
79
49
  }
80
50
  return normalizeResult(
81
- renderHtmlConfigured(
51
+ renderHtmlConfiguredAdvanced(
82
52
  String(markdown),
83
53
  stringOption(options.font),
84
54
  darkModeOption(options.darkMode),
85
55
  verbatimOption(options.title),
86
56
  verbatimOption(options.customCss),
87
- Boolean(options.allowRawHtml)
57
+ Boolean(options.allowRawHtml),
58
+ fontScale,
59
+ fontBytesForSlot(fontAssets, "body-regular"),
60
+ fontBytesForSlot(fontAssets, "body-bold"),
61
+ fontBytesForSlot(fontAssets, "body-italic"),
62
+ fontBytesForSlot(fontAssets, "body-bold-italic"),
63
+ fontBytesForSlot(fontAssets, "mono-regular"),
64
+ fontWeightsForSlots(fontAssets),
65
+ destinations,
66
+ flatBytes,
67
+ lengths,
68
+ stringOption(options.lang),
69
+ Boolean(options.toc),
70
+ integerOption(options.tocDepth, "tocDepth")
88
71
  )
89
72
  );
90
73
  }
@@ -93,6 +76,8 @@ export async function renderPdf(markdown, options = {}) {
93
76
  await init();
94
77
  const pdfImages = pdfImagesOption(options.pdfImages);
95
78
  const fontAssets = fontAssetsOption(options.fontAssets);
79
+ const fontScale = fontScaleOption(options.fontScale ?? options.typeSize);
80
+ const baseFontSize = numberOption(options.baseFontSize) ?? (fontScale !== undefined ? 11 * fontScale : undefined);
96
81
 
97
82
  // Flatten any number of images into the three parallel arrays the core ABI
98
83
  // accepts (wasm-bindgen cannot pass a Vec<Vec<u8>>): a destination per image,
@@ -126,20 +111,134 @@ export async function renderPdf(markdown, options = {}) {
126
111
  fontBytesForSlot(fontAssets, "body-bold-italic"),
127
112
  fontBytesForSlot(fontAssets, "mono-regular"),
128
113
  fontWeightsForSlots(fontAssets),
129
- numberOption(options.baseFontSize),
114
+ baseFontSize,
130
115
  numberOption(options.headingScale),
131
116
  numberOption(options.tableFontSize),
132
- Boolean(options.pageNumbers)
117
+ Boolean(options.pageNumbers),
118
+ fontScale,
119
+ stringOption(options.lang),
120
+ Boolean(options.toc),
121
+ integerOption(options.tocDepth, "tocDepth"),
122
+ integerOption(options.fitToPages, "fitToPages"),
123
+ options.microtype === "protrusion" || options.microtypeProtrusion === true
133
124
  )
134
125
  );
135
126
  }
136
127
 
128
+ export async function renderSvg(markdown, options = {}) {
129
+ await init();
130
+ return normalizeResult(renderSvgConfigured(
131
+ String(markdown),
132
+ stringOption(options.font),
133
+ darkModeOption(options.darkMode),
134
+ fontScaleOption(options.fontScale ?? options.typeSize),
135
+ numberOption(options.maxWidthPt)
136
+ ));
137
+ }
138
+
139
+ export async function renderEpub(markdown, options = {}) {
140
+ await init();
141
+ return normalizeResult(renderEpubConfigured(
142
+ String(markdown),
143
+ stringOption(options.font),
144
+ darkModeOption(options.darkMode),
145
+ verbatimOption(options.title),
146
+ stringOption(options.lang),
147
+ fontScaleOption(options.fontScale ?? options.typeSize)
148
+ ));
149
+ }
150
+
151
+ export async function renderInteractiveHtml(markdown, options = {}) {
152
+ await init();
153
+ return normalizeResult(renderInteractiveHtmlConfigured(
154
+ String(markdown),
155
+ stringOption(options.font),
156
+ darkModeOption(options.darkMode),
157
+ verbatimOption(options.title),
158
+ stringOption(options.lang),
159
+ fontScaleOption(options.fontScale ?? options.typeSize)
160
+ ));
161
+ }
162
+
163
+ export async function documentStats(markdown) {
164
+ await init();
165
+ return parseJson(wasmDocumentStats(String(markdown)), "document stats JSON");
166
+ }
167
+
168
+ export async function searchIndex(markdown) {
169
+ await init();
170
+ return parseJson(wasmSearchIndex(String(markdown)), "search index JSON");
171
+ }
172
+
173
+ export async function accessibilityAudit(markdown) {
174
+ await init();
175
+ return parseJson(wasmAccessibilityAudit(String(markdown)), "accessibility audit JSON");
176
+ }
177
+
178
+ export async function semanticDiff(oldMarkdown, newMarkdown, options = {}) {
179
+ await init();
180
+ return parseJson(wasmSemanticDiff(
181
+ String(oldMarkdown),
182
+ String(newMarkdown),
183
+ verbatimOption(options.oldName),
184
+ verbatimOption(options.newName)
185
+ ), "semantic diff JSON");
186
+ }
187
+
188
+ export async function renderSemanticDiff(oldMarkdown, newMarkdown, options = {}) {
189
+ await init();
190
+ return normalizeResult(wasmRenderSemanticDiffHtml(
191
+ String(oldMarkdown),
192
+ String(newMarkdown),
193
+ verbatimOption(options.oldName),
194
+ verbatimOption(options.newName)
195
+ ));
196
+ }
197
+
198
+ export async function renderBookSite(files, options = {}) {
199
+ await init();
200
+ const normalized = bookFilesOption(files);
201
+ return normalizeResult(wasmRenderBookSite(
202
+ normalized.map((file) => file.path),
203
+ normalized.map((file) => file.source),
204
+ verbatimOption(options.title),
205
+ stringOption(options.font),
206
+ darkModeOption(options.darkMode),
207
+ fontScaleOption(options.fontScale ?? options.typeSize)
208
+ ));
209
+ }
210
+
211
+ export async function renderBookPdf(files, options = {}) {
212
+ await init();
213
+ const normalized = bookFilesOption(files);
214
+ return normalizeResult(wasmRenderBookPdf(
215
+ normalized.map((file) => file.path),
216
+ normalized.map((file) => file.source),
217
+ verbatimOption(options.title),
218
+ verbatimOption(options.author),
219
+ stringOption(options.font),
220
+ darkModeOption(options.darkMode),
221
+ fontScaleOption(options.fontScale ?? options.typeSize),
222
+ options.pageNumbers !== false
223
+ ));
224
+ }
225
+
137
226
  export async function createRenderer(input) {
138
227
  await init(input);
139
228
  return Object.freeze({
140
229
  capabilities,
230
+ accessibilityAudit,
231
+ documentStats,
232
+ renderBookPdf,
233
+ renderBookSite,
234
+ renderEpub,
141
235
  renderHtml,
142
- renderPdf
236
+ renderInteractiveHtml,
237
+ renderPdf,
238
+ renderSemanticDiff,
239
+ renderSvg,
240
+ searchIndex,
241
+ semanticDiff
143
242
  });
144
243
  }
145
244
 
@@ -267,6 +366,118 @@ function numberOption(value) {
267
366
  }
268
367
  return value;
269
368
  }
369
+
370
+ function integerOption(value, label) {
371
+ if (value === undefined || value === null) {
372
+ return undefined;
373
+ }
374
+ if (!Number.isSafeInteger(value) || value <= 0) {
375
+ throw new TypeError(`${label} must be a positive integer`);
376
+ }
377
+ return value;
378
+ }
379
+
380
+ function bookFilesOption(value) {
381
+ if (!Array.isArray(value) || value.length === 0) {
382
+ throw new TypeError("book files must be a non-empty array of { path, source } objects");
383
+ }
384
+ return value.map((file, index) => {
385
+ if (file === null || typeof file !== "object") {
386
+ throw new TypeError(`book files[${index}] must be an object`);
387
+ }
388
+ const path = stringOption(file.path);
389
+ if (path === undefined) {
390
+ throw new TypeError(`book files[${index}].path must be a non-empty string`);
391
+ }
392
+ return Object.freeze({ path, source: String(file.source ?? "") });
393
+ });
394
+ }
395
+
396
+ function fontScaleOption(value) {
397
+ if (value === undefined || value === null) {
398
+ return undefined;
399
+ }
400
+ if (typeof value === "number") {
401
+ if (!Number.isFinite(value) || value <= 0) {
402
+ throw new TypeError("fontScale must be a positive finite number");
403
+ }
404
+ return Math.min(3.0, Math.max(0.5, value));
405
+ }
406
+ if (typeof value === "string") {
407
+ const trimmed = value.trim().toLowerCase();
408
+ switch (trimmed) {
409
+ case "xs":
410
+ case "x-small":
411
+ case "extra-small":
412
+ case "extrasmall":
413
+ case "tiny":
414
+ return 0.75;
415
+ case "sm":
416
+ case "small":
417
+ case "compact":
418
+ return 0.875;
419
+ case "md":
420
+ case "medium":
421
+ case "normal":
422
+ case "default":
423
+ case "regular":
424
+ case "standard":
425
+ return 1.0;
426
+ case "lg":
427
+ case "large":
428
+ case "comfortable":
429
+ return 1.125;
430
+ case "xl":
431
+ case "x-large":
432
+ case "extra-large":
433
+ case "extralarge":
434
+ return 1.25;
435
+ case "2xl":
436
+ case "xxl":
437
+ case "huge":
438
+ case "display":
439
+ return 1.5;
440
+ default:
441
+ break;
442
+ }
443
+ if (trimmed.endsWith("%")) {
444
+ const parsed = parseFloat(trimmed.slice(0, -1));
445
+ if (Number.isFinite(parsed) && parsed > 0) {
446
+ return Math.min(3.0, Math.max(0.5, parsed / 100));
447
+ }
448
+ }
449
+ if (trimmed.endsWith("rem")) {
450
+ const parsed = parseFloat(trimmed.slice(0, -3));
451
+ if (Number.isFinite(parsed) && parsed > 0) {
452
+ return Math.min(3.0, Math.max(0.5, parsed));
453
+ }
454
+ }
455
+ if (trimmed.endsWith("em")) {
456
+ const parsed = parseFloat(trimmed.slice(0, -2));
457
+ if (Number.isFinite(parsed) && parsed > 0) {
458
+ return Math.min(3.0, Math.max(0.5, parsed));
459
+ }
460
+ }
461
+ if (trimmed.endsWith("px")) {
462
+ const parsed = parseFloat(trimmed.slice(0, -2));
463
+ if (Number.isFinite(parsed) && parsed > 0) {
464
+ return Math.min(3.0, Math.max(0.5, parsed / 16));
465
+ }
466
+ }
467
+ if (trimmed.endsWith("pt")) {
468
+ const parsed = parseFloat(trimmed.slice(0, -2));
469
+ if (Number.isFinite(parsed) && parsed > 0) {
470
+ return Math.min(3.0, Math.max(0.5, parsed / 11));
471
+ }
472
+ }
473
+ const parsed = parseFloat(trimmed);
474
+ if (Number.isFinite(parsed) && parsed > 0) {
475
+ return Math.min(3.0, Math.max(0.5, parsed));
476
+ }
477
+ throw new TypeError(`unknown fontScale '${value}'. Valid choices: xs, sm, md, lg, xl, 2xl, or a number/percentage.`);
478
+ }
479
+ throw new TypeError("fontScale must be a number or string");
480
+ }
270
481
  function pdfImagesOption(value) {
271
482
  if (value === undefined || value === null) {
272
483
  return [];
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@franken-suite/franken-markdown",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Browser/WASM package for franken_markdown Markdown to HTML/PDF rendering.",
5
5
  "type": "module",
6
- "sideEffects": false,
6
+ "sideEffects": [
7
+ "./fmd-view.js"
8
+ ],
7
9
  "license": "LicenseRef-MIT-OpenAI-Anthropic-Rider",
8
10
  "author": "Jeffrey Emanuel",
9
11
  "homepage": "https://github.com/Dicklesworthstone/franken_markdown#readme",
@@ -35,6 +37,10 @@
35
37
  ".": {
36
38
  "types": "./franken_markdown.d.ts",
37
39
  "import": "./franken_markdown.js"
40
+ },
41
+ "./web-component": {
42
+ "types": "./fmd-view.d.ts",
43
+ "import": "./fmd-view.js"
38
44
  }
39
45
  },
40
46
  "files": [
@@ -45,6 +51,8 @@
45
51
  "pkg/franken_markdown.js",
46
52
  "pkg/franken_markdown_bg.wasm",
47
53
  "pkg/franken_markdown_bg.wasm.d.ts",
48
- "pkg/franken_markdown.d.ts"
54
+ "pkg/franken_markdown.d.ts",
55
+ "fmd-view.js",
56
+ "fmd-view.d.ts"
49
57
  ]
50
58
  }