@bendyline/squisq 2.3.2 → 2.4.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.
Files changed (34) hide show
  1. package/dist/{Doc-BrgZC7SE.d.ts → Doc-DLpyOAXJ.d.ts} +87 -2
  2. package/dist/{ImageEditDoc-rum0Xb9P.d.ts → ImageEditDoc-Cu30xb9b.d.ts} +1 -1
  3. package/dist/{chunk-2ZWIXGAC.js → chunk-2S74DPJH.js} +4 -0
  4. package/dist/{chunk-ZQKZSJAX.js → chunk-2VCNTDNZ.js} +245 -0
  5. package/dist/{chunk-RS5AP3J4.js → chunk-AE3IUKMM.js} +1 -1
  6. package/dist/{chunk-QWCFK5FN.js → chunk-CUYHFOFL.js} +5 -1
  7. package/dist/{chunk-ACZOVWX3.js → chunk-D4LPP3P5.js} +44 -15
  8. package/dist/{chunk-CKZY6K5R.js → chunk-F2IEPSMA.js} +2565 -1143
  9. package/dist/{chunk-ZAFLMJPD.js → chunk-KPBZZVGY.js} +37 -7
  10. package/dist/{chunk-BCCXTMN5.js → chunk-O7JILDEF.js} +7 -0
  11. package/dist/{chunk-SPTY4C6F.js → chunk-REDUXXDJ.js} +1 -1
  12. package/dist/{chunk-BCHAXKKI.js → chunk-ZHLNKB2I.js} +1 -1
  13. package/dist/chunk-ZYO3DBTA.js +983 -0
  14. package/dist/doc/index.d.ts +111 -6
  15. package/dist/doc/index.js +11 -7
  16. package/dist/generate/index.d.ts +1 -1
  17. package/dist/imageEdit/index.d.ts +3 -3
  18. package/dist/index.d.ts +6 -6
  19. package/dist/index.js +34 -14
  20. package/dist/jsonForm/index.d.ts +1 -1
  21. package/dist/jsonForm/index.js +2 -2
  22. package/dist/markdown/index.d.ts +118 -1
  23. package/dist/markdown/index.js +24 -8
  24. package/dist/{materializePageSection-DAbyLi-k.d.ts → materializePageSection-CgNs5Qmw.d.ts} +4 -2
  25. package/dist/narration/index.d.ts +1 -1
  26. package/dist/narration/index.js +5 -5
  27. package/dist/recommend/index.js +2 -2
  28. package/dist/schemas/index.d.ts +4 -4
  29. package/dist/schemas/index.js +2 -2
  30. package/dist/{themeLibrary-DJt89gyP.d.ts → themeLibrary-RWsNtlOu.d.ts} +1 -1
  31. package/dist/transform/index.d.ts +2 -2
  32. package/dist/transform/index.js +2 -2
  33. package/package.json +1 -1
  34. package/dist/chunk-3IGPAPQ7.js +0 -645
