@hraness/kb 0.17.1

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 (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,2046 @@
1
+ // @bun
2
+ import {
3
+ sanitizeTerminalText
4
+ } from "./index-1xxnjn0d.js";
5
+
6
+ // src/clip/lib.ts
7
+ var articleMetadataLimits = {
8
+ title: 2048,
9
+ author: 1024,
10
+ published: 256,
11
+ description: 8192
12
+ };
13
+ var MAX_SLUG_INPUT_CODE_UNITS = 4096;
14
+ var MAX_YAML_SCALAR_CODE_UNITS = 16384;
15
+ function boundedPrefix(value, maxCodeUnits, marker = "") {
16
+ if (value.length <= maxCodeUnits)
17
+ return value;
18
+ const markerLength = Math.min(marker.length, maxCodeUnits);
19
+ let end = maxCodeUnits - markerLength;
20
+ const finalCode = value.charCodeAt(end - 1);
21
+ if (finalCode >= 55296 && finalCode <= 56319)
22
+ end -= 1;
23
+ return value.slice(0, Math.max(0, end)) + marker.slice(0, markerLength);
24
+ }
25
+ function boundedMetadata(value, maxCodeUnits) {
26
+ return value === null ? null : boundedPrefix(value, maxCodeUnits, "\u2026");
27
+ }
28
+ function slugify(value) {
29
+ const normalized = boundedPrefix(value, MAX_SLUG_INPUT_CODE_UNITS).normalize("NFKC").toLowerCase().replace(/['\u2019]/g, "").replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-+|-+$/g, "").replace(/-+$/g, "");
30
+ let end = 0;
31
+ let characters = 0;
32
+ for (const character of normalized) {
33
+ if (characters === 80)
34
+ break;
35
+ end += character.length;
36
+ characters += 1;
37
+ }
38
+ return normalized.slice(0, end).replace(/-+$/g, "");
39
+ }
40
+ function yamlString(value) {
41
+ const sanitized = sanitizeTerminalText(boundedPrefix(value, MAX_YAML_SCALAR_CODE_UNITS, "\u2026"));
42
+ const chunks = ['"'];
43
+ let unchangedStart = 0;
44
+ for (let cursor = 0;cursor < sanitized.length; cursor += 1) {
45
+ const character = sanitized[cursor] ?? "";
46
+ const codePoint = sanitized.charCodeAt(cursor);
47
+ let replacement = null;
48
+ if (character === "\\")
49
+ replacement = "\\\\";
50
+ else if (character === '"')
51
+ replacement = "\\\"";
52
+ else if (character === `
53
+ `)
54
+ replacement = "\\n";
55
+ else if (character === "\r")
56
+ replacement = "\\r";
57
+ else if (character === "\t")
58
+ replacement = "\\t";
59
+ else if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159) {
60
+ replacement = `\\x${codePoint.toString(16).padStart(2, "0")}`;
61
+ } else if (codePoint === 8232 || codePoint === 8233 || codePoint === 65279) {
62
+ replacement = `\\u${codePoint.toString(16).padStart(4, "0")}`;
63
+ }
64
+ if (replacement === null)
65
+ continue;
66
+ chunks.push(sanitized.slice(unchangedStart, cursor), replacement);
67
+ unchangedStart = cursor + 1;
68
+ }
69
+ chunks.push(sanitized.slice(unchangedStart), '"');
70
+ return chunks.join("");
71
+ }
72
+ function resolveRemote(source, base) {
73
+ if (source.length > MAX_REMOTE_SOURCE_CODE_UNITS || source.startsWith("data:") || source.startsWith("#"))
74
+ return null;
75
+ try {
76
+ const url = new URL(source, base);
77
+ return (url.protocol === "http:" || url.protocol === "https:") && url.href.length <= MAX_RESOLVED_URL_CODE_UNITS ? url : null;
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+ function inertRemoteImageHref(url) {
83
+ const inert = new URL(url);
84
+ inert.username = "";
85
+ inert.password = "";
86
+ inert.search = "";
87
+ inert.hash = "";
88
+ return inert.href.replace(/\(/g, "%28").replace(/\)/g, "%29");
89
+ }
90
+ var balancedParentheses = /[^()\s]*(?:\([^()\s]*(?:\([^()\s]*(?:\([^()\s]*\)[^()\s]*)?\)[^()\s]*)?\)[^()\s]*)*/;
91
+ var markdownImage = new RegExp(`!\\[([^\\]]*)\\]\\((?:<([^<>]*)>|(${balancedParentheses.source}))((?:\\s+"[^"]*")?)\\)`, "g");
92
+ var htmlImage = /<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi;
93
+ var plainLink = new RegExp(`(\\]\\()(?:<([^<>]*)>|(${balancedParentheses.source}))((?:\\s+"[^"]*")?)(\\))`, "g");
94
+ var referenceImage = /!\[([^\]\r\n]*)\]\[([^\]\r\n]*)\]/g;
95
+ var referenceDefinition = /^([ \t]{0,3}\[([^\]\r\n]+)\]:[ \t]*)(?:<([^<>\r\n]*)>|(\S+))([^\r\n]*)$/gm;
96
+ var obsidianEmbed = /!\[\[([^\]\r\n]+)\]\]/g;
97
+ var safeMarkdownHtmlElements = new Set([
98
+ "abbr",
99
+ "b",
100
+ "bdi",
101
+ "bdo",
102
+ "blockquote",
103
+ "br",
104
+ "caption",
105
+ "cite",
106
+ "code",
107
+ "col",
108
+ "colgroup",
109
+ "dd",
110
+ "del",
111
+ "details",
112
+ "dfn",
113
+ "div",
114
+ "dl",
115
+ "dt",
116
+ "em",
117
+ "figcaption",
118
+ "figure",
119
+ "h1",
120
+ "h2",
121
+ "h3",
122
+ "h4",
123
+ "h5",
124
+ "h6",
125
+ "hr",
126
+ "i",
127
+ "kbd",
128
+ "li",
129
+ "mark",
130
+ "ol",
131
+ "p",
132
+ "pre",
133
+ "q",
134
+ "rp",
135
+ "rt",
136
+ "ruby",
137
+ "s",
138
+ "samp",
139
+ "small",
140
+ "span",
141
+ "strong",
142
+ "sub",
143
+ "summary",
144
+ "sup",
145
+ "table",
146
+ "tbody",
147
+ "td",
148
+ "tfoot",
149
+ "th",
150
+ "thead",
151
+ "time",
152
+ "tr",
153
+ "u",
154
+ "ul",
155
+ "var",
156
+ "wbr"
157
+ ]);
158
+ var rawHtmlTag = /<\s*(?!https?:\/\/)(\/?)\s*([a-zA-Z][a-zA-Z0-9:-]*)\b[^>]*>/gi;
159
+ var unsafeMarkdownImage = /!\[([^\]]*)\]\(\s*<?\s*(?:data|javascript|vbscript|file|blob):[^\r\n]*\)/gi;
160
+ var protectedPlaceholder = /\0PROTECTED(\d+)\0/g;
161
+ var MAX_PROTECTED_MARKDOWN_SPANS = 4096;
162
+ var MAX_INLINE_CODE_RUNS_PER_LINE = 4096;
163
+ var MAX_IMAGE_CANDIDATES = 250000;
164
+ var MAX_MARKUP_CANDIDATES = 50000;
165
+ var MAX_INERT_OVERFLOW_PREVIEW_CODE_UNITS = 256 * 1024;
166
+ var MAX_IMAGE_SOURCES = 10001;
167
+ var MAX_REFERENCE_LABELS = 10001;
168
+ var MAX_REFERENCE_LABEL_CODE_UNITS = 1024;
169
+ var MAX_REMOTE_SOURCE_CODE_UNITS = 8192;
170
+ var MAX_RESOLVED_URL_CODE_UNITS = 16384;
171
+ var MAX_IMAGE_ALT_CODE_UNITS = 2048;
172
+ function openingFence(content, lineStart, lineEnd) {
173
+ let cursor = lineStart;
174
+ let indentation = 0;
175
+ while (indentation < 3 && content[cursor] === " ") {
176
+ cursor += 1;
177
+ indentation += 1;
178
+ }
179
+ const marker = content[cursor];
180
+ if (marker !== "`" && marker !== "~")
181
+ return null;
182
+ const runStart = cursor;
183
+ while (cursor < lineEnd && content[cursor] === marker)
184
+ cursor += 1;
185
+ const length = cursor - runStart;
186
+ if (length < 3)
187
+ return null;
188
+ const laterBacktick = marker === "`" ? content.indexOf("`", cursor) : -1;
189
+ if (laterBacktick !== -1 && laterBacktick < lineEnd)
190
+ return null;
191
+ return { marker, length };
192
+ }
193
+ function isClosingFence(content, lineStart, lineEnd, delimiter) {
194
+ let cursor = lineStart;
195
+ let indentation = 0;
196
+ while (indentation < 3 && content[cursor] === " ") {
197
+ cursor += 1;
198
+ indentation += 1;
199
+ }
200
+ const runStart = cursor;
201
+ while (cursor < lineEnd && content[cursor] === delimiter.marker)
202
+ cursor += 1;
203
+ if (cursor - runStart < delimiter.length)
204
+ return false;
205
+ while (cursor < lineEnd && (content[cursor] === " " || content[cursor] === "\t" || content[cursor] === "\r")) {
206
+ cursor += 1;
207
+ }
208
+ return cursor === lineEnd;
209
+ }
210
+ function protectMarkdownFences(content, protectedSpans) {
211
+ const chunks = [];
212
+ let unchangedStart = 0;
213
+ let lineStart = 0;
214
+ let active = null;
215
+ while (lineStart < content.length) {
216
+ const newline = content.indexOf(`
217
+ `, lineStart);
218
+ const lineEnd = newline === -1 ? content.length : newline;
219
+ if (active === null) {
220
+ const delimiter = newline === -1 ? null : openingFence(content, lineStart, lineEnd);
221
+ if (delimiter !== null)
222
+ active = { start: lineStart, delimiter };
223
+ } else if (isClosingFence(content, lineStart, lineEnd, active.delimiter)) {
224
+ if (protectedSpans.length >= MAX_PROTECTED_MARKDOWN_SPANS)
225
+ return null;
226
+ chunks.push(content.slice(unchangedStart, active.start), `\x00PROTECTED${protectedSpans.length}\x00`);
227
+ protectedSpans.push(content.slice(active.start, lineEnd));
228
+ unchangedStart = lineEnd;
229
+ active = null;
230
+ }
231
+ if (newline === -1)
232
+ break;
233
+ lineStart = newline + 1;
234
+ }
235
+ if (active !== null)
236
+ return null;
237
+ if (chunks.length === 0)
238
+ return content;
239
+ chunks.push(content.slice(unchangedStart));
240
+ return chunks.join("");
241
+ }
242
+ function protectInlineCodeSpans(content, protectedSpans) {
243
+ if (!content.includes("`"))
244
+ return content;
245
+ const chunks = [];
246
+ let unchangedStart = 0;
247
+ let lineStart = 0;
248
+ while (lineStart < content.length) {
249
+ const newline = content.indexOf(`
250
+ `, lineStart);
251
+ const lineEnd = newline === -1 ? content.length : newline;
252
+ const runs = [];
253
+ let cursor = lineStart;
254
+ for (;; ) {
255
+ const start = content.indexOf("`", cursor);
256
+ if (start === -1 || start >= lineEnd)
257
+ break;
258
+ let end = start + 1;
259
+ while (end < lineEnd && content[end] === "`")
260
+ end += 1;
261
+ if (runs.length >= MAX_INLINE_CODE_RUNS_PER_LINE)
262
+ return null;
263
+ runs.push({ start, end, length: end - start });
264
+ cursor = end;
265
+ }
266
+ const nextSameLength = [];
267
+ nextSameLength.length = runs.length;
268
+ const laterByLength = new Map;
269
+ for (let index = runs.length - 1;index >= 0; index -= 1) {
270
+ const run = runs[index];
271
+ if (run === undefined)
272
+ continue;
273
+ nextSameLength[index] = laterByLength.get(run.length);
274
+ laterByLength.set(run.length, index);
275
+ }
276
+ for (let index = 0;index < runs.length; ) {
277
+ const closingIndex = nextSameLength[index];
278
+ if (closingIndex === undefined) {
279
+ index += 1;
280
+ continue;
281
+ }
282
+ const opening = runs[index];
283
+ const closing = runs[closingIndex];
284
+ if (opening === undefined || closing === undefined) {
285
+ index += 1;
286
+ continue;
287
+ }
288
+ if (protectedSpans.length >= MAX_PROTECTED_MARKDOWN_SPANS)
289
+ return null;
290
+ chunks.push(content.slice(unchangedStart, opening.start), `\x00PROTECTED${protectedSpans.length}\x00`);
291
+ protectedSpans.push(content.slice(opening.start, closing.end));
292
+ unchangedStart = closing.end;
293
+ index = closingIndex + 1;
294
+ }
295
+ if (newline === -1)
296
+ break;
297
+ lineStart = newline + 1;
298
+ }
299
+ if (chunks.length === 0)
300
+ return content;
301
+ chunks.push(content.slice(unchangedStart));
302
+ return chunks.join("");
303
+ }
304
+ function restoreMarkdownSpans(content, protectedSpans) {
305
+ return content.replace(protectedPlaceholder, (_whole, index) => protectedSpans[Number(index)] ?? "");
306
+ }
307
+ function inertProtectedOverflow(content) {
308
+ let previewEnd = Math.min(content.length, MAX_INERT_OVERFLOW_PREVIEW_CODE_UNITS);
309
+ const finalCode = content.charCodeAt(previewEnd - 1);
310
+ if (finalCode >= 55296 && finalCode <= 56319)
311
+ previewEnd -= 1;
312
+ const escaped = content.slice(0, previewEnd).replace(/[&<>]/g, (character) => character === "&" ? "&amp;" : character === "<" ? "&lt;" : "&gt;");
313
+ const omitted = content.length - previewEnd;
314
+ const omission = omitted === 0 ? "" : `
315
+
316
+ [${omitted} source code unit(s) omitted]`;
317
+ return `*[Content rendered inert because the protected Markdown span limit was exceeded.]*
318
+
319
+ <pre>
320
+ ${escaped}${omission}
321
+ </pre>`;
322
+ }
323
+ function inertCandidateOverflow(content) {
324
+ let previewEnd = Math.min(content.length, MAX_INERT_OVERFLOW_PREVIEW_CODE_UNITS);
325
+ const finalCode = content.charCodeAt(previewEnd - 1);
326
+ if (finalCode >= 55296 && finalCode <= 56319)
327
+ previewEnd -= 1;
328
+ const escaped = content.slice(0, previewEnd).replace(/[&<>]/g, (character) => character === "&" ? "&amp;" : character === "<" ? "&lt;" : "&gt;");
329
+ const omitted = content.length - previewEnd;
330
+ const omission = omitted === 0 ? "" : `
331
+
332
+ [${omitted} source code unit(s) omitted]`;
333
+ return `*[Content rendered inert because a markup/image-candidate safety limit was exceeded.]*
334
+
335
+ <pre>
336
+ ${escaped}${omission}
337
+ </pre>`;
338
+ }
339
+ function escapeMarkdownLabel(value) {
340
+ return boundedPrefix(value, MAX_IMAGE_ALT_CODE_UNITS, "\u2026").replace(/\\/g, "\\\\").replace(/[[\]`]/g, "\\$&").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/[\r\n]+/g, " ").trim();
341
+ }
342
+ function normalizedReferenceLabel(value) {
343
+ if (value.length > MAX_REFERENCE_LABEL_CODE_UNITS)
344
+ return null;
345
+ return value.replace(/\\([\\[\]])/g, "$1").replace(/\s+/g, " ").trim().toLowerCase();
346
+ }
347
+ function referenceLabels(content) {
348
+ const labels = new Set;
349
+ let truncated = false;
350
+ let cardinalityExceeded = false;
351
+ referenceImage.lastIndex = 0;
352
+ try {
353
+ for (;; ) {
354
+ const match = referenceImage.exec(content);
355
+ if (match === null)
356
+ break;
357
+ const alt = match[1] ?? "";
358
+ const rawLabel = match[2] === "" ? alt : match[2] ?? "";
359
+ const label = normalizedReferenceLabel(rawLabel);
360
+ if (label === null) {
361
+ truncated = true;
362
+ continue;
363
+ }
364
+ if (labels.has(label))
365
+ continue;
366
+ if (labels.size >= MAX_REFERENCE_LABELS) {
367
+ truncated = true;
368
+ cardinalityExceeded = true;
369
+ break;
370
+ }
371
+ labels.add(label);
372
+ }
373
+ } finally {
374
+ referenceImage.lastIndex = 0;
375
+ }
376
+ return { labels, truncated, cardinalityExceeded };
377
+ }
378
+ function referenceTargets(content, labels) {
379
+ const targets = new Map;
380
+ let truncated = false;
381
+ if (labels.size === 0)
382
+ return { targets, truncated };
383
+ referenceDefinition.lastIndex = 0;
384
+ try {
385
+ for (;; ) {
386
+ const match = referenceDefinition.exec(content);
387
+ if (match === null)
388
+ break;
389
+ const rawLabel = match[2];
390
+ const target = match[3] ?? match[4];
391
+ if (rawLabel === undefined || target === undefined)
392
+ continue;
393
+ const label = normalizedReferenceLabel(rawLabel);
394
+ if (label === null || !labels.has(label))
395
+ continue;
396
+ if (target.length > MAX_REMOTE_SOURCE_CODE_UNITS) {
397
+ targets.delete(label);
398
+ truncated = true;
399
+ continue;
400
+ }
401
+ targets.set(label, target);
402
+ }
403
+ } finally {
404
+ referenceDefinition.lastIndex = 0;
405
+ }
406
+ return { targets, truncated };
407
+ }
408
+ function sanitizeMarkdownHtml(content) {
409
+ return content.replace(/<!--[\s\S]*?-->/g, "").replace(rawHtmlTag, (_whole, closing, rawName) => {
410
+ const name = rawName.toLowerCase();
411
+ if (!safeMarkdownHtmlElements.has(name)) {
412
+ return `&lt;${closing === "/" ? "/" : ""}${name}&gt;`;
413
+ }
414
+ return `<${closing === "/" ? "/" : ""}${name}>`;
415
+ });
416
+ }
417
+ function imageCandidateStructure(content) {
418
+ let cursor = 0;
419
+ let count = 0;
420
+ for (;; ) {
421
+ const start = content.indexOf("![", cursor);
422
+ if (start === -1)
423
+ return { safeForRegexScan: true, cardinalityExceeded: false };
424
+ count += 1;
425
+ if (count > MAX_IMAGE_CANDIDATES) {
426
+ return { safeForRegexScan: false, cardinalityExceeded: true };
427
+ }
428
+ const altEnd = content.indexOf("]", start + 2);
429
+ const nestedImage = content.indexOf("![", start + 2);
430
+ if (altEnd === -1 || nestedImage !== -1 && nestedImage < altEnd) {
431
+ return { safeForRegexScan: false, cardinalityExceeded: false };
432
+ }
433
+ const targetMarker = content[altEnd + 1];
434
+ if (targetMarker === "(" || targetMarker === "[") {
435
+ const targetEnd = content.indexOf(targetMarker === "(" ? ")" : "]", altEnd + 2);
436
+ const nestedTargetImage = content.indexOf("![", altEnd + 2);
437
+ if (targetEnd === -1 || nestedTargetImage !== -1 && nestedTargetImage < targetEnd) {
438
+ return { safeForRegexScan: false, cardinalityExceeded: false };
439
+ }
440
+ cursor = targetEnd + 1;
441
+ continue;
442
+ }
443
+ cursor = targetMarker === "]" ? altEnd + 2 : altEnd + 1;
444
+ }
445
+ }
446
+ function markupCandidateStructure(content) {
447
+ let cursor = 0;
448
+ let count = 0;
449
+ for (;; ) {
450
+ const start = content.indexOf("<", cursor);
451
+ if (start === -1)
452
+ return { safeForRegexScan: true, cardinalityExceeded: false };
453
+ if (content.startsWith("<!--", start)) {
454
+ count += 1;
455
+ if (count > MAX_MARKUP_CANDIDATES) {
456
+ return { safeForRegexScan: false, cardinalityExceeded: true };
457
+ }
458
+ const end2 = content.indexOf("-->", start + 4);
459
+ const nested2 = content.indexOf("<!--", start + 4);
460
+ if (end2 === -1) {
461
+ return nested2 === -1 ? { safeForRegexScan: true, cardinalityExceeded: false } : { safeForRegexScan: false, cardinalityExceeded: false };
462
+ }
463
+ if (nested2 !== -1 && nested2 < end2) {
464
+ return { safeForRegexScan: false, cardinalityExceeded: false };
465
+ }
466
+ cursor = end2 + 3;
467
+ continue;
468
+ }
469
+ let nameStart = start + 1;
470
+ while (content[nameStart] === " " || content[nameStart] === "\t")
471
+ nameStart += 1;
472
+ if (content[nameStart] === "/")
473
+ nameStart += 1;
474
+ while (content[nameStart] === " " || content[nameStart] === "\t")
475
+ nameStart += 1;
476
+ const first = content.charCodeAt(nameStart);
477
+ if (!(first >= 65 && first <= 90 || first >= 97 && first <= 122)) {
478
+ cursor = start + 1;
479
+ continue;
480
+ }
481
+ const schemePrefix = content.slice(nameStart, nameStart + 8).toLowerCase();
482
+ if (schemePrefix.startsWith("http://") || schemePrefix.startsWith("https://")) {
483
+ cursor = start + 1;
484
+ continue;
485
+ }
486
+ count += 1;
487
+ if (count > MAX_MARKUP_CANDIDATES) {
488
+ return { safeForRegexScan: false, cardinalityExceeded: true };
489
+ }
490
+ const end = content.indexOf(">", nameStart + 1);
491
+ const nested = content.indexOf("<", nameStart + 1);
492
+ if (end === -1) {
493
+ return nested === -1 ? { safeForRegexScan: true, cardinalityExceeded: false } : { safeForRegexScan: false, cardinalityExceeded: false };
494
+ }
495
+ if (nested !== -1 && nested < end) {
496
+ return { safeForRegexScan: false, cardinalityExceeded: false };
497
+ }
498
+ cursor = end + 1;
499
+ }
500
+ }
501
+ function scanImageSources(content, requestedMaximum = MAX_IMAGE_SOURCES) {
502
+ const maximum = Number.isSafeInteger(requestedMaximum) ? Math.max(0, Math.min(requestedMaximum, MAX_IMAGE_SOURCES)) : MAX_IMAGE_SOURCES;
503
+ const protectedSpans = [];
504
+ const fenced = protectMarkdownFences(content, protectedSpans);
505
+ if (fenced === null) {
506
+ return { sources: new Set, truncated: true, cardinalityExceeded: true, requiresInertFallback: true };
507
+ }
508
+ const searchable = protectInlineCodeSpans(fenced, protectedSpans);
509
+ if (searchable === null) {
510
+ return { sources: new Set, truncated: true, cardinalityExceeded: true, requiresInertFallback: true };
511
+ }
512
+ const candidateStructure = imageCandidateStructure(searchable);
513
+ if (!candidateStructure.safeForRegexScan) {
514
+ return {
515
+ sources: new Set,
516
+ truncated: true,
517
+ cardinalityExceeded: candidateStructure.cardinalityExceeded,
518
+ requiresInertFallback: true
519
+ };
520
+ }
521
+ const markupStructure = markupCandidateStructure(searchable);
522
+ if (!markupStructure.safeForRegexScan) {
523
+ return {
524
+ sources: new Set,
525
+ truncated: true,
526
+ cardinalityExceeded: markupStructure.cardinalityExceeded,
527
+ requiresInertFallback: true
528
+ };
529
+ }
530
+ const sources = new Set;
531
+ let truncated = false;
532
+ let cardinalityExceeded = false;
533
+ const addSource = (source) => {
534
+ if (source === undefined || source === "")
535
+ return true;
536
+ if (source.length > MAX_REMOTE_SOURCE_CODE_UNITS) {
537
+ truncated = true;
538
+ return true;
539
+ }
540
+ if (sources.has(source))
541
+ return true;
542
+ if (sources.size >= maximum) {
543
+ truncated = true;
544
+ cardinalityExceeded = true;
545
+ return false;
546
+ }
547
+ sources.add(source);
548
+ return true;
549
+ };
550
+ markdownImage.lastIndex = 0;
551
+ try {
552
+ for (;; ) {
553
+ const match = markdownImage.exec(searchable);
554
+ if (match === null)
555
+ break;
556
+ if (!addSource(match[2] ?? match[3]))
557
+ break;
558
+ }
559
+ } finally {
560
+ markdownImage.lastIndex = 0;
561
+ }
562
+ if (!truncated) {
563
+ htmlImage.lastIndex = 0;
564
+ try {
565
+ for (;; ) {
566
+ const match = htmlImage.exec(searchable);
567
+ if (match === null)
568
+ break;
569
+ if (!addSource(match[1]))
570
+ break;
571
+ }
572
+ } finally {
573
+ htmlImage.lastIndex = 0;
574
+ }
575
+ }
576
+ if (!truncated) {
577
+ const labelScan = referenceLabels(searchable);
578
+ const definitionScan = referenceTargets(searchable, labelScan.labels);
579
+ truncated ||= labelScan.truncated || definitionScan.truncated;
580
+ cardinalityExceeded ||= labelScan.cardinalityExceeded;
581
+ for (const label of labelScan.labels) {
582
+ if (!addSource(definitionScan.targets.get(label)))
583
+ break;
584
+ }
585
+ }
586
+ return {
587
+ sources,
588
+ truncated,
589
+ cardinalityExceeded,
590
+ requiresInertFallback: cardinalityExceeded
591
+ };
592
+ }
593
+ var CONTENT_REWRITE_TRUNCATION_WARNING = "Content rewriting reached a safety limit; the final Markdown is truncated, so a complete extraction is reported as partial.";
594
+ function rewriteContentWithStatus(content, base, localBySource, options = {}) {
595
+ const sanitizedContent = sanitizeTerminalText(content);
596
+ const protectedSpans = [];
597
+ const fenced = protectMarkdownFences(sanitizedContent, protectedSpans);
598
+ if (fenced === null)
599
+ return { content: inertProtectedOverflow(sanitizedContent), truncated: true };
600
+ let output = protectInlineCodeSpans(fenced, protectedSpans);
601
+ if (output === null)
602
+ return { content: inertProtectedOverflow(sanitizedContent), truncated: true };
603
+ const imageSafety = scanImageSources(output, options.maxImageSources ?? MAX_IMAGE_SOURCES);
604
+ if (imageSafety.requiresInertFallback) {
605
+ return { content: inertCandidateOverflow(sanitizedContent), truncated: true };
606
+ }
607
+ let truncated = imageSafety.truncated;
608
+ output = output.replace(unsafeMarkdownImage, (_whole, alt) => `*[omitted unsafe image: ${escapeMarkdownLabel(alt) || "image"}]*`);
609
+ output = output.replace(obsidianEmbed, (_whole, target) => `*[omitted local embed: ${escapeMarkdownLabel(target) || "attachment"}]*`);
610
+ const labelScan = referenceLabels(output);
611
+ const definitionScan = referenceTargets(output, labelScan.labels);
612
+ const definitions = definitionScan.targets;
613
+ const referenceScanTruncated = labelScan.truncated || definitionScan.truncated;
614
+ output = output.replace(referenceImage, (_whole, alt, rawLabel) => {
615
+ const label = normalizedReferenceLabel(rawLabel === "" ? alt : rawLabel);
616
+ if (label === null) {
617
+ truncated = true;
618
+ return `*[omitted over-limit image reference: ${escapeMarkdownLabel(alt) || "image"}]*`;
619
+ }
620
+ const source = definitions.get(label);
621
+ if (source === undefined)
622
+ return `*[omitted unresolved image reference: ${escapeMarkdownLabel(alt) || "image"}]*`;
623
+ const local = localBySource.get(source);
624
+ if (local !== undefined)
625
+ return `![${alt}](${local})`;
626
+ const absolute = resolveRemote(source, base);
627
+ if (absolute === null)
628
+ return `*[omitted unsafe image: ${escapeMarkdownLabel(alt) || "image"}]*`;
629
+ const target = inertRemoteImageHref(absolute);
630
+ return options.remoteImages === "embed" ? `![${alt}](${target})` : `[remote image: ${escapeMarkdownLabel(alt) || "image"}](${target})`;
631
+ });
632
+ const localPaths = new Set;
633
+ for (const localPath of localBySource.values()) {
634
+ if (localPaths.size >= MAX_IMAGE_SOURCES)
635
+ break;
636
+ localPaths.add(localPath);
637
+ }
638
+ output = output.replace(markdownImage, (whole, alt, bracketed, bare, title) => {
639
+ const source = bracketed ?? bare ?? "";
640
+ if (source.length > MAX_REMOTE_SOURCE_CODE_UNITS) {
641
+ truncated = true;
642
+ return `*[omitted over-limit image: ${escapeMarkdownLabel(alt) || "image"}]*`;
643
+ }
644
+ const local = localBySource.get(source);
645
+ if (local !== undefined)
646
+ return `![${alt}](${local}${title})`;
647
+ if (localPaths.has(source))
648
+ return whole;
649
+ const absolute = resolveRemote(source, base);
650
+ return absolute === null ? `*[omitted unsafe image: ${escapeMarkdownLabel(alt) || "image"}]*` : options.remoteImages === "embed" ? `![${alt}](${inertRemoteImageHref(absolute)}${title})` : `[remote image: ${escapeMarkdownLabel(alt) || "image"}](${inertRemoteImageHref(absolute)}${title})`;
651
+ });
652
+ output = output.replace(htmlImage, (_whole, source) => {
653
+ if (source.length > MAX_REMOTE_SOURCE_CODE_UNITS) {
654
+ truncated = true;
655
+ return "*[omitted over-limit image]*";
656
+ }
657
+ const local = localBySource.get(source);
658
+ if (local !== undefined)
659
+ return `![](${local})`;
660
+ if (localPaths.has(source))
661
+ return `![](${source})`;
662
+ const absolute = resolveRemote(source, base);
663
+ return absolute === null ? "*[omitted unsafe image]*" : options.remoteImages === "embed" ? `![](${inertRemoteImageHref(absolute)})` : `[remote image](${inertRemoteImageHref(absolute)})`;
664
+ });
665
+ output = output.replace(plainLink, (whole, open, bracketed, bare, title, close) => {
666
+ const target = bracketed ?? bare ?? "";
667
+ if (target.length > MAX_REMOTE_SOURCE_CODE_UNITS) {
668
+ truncated = true;
669
+ return `${open}#${title}${close}`;
670
+ }
671
+ if (/^(https?:|mailto:|#)/i.test(target) || localPaths.has(target))
672
+ return whole;
673
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target))
674
+ return `${open}#${title}${close}`;
675
+ const absolute = resolveRemote(target, base);
676
+ return absolute === null ? `${open}#${title}${close}` : `${open}${absolute.href.replace(/\(/g, "%28").replace(/\)/g, "%29")}${title}${close}`;
677
+ });
678
+ output = output.replace(referenceDefinition, (_whole, prefix, label, bracketed, bare, title) => {
679
+ const target = bracketed ?? bare ?? "";
680
+ if (label.length > MAX_REFERENCE_LABEL_CODE_UNITS || target.length > MAX_REMOTE_SOURCE_CODE_UNITS) {
681
+ truncated = true;
682
+ return `${prefix}#${title}`;
683
+ }
684
+ if (/^(?:https?:|mailto:|#)/i.test(target) || localPaths.has(target)) {
685
+ return `${prefix}${bracketed === undefined ? target : `<${target}>`}${title}`;
686
+ }
687
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target))
688
+ return `${prefix}#${title}`;
689
+ const absolute = resolveRemote(target, base);
690
+ return `${prefix}${absolute === null ? "#" : absolute.href}${title}`;
691
+ });
692
+ output = sanitizeMarkdownHtml(output);
693
+ if (referenceScanTruncated) {
694
+ truncated = true;
695
+ output = `*[Some image references were omitted because capture safety limits were exceeded.]*
696
+
697
+ ` + output;
698
+ }
699
+ return { content: restoreMarkdownSpans(output, protectedSpans), truncated };
700
+ }
701
+ function rewriteContent(content, base, localBySource, options = {}) {
702
+ return rewriteContentWithStatus(content, base, localBySource, options).content;
703
+ }
704
+ function buildClipMarkdown(article, options) {
705
+ const title = boundedMetadata(article.title, articleMetadataLimits.title);
706
+ const author = boundedMetadata(article.author, articleMetadataLimits.author);
707
+ const published = boundedMetadata(article.published, articleMetadataLimits.published);
708
+ const description = boundedMetadata(article.description, articleMetadataLimits.description);
709
+ const frontmatter = [
710
+ "---",
711
+ `title: ${yamlString(title ?? options.slug)}`,
712
+ `source: ${yamlString(options.sourceHref)}`,
713
+ ...author === null ? [] : [`author: ${yamlString(author)}`],
714
+ ...published === null ? [] : [`published: ${yamlString(published)}`],
715
+ ...description === null ? [] : [`description: ${yamlString(description)}`],
716
+ `clipped: ${yamlString(options.clipped)}`,
717
+ ...options.platform === undefined ? [] : [`platform: ${yamlString(options.platform)}`],
718
+ ...options.captureStatus === undefined ? [] : [`capture_status: ${yamlString(options.captureStatus)}`],
719
+ ...options.captureMethod === undefined ? [] : [`capture_method: ${yamlString(options.captureMethod)}`],
720
+ ...options.captureScope === undefined ? [] : [`capture_scope: ${yamlString(options.captureScope)}`],
721
+ "---",
722
+ ""
723
+ ].join(`
724
+ `);
725
+ const headingTitle = title === null ? null : escapeMarkdownLabel(title).replace(/\s+/g, " ").trim();
726
+ const heading = headingTitle === null || headingTitle === "" ? "" : `# ${headingTitle}
727
+
728
+ `;
729
+ return sanitizeTerminalText(frontmatter + heading + options.content.trimEnd() + `
730
+ `);
731
+ }
732
+
733
+ // src/clip/platforms.ts
734
+ var DEFAULT_CAPTURE_LIMITS = {
735
+ maxDepth: 24,
736
+ maxItems: 1000,
737
+ maxTextLength: 1e5,
738
+ maxMediaPerEntry: 32
739
+ };
740
+ var HARD_CAPTURE_LIMITS = {
741
+ maxDepth: 64,
742
+ maxItems: 1e4,
743
+ maxTextLength: 1e6,
744
+ maxMediaPerEntry: 128
745
+ };
746
+ var isUnknownArray = (value) => Array.isArray(value);
747
+ var isRecord = (value) => typeof value === "object" && value !== null && !isUnknownArray(value);
748
+ var nonEmptyString = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : null;
749
+ var stringValue = (value) => typeof value === "string" ? value : null;
750
+ var booleanValue = (value) => typeof value === "boolean" ? value : null;
751
+ var safeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
752
+ var signedSafeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : null;
753
+ var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
754
+ var foreignId = (value) => {
755
+ const text = nonEmptyString(value);
756
+ if (text !== null)
757
+ return text;
758
+ const number = safeInteger(value);
759
+ return number === null ? null : String(number);
760
+ };
761
+ var readRecord = (record, key) => {
762
+ const value = record[key];
763
+ return isRecord(value) ? value : null;
764
+ };
765
+ var readArray = (record, key) => {
766
+ const value = record[key];
767
+ return isUnknownArray(value) ? value : null;
768
+ };
769
+ var clampLimit = (value, fallback, ceiling) => {
770
+ if (value === undefined || !Number.isFinite(value))
771
+ return fallback;
772
+ return Math.max(1, Math.min(Math.floor(value), ceiling));
773
+ };
774
+ var createContext = (options) => ({
775
+ limits: {
776
+ maxDepth: clampLimit(options?.limits?.maxDepth, DEFAULT_CAPTURE_LIMITS.maxDepth, HARD_CAPTURE_LIMITS.maxDepth),
777
+ maxItems: clampLimit(options?.limits?.maxItems, DEFAULT_CAPTURE_LIMITS.maxItems, HARD_CAPTURE_LIMITS.maxItems),
778
+ maxTextLength: clampLimit(options?.limits?.maxTextLength, DEFAULT_CAPTURE_LIMITS.maxTextLength, HARD_CAPTURE_LIMITS.maxTextLength),
779
+ maxMediaPerEntry: clampLimit(options?.limits?.maxMediaPerEntry, DEFAULT_CAPTURE_LIMITS.maxMediaPerEntry, HARD_CAPTURE_LIMITS.maxMediaPerEntry)
780
+ },
781
+ usedItems: 0,
782
+ warnings: []
783
+ });
784
+ var warn = (context, message) => {
785
+ if (context.warnings.length < 100 && !context.warnings.includes(message)) {
786
+ context.warnings.push(message);
787
+ }
788
+ };
789
+ var reserveItem = (context) => {
790
+ if (context.usedItems >= context.limits.maxItems) {
791
+ warn(context, `Capture stopped at ${context.limits.maxItems} items.`);
792
+ return false;
793
+ }
794
+ context.usedItems += 1;
795
+ return true;
796
+ };
797
+ var boundary = (reason, detail) => ({
798
+ kind: "boundary",
799
+ reason,
800
+ detail
801
+ });
802
+ var emptyMetrics = () => ({
803
+ score: null,
804
+ replies: null,
805
+ likes: null,
806
+ reposts: null,
807
+ quotes: null
808
+ });
809
+ var httpUrl = (value, base) => {
810
+ const text = nonEmptyString(value);
811
+ if (text === null)
812
+ return null;
813
+ try {
814
+ const url = base === undefined ? new URL(text) : new URL(text, base);
815
+ return url.protocol === "http:" || url.protocol === "https:" ? url.href : null;
816
+ } catch {
817
+ return null;
818
+ }
819
+ };
820
+ var normalizedSource = (sourceUrl) => httpUrl(sourceUrl);
821
+ var boundedText = (value, context, label) => {
822
+ if (value.length <= context.limits.maxTextLength)
823
+ return value;
824
+ warn(context, `${label} was truncated to ${context.limits.maxTextLength} characters.`);
825
+ return `${value.slice(0, context.limits.maxTextLength)}
826
+
827
+ [Text truncated.]`;
828
+ };
829
+ var boundedTitle = (value, context, label) => {
830
+ const limit = Math.min(context.limits.maxTextLength, 512);
831
+ if (value.length <= limit)
832
+ return value;
833
+ warn(context, `${label} was truncated to ${limit} characters.`);
834
+ return `${value.slice(0, Math.max(1, limit - 1))}\u2026`;
835
+ };
836
+ var isoTimestamp = (value) => {
837
+ const text = nonEmptyString(value);
838
+ if (text === null)
839
+ return null;
840
+ const milliseconds = Date.parse(text);
841
+ return Number.isFinite(milliseconds) ? new Date(milliseconds).toISOString() : null;
842
+ };
843
+ var epochTimestamp = (value) => {
844
+ const seconds = finiteNumber(value);
845
+ if (seconds === null || seconds > 253402300799)
846
+ return null;
847
+ return new Date(seconds * 1000).toISOString();
848
+ };
849
+ var cleanPathSegments = (url) => {
850
+ const segments = [];
851
+ for (const rawSegment of url.pathname.split("/")) {
852
+ if (rawSegment === "")
853
+ continue;
854
+ try {
855
+ segments.push(decodeURIComponent(rawSegment));
856
+ } catch {
857
+ segments.push(rawSegment);
858
+ }
859
+ }
860
+ return segments;
861
+ };
862
+ var domainMatches = (hostname, domain) => hostname === domain || hostname.endsWith(`.${domain}`);
863
+ var canonicalWithoutFragment = (url) => {
864
+ const canonical = new URL(url.href);
865
+ canonical.hash = "";
866
+ return canonical.href;
867
+ };
868
+ function classifyPlatformUrl(value) {
869
+ let url;
870
+ try {
871
+ url = new URL(typeof value === "string" ? value : value.href);
872
+ } catch {
873
+ return null;
874
+ }
875
+ if (url.protocol !== "http:" && url.protocol !== "https:")
876
+ return null;
877
+ const hostname = url.hostname.toLowerCase();
878
+ const segments = cleanPathSegments(url);
879
+ if (domainMatches(hostname, "x.com") || domainMatches(hostname, "twitter.com")) {
880
+ const handle = segments[0];
881
+ const status = segments[1];
882
+ const postId = segments[2];
883
+ if (handle !== undefined && status === "status" && postId !== undefined && /^[a-zA-Z0-9_]{1,32}$/.test(handle) && /^\d+$/.test(postId)) {
884
+ return {
885
+ platform: "x",
886
+ href: `https://x.com/${handle}/status/${postId}`,
887
+ handle,
888
+ postId
889
+ };
890
+ }
891
+ }
892
+ if (hostname === "news.ycombinator.com" && url.pathname === "/item") {
893
+ const itemId = url.searchParams.get("id");
894
+ if (itemId !== null && /^\d+$/.test(itemId)) {
895
+ return {
896
+ platform: "hacker-news",
897
+ href: `https://news.ycombinator.com/item?id=${itemId}`,
898
+ itemId
899
+ };
900
+ }
901
+ }
902
+ if (domainMatches(hostname, "reddit.com")) {
903
+ const commentsIndex = segments.indexOf("comments");
904
+ const postId = commentsIndex >= 0 ? segments[commentsIndex + 1] : undefined;
905
+ if (postId !== undefined && /^[a-zA-Z0-9]+$/.test(postId)) {
906
+ const subreddit = commentsIndex >= 2 && segments[0] === "r" ? segments[1] ?? null : null;
907
+ const possibleComment = segments[commentsIndex + 3];
908
+ const commentId = possibleComment !== undefined && /^[a-zA-Z0-9]+$/.test(possibleComment) ? possibleComment : null;
909
+ return {
910
+ platform: "reddit",
911
+ href: canonicalWithoutFragment(url),
912
+ postId,
913
+ subreddit,
914
+ commentId
915
+ };
916
+ }
917
+ }
918
+ if (hostname === "redd.it") {
919
+ const postId = segments[0];
920
+ if (postId !== undefined && /^[a-zA-Z0-9]+$/.test(postId)) {
921
+ return {
922
+ platform: "reddit",
923
+ href: canonicalWithoutFragment(url),
924
+ postId,
925
+ subreddit: null,
926
+ commentId: null
927
+ };
928
+ }
929
+ }
930
+ if (hostname === "bsky.app" && segments[0] === "profile" && segments[2] === "post") {
931
+ const actor = segments[1];
932
+ const postId = segments[3];
933
+ if (actor !== undefined && actor !== "" && postId !== undefined && postId !== "") {
934
+ return {
935
+ platform: "bluesky",
936
+ href: `https://bsky.app/profile/${encodeURIComponent(actor)}/post/${encodeURIComponent(postId)}`,
937
+ actor,
938
+ postId
939
+ };
940
+ }
941
+ }
942
+ if (hostname === "substack.com" || domainMatches(hostname, "substack.com")) {
943
+ const publication = hostname === "substack.com" ? null : hostname.slice(0, -".substack.com".length);
944
+ return { platform: "substack", href: canonicalWithoutFragment(url), publication };
945
+ }
946
+ if (domainMatches(hostname, "instagram.com")) {
947
+ const contentId = ["p", "reel", "tv"].includes(segments[0] ?? "") ? segments[1] ?? null : null;
948
+ return { platform: "instagram", href: canonicalWithoutFragment(url), contentId };
949
+ }
950
+ if (domainMatches(hostname, "linkedin.com")) {
951
+ const contentId = segments.find((segment) => /(?:activity|ugcPost|share)[:-]?\d+/.test(segment)) ?? segments[1] ?? null;
952
+ return { platform: "linkedin", href: canonicalWithoutFragment(url), contentId };
953
+ }
954
+ if (domainMatches(hostname, "facebook.com") || hostname === "fb.com" || hostname === "fb.watch") {
955
+ const contentId = url.searchParams.get("story_fbid") ?? url.searchParams.get("v") ?? segments.at(-1) ?? null;
956
+ return { platform: "facebook", href: canonicalWithoutFragment(url), contentId };
957
+ }
958
+ if (domainMatches(hostname, "tiktok.com")) {
959
+ const videoIndex = segments.indexOf("video");
960
+ const contentId = videoIndex >= 0 ? segments[videoIndex + 1] ?? null : segments[0] ?? null;
961
+ return { platform: "tiktok", href: canonicalWithoutFragment(url), contentId };
962
+ }
963
+ if (domainMatches(hostname, "threads.com") || domainMatches(hostname, "threads.net")) {
964
+ const postIndex = segments.indexOf("post");
965
+ const contentId = postIndex >= 0 ? segments[postIndex + 1] ?? null : null;
966
+ return { platform: "threads", href: canonicalWithoutFragment(url), contentId };
967
+ }
968
+ if (hostname === "web.whatsapp.com") {
969
+ return { platform: "whatsapp", href: canonicalWithoutFragment(url), contentId: null };
970
+ }
971
+ if (domainMatches(hostname, "youtube.com") || hostname === "youtu.be") {
972
+ const contentId = hostname === "youtu.be" ? segments[0] ?? null : url.searchParams.get("v") ?? (segments[0] === "shorts" || segments[0] === "live" ? segments[1] ?? null : null);
973
+ return { platform: "youtube", href: canonicalWithoutFragment(url), contentId };
974
+ }
975
+ if (hostname === "github.com") {
976
+ const owner = segments[0];
977
+ const repository = segments[1];
978
+ const route = segments[2];
979
+ const contentId = segments[3];
980
+ const contentKind = route === "issues" ? "issue" : route === "pull" ? "pull-request" : route === "discussions" ? "discussion" : null;
981
+ if (owner !== undefined && repository !== undefined && contentKind !== null && contentId !== undefined && /^[A-Za-z0-9_.-]+$/.test(owner) && /^[A-Za-z0-9_.-]+$/.test(repository) && /^\d+$/.test(contentId)) {
982
+ return {
983
+ platform: "github",
984
+ href: canonicalWithoutFragment(url),
985
+ owner,
986
+ repository,
987
+ contentKind,
988
+ contentId
989
+ };
990
+ }
991
+ }
992
+ const discourseHostHint = hostname.split(".").some((label) => label === "discourse" || label === "discuss" || label === "forum" || label === "community");
993
+ if (discourseHostHint && segments[0] === "t") {
994
+ const topicId = /^\d+$/.test(segments[1] ?? "") ? segments[1] : /^\d+$/.test(segments[2] ?? "") ? segments[2] : undefined;
995
+ if (topicId !== undefined) {
996
+ return { platform: "discourse", href: canonicalWithoutFragment(url), topicId };
997
+ }
998
+ }
999
+ return { platform: "generic", href: canonicalWithoutFragment(url), host: hostname };
1000
+ }
1001
+ var invalidSource = () => ({
1002
+ ok: false,
1003
+ error: { code: "invalid-source", message: "The capture source must be an HTTP(S) URL." }
1004
+ });
1005
+ var invalidShape = (message) => ({
1006
+ ok: false,
1007
+ error: { code: "invalid-shape", message }
1008
+ });
1009
+ var firstLine = (text, maxLength) => {
1010
+ const line = text.split(/\r?\n/, 1)[0]?.trim() ?? "";
1011
+ return line.length <= maxLength ? line : `${line.slice(0, Math.max(1, maxLength - 1))}\u2026`;
1012
+ };
1013
+ var decodeHtmlEntities = (value) => value.replace(/&(?:#(\d+)|#x([0-9a-f]+)|([a-z]+));/gi, (whole, decimal, hexadecimal, named) => {
1014
+ if (typeof decimal === "string") {
1015
+ const point = Number.parseInt(decimal, 10);
1016
+ return Number.isSafeInteger(point) && point <= 1114111 ? String.fromCodePoint(point) : whole;
1017
+ }
1018
+ if (typeof hexadecimal === "string") {
1019
+ const point = Number.parseInt(hexadecimal, 16);
1020
+ return Number.isSafeInteger(point) && point <= 1114111 ? String.fromCodePoint(point) : whole;
1021
+ }
1022
+ if (typeof named !== "string")
1023
+ return whole;
1024
+ const entities = {
1025
+ amp: "&",
1026
+ apos: "'",
1027
+ gt: ">",
1028
+ lt: "<",
1029
+ nbsp: " ",
1030
+ quot: '"'
1031
+ };
1032
+ return entities[named.toLowerCase()] ?? whole;
1033
+ });
1034
+ var parseHtmlTag = (raw) => {
1035
+ let cursor = 0;
1036
+ let closing = false;
1037
+ if (raw.charCodeAt(cursor) === 47) {
1038
+ closing = true;
1039
+ cursor += 1;
1040
+ }
1041
+ const start = cursor;
1042
+ while (cursor < raw.length) {
1043
+ const code = raw.charCodeAt(cursor);
1044
+ const alphaNumeric = code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
1045
+ if (!alphaNumeric)
1046
+ break;
1047
+ cursor += 1;
1048
+ }
1049
+ if (cursor === start)
1050
+ return null;
1051
+ return { closing, name: raw.slice(start, cursor).toLowerCase(), raw };
1052
+ };
1053
+ var stripHtmlTagsLinear = (html) => {
1054
+ const chunks = [];
1055
+ let cursor = 0;
1056
+ while (cursor < html.length) {
1057
+ const opening = html.indexOf("<", cursor);
1058
+ if (opening < 0) {
1059
+ chunks.push(html.slice(cursor));
1060
+ break;
1061
+ }
1062
+ if (opening > cursor)
1063
+ chunks.push(html.slice(cursor, opening));
1064
+ const closing = html.indexOf(">", opening + 1);
1065
+ if (closing < 0) {
1066
+ chunks.push(html.slice(opening));
1067
+ break;
1068
+ }
1069
+ cursor = closing + 1;
1070
+ }
1071
+ return chunks.join("");
1072
+ };
1073
+ var quotedHref = (tag) => {
1074
+ let cursor = tag.name.length;
1075
+ while (cursor < tag.raw.length) {
1076
+ while (cursor < tag.raw.length && /\s/.test(tag.raw[cursor] ?? ""))
1077
+ cursor += 1;
1078
+ if (tag.raw[cursor] === "/") {
1079
+ cursor += 1;
1080
+ continue;
1081
+ }
1082
+ const nameStart = cursor;
1083
+ while (cursor < tag.raw.length) {
1084
+ const character = tag.raw[cursor] ?? "";
1085
+ if (/\s/.test(character) || character === "=" || character === "/")
1086
+ break;
1087
+ cursor += 1;
1088
+ }
1089
+ if (cursor === nameStart) {
1090
+ cursor += 1;
1091
+ continue;
1092
+ }
1093
+ const name = tag.raw.slice(nameStart, cursor).toLowerCase();
1094
+ while (cursor < tag.raw.length && /\s/.test(tag.raw[cursor] ?? ""))
1095
+ cursor += 1;
1096
+ if (tag.raw[cursor] !== "=")
1097
+ continue;
1098
+ cursor += 1;
1099
+ while (cursor < tag.raw.length && /\s/.test(tag.raw[cursor] ?? ""))
1100
+ cursor += 1;
1101
+ const quote = tag.raw[cursor];
1102
+ if (quote !== '"' && quote !== "'")
1103
+ continue;
1104
+ cursor += 1;
1105
+ const valueStart = cursor;
1106
+ while (cursor < tag.raw.length && tag.raw[cursor] !== quote)
1107
+ cursor += 1;
1108
+ if (cursor >= tag.raw.length)
1109
+ return null;
1110
+ if (name === "href")
1111
+ return tag.raw.slice(valueStart, cursor);
1112
+ cursor += 1;
1113
+ }
1114
+ return null;
1115
+ };
1116
+ var isWhitespace = (value) => /\s/.test(value);
1117
+ var isPlainBreakTag = (tag) => {
1118
+ if (tag.closing || tag.name !== "br")
1119
+ return false;
1120
+ let cursor = tag.name.length;
1121
+ while (cursor < tag.raw.length && isWhitespace(tag.raw[cursor] ?? ""))
1122
+ cursor += 1;
1123
+ if (tag.raw[cursor] === "/")
1124
+ cursor += 1;
1125
+ while (cursor < tag.raw.length && isWhitespace(tag.raw[cursor] ?? ""))
1126
+ cursor += 1;
1127
+ return cursor === tag.raw.length;
1128
+ };
1129
+ var isExactFormattingTag = (tag, name) => tag.name === name && tag.raw.length === name.length + (tag.closing ? 1 : 0);
1130
+ var htmlToMarkdown = (html) => {
1131
+ const chunks = [];
1132
+ const lower = html.toLowerCase();
1133
+ let nextAnchorClosing = lower.indexOf("</a>");
1134
+ let cursor = 0;
1135
+ while (cursor < html.length) {
1136
+ const opening = html.indexOf("<", cursor);
1137
+ if (opening < 0) {
1138
+ chunks.push(html.slice(cursor));
1139
+ break;
1140
+ }
1141
+ if (opening > cursor)
1142
+ chunks.push(html.slice(cursor, opening));
1143
+ const closing = html.indexOf(">", opening + 1);
1144
+ if (closing < 0) {
1145
+ chunks.push(html.slice(opening));
1146
+ break;
1147
+ }
1148
+ const tag = parseHtmlTag(html.slice(opening + 1, closing));
1149
+ if (tag === null) {
1150
+ cursor = closing + 1;
1151
+ continue;
1152
+ }
1153
+ if (!tag.closing && tag.name === "a") {
1154
+ const target = quotedHref(tag);
1155
+ while (nextAnchorClosing >= 0 && nextAnchorClosing < closing + 1) {
1156
+ nextAnchorClosing = lower.indexOf("</a>", nextAnchorClosing + 4);
1157
+ }
1158
+ if (target !== null && nextAnchorClosing >= 0) {
1159
+ const label = html.slice(closing + 1, nextAnchorClosing);
1160
+ const cleanLabel = decodeHtmlEntities(stripHtmlTagsLinear(label)).trim();
1161
+ const url = httpUrl(decodeHtmlEntities(target), "https://news.ycombinator.com/");
1162
+ chunks.push(url === null ? cleanLabel : `[${cleanLabel || url}](<${url}>)`);
1163
+ cursor = nextAnchorClosing + 4;
1164
+ nextAnchorClosing = lower.indexOf("</a>", cursor);
1165
+ continue;
1166
+ }
1167
+ }
1168
+ const nextOpening = closing + 1;
1169
+ if (isExactFormattingTag(tag, "pre") && !tag.closing && lower.startsWith("<code>", nextOpening)) {
1170
+ chunks.push("\n\n```\n");
1171
+ cursor = nextOpening + 6;
1172
+ continue;
1173
+ }
1174
+ if (isExactFormattingTag(tag, "code") && tag.closing && lower.startsWith("</pre>", nextOpening)) {
1175
+ chunks.push("\n```\n\n");
1176
+ cursor = nextOpening + 6;
1177
+ continue;
1178
+ }
1179
+ if (!tag.closing && tag.name === "p")
1180
+ chunks.push(`
1181
+
1182
+ `);
1183
+ else if (isPlainBreakTag(tag))
1184
+ chunks.push(`
1185
+ `);
1186
+ else if (isExactFormattingTag(tag, "i") || isExactFormattingTag(tag, "em"))
1187
+ chunks.push("*");
1188
+ else if (isExactFormattingTag(tag, "b") || isExactFormattingTag(tag, "strong"))
1189
+ chunks.push("**");
1190
+ cursor = closing + 1;
1191
+ }
1192
+ return decodeHtmlEntities(chunks.join("")).replace(/\n{3,}/g, `
1193
+
1194
+ `).trim();
1195
+ };
1196
+ var hackerNewsEnvelope = (input) => {
1197
+ if (isUnknownArray(input)) {
1198
+ const [root2, ...descendants2] = input;
1199
+ return root2 === undefined ? null : { root: root2, descendants: descendants2 };
1200
+ }
1201
+ if (!isRecord(input))
1202
+ return null;
1203
+ const root = input.root ?? input.rootItem;
1204
+ const descendants = readArray(input, "descendants") ?? readArray(input, "items") ?? [];
1205
+ return root === undefined ? null : { root, descendants };
1206
+ };
1207
+ var hackerNewsKids = (record, context) => {
1208
+ const kids = readArray(record, "kids");
1209
+ if (kids === null)
1210
+ return [];
1211
+ const result = [];
1212
+ const limit = Math.min(kids.length, context.limits.maxItems);
1213
+ for (let index = 0;index < limit; index += 1) {
1214
+ const id = foreignId(kids[index]);
1215
+ if (id !== null)
1216
+ result.push(id);
1217
+ }
1218
+ if (kids.length > limit)
1219
+ warn(context, `Hacker News child IDs were truncated to ${limit}.`);
1220
+ return result;
1221
+ };
1222
+ function parseHackerNewsCapture(input, sourceUrl, options) {
1223
+ const source = normalizedSource(sourceUrl);
1224
+ if (source === null)
1225
+ return invalidSource();
1226
+ const envelope = hackerNewsEnvelope(input);
1227
+ if (envelope === null || !isRecord(envelope.root)) {
1228
+ return invalidShape("Hacker News input must provide a root item and descendant items.");
1229
+ }
1230
+ const rootId = foreignId(envelope.root.id);
1231
+ if (rootId === null)
1232
+ return invalidShape("The Hacker News root item has no valid id.");
1233
+ const context = createContext(options);
1234
+ const byId = new Map;
1235
+ byId.set(rootId, envelope.root);
1236
+ const scanLimit = Math.min(envelope.descendants.length, context.limits.maxItems - 1);
1237
+ for (let index = 0;index < scanLimit; index += 1) {
1238
+ const value = envelope.descendants[index];
1239
+ if (!isRecord(value)) {
1240
+ warn(context, `Malformed Hacker News descendant at index ${index} was skipped.`);
1241
+ continue;
1242
+ }
1243
+ const id = foreignId(value.id);
1244
+ if (id === null) {
1245
+ warn(context, `Hacker News descendant at index ${index} has no id.`);
1246
+ continue;
1247
+ }
1248
+ if (byId.has(id))
1249
+ warn(context, `Duplicate Hacker News item ${id} was skipped.`);
1250
+ else
1251
+ byId.set(id, value);
1252
+ }
1253
+ if (envelope.descendants.length > scanLimit) {
1254
+ warn(context, `Hacker News descendants were truncated to ${scanLimit}.`);
1255
+ }
1256
+ const buildItem = (id, role, path, depth) => {
1257
+ if (depth >= context.limits.maxDepth) {
1258
+ return boundary("depth-limit", `Hacker News nesting exceeded ${context.limits.maxDepth}.`);
1259
+ }
1260
+ if (path.has(id)) {
1261
+ warn(context, "A cycle in Hacker News child IDs was stopped.");
1262
+ return boundary("cycle", `Hacker News item ${id} repeats in its ancestry.`);
1263
+ }
1264
+ if (!reserveItem(context))
1265
+ return boundary("item-limit", "The Hacker News item limit was reached.");
1266
+ const record = byId.get(id);
1267
+ if (record === undefined) {
1268
+ return {
1269
+ kind: "unavailable",
1270
+ role,
1271
+ id,
1272
+ reason: "not-found",
1273
+ sourceUrl: `https://news.ycombinator.com/item?id=${encodeURIComponent(id)}`,
1274
+ replies: []
1275
+ };
1276
+ }
1277
+ const nextPath = new Set(path);
1278
+ nextPath.add(id);
1279
+ const replies = [];
1280
+ for (const childId of hackerNewsKids(record, context)) {
1281
+ if (context.usedItems >= context.limits.maxItems) {
1282
+ replies.push(boundary("item-limit", "Additional Hacker News descendants were omitted."));
1283
+ break;
1284
+ }
1285
+ replies.push(buildItem(childId, "comment", nextPath, depth + 1));
1286
+ }
1287
+ const sourceForItem = `https://news.ycombinator.com/item?id=${encodeURIComponent(id)}`;
1288
+ if (booleanValue(record.deleted) === true || booleanValue(record.dead) === true) {
1289
+ return {
1290
+ kind: "unavailable",
1291
+ role,
1292
+ id,
1293
+ reason: booleanValue(record.deleted) === true ? "deleted" : "dead",
1294
+ sourceUrl: sourceForItem,
1295
+ replies
1296
+ };
1297
+ }
1298
+ const authorHandle = nonEmptyString(record.by);
1299
+ const author = authorHandle === null ? null : {
1300
+ name: authorHandle,
1301
+ handle: authorHandle,
1302
+ profileUrl: `https://news.ycombinator.com/user?id=${encodeURIComponent(authorHandle)}`
1303
+ };
1304
+ const rawText = stringValue(record.text) ?? "";
1305
+ const body = htmlToMarkdown(boundedText(rawText, context, `Hacker News item ${id}`));
1306
+ const external = httpUrl(record.url, "https://news.ycombinator.com/");
1307
+ const text = external === null ? body : `${body}${body === "" ? "" : `
1308
+
1309
+ `}[Linked article](<${external}>)`;
1310
+ return {
1311
+ kind: "content",
1312
+ role,
1313
+ id,
1314
+ author,
1315
+ createdAt: epochTimestamp(record.time),
1316
+ sourceUrl: sourceForItem,
1317
+ text,
1318
+ media: [],
1319
+ metrics: {
1320
+ ...emptyMetrics(),
1321
+ score: safeInteger(record.score),
1322
+ replies: safeInteger(record.descendants) ?? (replies.length === 0 ? null : replies.length)
1323
+ },
1324
+ quotes: [],
1325
+ replies
1326
+ };
1327
+ };
1328
+ const root = buildItem(rootId, "post", new Set, 0);
1329
+ const title = boundedTitle(nonEmptyString(envelope.root.title) ?? `Hacker News item ${rootId}`, context, "Hacker News title");
1330
+ return {
1331
+ ok: true,
1332
+ document: {
1333
+ platform: "hacker-news",
1334
+ sourceUrl: source,
1335
+ title,
1336
+ ancestors: [],
1337
+ roots: [root],
1338
+ warnings: context.warnings
1339
+ }
1340
+ };
1341
+ }
1342
+ var redditListingChildren = (value) => {
1343
+ if (!isRecord(value))
1344
+ return null;
1345
+ if (value.kind === "Listing") {
1346
+ const data2 = readRecord(value, "data");
1347
+ return data2 === null ? null : readArray(data2, "children");
1348
+ }
1349
+ const data = readRecord(value, "data");
1350
+ return data === null ? null : readArray(data, "children");
1351
+ };
1352
+ var redditEnvelope = (input) => {
1353
+ if (isUnknownArray(input)) {
1354
+ const post = input[0];
1355
+ if (post === undefined)
1356
+ return null;
1357
+ return { post, comments: input[1] ?? null };
1358
+ }
1359
+ if (!isRecord(input) || input.post === undefined)
1360
+ return null;
1361
+ return { post: input.post, comments: input.comments ?? null };
1362
+ };
1363
+ var redditPostData = (value, maxItems) => {
1364
+ if (isRecord(value) && value.kind === "t3")
1365
+ return readRecord(value, "data");
1366
+ const children = redditListingChildren(value);
1367
+ if (children === null)
1368
+ return null;
1369
+ const limit = Math.min(children.length, maxItems);
1370
+ for (let index = 0;index < limit; index += 1) {
1371
+ const child = children[index];
1372
+ if (isRecord(child) && child.kind === "t3")
1373
+ return readRecord(child, "data");
1374
+ }
1375
+ return null;
1376
+ };
1377
+ var redditAuthor = (value) => {
1378
+ const handle = nonEmptyString(value);
1379
+ if (handle === null || handle === "[deleted]")
1380
+ return null;
1381
+ return {
1382
+ name: handle,
1383
+ handle,
1384
+ profileUrl: `https://www.reddit.com/user/${encodeURIComponent(handle)}`
1385
+ };
1386
+ };
1387
+ var redditPermalink = (value) => httpUrl(value, "https://www.reddit.com/");
1388
+ function parseRedditCapture(input, sourceUrl, options) {
1389
+ const source = normalizedSource(sourceUrl);
1390
+ if (source === null)
1391
+ return invalidSource();
1392
+ const envelope = redditEnvelope(input);
1393
+ if (envelope === null)
1394
+ return invalidShape("Reddit input must contain a post listing.");
1395
+ const context = createContext(options);
1396
+ const post = redditPostData(envelope.post, context.limits.maxItems);
1397
+ if (post === null)
1398
+ return invalidShape("Reddit input contained no valid post object.");
1399
+ const postId = foreignId(post.id) ?? foreignId(post.name);
1400
+ const rawTitle = nonEmptyString(post.title);
1401
+ if (postId === null || rawTitle === null)
1402
+ return invalidShape("The Reddit post has no valid id or title.");
1403
+ const title = boundedTitle(rawTitle, context, "Reddit title");
1404
+ if (!reserveItem(context))
1405
+ return invalidShape("The capture item limit cannot hold the Reddit post.");
1406
+ const active = new WeakSet;
1407
+ const parseThing = (value, depth) => {
1408
+ if (!isRecord(value))
1409
+ return null;
1410
+ if (depth >= context.limits.maxDepth) {
1411
+ return boundary("depth-limit", `Reddit nesting exceeded ${context.limits.maxDepth}.`);
1412
+ }
1413
+ if (active.has(value)) {
1414
+ warn(context, "A cycle in Reddit replies was stopped.");
1415
+ return boundary("cycle", "A Reddit reply object repeats in its ancestry.");
1416
+ }
1417
+ const kind = nonEmptyString(value.kind);
1418
+ const data = readRecord(value, "data");
1419
+ if (kind === null || data === null)
1420
+ return null;
1421
+ if (!reserveItem(context))
1422
+ return boundary("item-limit", "Additional Reddit comments were omitted.");
1423
+ active.add(value);
1424
+ if (kind === "more") {
1425
+ const childValues = readArray(data, "children") ?? [];
1426
+ const childIds = [];
1427
+ const limit2 = Math.min(childValues.length, context.limits.maxItems);
1428
+ for (let index = 0;index < limit2; index += 1) {
1429
+ const id2 = foreignId(childValues[index]);
1430
+ if (id2 !== null)
1431
+ childIds.push(id2);
1432
+ }
1433
+ active.delete(value);
1434
+ return {
1435
+ kind: "more",
1436
+ id: foreignId(data.id) ?? "more",
1437
+ count: safeInteger(data.count),
1438
+ childIds
1439
+ };
1440
+ }
1441
+ if (kind !== "t1") {
1442
+ active.delete(value);
1443
+ warn(context, `Unsupported Reddit thing kind ${kind} was skipped.`);
1444
+ return null;
1445
+ }
1446
+ const id = foreignId(data.id) ?? foreignId(data.name) ?? "unknown-comment";
1447
+ const replyValues = redditListingChildren(data.replies) ?? [];
1448
+ const replies2 = [];
1449
+ const limit = Math.min(replyValues.length, context.limits.maxItems);
1450
+ for (let index = 0;index < limit; index += 1) {
1451
+ if (context.usedItems >= context.limits.maxItems) {
1452
+ warn(context, `Capture stopped at ${context.limits.maxItems} items.`);
1453
+ replies2.push(boundary("item-limit", "Additional Reddit replies were omitted."));
1454
+ break;
1455
+ }
1456
+ const reply = parseThing(replyValues[index], depth + 1);
1457
+ if (reply !== null)
1458
+ replies2.push(reply);
1459
+ }
1460
+ const itemSource = redditPermalink(data.permalink);
1461
+ const body2 = stringValue(data.body) ?? "";
1462
+ active.delete(value);
1463
+ if (body2.trim() === "[deleted]" || body2.trim() === "[removed]") {
1464
+ return {
1465
+ kind: "unavailable",
1466
+ role: "comment",
1467
+ id,
1468
+ reason: body2.trim() === "[deleted]" ? "deleted" : "removed",
1469
+ sourceUrl: itemSource,
1470
+ replies: replies2
1471
+ };
1472
+ }
1473
+ return {
1474
+ kind: "content",
1475
+ role: "comment",
1476
+ id,
1477
+ author: redditAuthor(data.author),
1478
+ createdAt: epochTimestamp(data.created_utc),
1479
+ sourceUrl: itemSource,
1480
+ text: boundedText(body2, context, `Reddit comment ${id}`),
1481
+ media: [],
1482
+ metrics: { ...emptyMetrics(), score: signedSafeInteger(data.score), replies: replies2.length || null },
1483
+ quotes: [],
1484
+ replies: replies2
1485
+ };
1486
+ };
1487
+ const commentValues = redditListingChildren(envelope.comments) ?? [];
1488
+ const replies = [];
1489
+ const commentLimit = Math.min(commentValues.length, context.limits.maxItems);
1490
+ for (let index = 0;index < commentLimit; index += 1) {
1491
+ if (context.usedItems >= context.limits.maxItems) {
1492
+ warn(context, `Capture stopped at ${context.limits.maxItems} items.`);
1493
+ replies.push(boundary("item-limit", "Additional Reddit comments were omitted."));
1494
+ break;
1495
+ }
1496
+ const reply = parseThing(commentValues[index], 1);
1497
+ if (reply !== null)
1498
+ replies.push(reply);
1499
+ }
1500
+ if (commentValues.length > commentLimit)
1501
+ warn(context, `Reddit comments were truncated to ${commentLimit}.`);
1502
+ const permalink = redditPermalink(post.permalink);
1503
+ const selfText = stringValue(post.selftext) ?? "";
1504
+ const linkedUrl = httpUrl(post.url);
1505
+ const linkText = linkedUrl === null || linkedUrl === permalink ? "" : `[Linked page](<${linkedUrl}>)`;
1506
+ const body = `${boundedText(selfText, context, `Reddit post ${postId}`)}${selfText.trim() === "" || linkText === "" ? "" : `
1507
+
1508
+ `}${linkText}`;
1509
+ const root = {
1510
+ kind: "content",
1511
+ role: "post",
1512
+ id: postId,
1513
+ author: redditAuthor(post.author),
1514
+ createdAt: epochTimestamp(post.created_utc),
1515
+ sourceUrl: permalink ?? source,
1516
+ text: body,
1517
+ media: [],
1518
+ metrics: {
1519
+ ...emptyMetrics(),
1520
+ score: signedSafeInteger(post.score),
1521
+ replies: safeInteger(post.num_comments) ?? replies.length
1522
+ },
1523
+ quotes: [],
1524
+ replies
1525
+ };
1526
+ return {
1527
+ ok: true,
1528
+ document: {
1529
+ platform: "reddit",
1530
+ sourceUrl: source,
1531
+ title,
1532
+ ancestors: [],
1533
+ roots: [root],
1534
+ warnings: context.warnings
1535
+ }
1536
+ };
1537
+ }
1538
+ var bskyAuthor = (value) => {
1539
+ if (!isRecord(value))
1540
+ return null;
1541
+ const handle = nonEmptyString(value.handle);
1542
+ const did = nonEmptyString(value.did);
1543
+ if (handle === null && did === null)
1544
+ return null;
1545
+ const actor = handle ?? did ?? "unknown";
1546
+ return {
1547
+ name: nonEmptyString(value.displayName) ?? actor,
1548
+ handle,
1549
+ profileUrl: `https://bsky.app/profile/${encodeURIComponent(actor)}`
1550
+ };
1551
+ };
1552
+ var bskyRkey = (uri) => {
1553
+ const segments = uri.split("/").filter((segment) => segment !== "");
1554
+ return segments.at(-1) ?? null;
1555
+ };
1556
+ var bskyPostUrl = (uri, author) => {
1557
+ if (uri === null || author === null)
1558
+ return null;
1559
+ const rkey = bskyRkey(uri);
1560
+ const actor = author.handle;
1561
+ return rkey === null || actor === null ? null : `https://bsky.app/profile/${encodeURIComponent(actor)}/post/${encodeURIComponent(rkey)}`;
1562
+ };
1563
+ var bskyMedia = (values, context) => {
1564
+ const media = [];
1565
+ const seen = new Set;
1566
+ const active = new WeakSet;
1567
+ const add = (item) => {
1568
+ const key = `${item.kind}:${item.url}`;
1569
+ if (seen.has(key) || media.length >= context.limits.maxMediaPerEntry)
1570
+ return;
1571
+ seen.add(key);
1572
+ media.push(item);
1573
+ };
1574
+ const visit = (value, depth) => {
1575
+ if (!isRecord(value) || depth > Math.min(8, context.limits.maxDepth) || active.has(value))
1576
+ return;
1577
+ active.add(value);
1578
+ const images = readArray(value, "images");
1579
+ if (images !== null) {
1580
+ const limit = Math.min(images.length, context.limits.maxMediaPerEntry);
1581
+ for (let index = 0;index < limit; index += 1) {
1582
+ const image = images[index];
1583
+ if (!isRecord(image))
1584
+ continue;
1585
+ const url = httpUrl(image.fullsize) ?? httpUrl(image.thumb);
1586
+ if (url === null)
1587
+ continue;
1588
+ const ratio = readRecord(image, "aspectRatio");
1589
+ const width = ratio === null ? null : safeInteger(ratio.width);
1590
+ const height = ratio === null ? null : safeInteger(ratio.height);
1591
+ add({
1592
+ kind: "image",
1593
+ url,
1594
+ previewUrl: httpUrl(image.thumb),
1595
+ alt: nonEmptyString(image.alt),
1596
+ title: null,
1597
+ dimensions: width === null || height === null ? null : { width, height }
1598
+ });
1599
+ }
1600
+ }
1601
+ const playlist = httpUrl(value.playlist);
1602
+ if (playlist !== null) {
1603
+ add({
1604
+ kind: "video",
1605
+ url: playlist,
1606
+ previewUrl: httpUrl(value.thumbnail),
1607
+ alt: nonEmptyString(value.alt),
1608
+ title: null,
1609
+ dimensions: null
1610
+ });
1611
+ }
1612
+ const external = readRecord(value, "external");
1613
+ if (external !== null) {
1614
+ const url = httpUrl(external.uri);
1615
+ if (url !== null) {
1616
+ add({
1617
+ kind: "link",
1618
+ url,
1619
+ previewUrl: httpUrl(external.thumb),
1620
+ alt: nonEmptyString(external.description),
1621
+ title: nonEmptyString(external.title),
1622
+ dimensions: null
1623
+ });
1624
+ }
1625
+ }
1626
+ if (value.media !== undefined)
1627
+ visit(value.media, depth + 1);
1628
+ const embeds = readArray(value, "embeds");
1629
+ if (embeds !== null) {
1630
+ const limit = Math.min(embeds.length, context.limits.maxMediaPerEntry);
1631
+ for (let index = 0;index < limit; index += 1)
1632
+ visit(embeds[index], depth + 1);
1633
+ }
1634
+ active.delete(value);
1635
+ };
1636
+ for (const value of values)
1637
+ visit(value, 0);
1638
+ if (media.length >= context.limits.maxMediaPerEntry) {
1639
+ warn(context, `Bluesky media was truncated to ${context.limits.maxMediaPerEntry} items on one entry.`);
1640
+ }
1641
+ return media;
1642
+ };
1643
+ var bskyQuoteRecord = (embed) => {
1644
+ if (!isRecord(embed))
1645
+ return null;
1646
+ const type = nonEmptyString(embed.$type) ?? "";
1647
+ if (type.includes("recordWithMedia")) {
1648
+ const outerRecord = readRecord(embed, "record");
1649
+ return outerRecord?.record ?? null;
1650
+ }
1651
+ if (type.includes("record#view") || type.includes("recordWithMedia#view"))
1652
+ return embed.record ?? null;
1653
+ return null;
1654
+ };
1655
+ function parseBskyQuote(value, context, depth, active) {
1656
+ if (!isRecord(value))
1657
+ return null;
1658
+ if (depth >= context.limits.maxDepth) {
1659
+ return boundary("depth-limit", `Bluesky quote nesting exceeded ${context.limits.maxDepth}.`);
1660
+ }
1661
+ if (active.has(value)) {
1662
+ warn(context, "A cycle in Bluesky quoted records was stopped.");
1663
+ return boundary("cycle", "A Bluesky quoted record repeats in its ancestry.");
1664
+ }
1665
+ const uri = nonEmptyString(value.uri) ?? "unknown-quote";
1666
+ if (booleanValue(value.notFound) === true || booleanValue(value.blocked) === true) {
1667
+ if (!reserveItem(context))
1668
+ return boundary("item-limit", "Additional Bluesky quotes were omitted.");
1669
+ return {
1670
+ kind: "unavailable",
1671
+ role: "quote",
1672
+ id: uri,
1673
+ reason: booleanValue(value.blocked) === true ? "blocked" : "not-found",
1674
+ sourceUrl: null,
1675
+ replies: []
1676
+ };
1677
+ }
1678
+ const author = bskyAuthor(value.author);
1679
+ const record = readRecord(value, "value") ?? readRecord(value, "record");
1680
+ if (record === null || !reserveItem(context))
1681
+ return null;
1682
+ active.add(value);
1683
+ const nestedEmbed = value.embeds ?? record.embed;
1684
+ const nestedQuoteValue = bskyQuoteRecord(nestedEmbed);
1685
+ const quotes = [];
1686
+ if (nestedQuoteValue !== null) {
1687
+ const nested = parseBskyQuote(nestedQuoteValue, context, depth + 1, active);
1688
+ if (nested !== null)
1689
+ quotes.push(nested);
1690
+ }
1691
+ active.delete(value);
1692
+ return {
1693
+ kind: "content",
1694
+ role: "quote",
1695
+ id: uri,
1696
+ author,
1697
+ createdAt: isoTimestamp(record.createdAt),
1698
+ sourceUrl: bskyPostUrl(uri, author),
1699
+ text: boundedText(stringValue(record.text) ?? "", context, `Bluesky quote ${uri}`),
1700
+ media: bskyMedia([value, record], context),
1701
+ metrics: emptyMetrics(),
1702
+ quotes,
1703
+ replies: []
1704
+ };
1705
+ }
1706
+ function parseBskyThreadNode(value, role, includeReplies, context, depth, active) {
1707
+ if (!isRecord(value))
1708
+ return null;
1709
+ if (depth >= context.limits.maxDepth) {
1710
+ return boundary("depth-limit", `Bluesky nesting exceeded ${context.limits.maxDepth}.`);
1711
+ }
1712
+ if (active.has(value)) {
1713
+ warn(context, "A cycle in Bluesky thread objects was stopped.");
1714
+ return boundary("cycle", "A Bluesky thread object repeats in its ancestry.");
1715
+ }
1716
+ const post = readRecord(value, "post");
1717
+ const fallbackUri = nonEmptyString(value.uri) ?? "unknown-post";
1718
+ const type = nonEmptyString(value.$type) ?? "";
1719
+ if (booleanValue(value.notFound) === true || booleanValue(value.blocked) === true || type.includes("notFoundPost") || type.includes("blockedPost")) {
1720
+ if (!reserveItem(context))
1721
+ return boundary("item-limit", "Additional Bluesky entries were omitted.");
1722
+ return {
1723
+ kind: "unavailable",
1724
+ role,
1725
+ id: fallbackUri,
1726
+ reason: booleanValue(value.blocked) === true || type.includes("blockedPost") ? "blocked" : "not-found",
1727
+ sourceUrl: null,
1728
+ replies: []
1729
+ };
1730
+ }
1731
+ if (post === null || !reserveItem(context))
1732
+ return null;
1733
+ const uri = nonEmptyString(post.uri) ?? fallbackUri;
1734
+ const author = bskyAuthor(post.author);
1735
+ const record = readRecord(post, "record");
1736
+ if (record === null)
1737
+ return null;
1738
+ active.add(value);
1739
+ const embedValues = [];
1740
+ if (post.embed !== undefined)
1741
+ embedValues.push(post.embed);
1742
+ if (record.embed !== undefined)
1743
+ embedValues.push(record.embed);
1744
+ const quotes = [];
1745
+ for (const embed of embedValues) {
1746
+ const quoteValue = bskyQuoteRecord(embed);
1747
+ if (quoteValue === null)
1748
+ continue;
1749
+ const quote = parseBskyQuote(quoteValue, context, depth + 1, active);
1750
+ if (quote !== null)
1751
+ quotes.push(quote);
1752
+ }
1753
+ const replies = [];
1754
+ const replyValues = includeReplies ? readArray(value, "replies") ?? [] : [];
1755
+ const limit = Math.min(replyValues.length, context.limits.maxItems);
1756
+ for (let index = 0;index < limit; index += 1) {
1757
+ if (context.usedItems >= context.limits.maxItems) {
1758
+ warn(context, `Capture stopped at ${context.limits.maxItems} items.`);
1759
+ replies.push(boundary("item-limit", "Additional Bluesky replies were omitted."));
1760
+ break;
1761
+ }
1762
+ const reply = parseBskyThreadNode(replyValues[index], "comment", true, context, depth + 1, active);
1763
+ if (reply !== null)
1764
+ replies.push(reply);
1765
+ }
1766
+ active.delete(value);
1767
+ return {
1768
+ kind: "content",
1769
+ role,
1770
+ id: uri,
1771
+ author,
1772
+ createdAt: isoTimestamp(record.createdAt) ?? isoTimestamp(post.indexedAt),
1773
+ sourceUrl: bskyPostUrl(uri, author),
1774
+ text: boundedText(stringValue(record.text) ?? "", context, `Bluesky post ${uri}`),
1775
+ media: bskyMedia(embedValues, context),
1776
+ metrics: {
1777
+ score: null,
1778
+ replies: safeInteger(post.replyCount),
1779
+ likes: safeInteger(post.likeCount),
1780
+ reposts: safeInteger(post.repostCount),
1781
+ quotes: safeInteger(post.quoteCount)
1782
+ },
1783
+ quotes,
1784
+ replies
1785
+ };
1786
+ }
1787
+ function parseBlueskyCapture(input, sourceUrl, options) {
1788
+ const source = normalizedSource(sourceUrl);
1789
+ if (source === null)
1790
+ return invalidSource();
1791
+ if (!isRecord(input))
1792
+ return invalidShape("Bluesky output must be an object containing a thread.");
1793
+ const thread = input.thread ?? input;
1794
+ if (!isRecord(thread))
1795
+ return invalidShape("Bluesky output contained no thread root.");
1796
+ const context = createContext(options);
1797
+ const root = parseBskyThreadNode(thread, "post", true, context, 0, new WeakSet);
1798
+ if (root === null)
1799
+ return invalidShape("Bluesky output contained no valid post at the thread root.");
1800
+ const ancestorsNearestFirst = [];
1801
+ const seenParents = new WeakSet;
1802
+ let parent = thread.parent;
1803
+ for (let depth = 1;parent !== undefined && parent !== null; depth += 1) {
1804
+ if (depth >= context.limits.maxDepth) {
1805
+ ancestorsNearestFirst.push(boundary("depth-limit", "Additional Bluesky parent context was omitted."));
1806
+ break;
1807
+ }
1808
+ if (!isRecord(parent))
1809
+ break;
1810
+ if (seenParents.has(parent)) {
1811
+ ancestorsNearestFirst.push(boundary("cycle", "A Bluesky parent object repeats in its ancestry."));
1812
+ warn(context, "A cycle in Bluesky parent context was stopped.");
1813
+ break;
1814
+ }
1815
+ seenParents.add(parent);
1816
+ const parsed = parseBskyThreadNode(parent, "post", false, context, depth, new WeakSet);
1817
+ if (parsed !== null)
1818
+ ancestorsNearestFirst.push(parsed);
1819
+ parent = parent.parent;
1820
+ }
1821
+ ancestorsNearestFirst.reverse();
1822
+ const titleAuthor = root.kind === "content" ? root.author : null;
1823
+ const titleText = root.kind === "content" ? firstLine(root.text, 96) : "";
1824
+ const title = titleText || `${titleAuthor?.name ?? "Unknown author"} on Bluesky`;
1825
+ return {
1826
+ ok: true,
1827
+ document: {
1828
+ platform: "bluesky",
1829
+ sourceUrl: source,
1830
+ title,
1831
+ ancestors: ancestorsNearestFirst,
1832
+ roots: [root],
1833
+ warnings: context.warnings
1834
+ }
1835
+ };
1836
+ }
1837
+ var platformLabel = (platform) => {
1838
+ switch (platform) {
1839
+ case "x":
1840
+ return "X";
1841
+ case "hacker-news":
1842
+ return "Hacker News";
1843
+ case "reddit":
1844
+ return "Reddit";
1845
+ case "bluesky":
1846
+ return "Bluesky";
1847
+ }
1848
+ };
1849
+ var escapeInline = (value) => value.replace(/\\/g, "\\\\").replace(/([*_[\]`])/g, "\\$1").replace(/\s+/g, " ").trim();
1850
+ var cleanHeading = (value) => value.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
1851
+ var authorLabel = (author) => {
1852
+ if (author === null)
1853
+ return "Unknown author";
1854
+ const name = escapeInline(author.name);
1855
+ if (author.handle === null || author.handle === author.name)
1856
+ return name;
1857
+ return `${name} (@${escapeInline(author.handle)})`;
1858
+ };
1859
+ var metadata = (entry) => {
1860
+ const pieces = [`**${authorLabel(entry.author)}**`];
1861
+ if (entry.createdAt !== null)
1862
+ pieces.push(entry.createdAt);
1863
+ if (entry.sourceUrl !== null)
1864
+ pieces.push(`[source](<${entry.sourceUrl}>)`);
1865
+ return pieces.join(" \xB7 ");
1866
+ };
1867
+ var metricLabel = (value, singular, plural = `${singular}s`) => `${value} ${value === 1 ? singular : plural}`;
1868
+ var metricsLine = (metrics) => {
1869
+ const pieces = [];
1870
+ if (metrics.score !== null)
1871
+ pieces.push(metricLabel(metrics.score, "point"));
1872
+ if (metrics.replies !== null)
1873
+ pieces.push(metricLabel(metrics.replies, "reply", "replies"));
1874
+ if (metrics.likes !== null)
1875
+ pieces.push(metricLabel(metrics.likes, "like"));
1876
+ if (metrics.reposts !== null)
1877
+ pieces.push(metricLabel(metrics.reposts, "repost"));
1878
+ if (metrics.quotes !== null)
1879
+ pieces.push(metricLabel(metrics.quotes, "quote"));
1880
+ return pieces.length === 0 ? null : pieces.join(" \xB7 ");
1881
+ };
1882
+ var indentLines = (lines, prefix) => lines.map((line) => line === "" ? prefix.trimEnd() : `${prefix}${line}`);
1883
+ var mediaLines = (media) => {
1884
+ const lines = [];
1885
+ for (const item of media) {
1886
+ const fallback = item.kind === "gif" ? "GIF" : `${item.kind[0]?.toUpperCase() ?? ""}${item.kind.slice(1)}`;
1887
+ const label = escapeInline(item.title ?? item.alt ?? fallback);
1888
+ if (item.kind === "image" || item.kind === "gif") {
1889
+ lines.push(`![${label}](<${item.url}>)`);
1890
+ } else {
1891
+ if (item.previewUrl !== null)
1892
+ lines.push(`![${label} preview](<${item.previewUrl}>)`);
1893
+ lines.push(`- [${label}](<${item.url}>)`);
1894
+ }
1895
+ }
1896
+ return lines;
1897
+ };
1898
+ var unavailableLabel = (entry) => {
1899
+ const noun = entry.role === "comment" ? "comment" : entry.role === "quote" ? "quoted post" : "post";
1900
+ return `${entry.reason} ${noun} ${escapeInline(entry.id)}`;
1901
+ };
1902
+ var renderQuote = (entry, depth, state) => {
1903
+ const lines = renderRootEntry(entry, depth + 1, state);
1904
+ return indentLines(lines, "> ");
1905
+ };
1906
+ var renderReplies = (entries, depth, state) => {
1907
+ const lines = [];
1908
+ for (const entry of entries)
1909
+ lines.push(...renderNestedEntry(entry, depth, state));
1910
+ return lines;
1911
+ };
1912
+ function renderNestedEntry(entry, depth, state) {
1913
+ const prefix = " ".repeat(depth);
1914
+ if (depth >= 64 || state.count >= 20000)
1915
+ return [`${prefix}- *[render limit reached]*`];
1916
+ if (state.active.has(entry))
1917
+ return [`${prefix}- *[cycle omitted]*`];
1918
+ state.count += 1;
1919
+ state.active.add(entry);
1920
+ let lines;
1921
+ switch (entry.kind) {
1922
+ case "boundary":
1923
+ lines = [`${prefix}- *[${escapeInline(entry.detail)}]*`];
1924
+ break;
1925
+ case "more": {
1926
+ const count = entry.count === null ? "More comments" : `${entry.count} more comments`;
1927
+ const ids = entry.childIds.length === 0 ? "" : ` (${entry.childIds.map(escapeInline).join(", ")})`;
1928
+ lines = [`${prefix}- *${count}${ids}*`];
1929
+ break;
1930
+ }
1931
+ case "unavailable": {
1932
+ const source = entry.sourceUrl === null ? "" : ` \xB7 [source](<${entry.sourceUrl}>)`;
1933
+ lines = [`${prefix}- *[${unavailableLabel(entry)}]*${source}`];
1934
+ lines.push(...renderReplies(entry.replies, depth + 1, state));
1935
+ break;
1936
+ }
1937
+ case "content": {
1938
+ lines = [`${prefix}- ${metadata(entry)}`];
1939
+ if (entry.text.trim() !== "") {
1940
+ lines.push(`${prefix} `, ...indentLines(entry.text.trim().split(`
1941
+ `), `${prefix} `));
1942
+ }
1943
+ const metricText = metricsLine(entry.metrics);
1944
+ if (metricText !== null)
1945
+ lines.push(`${prefix} `, `${prefix} _${metricText}_`);
1946
+ if (entry.media.length > 0) {
1947
+ lines.push(`${prefix} `, ...indentLines(mediaLines(entry.media), `${prefix} `));
1948
+ }
1949
+ for (const quote of entry.quotes) {
1950
+ lines.push(`${prefix} `, ...indentLines(renderQuote(quote, depth, state), `${prefix} `));
1951
+ }
1952
+ lines.push(...renderReplies(entry.replies, depth + 1, state));
1953
+ break;
1954
+ }
1955
+ }
1956
+ state.active.delete(entry);
1957
+ return lines;
1958
+ }
1959
+ function renderRootEntry(entry, depth, state) {
1960
+ if (depth >= 64 || state.count >= 20000)
1961
+ return ["*[render limit reached]*"];
1962
+ if (state.active.has(entry))
1963
+ return ["*[cycle omitted]*"];
1964
+ state.count += 1;
1965
+ state.active.add(entry);
1966
+ let lines;
1967
+ switch (entry.kind) {
1968
+ case "boundary":
1969
+ lines = [`*[${escapeInline(entry.detail)}]*`];
1970
+ break;
1971
+ case "more": {
1972
+ const count = entry.count === null ? "More comments" : `${entry.count} more comments`;
1973
+ lines = [`*${count}*`];
1974
+ break;
1975
+ }
1976
+ case "unavailable": {
1977
+ const source = entry.sourceUrl === null ? "" : ` \xB7 [source](<${entry.sourceUrl}>)`;
1978
+ lines = [`*[${unavailableLabel(entry)}]*${source}`];
1979
+ if (entry.replies.length > 0)
1980
+ lines.push("", "#### Replies", "", ...renderReplies(entry.replies, 0, state));
1981
+ break;
1982
+ }
1983
+ case "content": {
1984
+ lines = [metadata(entry)];
1985
+ if (entry.text.trim() !== "")
1986
+ lines.push("", entry.text.trim());
1987
+ const metricText = metricsLine(entry.metrics);
1988
+ if (metricText !== null)
1989
+ lines.push("", `_${metricText}_`);
1990
+ if (entry.media.length > 0)
1991
+ lines.push("", ...mediaLines(entry.media));
1992
+ if (entry.quotes.length > 0) {
1993
+ lines.push("", "#### Quoted posts", "");
1994
+ for (const quote of entry.quotes)
1995
+ lines.push(...renderQuote(quote, depth, state), "");
1996
+ if (lines.at(-1) === "")
1997
+ lines.pop();
1998
+ }
1999
+ if (entry.replies.length > 0)
2000
+ lines.push("", "#### Replies", "", ...renderReplies(entry.replies, 0, state));
2001
+ break;
2002
+ }
2003
+ }
2004
+ state.active.delete(entry);
2005
+ return lines;
2006
+ }
2007
+ function renderCapturedDocument(document) {
2008
+ const lines = [
2009
+ `# ${cleanHeading(document.title) || "Captured post"}`,
2010
+ "",
2011
+ `Source: [${document.sourceUrl}](<${document.sourceUrl}>)`,
2012
+ `Platform: ${platformLabel(document.platform)}`
2013
+ ];
2014
+ const state = { count: 0, active: new WeakSet };
2015
+ if (document.ancestors.length > 0) {
2016
+ lines.push("", "## Parent context", "");
2017
+ for (let index = 0;index < document.ancestors.length; index += 1) {
2018
+ lines.push(`### Parent ${index + 1}`, "", ...renderRootEntry(document.ancestors[index] ?? boundary("cycle", "Missing parent."), 0, state), "");
2019
+ }
2020
+ if (lines.at(-1) === "")
2021
+ lines.pop();
2022
+ }
2023
+ lines.push("", document.roots.length === 1 ? "## Post" : "## Posts", "");
2024
+ for (let index = 0;index < document.roots.length; index += 1) {
2025
+ if (document.roots.length > 1)
2026
+ lines.push(`### Post ${index + 1}`, "");
2027
+ const root = document.roots[index];
2028
+ if (root !== undefined)
2029
+ lines.push(...renderRootEntry(root, 0, state));
2030
+ if (index < document.roots.length - 1)
2031
+ lines.push("");
2032
+ }
2033
+ if (document.warnings.length > 0) {
2034
+ lines.push("", "## Capture notes", "");
2035
+ for (const warning of document.warnings)
2036
+ lines.push(`- ${warning}`);
2037
+ }
2038
+ const markdown = `${lines.join(`
2039
+ `).replace(/\n{3,}/g, `
2040
+
2041
+ `).trimEnd()}
2042
+ `;
2043
+ return rewriteContent(markdown, new URL(document.sourceUrl), new Map, { remoteImages: "embed" });
2044
+ }
2045
+
2046
+ export { articleMetadataLimits, slugify, yamlString, resolveRemote, scanImageSources, CONTENT_REWRITE_TRUNCATION_WARNING, rewriteContentWithStatus, buildClipMarkdown, classifyPlatformUrl, parseHackerNewsCapture, parseRedditCapture, parseBlueskyCapture, renderCapturedDocument };