@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 +5 -0
- package/dist/parsers.js +156 -16
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
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.
|
|
36
|
+
"@arnilo/prism": "^0.3.1",
|
|
37
37
|
"@arnilo/prism-memory": "^0.3.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|