@arnilo/prism-rag 0.3.1 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] - 2026-08-29
4
+
5
+ ### Changed
6
+ - Plan 035-039 changed-package cut: additive runtime performance, tooling, and documentation deltas; peer window refresh.
7
+
3
8
  ## [0.3.1] - 2026-08-26
4
9
 
5
10
  ### Added
package/dist/parsers.js CHANGED
@@ -36,7 +36,19 @@ async function parsePdf(document, options = {}) {
36
36
  const pages = [...pdf.matchAll(/\/Type\s*\/Page\b/gu)].length;
37
37
  if (pages > limits.maxPdfPages)
38
38
  throw new RagLimitError(`PDF exceeds ${limits.maxPdfPages} pages`);
39
- const blocks = pdf.match(/BT[\s\S]*?ET/gu) ?? [];
39
+ // Index-scanned BT..ET block extraction instead of /BT[\s\S]*?ET/gu (CodeQL js/polynomial-redos, alert 11).
40
+ const blocks = [];
41
+ let cursor = 0;
42
+ while (cursor < pdf.length) {
43
+ const begin = pdf.indexOf("BT", cursor);
44
+ if (begin === -1)
45
+ break;
46
+ const end = pdf.indexOf("ET", begin + 2);
47
+ if (end === -1)
48
+ break;
49
+ blocks.push(pdf.slice(begin, end + 2));
50
+ cursor = end + 2;
51
+ }
40
52
  const text = blocks.flatMap(pdfBlockText).join("\n").trim();
41
53
  if (!text)
42
54
  throw new RagValidationError("PDF has no uncompressed text");
@@ -65,17 +77,86 @@ function assertParseTime(started, maxParseMs) {
65
77
  if (Date.now() - started > maxParseMs)
66
78
  throw new RagLimitError(`document parsing exceeded ${maxParseMs}ms`);
67
79
  }
80
+ // Single-pass linear HTML-to-text scanner (CodeQL js/incomplete-multi-character-sanitization
81
+ // and js/polynomial-redos on the former regex chain, alerts 17-19): comments, script/style
82
+ // bodies, and tags are consumed by index — hostile adjacency cannot re-form dangerous tags.
68
83
  function htmlToText(html) {
69
- return decodeEntities(html
70
- .replace(/<!--[\s\S]*?-->/gu, "")
71
- .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/giu, "")
72
- .replace(/<(?:br|p|div|li|h[1-6]|tr)\b[^>]*>/giu, "\n")
73
- .replace(/<[^>]*>/gu, " "))
74
- .replace(/[ \t]{2,}/gu, " ")
75
- .replace(/[ \t]+\n/gu, "\n")
76
- .replace(/\n[ \t]+/gu, "\n")
77
- .replace(/\n{3,}/gu, "\n\n")
78
- .trim();
84
+ const out = [];
85
+ let i = 0;
86
+ while (i < html.length) {
87
+ if (html.startsWith("<!--", i)) {
88
+ const close = html.indexOf("-->", i + 4);
89
+ i = close === -1 ? html.length : close + 3;
90
+ continue;
91
+ }
92
+ if (html.charCodeAt(i) === 60) {
93
+ const gt = html.indexOf(">", i + 1);
94
+ if (gt === -1) {
95
+ out.push(html.slice(i));
96
+ break;
97
+ }
98
+ const tag = html.slice(i + 1, gt);
99
+ i = gt + 1;
100
+ const closing = tag.startsWith("/");
101
+ const name = (closing ? tag.slice(1) : tag).match(/^[A-Za-z]+/u)?.[0]?.toLowerCase() ?? "";
102
+ if (closing) {
103
+ out.push(" ");
104
+ }
105
+ else if (name === "br" || name === "p" || name === "div" || name === "li" || name === "tr" || /^h[1-6]$/u.test(name)) {
106
+ out.push("\n");
107
+ }
108
+ else if (name === "script" || name === "style") {
109
+ // Consume the raw element body through its matching close tag.
110
+ const closeIdx = html.toLowerCase().indexOf(`</${name}`, i);
111
+ const next = closeIdx === -1 ? html.length : html.indexOf(">", closeIdx);
112
+ i = next === -1 ? html.length : next + 1;
113
+ }
114
+ else {
115
+ out.push(" ");
116
+ }
117
+ continue;
118
+ }
119
+ out.push(html[i++]);
120
+ }
121
+ return collapseTextWhitespace(decodeEntities(out.join(""))).trim();
122
+ }
123
+ /** Linear whitespace normalization: `[ \t]` runs collapse; 3+ newlines collapse to 2; edge spaces drop. */
124
+ function collapseTextWhitespace(text) {
125
+ const out = [];
126
+ let i = 0;
127
+ let lineStart = true;
128
+ let spaceRun = false;
129
+ let newlineRun = 0;
130
+ while (i < text.length) {
131
+ const c = text[i];
132
+ if (c === " " || c === "\t") {
133
+ if (!lineStart)
134
+ spaceRun = true;
135
+ i += 1;
136
+ continue;
137
+ }
138
+ if (c === "\n") {
139
+ newlineRun += 1;
140
+ spaceRun = false;
141
+ lineStart = true;
142
+ i += 1;
143
+ continue;
144
+ }
145
+ if (newlineRun > 0) {
146
+ out.push("\n".repeat(Math.min(newlineRun, 2)));
147
+ newlineRun = 0;
148
+ }
149
+ else if (spaceRun) {
150
+ out.push(" ");
151
+ spaceRun = false;
152
+ }
153
+ lineStart = false;
154
+ out.push(c);
155
+ i += 1;
156
+ }
157
+ if (newlineRun > 0)
158
+ out.push("\n".repeat(Math.min(newlineRun, 2)));
159
+ return out.join("");
79
160
  }
