@juspay/neurolink 13.4.0 → 13.4.2

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.
@@ -9,15 +9,18 @@
9
9
  * - ppt/slideMasters/ — master slide templates
10
10
  * - ppt/slideLayouts/ — slide layout definitions
11
11
  *
12
- * Text is extracted from `<a:t>` elements in the slide XML files.
13
- * Slides are sorted by number and presented in reading order.
12
+ * Text is extracted from `<a:t>` elements in the slide XML files, with XML
13
+ * character references decoded (`&amp;` becomes `&`). Text inside a table is
14
+ * grouped by cell and row, so a table row comes out as one line
15
+ * (`Region | Owner | Volume`) instead of one line per run. Slides are sorted
16
+ * by number and presented in reading order, each followed by its speaker notes.
14
17
  *
15
18
  * Uses `adm-zip` (already a project dependency) for ZIP extraction.
16
19
  *
17
20
  * **XML-entity attacks are not applicable here.** Unlike `WordProcessor`
18
21
  * (mammoth/@xmldom/xmldom) and `ExcelProcessor` (exceljs/saxes), this
19
22
  * processor never hands slide XML to a DOM or SAX parser at all — extraction
20
- * is a single regex (`TEXT_ELEMENT_REGEX`, below) run over the raw decoded
23
+ * is a single regex (`SLIDE_TOKEN_REGEX`, below) run over the raw decoded
21
24
  * bytes. There is no DOCTYPE/ENTITY resolution path for a payload to reach,
22
25
  * so there is nothing for a `.pptx`-specific XXE or billion-laughs test to
