@juspay/neurolink 12.30.1 → 12.31.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.
@@ -0,0 +1,442 @@
1
+ /**
2
+ * HTML to Markdown conversion for document processors.
3
+ *
4
+ * Scoped deliberately to the small, well-formed HTML subset that document
5
+ * converters emit (mammoth for DOCX): headings, paragraphs, emphasis, links,
6
+ * images, lists, block quotes, preformatted text and tables. It is not a
7
+ * general-purpose web-page converter, which is why the repository gains no
8
+ * dependency for it.
9
+ *
10
+ * @module utils/htmlToMarkdown
11
+ */
12
+ /** Tags that never have a closing tag and so never open a scope. */
13
+ const VOID_TAGS = new Set([
14
+ "area",
15
+ "base",
16
+ "br",
17
+ "col",
18
+ "embed",
19
+ "hr",
20
+ "img",
21
+ "input",
22
+ "link",
23
+ "meta",
24
+ "source",
25
+ "track",
26
+ "wbr",
27
+ ]);
28
+ const NAMED_ENTITIES = {
29
+ amp: "&",
30
+ lt: "<",
31
+ gt: ">",
32
+ quot: '"',
33
+ apos: "'",
34
+ nbsp: " ",
35
+ };
36
+ /**
37
+ * Decode the numeric and named character references a document converter can
38
+ * emit. Unknown references are left verbatim rather than dropped, so no text
39
+ * is silently lost.
40
+ */
41
+ function decodeEntities(value) {
42
+ return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, body) => {
43
+ if (body.startsWith("#")) {
44
+ const codePoint = body.startsWith("#x") || body.startsWith("#X")
45
+ ? Number.parseInt(body.slice(2), 16)
46
+ : Number.parseInt(body.slice(1), 10);
47
+ if (Number.isNaN(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {
48
+ return match;
49
+ }
50
+ try {
51
+ return String.fromCodePoint(codePoint);
52
+ }
53
+ catch {
54
+ return match;
55
+ }
56
+ }
57
+ const named = NAMED_ENTITIES[body.toLowerCase()];
58
+ return named ?? match;
59
+ });
60
+ }
61
+ /**
62
+ * Parse the HTML subset into a node tree.
63
+ *
64
+ * Unbalanced closing tags are ignored and unclosed elements are closed at the
65
+ * end of input, so malformed markup degrades to partial structure instead of
66
+ * throwing.
67
+ */
68
+ function parseHtml(html) {
69
+ const root = {
70
+ kind: "element",
71
+ tag: "#root",
72
+ attrs: {},
73
+ children: [],
74
+ };
75
+ const stack = [root];
76
+ const tagPattern = /<\/?([a-zA-Z][a-zA-Z0-9-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g;
77
+ let cursor = 0;
78
+ let match;
79
+ const pushText = (raw) => {
80
+ if (!raw) {
81
+ return;
82
+ }
83
+ const current = stack[stack.length - 1];
84
+ current.children.push({ kind: "text", value: decodeEntities(raw) });
85
+ };
86
+ while ((match = tagPattern.exec(html)) !== null) {
87
+ pushText(html.slice(cursor, match.index));
88
+ cursor = match.index + match[0].length;
89
+ const tag = match[1].toLowerCase();
90
+ const isClosing = match[0].startsWith("</");
91
+ const isSelfClosing = match[2].trimEnd().endsWith("/");
92
+ if (isClosing) {
93
+ // Only unwind if the tag is actually open, so a stray `</div>` is
94
+ // inert. Search from the top of the stack down so a closing tag
95
+ // matches the innermost (most recently opened) element with that
96
+ // name — `findIndex` would match the OUTERMOST one instead, and for
97
+ // nested same-tag elements (`<div><div>A</div>B</div>`) that pops both
98
+ // levels at once, reattaching everything after the real inner close to
99
+ // the wrong ancestor.
100
+ let openIndex = -1;
101
+ for (let i = stack.length - 1; i >= 0; i--) {
102
+ if (stack[i].tag === tag) {
103
+ openIndex = i;
104
+ break;
105
+ }
106
+ }
107
+ if (openIndex > 0) {
108
+ stack.length = openIndex;
109
+ }
110
+ continue;
111
+ }
112
+ const element = {
113
+ kind: "element",
114
+ tag,
115
+ attrs: parseAttributes(match[2]),
116
+ children: [],
117
+ };
118
+ stack[stack.length - 1].children.push(element);
119
+ if (!isSelfClosing && !VOID_TAGS.has(tag)) {
120
+ stack.push(element);
121
+ }
122
+ }
123
+ pushText(html.slice(cursor));
124
+ return root.children;
125
+ }
126
+ function parseAttributes(raw) {
127
+ const attrs = {};
128
+ const pattern = /([a-zA-Z_:][a-zA-Z0-9_.:-]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/g;
129
+ let match;
130
+ while ((match = pattern.exec(raw)) !== null) {
131
+ const value = match[3] ?? match[4] ?? match[5] ?? "";
132
+ attrs[match[1].toLowerCase()] = decodeEntities(value);
133
+ }
134
+ return attrs;
135
+ }
136
+ function isElement(node) {
137
+ return node.kind === "element";
138
+ }
139
+ /** Collapse runs of whitespace the way an HTML renderer would. */
140
+ function collapseWhitespace(value) {
141
+ return value
142
+ .replace(/\s+/g, " ")
143
+ .replace(/&/g, "&amp;")
144
+ .replace(/</g, "&lt;")
145
+ .replace(/>/g, "&gt;");
146
+ }
147
+ /**
148
+ * Format a link/image destination per CommonMark's angle-bracket form.
149
+ *
150
+ * A bare `(dest)` destination ends at the first whitespace or unbalanced
151
+ * `)`, so a value containing either — an `href`/`src` with a space, or one
152
+ * with unbalanced parentheses — would silently truncate or corrupt the
153
+ * surrounding `[text](dest)` / `![alt](dest)` markup. Wrapping in `<...>`
154
+ * makes the destination unambiguous; CommonMark requires any literal `<`
155
+ * or `>` inside that form to be escaped. Ordinary destinations (no
156
+ * whitespace or parens) are returned byte-identical to before.
157
+ */
158
+ function formatLinkDestination(dest) {
159
+ if (!/[\s()]/.test(dest)) {
160
+ return dest;
161
+ }
162
+ // Backslash first: escaping only `<`/`>` turns `\>` into `\\>`, which
163
+ // CommonMark reads as a literal backslash followed by a bare `>`.
164
+ const escaped = dest
165
+ .replace(/\\/g, "\\\\")
166
+ .replace(/</g, "\\<")
167
+ .replace(/>/g, "\\>");
168
+ return `<${escaped}>`;
169
+ }
170
+ /** Render inline content (emphasis, links, images, line breaks) to markdown. */
171
+ function renderInline(nodes) {
172
+ let out = "";
173
+ for (const node of nodes) {
174
+ if (node.kind === "text") {
175
+ out += collapseWhitespace(node.value);
176
+ continue;
177
+ }
178
+ switch (node.tag) {
179
+ case "script":
180
+ case "style":
181
+ break;
182
+ case "strong":
183
+ case "b": {
184
+ const inner = renderInline(node.children).trim();
185
+ out += inner ? `**${inner}**` : "";
186
+ break;
187
+ }
188
+ case "em":
189
+ case "i": {
190
+ const inner = renderInline(node.children).trim();
191
+ out += inner ? `*${inner}*` : "";
192
+ break;
193
+ }
194
+ case "code": {
195
+ const inner = renderInline(node.children).trim();
196
+ out += inner ? `\`${inner}\`` : "";
197
+ break;
198
+ }
199
+ case "a": {
200
+ const inner = renderInline(node.children).trim();
201
+ const href = node.attrs.href;
202
+ out += href ? `[${inner}](${formatLinkDestination(href)})` : inner;
203
+ break;
204
+ }
205
+ case "img": {
206
+ const alt = node.attrs.alt ?? "";
207
+ const src = node.attrs.src ?? "";
208
+ // A DOCX image is inlined as a multi-megabyte data URI; keep the
209
+ // alt text and drop the payload rather than flooding the prompt.
210
+ out += src.startsWith("data:")
211
+ ? `![${alt}](embedded-image)`
212
+ : `![${alt}](${formatLinkDestination(src)})`;
213
+ break;
214
+ }
215
+ case "br":
216
+ out += "\n";
217
+ break;
218
+ default:
219
+ out += renderInline(node.children);
220
+ break;
221
+ }
222
+ }
223
+ return out;
224
+ }
225
+ /**
226
+ * Extract a `<pre>` block's text content verbatim.
227
+ *
228
+ * `renderInline()` funnels every text node through `collapseWhitespace()`,
229
+ * which does `.replace(/\s+/g, " ")` — right for prose, destructive for
230
+ * code: it collapses every newline, tab and indentation run onto one line.
231
+ * This walks the same tree but keeps each text node's raw value, so a
232
+ * multi-line, indented code sample survives into the fenced block intact.
233
+ * A nested element (e.g. `<pre><code>…</code></pre>`) still contributes its
234
+ * text, but ignores inline markdown semantics like `**`/`*`/`` ` `` —
235
+ * `<pre>` content is literal code, not prose, and injecting markdown markers
236
+ * into it would corrupt the code rather than format it.
237
+ */
238
+ function renderPreformattedText(nodes) {
239
+ let out = "";
240
+ for (const node of nodes) {
241
+ if (node.kind === "text") {
242
+ out += node.value;
243
+ continue;
244
+ }
245
+ if (node.tag === "script" || node.tag === "style") {
246
+ continue;
247
+ }
248
+ if (node.tag === "br") {
249
+ out += "\n";
250
+ continue;
251
+ }
252
+ out += renderPreformattedText(node.children);
253
+ }
254
+ return out;
255
+ }
256
+ /** Collect the `<tr>` rows under a table, seeing through thead/tbody/tfoot. */
257
+ function collectRows(node) {
258
+ const rows = [];
259
+ for (const child of node.children.filter(isElement)) {
260
+ if (child.tag === "tr") {
261
+ rows.push(child);
262
+ }
263
+ else if (["thead", "tbody", "tfoot"].includes(child.tag)) {
264
+ rows.push(...collectRows(child));
265
+ }
266
+ }
267
+ return rows;
268
+ }
269
+ /** A pipe inside a cell would otherwise split it into two columns. */
270
+ function escapeCell(value) {
271
+ return value
272
+ .replace(/\\/g, "\\\\")
273
+ .replace(/\|/g, "\\|")
274
+ .replace(/\n/g, " ")
275
+ .trim();
276
+ }
277
+ function renderTable(node) {
278
+ const rows = collectRows(node);
279
+ if (rows.length === 0) {
280
+ return "";
281
+ }
282
+ const cellsOf = (row) => row.children
283
+ .filter(isElement)
284
+ .filter((cell) => cell.tag === "td" || cell.tag === "th")
285
+ .map((cell) => escapeCell(renderBlocks(cell.children).replace(/\n+/g, " ")));
286
+ // Markdown has no headerless table, so when the source marked no header row
287
+ // the first row is promoted — that is what a reader sees anyway.
288
+ const header = cellsOf(rows[0]);
289
+ const bodyRows = rows.slice(1).map(cellsOf);
290
+ const columnCount = Math.max(header.length, ...bodyRows.map((r) => r.length), 1);
291
+ const pad = (cells) => Array.from({ length: columnCount }, (_, i) => cells[i] ?? "");
292
+ const lines = [
293
+ `| ${pad(header).join(" | ")} |`,
294
+ `| ${Array.from({ length: columnCount }, () => "---").join(" | ")} |`,
295
+ ...bodyRows.map((cells) => `| ${pad(cells).join(" | ")} |`),
296
+ ];
297
+ return lines.join("\n");
298
+ }
299
+ function renderList(node, depth) {
300
+ const ordered = node.tag === "ol";
301
+ const items = node.children
302
+ .filter(isElement)
303
+ .filter((child) => child.tag === "li");
304
+ const indent = " ".repeat(depth);
305
+ return items
306
+ .map((item, index) => {
307
+ const marker = ordered ? `${index + 1}.` : "-";
308
+ // Nested lists render themselves at the next depth; everything else on
309
+ // the item is inline content belonging to this bullet.
310
+ const nested = item.children
311
+ .filter(isElement)
312
+ .filter((child) => child.tag === "ul" || child.tag === "ol");
313
+ const own = item.children.filter((child) => !(isElement(child) && (child.tag === "ul" || child.tag === "ol")));
314
+ const label = renderBlocks(own).replace(/\n+/g, " ").trim();
315
+ const nestedText = nested
316
+ .map((child) => renderList(child, depth + 1))
317
+ .filter(Boolean)
318
+ .join("\n");
319
+ return [`${indent}${marker} ${label}`.trimEnd(), nestedText]
320
+ .filter(Boolean)
321
+ .join("\n");
322
+ })
323
+ .join("\n");
324
+ }
325
+ /** Render block-level content, separating blocks with a blank line. */
326
+ function renderBlocks(nodes) {
327
+ const blocks = [];
328
+ let inlineBuffer = "";
329
+ const flushInline = () => {
330
+ const text = inlineBuffer.trim();
331
+ if (text) {
332
+ blocks.push(text);
333
+ }
334
+ inlineBuffer = "";
335
+ };
336
+ for (const node of nodes) {
337
+ if (node.kind === "text") {
338
+ inlineBuffer += collapseWhitespace(node.value);
339
+ continue;
340
+ }
341
+ const headingMatch = /^h([1-6])$/.exec(node.tag);
342
+ if (headingMatch) {
343
+ flushInline();
344
+ const level = Number.parseInt(headingMatch[1], 10);
345
+ const text = renderInline(node.children).replace(/\n+/g, " ").trim();
346
+ if (text) {
347
+ blocks.push(`${"#".repeat(level)} ${text}`);
348
+ }
349
+ continue;
350
+ }
351
+ switch (node.tag) {
352
+ case "script":
353
+ case "style":
354
+ flushInline();
355
+ break;
356
+ case "p": {
357
+ flushInline();
358
+ const text = renderBlocks(node.children).trim();
359
+ if (text) {
360
+ blocks.push(text);
361
+ }
362
+ break;
363
+ }
364
+ case "ul":
365
+ case "ol": {
366
+ flushInline();
367
+ const text = renderList(node, 0);
368
+ if (text) {
369
+ blocks.push(text);
370
+ }
371
+ break;
372
+ }
373
+ case "table": {
374
+ flushInline();
375
+ const text = renderTable(node);
376
+ if (text) {
377
+ blocks.push(text);
378
+ }
379
+ break;
380
+ }
381
+ case "blockquote": {
382
+ flushInline();
383
+ const inner = renderBlocks(node.children).trim();
384
+ if (inner) {
385
+ blocks.push(inner
386
+ .split("\n")
387
+ .map((line) => (line ? `> ${line}` : ">"))
388
+ .join("\n"));
389
+ }
390
+ break;
391
+ }
392
+ case "pre": {
393
+ flushInline();
394
+ const inner = renderPreformattedText(node.children).replace(/^\n+|\n+$/g, "");
395
+ if (inner) {
396
+ blocks.push(`\`\`\`\n${inner}\n\`\`\``);
397
+ }
398
+ break;
399
+ }
400
+ case "hr": {
401
+ flushInline();
402
+ blocks.push("---");
403
+ break;
404
+ }
405
+ case "div":
406
+ case "section":
407
+ case "article": {
408
+ flushInline();
409
+ const inner = renderBlocks(node.children).trim();
410
+ if (inner) {
411
+ blocks.push(inner);
412
+ }
413
+ break;
414
+ }
415
+ default:
416
+ inlineBuffer += renderInline([node]);
417
+ break;
418
+ }
419
+ }
420
+ flushInline();
421
+ return blocks.join("\n\n");
422
+ }
423
+ /**
424
+ * Convert an HTML fragment to Markdown.
425
+ *
426
+ * @param html - HTML produced by a document converter
427
+ * @returns Markdown text, or an empty string when the fragment has no content
428
+ *
429
+ * @example
430
+ * ```typescript
431
+ * htmlToMarkdown("<h1>Title</h1><p>Body</p>"); // "# Title\n\nBody"
432
+ * ```
433
+ */
434
+ export function htmlToMarkdown(html) {
435
+ if (!html || !html.trim()) {
436
+ return "";
437
+ }
438
+ return renderBlocks(parseHtml(html))
439
+ .replace(/[ \t]+$/gm, "")
440
+ .replace(/\n{3,}/g, "\n\n")
441
+ .trim();
442
+ }
@@ -895,6 +895,9 @@ async function detectFileForUnifiedArray(file, options, provider, genericFileMax
895
895
  "unknown",
896
896
  ],
897
897
  csvOptions: options.csvOptions,
898
+ // Office options travel the same unified `files` path, so this is where a
899
+ // caller's sheet selection / format choice reaches the XLSX processor.
900
+ officeOptions: options.officeOptions,
898
901
  // #478: videos arrive through this unified `files` path, so this is
899
902
  // where the CLI's frame/quality/format request has to be handed on.
900
903
  videoOptions: options.videoOptions
@@ -16,6 +16,7 @@ import type { StreamOptions } from "../types/index.js";
16
16
  * - input.videoFiles: Video files (Buffer | string paths)
17
17
  * - csvOptions: CSV parsing options
18
18
  * - videoOptions: Video frame/quality/format/transcription options
19
+ * - officeOptions: DOCX/XLSX sheet selection and output-format options
19
20
  * - systemPrompt: System-level instructions
20
21
  * - conversationMessages: Chat history
21
22
  * - temperature: Model temperature (0-1)
@@ -28,6 +29,7 @@ import type { StreamOptions } from "../types/index.js";
28
29
  * @returns {object} Normalized options object with:
29
30
  * - input: { text, images, content, files, csvFiles, pdfFiles, audioFiles, videoFiles }
30
31
  * - csvOptions: CSV processing options
32
+ * - officeOptions: Office (DOCX/XLSX) processing options
31
33
  * - systemPrompt: System prompt string
32
34
  * - conversationHistory: Message history array
33
35
  * - provider: Provider name
@@ -66,6 +68,7 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
66
68
  outputFormat?: import("../index.js").VisionImageOutputFormat;
67
69
  } | undefined;
68
70
  videoOptions: import("../index.js").VideoProcessorOptions | undefined;
71
+ officeOptions: import("../index.js").OfficeProcessorOptions | undefined;
69
72
  systemPrompt: string | undefined;
70
73
  conversationHistory: import("../index.js").ChatMessage[] | undefined;
71
74
  provider: string;
@@ -15,6 +15,7 @@
15
15
  * - input.videoFiles: Video files (Buffer | string paths)
16
16
  * - csvOptions: CSV parsing options
17
17
  * - videoOptions: Video frame/quality/format/transcription options
18
+ * - officeOptions: DOCX/XLSX sheet selection and output-format options
18
19
  * - systemPrompt: System-level instructions
19
20
  * - conversationMessages: Chat history
20
21
  * - temperature: Model temperature (0-1)
@@ -27,6 +28,7 @@
27
28
  * @returns {object} Normalized options object with:
28
29
  * - input: { text, images, content, files, csvFiles, pdfFiles, audioFiles, videoFiles }
29
30
  * - csvOptions: CSV processing options
31
+ * - officeOptions: Office (DOCX/XLSX) processing options
30
32
  * - systemPrompt: System prompt string
31
33
  * - conversationHistory: Message history array
32
34
  * - provider: Provider name
@@ -67,6 +69,10 @@ export function buildMultimodalOptions(options, providerName, modelName) {
67
69
  // `StreamOptions` never reach VideoProcessor on the Bedrock branch, and
68
70
  // it silently falls back to the duration-tier default.
69
71
  videoOptions: options.videoOptions,
72
+ // Same #1259 whitelist-drop bug class as audioFiles/videoFiles above:
73
+ // omitted here, Bedrock's multimodal generate()/stream() path always got
74
+ // sheetName/formatStyle undefined regardless of what the caller passed.
75
+ officeOptions: options.officeOptions,
70
76
  systemPrompt: options.systemPrompt,
71
77
  conversationHistory: options.conversationMessages,
72
78
  provider: providerName,