80
161
  function decodeEntities(text) {
81
162
  const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", "#39": "'" };
@@ -83,11 +164,70 @@ function decodeEntities(text) {
83
164
  }
84
165
  function pdfBlockText(block) {
85
166
  const strings = [];
86
- for (const match of block.matchAll(/\((?:\\.|[^\\)])*\)\s*(?:Tj|['"])/gu))
87
- strings.push(decodePdfString(match[0].replace(/\s*(?:Tj|['"])$/u, "")));
88
- for (const match of block.matchAll(/\[([\s\S]*?)\]\s*TJ/gu)) {
89
- for (const value of match[1].matchAll(/\((?:\\.|[^\\)])*\)/gu))
90
- strings.push(decodePdfString(value[0]));
167
+ // Linear index scanners replace ambiguous nested-quantifier regexes (CodeQL js/polynomial-redos, alerts 15-16, 20-21).
168
+ const extractLiteral = (text, from) => {
169
+ const open = text.indexOf("(", from);
170
+ if (open === -1)
171
+ return undefined;
172
+ let i = open + 1;
173
+ while (i < text.length) {
174
+ const c = text[i];
175
+ if (c === "\\") {
176
+ i += 2;
177
+ continue;
178
+ }
179
+ if (c === ")")
180
+ return { literal: text.slice(open + 1, i), next: i + 1 };
181
+ i += 1;
182
+ }
183
+ return undefined; // unterminated literal - ignore
184
+ };
185
+ let cursor = 0;
186
+ while (cursor < block.length) {
187
+ const literal = extractLiteral(block, cursor);
188
+ if (!literal)
189
+ break;
190
+ let j = literal.next;
191
+ while (j < block.length && /\s/.test(block[j]))
192
+ j += 1;
193
+ if (block[j] === "T" && block[j + 1] === "j") {
194
+ strings.push(decodePdfString(`(${literal.literal})`));
195
+ cursor = j + 2;
196
+ }
197
+ else if (block[j] === "'" || block[j] === '"') {
198
+ strings.push(decodePdfString(`(${literal.literal})`));
199
+ cursor = j + 1;
200
+ }
201
+ else {
202
+ cursor = literal.next;
203
+ }
204
+ }
205
+ cursor = 0;
206
+ while (cursor < block.length) {
207
+ const open = block.indexOf("[", cursor);
208
+ if (open === -1)
209
+ break;
210
+ const close = block.indexOf("]", open + 1);
211
+ if (close === -1)
212
+ break;
213
+ let j = close + 1;
214
+ while (j < block.length && /\s/.test(block[j]))
215
+ j += 1;
216
+ if (block.startsWith("TJ", j)) {
217
+ const arr = block.slice(open + 1, close);
218
+ let arrCursor = 0;
219
+ for (;;) {
220
+ const literal = extractLiteral(arr, arrCursor);
221
+ if (!literal)
222
+ break;
223
+ strings.push(decodePdfString(`(${literal.literal})`));
224
+ arrCursor = literal.next;
225
+ }
226
+ cursor = j + 2;
227
+ }
228
+ else {
229
+ cursor = open + 1;
230
+ }
91
231
  }
92
232
  return strings.filter(Boolean);
93
233
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism-rag",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Optional bounded text and Markdown RAG primitives for Prism.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "pack:dry-run": "npm pack --dry-run"
34
34
  },
35
35
  "peerDependencies": {
36
- "@arnilo/prism": "^0.3.0",
36
+ "@arnilo/prism": "^0.3.1",
37
37
  "@arnilo/prism-memory": "^0.3.0"
38
38
  },
39
39
  "devDependencies": {