23
26
  * falsify; one is deliberately not written (see
@@ -67,7 +70,14 @@ export declare class PptxProcessor {
67
70
  private static extractNotesForSlide;
68
71
  /**
69
72
  * Extract text strings from a slide XML document.
70
- * Finds all `<a:t>` elements and returns their text content.
73
+ *
74
+ * Finds every `<a:t>` element and returns its decoded text, one string per
75
+ * run, except inside a table: there the runs of a cell are joined (runs of
76
+ * one paragraph directly, paragraphs and line breaks with a space) and the
77
+ * cells of a row come back as one `a | b | c` string. A row with no text in
78
+ * any cell is dropped. An empty cell keeps its separator, so the number of
79
+ * cells in a row can be counted from the line (`a | |`, `| b`), but the
80
+ * line itself is trimmed and a literal `|` in cell text is not escaped.
71
81
  *
72
82
  * @param xml - Raw XML string from a slide file
73
83
  * @returns Array of text strings found in the slide
@@ -76,7 +86,8 @@ export declare class PptxProcessor {
76
86
  /**
77
87
  * Extract text from specific slides in a PPTX file.
78
88
  *
79
- * Called by the `extract_file_content` tool for targeted slide access.
89
+ * Called by the `extract_file_content` tool for targeted slide access. Each
90
+ * slide carries its speaker notes, as in {@link PptxProcessor.extractText}.
80
91
  *
81
92
  * @param content - Raw PPTX file buffer
82
93
  * @param slideNumbers - Array of 1-indexed slide numbers to extract
@@ -9,15 +9,18 @@
9
9
  * - ppt/slideMasters/ — master slide templates
10
10
  * - ppt/slideLayouts/ — slide layout definitions
11
11
  *
12
- * Text is extracted from `<a:t>` elements in the slide XML files.
13
- * Slides are sorted by number and presented in reading order.
12
+ * Text is extracted from `<a:t>` elements in the slide XML files, with XML
13
+ * character references decoded (`&amp;` becomes `&`). Text inside a table is
14
+ * grouped by cell and row, so a table row comes out as one line
15
+ * (`Region | Owner | Volume`) instead of one line per run. Slides are sorted
16
+ * by number and presented in reading order, each followed by its speaker notes.
14
17
  *
15
18
  * Uses `adm-zip` (already a project dependency) for ZIP extraction.
16
19
  *
17
20
  * **XML-entity attacks are not applicable here.** Unlike `WordProcessor`
18
21
  * (mammoth/@xmldom/xmldom) and `ExcelProcessor` (exceljs/saxes), this
19
22
  * processor never hands slide XML to a DOM or SAX parser at all — extraction
20
- * is a single regex (`TEXT_ELEMENT_REGEX`, below) run over the raw decoded
23
+ * is a single regex (`SLIDE_TOKEN_REGEX`, below) run over the raw decoded
21
24
  * bytes. There is no DOCTYPE/ENTITY resolution path for a payload to reach,
22
25
  * so there is nothing for a `.pptx`-specific XXE or billion-laughs test to
23
26
  * falsify; one is deliberately not written (see
@@ -78,10 +81,75 @@ function createBoundedEntryReader() {
78
81
  };
79
82
  }
80
83
  /**
81
- * Regex to match text content within PowerPoint XML `<a:t>` elements.
82
- * These elements contain the actual visible text on slides.
84
+ * Regex that tokenises the parts of slide XML the extractor reads, in document
85
+ * order:
86
+ *
87
+ * - `text`: an `<a:t>` element, which holds the visible text of one run.
88
+ * - `closing` and `tag`: the start or end tag of a paragraph (`a:p`), table
89
+ * cell (`a:tc`) or table row (`a:tr`), which decide how runs are grouped.
90
+ * - a `<a:br>` line break (matched with no group).
91
+ * - `end`: the end of the input, so the last open row or cell is flushed by the
92
+ * same code that handles every other boundary.
93
+ *
94
+ * Each tag name is followed by whitespace or `>`, never by more name
95
+ * characters. The old pattern `<a:t[^>]*>` also matched `<a:tbl>`, `<a:tc>`,
96
+ * `<a:tr>`, `<a:tab>` and `<a:tailEnd>`, so a table cell was read as a run
97
+ * whose "text" ran from the table markup up to the next `</a:t>`.
98
+ *
99
+ * A self-closing tag (`<a:t/>`, `<a:p/>`) is not a start tag, so it is
100
+ * excluded by the lookbehind.
101
+ *
102
+ * Neither run text nor a tag's attributes may contain `<`: well-formed XML
103
+ * escapes it in text and forbids it in an attribute value. Run text is
104
+ * `[^<]*` rather than a lazy `[\s\S]*?`, and attributes are `[^<>]*` rather
105
+ * than `[^>]*`, so a match attempt stops at the next `<` instead of scanning to
106
+ * the end of the slide. Without that, input made of unclosed `<a:t>` elements
107
+ * or unterminated start tags (`<a:t `, `<a:p `, `<a:br `) rescans the rest of
108
+ * the slide from every start position, which is quadratic time.
109
+ */
110
+ const SLIDE_TOKEN_REGEX = /<a:t(?:\s[^<>]*)?(?<!\/)>(?<text>[^<]*)<\/a:t>|<(?<closing>\/?)a:(?<tag>tr|tc|p)(?:\s[^<>]*)?(?<!\/)>|<a:br(?:\s[^<>]*|\/)?>|(?<end>$)/g;
111
+ /** The XML predefined entities, the only named references XML defines. */
112
+ const XML_NAMED_ENTITIES = {
113
+ amp: "&",
114
+ lt: "<",
115
+ gt: ">",
116
+ quot: '"',
117
+ apos: "'",
118
+ };
119
+ /** A named, decimal (`&#169;`) or hexadecimal (`&#xA9;`) character reference. */
120
+ const XML_ENTITY_REGEX = /&(?:#(\d+)|#x([0-9a-fA-F]+)|(amp|lt|gt|quot|apos));/g;
121
+ /** Whether a code point is a character XML 1.0 allows in a document. */
122
+ function isXmlChar(codePoint) {
123
+ return (codePoint === 0x9 ||
124
+ codePoint === 0xa ||
125
+ codePoint === 0xd ||
126
+ (codePoint >= 0x20 && codePoint <= 0xd7ff) ||
127
+ (codePoint >= 0xe000 && codePoint <= 0xfffd) ||
128
+ (codePoint >= 0x10000 && codePoint <= 0x10ffff));
129
+ }
130
+ /**
131
+ * Decode XML character references in run text.
132
+ *
133
+ * One pass over the original string, so `&amp;lt;` becomes the literal text
134
+ * `&lt;` and never `<`. A reference to a character XML forbids, or one outside
135
+ * the Unicode range, is left as written rather than turned into a control
136
+ * character. The slide XML is never handed to a parser, so there are no
137
+ * user-defined entities to resolve (see the file header).
83
138
  */
84
- const TEXT_ELEMENT_REGEX = /<a:t[^>]*>([\s\S]*?)<\/a:t>/g;
139
+ function decodeXmlEntities(text) {
140
+ if (!text.includes("&")) {
141
+ return text;
142
+ }
143
+ return text.replace(XML_ENTITY_REGEX, (reference, decimal, hex, named) => {
144
+ if (named !== undefined) {
145
+ return XML_NAMED_ENTITIES[named];
146
+ }
147
+ const codePoint = decimal !== undefined
148
+ ? Number.parseInt(decimal, 10)
149
+ : Number.parseInt(hex ?? "", 16);
150
+ return isXmlChar(codePoint) ? String.fromCodePoint(codePoint) : reference;
151
+ });
152
+ }
85
153
  /**
86
154
  * Regex to match slide filenames and extract slide number.
87
155
  * Matches entries like "ppt/slides/slide1.xml", "ppt/slides/slide12.xml".
@@ -195,22 +263,77 @@ export class PptxProcessor {
195
263
  }
196
264
  /**
197
265
  * Extract text strings from a slide XML document.
198
- * Finds all `<a:t>` elements and returns their text content.
266
+ *
267
+ * Finds every `<a:t>` element and returns its decoded text, one string per
268
+ * run, except inside a table: there the runs of a cell are joined (runs of
269
+ * one paragraph directly, paragraphs and line breaks with a space) and the
270
+ * cells of a row come back as one `a | b | c` string. A row with no text in
271
+ * any cell is dropped. An empty cell keeps its separator, so the number of
272
+ * cells in a row can be counted from the line (`a | |`, `| b`), but the
273
+ * line itself is trimmed and a literal `|` in cell text is not escaped.
199
274
  *
200
275
  * @param xml - Raw XML string from a slide file
201
276
  * @returns Array of text strings found in the slide
202
277
  */
203
278
  static extractTextFromXml(xml) {
204
- const texts = [];
205
- // Reset regex state for re-entrant usage
206
- TEXT_ELEMENT_REGEX.lastIndex = 0;
207
- for (let match = TEXT_ELEMENT_REGEX.exec(xml); match !== null; match = TEXT_ELEMENT_REGEX.exec(xml)) {
208
- const text = match[1].trim();
209
- if (text) {
210
- texts.push(text);
279
+ const lines = [];
280
+ // DrawingML tables do not nest, so one open row, cell and paragraph is
281
+ // enough. A start tag implicitly closes whatever the same or a deeper level
282
+ // left open, which keeps a slide with a missing end tag readable.
283
+ let row = null;
284
+ let cell = null;
285
+ let paragraph = "";
286
+ for (const match of xml.matchAll(SLIDE_TOKEN_REGEX)) {
287
+ const { text, closing, tag, end } = match.groups ?? {};
288
+ if (text !== undefined) {
289
+ const decoded = decodeXmlEntities(text);
290
+ if (cell) {
291
+ paragraph += decoded;
292
+ }
293
+ else if (decoded.trim()) {
294
+ lines.push(decoded.trim());
295
+ }
296
+ continue;
297
+ }
298
+ if (tag === undefined && end === undefined) {
299
+ // `<a:br>`: a line break inside a cell is a space, since the row is
300
+ // one line. Outside a table each run is already its own line.
301
+ if (cell) {
302
+ paragraph += " ";
303
+ }
304
+ continue;
305
+ }
306
+ // 1 = paragraph, 2 = cell, 3 = row (also the end of the input).
307
+ const level = tag === "p" ? 1 : tag === "tc" ? 2 : 3;
308
+ if (cell && level >= 1) {
309
+ if (paragraph.trim()) {
310
+ cell.push(paragraph.trim());
311
+ }
312
+ paragraph = "";
313
+ }
314
+ if (cell && level >= 2) {
315
+ if (!row) {
316
+ row = [];
317
+ }
318
+ row.push(cell.join(" "));
319
+ cell = null;
320
+ }
321
+ if (row && level >= 3) {
322
+ if (row.some((cellText) => cellText)) {
323
+ lines.push(row.join(" | ").trim());
324
+ }
325
+ row = null;
326
+ }
327
+ if (closing === "") {
328
+ if (tag === "tr") {
329
+ row = [];
330
+ }
331
+ else if (tag === "tc") {
332
+ cell = [];
333
+ }
211
334
  }
212
335
  }
213
- return texts;
336
+ return lines;
214
337
  }
215
338
  // ===========================================================================
216
339
  // TARGETED EXTRACTION API
@@ -218,7 +341,8 @@ export class PptxProcessor {
218
341
  /**
219
342
  * Extract text from specific slides in a PPTX file.
220
343
  *
221
- * Called by the `extract_file_content` tool for targeted slide access.
344
+ * Called by the `extract_file_content` tool for targeted slide access. Each
345
+ * slide carries its speaker notes, as in {@link PptxProcessor.extractText}.
222
346
  *
223
347
  * @param content - Raw PPTX file buffer
224
348
  * @param slideNumbers - Array of 1-indexed slide numbers to extract
@@ -256,11 +380,15 @@ export class PptxProcessor {
256
380
  const parts = [];
257
381
  for (const slide of slides) {
258
382
  const texts = PptxProcessor.extractTextFromXml(slide.xml);
383
+ const notes = PptxProcessor.extractNotesForSlide(zip, slide.slideNumber, readEntry);
259
384
  parts.push(`### Slide ${slide.slideNumber}`);
260
385
  if (texts.length > 0) {
261
386
  parts.push(texts.join("\n"));
262
387
  }
263
- else {
388
+ if (notes) {
389
+ parts.push(`**Speaker notes:** ${notes}`);
390
+ }
391
+ if (texts.length === 0 && !notes) {
264
392
  parts.push("(No text content on this slide)");
265
393
  }
266
394
  parts.push("");
@@ -25,6 +25,7 @@
25
25
  "timeouts": { "decideMs": 10000 },
26
26
  "decisionLimits": {
27
27
  "maxStateTokens": 922000,
28
+ "maxQuestions": 200,
28
29
  "advisory": true,
29
30
  "media": { "maxImages": 8, "video": false, "maxRequestBytes": 33554432 }
30
31
  },
@@ -67,7 +68,7 @@
67
68
  "evidence": {
68
69
  "rosterVerified": {
69
70
  "date": "2026-10-07",
70
- "method": "OpenAI's Decisions guide and gpt-6-luna model page saved 2026-10-07: gpt-6-luna is the only model the endpoint supports. Read from documentation; no live call"
71
+ "method": "OpenAI's Decisions guide and gpt-6-luna model page saved 2026-10-07: gpt-6-luna is the only model the endpoint supports. The 200-question cap was read on 2026-10-17 from the questions docstring (Ask 1–200 questions about the input) in OpenAI's Python SDK, src/openai/types/decision_create_params.py in github.com/openai/openai-python, which is generated from OpenAI's API definition; the Decisions guide itself states no figure. Read from documentation; no live call, and a request over 200 questions was not tried"
71
72
  },
72
73
  "liveMatrix": null,
73
74
  "addedInPR": "https://github.com/juspay/neurolink/pull/1996"