@@ -0,0 +1,983 @@
1
+ import {
2
+ assertMarkdownDocumentWithinLimits,
3
+ parseMarkdown,
4
+ toMdast
5
+ } from "./chunk-D4LPP3P5.js";
6
+ import {
7
+ formatFrontmatterYaml,
8
+ getChildren,
9
+ splitFrontmatterBlock
10
+ } from "./chunk-O7JILDEF.js";
11
+
12
+ // src/markdown/stringify.ts
13
+ import { unified } from "unified";
14
+ import remarkStringify from "remark-stringify";
15
+ import remarkGfm from "remark-gfm";
16
+ import remarkMath from "remark-math";
17
+ import remarkDirective from "remark-directive";
18
+ var defaultProcessor;
19
+ var DQ_RUN = `"(?:[^"\\\\]|\\\\.)*"`;
20
+ var SQ_RUN = `'(?:[^'\\\\]|\\\\.)*'`;
21
+ var ESCAPED_TEMPLATE_SPAN_RE = new RegExp(
22
+ `\\{\\\\\\[((?:${DQ_RUN}|${SQ_RUN}|\\\\.|[^\\]\\\\])*)\\]\\}`,
23
+ "g"
24
+ );
25
+ var HEADING_LINE_RE = /^#{1,6} .*$/gm;
26
+ var TRAILING_ESCAPED_PANDOC_SPAN_RE = new RegExp(
27
+ `\\{(?!\\\\?\\[)((?:${DQ_RUN}|${SQ_RUN}|\\\\.|[^}\\\\])*)\\}(?=\\s*(?:\\{\\[.*)?$)`
28
+ );
29
+ var UNESCAPE_PUNCT_RE = /\\([\\[\]:#.,+=/?;@%$(){}'"-])/g;
30
+ function unescapeMarkdownPunct(text) {
31
+ return text.replace(UNESCAPE_PUNCT_RE, "$1");
32
+ }
33
+ var PREFERRED_MASK_SENTINEL = "\0";
34
+ function pickMaskSentinel(text) {
35
+ const present = new Set(text);
36
+ if (!present.has(PREFERRED_MASK_SENTINEL)) return PREFERRED_MASK_SENTINEL;
37
+ for (let cp = 57344; cp <= 63743; cp++) {
38
+ const ch = String.fromCodePoint(cp);
39
+ if (!present.has(ch)) return ch;
40
+ }
41
+ return null;
42
+ }
43
+ var FENCE_OPEN_RE = /^([ \t]*)(`{3,}|~{3,})(.*)$/;
44
+ function findFencedRanges(text) {
45
+ const ranges = [];
46
+ let offset = 0;
47
+ let open = null;
48
+ for (const line of text.split("\n")) {
49
+ const lineEnd = offset + line.length;
50
+ if (open) {
51
+ const close = /^([ \t]*)(`{3,}|~{3,})[ \t]*$/.exec(line);
52
+ if (close && close[2][0] === open.marker && close[2].length >= open.length && close[1].length <= open.indent + 3) {
53
+ ranges.push([open.start, lineEnd]);
54
+ open = null;
55
+ }
56
+ } else {
57
+ const match = FENCE_OPEN_RE.exec(line);
58
+ if (match && !(match[2][0] === "`" && match[3].includes("`"))) {
59
+ open = {
60
+ marker: match[2][0],
61
+ length: match[2].length,
62
+ indent: match[1].length,
63
+ start: offset
64
+ };
65
+ }
66
+ }
67
+ offset = lineEnd + 1;
68
+ }
69
+ if (open) ranges.push([open.start, text.length]);
70
+ return ranges;
71
+ }
72
+ function isEscapedAt(text, i) {
73
+ let slashes = 0;
74
+ for (let k = i - 1; k >= 0 && text[k] === "\\"; k--) slashes++;
75
+ return slashes % 2 === 1;
76
+ }
77
+ function findInlineCodeRanges(text, from, to) {
78
+ const ranges = [];
79
+ let i = from;
80
+ while (i < to) {
81
+ if (text[i] !== "`" || isEscapedAt(text, i)) {
82
+ i++;
83
+ continue;
84
+ }
85
+ let openEnd = i;
86
+ while (openEnd < to && text[openEnd] === "`") openEnd++;
87
+ const runLength = openEnd - i;
88
+ let j = openEnd;
89
+ let closed = -1;
90
+ while (j < to) {
91
+ if (text[j] !== "`") {
92
+ j++;
93
+ continue;
94
+ }
95
+ let runEnd = j;
96
+ while (runEnd < to && text[runEnd] === "`") runEnd++;
97
+ if (runEnd - j === runLength) {
98
+ closed = runEnd;
99
+ break;
100
+ }
101
+ j = runEnd;
102
+ }
103
+ if (closed === -1) {
104
+ i = openEnd;
105
+ continue;
106
+ }
107
+ ranges.push([i, closed]);
108
+ i = closed;
109
+ }
110
+ return ranges;
111
+ }
112
+ function transformOutsideCode(text, transform) {
113
+ const fenced = findFencedRanges(text);
114
+ const ranges = [];
115
+ let cursor = 0;
116
+ for (const [start, end] of fenced) {
117
+ ranges.push(...findInlineCodeRanges(text, cursor, start));
118
+ ranges.push([start, end]);
119
+ cursor = end;
120
+ }
121
+ ranges.push(...findInlineCodeRanges(text, cursor, text.length));
122
+ if (ranges.length === 0) return transform(text);
123
+ const sentinel = pickMaskSentinel(text);
124
+ if (sentinel === null) return text;
125
+ const protectedText = [];
126
+ let masked = "";
127
+ let last = 0;
128
+ for (const [start, end] of ranges) {
129
+ masked += text.slice(last, start);
130
+ masked += `${sentinel}${protectedText.length}${sentinel}`;
131
+ protectedText.push(text.slice(start, end));
132
+ last = end;
133
+ }
134
+ masked += text.slice(last);
135
+ return transform(masked).replace(
136
+ new RegExp(`${sentinel}(\\d+)${sentinel}`, "g"),
137
+ (_m, index) => protectedText[Number(index)]
138
+ );
139
+ }
140
+ function stringifyMarkdown(doc, options) {
141
+ options?.signal?.throwIfAborted();
142
+ assertMarkdownDocumentWithinLimits(doc, options?.limits, options?.signal);
143
+ const mdastTree = toMdast(doc);
144
+ const useDefaults = !options || options.gfm !== false && options.math !== false && options.directive !== false && !options.bullet && !options.bulletOrdered && !options.emphasis && !options.strong && !options.rule && !options.fence && options.setext == null;
145
+ let processor;
146
+ if (useDefaults) {
147
+ if (!defaultProcessor) {
148
+ defaultProcessor = unified().use(remarkGfm).use(remarkMath).use(remarkDirective).use(remarkStringify, {
149
+ bullet: "-",
150
+ bulletOrdered: ".",
151
+ emphasis: "*",
152
+ strong: "*",
153
+ rule: "-",
154
+ fence: "`",
155
+ setext: false
156
+ });
157
+ }
158
+ processor = defaultProcessor;
159
+ } else {
160
+ processor = unified();
161
+ if (options?.gfm !== false) {
162
+ processor = processor.use(remarkGfm);
163
+ }
164
+ if (options?.math !== false) {
165
+ processor = processor.use(remarkMath);
166
+ }
167
+ if (options?.directive !== false) {
168
+ processor = processor.use(remarkDirective);
169
+ }
170
+ processor = processor.use(remarkStringify, {
171
+ bullet: options?.bullet ?? "-",
172
+ bulletOrdered: options?.bulletOrdered ?? ".",
173
+ emphasis: options?.emphasis ?? "*",
174
+ strong: options?.strong ?? "*",
175
+ rule: options?.rule ?? "-",
176
+ fence: options?.fence ?? "`",
177
+ setext: options?.setext ?? false
178
+ });
179
+ }
180
+ const result = processor.stringify(mdastTree);
181
+ const cleaned = transformOutsideCode(result, (text) => {
182
+ let out = text.replace(
183
+ ESCAPED_TEMPLATE_SPAN_RE,
184
+ (_m, inner) => `{[${unescapeMarkdownPunct(inner)}]}`
185
+ );
186
+ out = out.replace(
187
+ HEADING_LINE_RE,
188
+ (line) => line.replace(
189
+ TRAILING_ESCAPED_PANDOC_SPAN_RE,
190
+ (_m, inner) => `{${unescapeMarkdownPunct(inner)}}`
191
+ )
192
+ );
193
+ return out.replace(/\{(?!\\?\[)[^}]*\}/g, (match) => match.replace(/\\:/g, ":"));
194
+ });
195
+ if (doc.frontmatter && Object.keys(doc.frontmatter).length > 0) {
196
+ const yaml = formatFrontmatterYaml(doc.frontmatter);
197
+ if (yaml) return `---
198
+ ${yaml}
199
+ ---
200
+
201
+ ${cleaned}`;
202
+ }
203
+ return cleaned;
204
+ }
205
+
206
+ // src/markdown/resourcePolicy.ts
207
+ var DEFAULT_RESOURCE_MAX_BYTES = 64 * 1024 * 1024;
208
+ var DEFAULT_RESOURCE_TIMEOUT_MS = 15e3;
209
+ var DEFAULT_INTERACTIVE_RESOURCE_POLICY = Object.freeze({
210
+ allowRemote: true,
211
+ allowRelative: true,
212
+ allowBlob: true,
213
+ allowData: true,
214
+ allowedHosts: Object.freeze([]),
215
+ maxBytes: DEFAULT_RESOURCE_MAX_BYTES,
216
+ timeoutMs: DEFAULT_RESOURCE_TIMEOUT_MS
217
+ });
218
+ var LOCAL_ONLY_RESOURCE_POLICY = Object.freeze({
219
+ allowRemote: false,
220
+ allowRelative: true,
221
+ allowBlob: true,
222
+ allowData: true,
223
+ allowedHosts: Object.freeze([]),
224
+ maxBytes: DEFAULT_RESOURCE_MAX_BYTES,
225
+ timeoutMs: DEFAULT_RESOURCE_TIMEOUT_MS
226
+ });
227
+ var ResourcePolicyError = class extends Error {
228
+ constructor(code, message) {
229
+ super(message);
230
+ this.name = "ResourcePolicyError";
231
+ this.code = code;
232
+ }
233
+ };
234
+ function isResourceUrlAllowed(input, policy = DEFAULT_INTERACTIVE_RESOURCE_POLICY) {
235
+ if (typeof input !== "string") return false;
236
+ const value = input.trim();
237
+ if (!value || hasUrlControlCharacters(value)) return false;
238
+ const resolved = resolvePolicy(policy);
239
+ if (value.startsWith("\\\\")) return false;
240
+ if (value.startsWith("//")) {
241
+ if (!resolved.allowRemote) return false;
242
+ return hostAllowed(parseUrl(`https:${value}`), resolved.allowedHosts);
243
+ }
244
+ const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(value);
245
+ if (!schemeMatch) return resolved.allowRelative;
246
+ const scheme = schemeMatch[1].toLowerCase();
247
+ if (scheme === "blob") return resolved.allowBlob;
248
+ if (scheme === "data") return resolved.allowData && isSafeMediaDataUrl(value);
249
+ if (scheme !== "http" && scheme !== "https") return false;
250
+ if (!resolved.allowRemote) return false;
251
+ return hostAllowed(parseUrl(value), resolved.allowedHosts);
252
+ }
253
+ async function fetchResourceBytes(input, options = {}) {
254
+ const policy = resolvePolicy(options.policy);
255
+ if (!isResourceUrlAllowed(input, policy)) {
256
+ throw new ResourcePolicyError(
257
+ "RESOURCE_BLOCKED",
258
+ `Resource URL is blocked by policy: ${input}`
259
+ );
260
+ }
261
+ const fetchImpl = options.fetch ?? globalThis.fetch;
262
+ if (typeof fetchImpl !== "function") {
263
+ throw new Error("fetchResourceBytes requires a fetch implementation");
264
+ }
265
+ const controller = new AbortController();
266
+ let timedOut = false;
267
+ const onAbort = () => controller.abort(options.signal?.reason);
268
+ if (options.signal?.aborted) onAbort();
269
+ else options.signal?.addEventListener("abort", onAbort, { once: true });
270
+ const timeout = setTimeout(() => {
271
+ timedOut = true;
272
+ controller.abort();
273
+ }, policy.timeoutMs);
274
+ try {
275
+ const response = await fetchImpl(input, {
276
+ signal: controller.signal,
277
+ credentials: "omit",
278
+ redirect: "follow",
279
+ referrerPolicy: "no-referrer"
280
+ });
281
+ if (!response.ok) throw new Error(`Resource request failed with HTTP ${response.status}`);
282
+ const finalUrl = response.url || input;
283
+ if (!isResourceUrlAllowed(finalUrl, policy)) {
284
+ throw new ResourcePolicyError(
285
+ "RESOURCE_BLOCKED",
286
+ `Resource redirect destination is blocked by policy: ${finalUrl}`
287
+ );
288
+ }
289
+ const contentType = response.headers?.get?.("content-type")?.split(";", 1)[0].trim() ?? "";
290
+ if (contentType && options.contentTypePrefixes?.length && !options.contentTypePrefixes.some((prefix) => contentType.toLowerCase().startsWith(prefix))) {
291
+ throw new ResourcePolicyError(
292
+ "RESOURCE_BLOCKED",
293
+ `Resource Content-Type is not allowed: ${contentType || "(missing)"}`
294
+ );
295
+ }
296
+ const declaredLength = Number(response.headers?.get?.("content-length"));
297
+ if (Number.isFinite(declaredLength) && declaredLength > policy.maxBytes) {
298
+ throw tooLarge(policy.maxBytes);
299
+ }
300
+ const bytes = await readResponseBounded(response, policy.maxBytes, controller.signal);
301
+ return { bytes, contentType, finalUrl };
302
+ } catch (error) {
303
+ if (timedOut) {
304
+ throw new ResourcePolicyError(
305
+ "RESOURCE_TIMEOUT",
306
+ `Resource request exceeded ${policy.timeoutMs} ms`
307
+ );
308
+ }
309
+ throw error;
310
+ } finally {
311
+ clearTimeout(timeout);
312
+ options.signal?.removeEventListener("abort", onAbort);
313
+ }
314
+ }
315
+ function resolvePolicy(policy) {
316
+ return {
317
+ ...DEFAULT_INTERACTIVE_RESOURCE_POLICY,
318
+ ...policy,
319
+ allowedHosts: policy?.allowedHosts ?? DEFAULT_INTERACTIVE_RESOURCE_POLICY.allowedHosts
320
+ };
321
+ }
322
+ function parseUrl(value) {
323
+ try {
324
+ return new URL(value);
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+ function hostAllowed(url, allowedHosts) {
330
+ if (!url || url.username || url.password) return false;
331
+ if (allowedHosts.length === 0) return true;
332
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
333
+ return allowedHosts.some((entry) => {
334
+ const allowed = entry.trim().toLowerCase().replace(/\.$/, "");
335
+ if (allowed.startsWith("*.")) {
336
+ const suffix = allowed.slice(2);
337
+ return host !== suffix && host.endsWith(`.${suffix}`);
338
+ }
339
+ return host === allowed;
340
+ });
341
+ }
342
+ function isSafeMediaDataUrl(value) {
343
+ return /^data:(?:image\/(?!svg\+xml)[a-z0-9.+-]+|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+);/i.test(
344
+ value
345
+ );
346
+ }
347
+ function hasUrlControlCharacters(value) {
348
+ for (const char of value) {
349
+ const code = char.charCodeAt(0);
350
+ if (code < 32 || code === 127) return true;
351
+ }
352
+ return false;
353
+ }
354
+ async function readResponseBounded(response, maxBytes, signal) {
355
+ if (!response.body) {
356
+ const buffer = response.arrayBuffer ? await response.arrayBuffer() : await (await response.blob()).arrayBuffer();
357
+ const bytes = new Uint8Array(buffer);
358
+ if (bytes.byteLength > maxBytes) throw tooLarge(maxBytes);
359
+ return bytes;
360
+ }
361
+ const reader = response.body.getReader();
362
+ const chunks = [];
363
+ let total = 0;
364
+ try {
365
+ while (true) {
366
+ signal.throwIfAborted();
367
+ const { done, value } = await reader.read();
368
+ if (done) break;
369
+ total += value.byteLength;
370
+ if (total > maxBytes) {
371
+ await reader.cancel();
372
+ throw tooLarge(maxBytes);
373
+ }
374
+ chunks.push(value);
375
+ }
376
+ } finally {
377
+ reader.releaseLock();
378
+ }
379
+ const out = new Uint8Array(total);
380
+ let offset = 0;
381
+ for (const chunk of chunks) {
382
+ out.set(chunk, offset);
383
+ offset += chunk.byteLength;
384
+ }
385
+ return out;
386
+ }
387
+ function tooLarge(maxBytes) {
388
+ return new ResourcePolicyError(
389
+ "RESOURCE_TOO_LARGE",
390
+ `Resource exceeds the ${maxBytes}-byte policy limit`
391
+ );
392
+ }
393
+
394
+ // src/markdown/sourceTransforms.ts
395
+ var DEFAULT_WRAP_WIDTH = 80;
396
+ var MIN_WRAP_WIDTH = 20;
397
+ var MAX_WRAP_WIDTH = 500;
398
+ var COMMON_WRAP_WIDTHS = [60, 72, 80, 100, 120];
399
+ var WIDTH_SNAP_TOLERANCE = 8;
400
+ var MIN_DETECTED_WRAP_WIDTH = 40;
401
+ var SANE_PREFIX_RE = /^[ \t>]*(?:(?:[-*+]|\d{1,9}[.)])[ \t]+)*(?:\[\^[^\]]+\]:[ \t]+)?(?:\[[ xX]\][ \t]+)?$/;
402
+ var CONTINUATION_PREFIX_RE = /^[ \t]*(?:>[ \t]*)*/;
403
+ var ANNOTATION_SPAN_RE = /\{\[(?:"(?:[^"\\\n]|\\[^\n])*"|'(?:[^'\\\n]|\\[^\n])*'|\\[^\n]|[^\]\\\n])*\]\}/g;
404
+ function nodeSpan(node) {
405
+ const position = node.position;
406
+ if (!position || position.start.offset == null || position.end.offset == null) return null;
407
+ return { start: position.start.offset, end: position.end.offset };
408
+ }
409
+ function mergeSpans(spans) {
410
+ const sorted = [...spans].sort((a, b) => a.start - b.start);
411
+ const merged = [];
412
+ for (const span of sorted) {
413
+ const last = merged[merged.length - 1];
414
+ if (last && span.start <= last.end) {
415
+ last.end = Math.max(last.end, span.end);
416
+ } else {
417
+ merged.push({ ...span });
418
+ }
419
+ }
420
+ return merged;
421
+ }
422
+ function clipSpan(span, start, end) {
423
+ const s = Math.max(span.start, start);
424
+ const e = Math.min(span.end, end);
425
+ return s < e ? { start: s, end: e } : null;
426
+ }
427
+ function collectInlineSpans(source, paragraph, info) {
428
+ const immovable = [];
429
+ const unbreakable = [];
430
+ const visit = (node) => {
431
+ const span = nodeSpan(node);
432
+ switch (node.type) {
433
+ case "break":
434
+ if (span) info.breakSpans.push(span);
435
+ return;
436
+ case "inlineCode":
437
+ case "inlineMath":
438
+ case "htmlInline":
439
+ case "image":
440
+ if (span) immovable.push(span);
441
+ return;
442
+ case "link": {
443
+ if (span) {
444
+ const children = getChildren(node);
445
+ let segStart = span.start;
446
+ for (const child of children) {
447
+ const childSpan = nodeSpan(child);
448
+ if (childSpan) segStart = Math.max(segStart, childSpan.end);
449
+ }
450
+ if (segStart < span.end) immovable.push({ start: segStart, end: span.end });
451
+ }
452
+ break;
453
+ }
454
+ case "mention": {
455
+ if (span) {
456
+ const start = span.start > 0 && source[span.start - 1] === "@" ? span.start - 1 : span.start;
457
+ unbreakable.push({ start, end: span.end });
458
+ }
459
+ return;
460
+ }
461
+ case "textDirective":
462
+ if (span) unbreakable.push(span);
463
+ return;
464
+ case "inlineIcon":
465
+ return;
466
+ default:
467
+ break;
468
+ }
469
+ for (const child of getChildren(node)) visit(child);
470
+ };
471
+ for (const child of getChildren(paragraph)) visit(child);
472
+ const slice = source.slice(info.start, info.end);
473
+ ANNOTATION_SPAN_RE.lastIndex = 0;
474
+ let match;
475
+ while ((match = ANNOTATION_SPAN_RE.exec(slice)) !== null) {
476
+ unbreakable.push({
477
+ start: info.start + match.index,
478
+ end: info.start + match.index + match[0].length
479
+ });
480
+ }
481
+ const clippedImmovable = [];
482
+ for (const span of immovable) {
483
+ const clipped = clipSpan(span, info.start, info.end);
484
+ if (clipped) clippedImmovable.push(clipped);
485
+ }
486
+ const clippedUnbreakable = [...clippedImmovable];
487
+ for (const span of unbreakable) {
488
+ const clipped = clipSpan(span, info.start, info.end);
489
+ if (clipped) clippedUnbreakable.push(clipped);
490
+ }
491
+ info.immovableSpans = mergeSpans(clippedImmovable);
492
+ info.unbreakableSpans = mergeSpans(clippedUnbreakable);
493
+ }
494
+ function analyzeParagraphs(source, doc) {
495
+ const paragraphs = [];
496
+ const walk = (node) => {
497
+ if (node.type === "paragraph") {
498
+ const span = nodeSpan(node);
499
+ if (span) {
500
+ const lineStart = source.lastIndexOf("\n", span.start - 1) + 1;
501
+ const firstLinePrefix = source.slice(lineStart, span.start);
502
+ if (SANE_PREFIX_RE.test(firstLinePrefix)) {
503
+ const info = {
504
+ node,
505
+ start: span.start,
506
+ end: span.end,
507
+ firstLinePrefix,
508
+ continuationPrefix: firstLinePrefix.replace(/[^>\t]/g, " "),
509
+ breakSpans: [],
510
+ immovableSpans: [],
511
+ unbreakableSpans: []
512
+ };
513
+ collectInlineSpans(source, node, info);
514
+ info.breakSpans.sort((a, b) => a.start - b.start);
515
+ paragraphs.push(info);
516
+ }
517
+ }
518
+ return;
519
+ }
520
+ for (const child of getChildren(node)) walk(child);
521
+ };
522
+ walk(doc);
523
+ return paragraphs;
524
+ }
525
+ function dominantEol(source) {
526
+ const crlf = (source.match(/\r\n/g) ?? []).length;
527
+ const lf = (source.match(/\n/g) ?? []).length - crlf;
528
+ return crlf > lf ? "\r\n" : "\n";
529
+ }
530
+ function spanAt(spans, offset) {
531
+ for (const span of spans) {
532
+ if (offset < span.start) return null;
533
+ if (offset < span.end) return span;
534
+ }
535
+ return null;
536
+ }
537
+ function tokenizeParagraph(source, info) {
538
+ const segments = [{ markerBefore: "", tokens: [] }];
539
+ let current = "";
540
+ let currentMultiline = false;
541
+ let i = info.start;
542
+ const flushToken = () => {
543
+ if (current.length > 0) {
544
+ segments[segments.length - 1].tokens.push({ text: current, multiline: currentMultiline });
545
+ current = "";
546
+ currentMultiline = false;
547
+ }
548
+ };
549
+ const consumeContinuationPrefix = () => {
550
+ const rest = source.slice(i, info.end);
551
+ const match = CONTINUATION_PREFIX_RE.exec(rest);
552
+ if (match) i += match[0].length;
553
+ };
554
+ while (i < info.end) {
555
+ const breakSpan = spanAt(info.breakSpans, i);
556
+ if (breakSpan && breakSpan.start === i) {
557
+ flushToken();
558
+ segments.push({ markerBefore: source.slice(breakSpan.start, breakSpan.end), tokens: [] });
559
+ i = breakSpan.end;
560
+ consumeContinuationPrefix();
561
+ continue;
562
+ }
563
+ const atom = spanAt(info.unbreakableSpans, i);
564
+ if (atom && atom.start === i) {
565
+ const bytes = source.slice(atom.start, Math.min(atom.end, info.end));
566
+ current += bytes;
567
+ if (bytes.includes("\n")) currentMultiline = true;
568
+ i = Math.min(atom.end, info.end);
569
+ continue;
570
+ }
571
+ const ch = source[i];
572
+ if (ch === "\n" || ch === "\r" && source[i + 1] === "\n") {
573
+ flushToken();
574
+ i += ch === "\r" ? 2 : 1;
575
+ consumeContinuationPrefix();
576
+ continue;
577
+ }
578
+ if (ch === " " || ch === " ") {
579
+ flushToken();
580
+ i += 1;
581
+ continue;
582
+ }
583
+ current += ch;
584
+ i += 1;
585
+ }
586
+ flushToken();
587
+ return segments;
588
+ }
589
+ var FORBIDDEN_LINE_START_RES = [
590
+ /^[-+*](?:[ \t]|$)/,
591
+ // bullet list
592
+ /^\d{1,9}[.)](?:[ \t]|$)/,
593
+ // ordered list (all numbers — belt and braces)
594
+ /^#{1,6}(?:[ \t]|$)/,
595
+ // ATX heading
596
+ /^>/,
597
+ // blockquote
598
+ /^\|/,
599
+ // table row
600
+ /^(?:`{3,}|~{3,})/,
601
+ // code fence
602
+ /^\$\$/,
603
+ // math flow
604
+ /^::/,
605
+ // leaf/container directive
606
+ /^\[\^[^\]]*\]:/,
607
+ // footnote definition
608
+ /^<[a-zA-Z!/?]/
609
+ // HTML block types 1–6
610
+ ];
611
+ var FORBIDDEN_WHOLE_LINE_RES = [
612
+ /^=+[ \t]*$/,
613
+ // setext H1
614
+ /^-+[ \t]*$/,
615
+ // setext H2 / thematic break
616
+ /^([*_-])(?:[ \t]*\1){2,}[ \t]*$/,
617
+ // thematic break, spaced forms included
618
+ /^[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)+\|?[ \t]*$/
619
+ // GFM table delimiter row
620
+ ];
621
+ function isForbiddenLineStart(line) {
622
+ return FORBIDDEN_LINE_START_RES.some((re) => re.test(line)) || FORBIDDEN_WHOLE_LINE_RES.some((re) => re.test(line));
623
+ }
624
+ function firstLineLength(token) {
625
+ const idx = token.text.indexOf("\n");
626
+ return idx === -1 ? token.text.length : idx;
627
+ }
628
+ function lastLineLength(token) {
629
+ const idx = token.text.lastIndexOf("\n");
630
+ return idx === -1 ? token.text.length : token.text.length - idx - 1;
631
+ }
632
+ function fillSegment(tokens, width, firstLinePrefixLen, continuationPrefixLen) {
633
+ const lines = [[]];
634
+ let column = firstLinePrefixLen;
635
+ for (const token of tokens) {
636
+ const line = lines[lines.length - 1];
637
+ const addition = (line.length > 0 ? 1 : 0) + firstLineLength(token);
638
+ if (line.length > 0 && column + addition > width) {
639
+ lines.push([token]);
640
+ column = continuationPrefixLen + lastLineLength(token);
641
+ } else {
642
+ line.push(token);
643
+ column = token.multiline ? lastLineLength(token) : column + addition;
644
+ }
645
+ }
646
+ for (let i = 1; i < lines.length; i++) {
647
+ for (; ; ) {
648
+ const line = lines[i];
649
+ if (line.length === 0) break;
650
+ const prev = lines[i - 1];
651
+ const prevLast = prev[prev.length - 1];
652
+ const startsForbidden = isForbiddenLineStart(line.map((t) => t.text).join(" "));
653
+ const prevEndsBackslash = prevLast !== void 0 && prevLast.text.endsWith("\\");
654
+ if (!startsForbidden && !prevEndsBackslash) break;
655
+ const moved = line.shift();
656
+ if (!moved) break;
657
+ prev.push(moved);
658
+ }
659
+ if (lines[i].length === 0) {
660
+ lines.splice(i, 1);
661
+ i -= 1;
662
+ }
663
+ }
664
+ return lines.filter((line) => line.length > 0);
665
+ }
666
+ function reflowParagraphSlice(source, info, width, eol) {
667
+ const segments = tokenizeParagraph(source, info);
668
+ const contPrefix = info.continuationPrefix;
669
+ const parts = [];
670
+ segments.forEach((segment, index) => {
671
+ if (index > 0) {
672
+ parts.push(segment.markerBefore, contPrefix);
673
+ }
674
+ if (width === null) {
675
+ parts.push(segment.tokens.map((t) => t.text).join(" "));
676
+ } else {
677
+ const firstPrefixLen = index === 0 ? info.firstLinePrefix.length : contPrefix.length;
678
+ const lines = fillSegment(segment.tokens, width, firstPrefixLen, contPrefix.length);
679
+ parts.push(
680
+ lines.map((line) => line.map((t) => t.text).join(" ")).join(`${eol}${contPrefix}`)
681
+ );
682
+ }
683
+ });
684
+ return parts.join("");
685
+ }
686
+ function collapseWhitespace(value) {
687
+ return value.replace(/\s+/g, " ");
688
+ }
689
+ var STRICT_VALUE_TYPES = /* @__PURE__ */ new Set([
690
+ "inlineCode",
691
+ "code",
692
+ "math",
693
+ "inlineMath",
694
+ "htmlInline",
695
+ "htmlBlock"
696
+ ]);
697
+ var COLLAPSE_STRING_KEYS = /* @__PURE__ */ new Set(["label", "alt", "displayName"]);
698
+ function normalizeForCompare(node) {
699
+ const out = {};
700
+ const record = node;
701
+ for (const key of Object.keys(record)) {
702
+ if (key === "position" || key === "children" || key === "htmlChildren") continue;
703
+ const value = record[key];
704
+ if (value === void 0) continue;
705
+ if (typeof value === "string" && key !== "type") {
706
+ if (key === "value") {
707
+ out[key] = STRICT_VALUE_TYPES.has(node.type) ? value : collapseWhitespace(value);
708
+ } else if (COLLAPSE_STRING_KEYS.has(key)) {
709
+ out[key] = collapseWhitespace(value);
710
+ } else {
711
+ out[key] = value;
712
+ }
713
+ } else if (typeof value === "object" && value !== null) {
714
+ out[key] = JSON.parse(JSON.stringify(value));
715
+ } else {
716
+ out[key] = value;
717
+ }
718
+ }
719
+ const children = getChildren(node);
720
+ if ("children" in record) {
721
+ const merged = [];
722
+ for (const child of children) {
723
+ const prev = merged[merged.length - 1];
724
+ if (child.type === "text" && prev && prev.type === "text") {
725
+ merged[merged.length - 1] = { ...prev, value: `${prev.value}${child.value}` };
726
+ } else {
727
+ merged.push(child);
728
+ }
729
+ }
730
+ out.children = merged.map((child) => normalizeForCompare(child));
731
+ }
732
+ return out;
733
+ }
734
+ function stableStringify(value) {
735
+ if (Array.isArray(value)) {
736
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
737
+ }
738
+ if (typeof value === "object" && value !== null) {
739
+ const record = value;
740
+ const keys = Object.keys(record).sort();
741
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(record[k])}`).join(",")}}`;
742
+ }
743
+ return JSON.stringify(value) ?? "null";
744
+ }
745
+ function documentsEquivalent(a, b) {
746
+ return stableStringify(normalizeForCompare(a)) === stableStringify(normalizeForCompare(b));
747
+ }
748
+ function noopResult(source, degraded) {
749
+ return { output: source, changed: false, degraded, edits: [] };
750
+ }
751
+ function clampWidth(width) {
752
+ if (width === void 0 || Number.isNaN(width)) return DEFAULT_WRAP_WIDTH;
753
+ return Math.min(MAX_WRAP_WIDTH, Math.max(MIN_WRAP_WIDTH, Math.floor(width)));
754
+ }
755
+ function degrade(id, source, strict, reason) {
756
+ if (strict) {
757
+ throw new Error(`squisq ${id} transform aborted: ${reason}`);
758
+ }
759
+ console.warn(`[squisq] ${id} transform left the document unchanged: ${reason}`);
760
+ return noopResult(source, true);
761
+ }
762
+ function reflowTransform(id, source, options) {
763
+ const width = id === "wrap" ? clampWidth(options?.width) : null;
764
+ let doc;
765
+ try {
766
+ doc = parseMarkdown(source, { parseHtml: false });
767
+ } catch (err) {
768
+ const message = err instanceof Error ? err.message : String(err);
769
+ return degrade(id, source, options?.strict, `the document could not be parsed (${message})`);
770
+ }
771
+ const eol = dominantEol(source);
772
+ const edits = [];
773
+ for (const info of analyzeParagraphs(source, doc)) {
774
+ const oldSlice = source.slice(info.start, info.end);
775
+ if (id === "unwrap" && !oldSlice.includes("\n")) continue;
776
+ const newSlice = reflowParagraphSlice(source, info, width, eol);
777
+ if (newSlice !== oldSlice) {
778
+ edits.push({ start: info.start, end: info.end, text: newSlice });
779
+ }
780
+ }
781
+ if (edits.length === 0) return noopResult(source, false);
782
+ edits.sort((a, b) => b.start - a.start);
783
+ let output = source;
784
+ for (const edit of edits) {
785
+ output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
786
+ }
787
+ try {
788
+ const reparsed = parseMarkdown(output, { parseHtml: false });
789
+ if (!documentsEquivalent(doc, reparsed)) {
790
+ return degrade(
791
+ id,
792
+ source,
793
+ options?.strict,
794
+ "the transformed markdown no longer parses to an equivalent document"
795
+ );
796
+ }
797
+ } catch (err) {
798
+ const message = err instanceof Error ? err.message : String(err);
799
+ return degrade(
800
+ id,
801
+ source,
802
+ options?.strict,
803
+ `the transformed markdown failed to reparse (${message})`
804
+ );
805
+ }
806
+ return { output, changed: true, degraded: false, edits };
807
+ }
808
+ function cleanupTransform(source, options) {
809
+ const { frontmatter, body } = splitFrontmatterBlock(source);
810
+ let output;
811
+ let parsedBody;
812
+ try {
813
+ parsedBody = parseMarkdown(body, { frontmatter: false, parseHtml: false });
814
+ const cleanedBody = stringifyMarkdown(parsedBody);
815
+ if (frontmatter !== null) {
816
+ let fm = frontmatter.replace(/\r\n?/g, "\n");
817
+ if (!fm.endsWith("\n")) fm += "\n";
818
+ output = cleanedBody.length > 0 ? `${fm}
819
+ ${cleanedBody}` : fm;
820
+ } else {
821
+ output = cleanedBody;
822
+ }
823
+ } catch (err) {
824
+ const message = err instanceof Error ? err.message : String(err);
825
+ return degrade(
826
+ "cleanup",
827
+ source,
828
+ options?.strict,
829
+ `the document could not be re-serialized (${message})`
830
+ );
831
+ }
832
+ if (output === source) return noopResult(source, false);
833
+ try {
834
+ const reparsed = parseMarkdown(splitFrontmatterBlock(output).body, {
835
+ frontmatter: false,
836
+ parseHtml: false
837
+ });
838
+ if (!documentsEquivalent(parsedBody, reparsed)) {
839
+ return degrade(
840
+ "cleanup",
841
+ source,
842
+ options?.strict,
843
+ "the cleaned markdown no longer parses to an equivalent document"
844
+ );
845
+ }
846
+ } catch (err) {
847
+ const message = err instanceof Error ? err.message : String(err);
848
+ return degrade(
849
+ "cleanup",
850
+ source,
851
+ options?.strict,
852
+ `the cleaned markdown failed to reparse (${message})`
853
+ );
854
+ }
855
+ return {
856
+ output,
857
+ changed: true,
858
+ degraded: false,
859
+ edits: [{ start: 0, end: source.length, text: output }]
860
+ };
861
+ }
862
+ var MARKDOWN_SOURCE_TRANSFORMS = Object.freeze([
863
+ {
864
+ id: "unwrap",
865
+ label: "Unwrap paragraphs",
866
+ description: "Remove forced line wrapping so each paragraph is a single line (hard breaks are kept).",
867
+ apply: (source, options) => reflowTransform("unwrap", source, options)
868
+ },
869
+ {
870
+ id: "wrap",
871
+ label: "Wrap at width",
872
+ description: "Hard-wrap paragraph prose at a column width on word boundaries; code, tables, and headings are untouched.",
873
+ apply: (source, options) => reflowTransform("wrap", source, options)
874
+ },
875
+ {
876
+ id: "cleanup",
877
+ label: "Clean up formatting",
878
+ description: "Re-serialize through the canonical house style: bullets, emphasis, headings, table padding, spacing.",
879
+ apply: (source, options) => cleanupTransform(source, options)
880
+ }
881
+ ]);
882
+ function applyMarkdownSourceTransform(id, source, options) {
883
+ const transform = MARKDOWN_SOURCE_TRANSFORMS.find((t) => t.id === id);
884
+ if (!transform) {
885
+ const known = MARKDOWN_SOURCE_TRANSFORMS.map((t) => t.id).join(", ");
886
+ throw new Error(`Unknown markdown source transform "${id}" (known: ${known})`);
887
+ }
888
+ return transform.apply(source, options);
889
+ }
890
+ function unwrapMarkdownSource(source, options) {
891
+ return reflowTransform("unwrap", source, options).output;
892
+ }
893
+ function wrapMarkdownSource(source, options) {
894
+ return reflowTransform("wrap", source, options).output;
895
+ }
896
+ function cleanupMarkdownSource(source, options) {
897
+ return cleanupTransform(source, options).output;
898
+ }
899
+ function detectMarkdownWrapState(source) {
900
+ let doc;
901
+ try {
902
+ doc = parseMarkdown(source, { parseHtml: false });
903
+ } catch {
904
+ return { kind: "no-prose", totalParagraphs: 0, wrappedParagraphs: 0, maxLineLength: 0 };
905
+ }
906
+ const paragraphs = analyzeParagraphs(source, doc);
907
+ if (paragraphs.length === 0) {
908
+ return { kind: "no-prose", totalParagraphs: 0, wrappedParagraphs: 0, maxLineLength: 0 };
909
+ }
910
+ let wrappedParagraphs = 0;
911
+ let maxLineLength = 0;
912
+ let candidateWidth = 0;
913
+ const approxLengths = [];
914
+ for (const info of paragraphs) {
915
+ const lineStart = info.start - info.firstLinePrefix.length;
916
+ const slice = source.slice(lineStart, info.end);
917
+ const lines = slice.split(/\r?\n/);
918
+ for (const line of lines) maxLineLength = Math.max(maxLineLength, line.length);
919
+ const evidenceLengths = [];
920
+ let offset = lineStart;
921
+ for (let i = 0; i < lines.length - 1; i++) {
922
+ const newlineOffset = offset + lines[i].length;
923
+ const isHard = spanAt(info.breakSpans, newlineOffset) !== null;
924
+ const isImmovable = spanAt(info.immovableSpans, newlineOffset) !== null;
925
+ if (!isHard && !isImmovable) evidenceLengths.push(lines[i].length);
926
+ offset = newlineOffset + (source[newlineOffset] === "\r" ? 2 : 1);
927
+ }
928
+ const wrapped = evidenceLengths.length > 0;
929
+ if (wrapped) {
930
+ wrappedParagraphs += 1;
931
+ candidateWidth = Math.max(candidateWidth, ...evidenceLengths);
932
+ }
933
+ approxLengths.push({
934
+ approxUnwrapped: info.firstLinePrefix.length + (info.end - info.start),
935
+ wrapped
936
+ });
937
+ }
938
+ if (wrappedParagraphs === 0) {
939
+ return {
940
+ kind: "unwrapped",
941
+ totalParagraphs: paragraphs.length,
942
+ wrappedParagraphs: 0,
943
+ maxLineLength
944
+ };
945
+ }
946
+ let width = candidateWidth;
947
+ for (const common of COMMON_WRAP_WIDTHS) {
948
+ if (candidateWidth <= common && common - candidateWidth <= WIDTH_SNAP_TOLERANCE) {
949
+ width = common;
950
+ break;
951
+ }
952
+ }
953
+ let needy = 0;
954
+ for (const p of approxLengths) {
955
+ if (p.wrapped || p.approxUnwrapped > width) needy += 1;
956
+ }
957
+ const kind = candidateWidth >= MIN_DETECTED_WRAP_WIDTH && wrappedParagraphs / Math.max(1, needy) >= 0.5 ? "wrapped" : "mixed";
958
+ return {
959
+ kind,
960
+ ...kind === "wrapped" ? { width } : {},
961
+ totalParagraphs: paragraphs.length,
962
+ wrappedParagraphs,
963
+ maxLineLength
964
+ };
965
+ }
966
+
967
+ export {
968
+ stringifyMarkdown,
969
+ DEFAULT_RESOURCE_MAX_BYTES,
970
+ DEFAULT_RESOURCE_TIMEOUT_MS,
971
+ DEFAULT_INTERACTIVE_RESOURCE_POLICY,
972
+ LOCAL_ONLY_RESOURCE_POLICY,
973
+ ResourcePolicyError,
974
+ isResourceUrlAllowed,
975
+ fetchResourceBytes,
976
+ DEFAULT_WRAP_WIDTH,
977
+ MARKDOWN_SOURCE_TRANSFORMS,
978
+ applyMarkdownSourceTransform,
979
+ unwrapMarkdownSource,
980
+ wrapMarkdownSource,
981
+ cleanupMarkdownSource,
982
+ detectMarkdownWrapState
983
+ };