@juspay/neurolink 12.51.3 → 12.51.4

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.
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Splits HTML content by semantic tags.
5
5
  */
6
+ import { removeHtmlMarkup } from "../../utils/htmlText.js";
6
7
  import { BaseChunker, DEFAULT_CHUNKER_CONFIG } from "./BaseChunker.js";
7
8
  /**
8
9
  * HTML Chunker
@@ -28,10 +29,12 @@ export class HTMLChunker extends BaseChunker {
28
29
  * Strip HTML tags from content
29
30
  */
30
31
  stripHtml(html) {
31
- return html
32
- .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
33
- .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
34
- .replace(/<[^>]+>/g, " ")
32
+ return removeHtmlMarkup(html, {
33
+ tagReplacement: " ",
34
+ commentReplacement: " ",
35
+ lineBreakEndTags: [],
36
+ lineBreakVoidTags: [],
37
+ })
35
38
  .replace(/\s+/g, " ")
36
39
  .trim();
37
40
  }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { randomUUID } from "crypto";
8
8
  import { decodeHtmlTextEntitiesOnce } from "../../utils/htmlEntities.js";
9
+ import { removeHtmlMarkup } from "../../utils/htmlText.js";
9
10
  /**
10
11
  * HTML-aware chunker implementation
11
12
  * Splits based on HTML structure (tags, elements)
@@ -171,16 +172,24 @@ export class HTMLChunker {
171
172
  * Extract plain text from HTML
172
173
  */
173
174
  extractText(html) {
174
- return decodeHtmlTextEntitiesOnce(html
175
- // Remove script and style elements
176
- .replace(/<script[\s\S]*?<\/script>/gi, "")
177
- .replace(/<style[\s\S]*?<\/style>/gi, "")
178
- // Remove HTML comments
179
- .replace(/<!--[\s\S]*?-->/g, "")
180
- // Replace block elements with newlines
181
- .replace(/<\/(p|div|br|h[1-6]|li|tr)>/gi, "\n")
182
- // Remove remaining tags
183
- .replace(/<[^>]+>/g, ""), { nbsp: " ", amp: "&", lt: "<", gt: ">", quot: '"', "#039": "'" })
175
+ return decodeHtmlTextEntitiesOnce(removeHtmlMarkup(html, {
176
+ tagReplacement: "",
177
+ commentReplacement: "",
178
+ lineBreakEndTags: [
179
+ "p",
180
+ "div",
181
+ "br",
182
+ "h1",
183
+ "h2",
184
+ "h3",
185
+ "h4",
186
+ "h5",
187
+ "h6",
188
+ "li",
189
+ "tr",
190
+ ],
191
+ lineBreakVoidTags: [],
192
+ }), { nbsp: " ", amp: "&", lt: "<", gt: ">", quot: '"', "#039": "'" })
184
193
  .replace(/\s+/g, " ")
185
194
  .trim();
186
195
  }
@@ -28,6 +28,7 @@ import { readFile } from "fs/promises";
28
28
  import { basename, extname } from "path";
29
29
  import { logger } from "../../utils/logger.js";
30
30
  import { decodeHtmlTextEntitiesOnce } from "../../utils/htmlEntities.js";
31
+ import { removeHtmlMarkup } from "../../utils/htmlText.js";
31
32
  import { CSVProcessor, stripBom, stripMetadataLine, splitCsvLines, dedupeColumnNames, sanitizeColumnName, } from "../../utils/csvProcessor.js";
32
33
  import { splitCSVFields } from "../../utils/csvUtils.js";
33
34
  import { MDocument } from "./MDocument.js";
@@ -429,17 +430,25 @@ export class WebLoader {
429
430
  * Convert HTML to plain text
430
431
  */
431
432
  htmlToText(html) {
432
- return decodeHtmlTextEntitiesOnce(html
433
- // Remove script and style elements
434
- .replace(/<script[\s\S]*?<\/script>/gi, "")
435
- .replace(/<style[\s\S]*?<\/style>/gi, "")
436
- // Remove HTML comments
437
- .replace(/<!--[\s\S]*?-->/g, "")
438
- // Replace common block elements with newlines
439
- .replace(/<\/(p|div|h[1-6]|br|li|tr|blockquote)>/gi, "\n")
440
- .replace(/<(br|hr)\s*\/?>/gi, "\n")
441
- // Remove remaining tags
442
- .replace(/<[^>]+>/g, ""), {
433
+ return decodeHtmlTextEntitiesOnce(removeHtmlMarkup(html, {
434
+ tagReplacement: "",
435
+ commentReplacement: "",
436
+ lineBreakEndTags: [
437
+ "p",
438
+ "div",
439
+ "h1",
440
+ "h2",
441
+ "h3",
442
+ "h4",
443
+ "h5",
444
+ "h6",
445
+ "br",
446
+ "li",
447
+ "tr",
448
+ "blockquote",
449
+ ],
450
+ lineBreakVoidTags: ["br", "hr"],
451
+ }), {
443
452
  nbsp: " ",
444
453
  amp: "&",
445
454
  lt: "<",
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Single-pass markup removal for turning HTML into indexable text.
3
+ *
4
+ * This is text conversion, not output sanitization. The result is plain text
5
+ * for retrieval and chunking; it is not meant to be written back into a page,
6
+ * and the caller's entity decoding can legitimately turn `&lt;b&gt;` into
7
+ * `<b>`.
8
+ *
9
+ * Why one pass: the previous implementation chained several global regex
10
+ * replacements (script, style, comment, block end, generic tag). Each removal
11
+ * could join the text on either side of it into a new tag for the next
12
+ * replacement (`<scr<!---->ipt>`), and the lazy `[\s\S]*?` bodies rescanned
13
+ * the rest of the input once per unclosed `<script`, which is quadratic.
14
+ * Here the input is read left to right exactly once. Every character of the
15
+ * output is copied from an unremoved stretch of the input, a removed stretch
16
+ * is never re-read, and every search either consumes what it scans or is
17
+ * answered in constant time from an index computed up front, so the work is
18
+ * linear in the input length.
19
+ *
20
+ * Invariant: the output never matches `<[^>]+>`. A `<` survives only when no
21
+ * `>` follows it in the input, or when the next character is the `>` itself
22
+ * (`<>`, which is not a tag). A removed span therefore cannot leave behind,
23
+ * or assemble, a tag.
24
+ */
25
+ /**
26
+ * Remove HTML markup from `html` in one linear pass.
27
+ *
28
+ * - `<script>` / `<style>` elements are removed with their bodies. The end
29
+ * tag may carry whitespace or attributes (`</script >`). An element with
30
+ * no usable end tag loses only its start tag, as before.
31
+ * - `<!-- ... -->` is replaced with `commentReplacement`. An unterminated
32
+ * comment is treated like any other tag and ends at the next `>`.
33
+ * - A closing tag named in `lineBreakEndTags` and a void tag named in
34
+ * `lineBreakVoidTags` (`<br>`, `<hr />`) become a newline.
35
+ * - Any other `<...>` becomes `tagReplacement`. A `<` with no `>` after it,
36
+ * and the empty `<>`, are literal text and are kept.
37
+ *
38
+ * Tag-name lists are lower case.
39
+ */
40
+ export declare function removeHtmlMarkup(html: string, options: {
41
+ readonly tagReplacement: string;
42
+ readonly commentReplacement: string;
43
+ readonly lineBreakEndTags: readonly string[];
44
+ readonly lineBreakVoidTags: readonly string[];
45
+ }): string;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Single-pass markup removal for turning HTML into indexable text.
3
+ *
4
+ * This is text conversion, not output sanitization. The result is plain text
5
+ * for retrieval and chunking; it is not meant to be written back into a page,
6
+ * and the caller's entity decoding can legitimately turn `&lt;b&gt;` into
7
+ * `<b>`.
8
+ *
9
+ * Why one pass: the previous implementation chained several global regex
10
+ * replacements (script, style, comment, block end, generic tag). Each removal
11
+ * could join the text on either side of it into a new tag for the next
12
+ * replacement (`<scr<!---->ipt>`), and the lazy `[\s\S]*?` bodies rescanned
13
+ * the rest of the input once per unclosed `<script`, which is quadratic.
14
+ * Here the input is read left to right exactly once. Every character of the
15
+ * output is copied from an unremoved stretch of the input, a removed stretch
16
+ * is never re-read, and every search either consumes what it scans or is
17
+ * answered in constant time from an index computed up front, so the work is
18
+ * linear in the input length.
19
+ *
20
+ * Invariant: the output never matches `<[^>]+>`. A `<` survives only when no
21
+ * `>` follows it in the input, or when the next character is the `>` itself
22
+ * (`<>`, which is not a tag). A removed span therefore cannot leave behind,
23
+ * or assemble, a tag.
24
+ */
25
+ const SLASH = 0x2f;
26
+ const GREATER_THAN = 0x3e;
27
+ const WHITESPACE = /\s/;
28
+ function isAsciiAlphanumeric(code) {
29
+ return ((code >= 0x30 && code <= 0x39) ||
30
+ (code >= 0x41 && code <= 0x5a) ||
31
+ (code >= 0x61 && code <= 0x7a));
32
+ }
33
+ // The characters after which an HTML tokenizer ends a tag name.
34
+ function endsTagName(code) {
35
+ return (code === 0x09 ||
36
+ code === 0x0a ||
37
+ code === 0x0c ||
38
+ code === 0x0d ||
39
+ code === 0x20 ||
40
+ code === SLASH ||
41
+ code === GREATER_THAN);
42
+ }
43
+ function scanTagName(html, from) {
44
+ let end = from;
45
+ while (end < html.length && isAsciiAlphanumeric(html.charCodeAt(end))) {
46
+ end++;
47
+ }
48
+ return end;
49
+ }
50
+ /**
51
+ * Index of the next `</name` that is followed by whitespace, `/` or `>`
52
+ * (so `</script >` and `</SCRIPT foo>` count), or -1. Case-insensitive.
53
+ */
54
+ function findEndTag(html, name, from) {
55
+ let at = html.indexOf("</", from);
56
+ while (at !== -1) {
57
+ const nameEnd = at + 2 + name.length;
58
+ if (html.slice(at + 2, nameEnd).toLowerCase() === name &&
59
+ endsTagName(html.charCodeAt(nameEnd))) {
60
+ return at;
61
+ }
62
+ at = html.indexOf("</", at + 2);
63
+ }
64
+ return -1;
65
+ }
66
+ /**
67
+ * Remove HTML markup from `html` in one linear pass.
68
+ *
69
+ * - `<script>` / `<style>` elements are removed with their bodies. The end
70
+ * tag may carry whitespace or attributes (`</script >`). An element with
71
+ * no usable end tag loses only its start tag, as before.
72
+ * - `<!-- ... -->` is replaced with `commentReplacement`. An unterminated
73
+ * comment is treated like any other tag and ends at the next `>`.
74
+ * - A closing tag named in `lineBreakEndTags` and a void tag named in
75
+ * `lineBreakVoidTags` (`<br>`, `<hr />`) become a newline.
76
+ * - Any other `<...>` becomes `tagReplacement`. A `<` with no `>` after it,
77
+ * and the empty `<>`, are literal text and are kept.
78
+ *
79
+ * Tag-name lists are lower case.
80
+ */
81
+ export function removeHtmlMarkup(html, options) {
82
+ const { tagReplacement, commentReplacement } = options;
83
+ const lineBreakEndTags = new Set(options.lineBreakEndTags);
84
+ const lineBreakVoidTags = new Set(options.lineBreakVoidTags);
85
+ // Answers "is there any `>` / `-->` after here" without a scan. Once an
86
+ // element finds no usable end tag, no later element of that name can.
87
+ const lastGreaterThan = html.lastIndexOf(">");
88
+ const lastCommentEnd = html.lastIndexOf("-->");
89
+ const withoutEndTag = new Set();
90
+ const markupAt = (at) => {
91
+ if (html.startsWith("<!--", at) && lastCommentEnd >= at + 4) {
92
+ return {
93
+ end: html.indexOf("-->", at + 4) + 3,
94
+ replacement: commentReplacement,
95
+ };
96
+ }
97
+ const closing = html.charCodeAt(at + 1) === SLASH;
98
+ const nameStart = closing ? at + 2 : at + 1;
99
+ const nameEnd = scanTagName(html, nameStart);
100
+ const name = html.slice(nameStart, nameEnd).toLowerCase();
101
+ if (!closing &&
102
+ (name === "script" || name === "style") &&
103
+ endsTagName(html.charCodeAt(nameEnd)) &&
104
+ !withoutEndTag.has(name)) {
105
+ const startTagEnd = html.indexOf(">", nameEnd);
106
+ const endTag = findEndTag(html, name, startTagEnd + 1);
107
+ if (endTag !== -1 && endTag < lastGreaterThan) {
108
+ return { end: html.indexOf(">", endTag) + 1, replacement: "" };
109
+ }
110
+ withoutEndTag.add(name);
111
+ }
112
+ if (closing &&
113
+ html.charCodeAt(nameEnd) === GREATER_THAN &&
114
+ lineBreakEndTags.has(name)) {
115
+ return { end: nameEnd + 1, replacement: "\n" };
116
+ }
117
+ if (!closing && lineBreakVoidTags.has(name)) {
118
+ let probe = nameEnd;
119
+ while (WHITESPACE.test(html.charAt(probe))) {
120
+ probe++;
121
+ }
122
+ if (html.charCodeAt(probe) === SLASH) {
123
+ probe++;
124
+ }
125
+ if (html.charCodeAt(probe) === GREATER_THAN) {
126
+ return { end: probe + 1, replacement: "\n" };
127
+ }
128
+ }
129
+ const end = html.indexOf(">", at + 1);
130
+ return end === at + 1
131
+ ? null
132
+ : { end: end + 1, replacement: tagReplacement };
133
+ };
134
+ let output = "";
135
+ let copied = 0;
136
+ let open = html.indexOf("<");
137
+ while (open !== -1 && open < lastGreaterThan) {
138
+ const markup = markupAt(open);
139
+ if (markup === null) {
140
+ open = html.indexOf("<", open + 1);
141
+ continue;
142
+ }
143
+ output += html.slice(copied, open) + markup.replacement;
144
+ copied = markup.end;
145
+ open = html.indexOf("<", copied);
146
+ }
147
+ return output + html.slice(copied);
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.51.3",
3
+ "version": "12.51.4",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
6
6
  "author": {
@@ -164,6 +164,7 @@
164
164
  "test:embedding-response-validation": "pnpm exec tsx test/continuous-test-suite-embedding-response-validation.ts",
165
165
  "test:pdf-image-streaming": "pnpm exec tsx test/continuous-test-suite-pdf-image-streaming.ts",
166
166
  "test:rag": "pnpm exec tsx test/continuous-test-suite-rag.ts",
167
+ "test:rag-html-markup": "pnpm exec tsx test/continuous-test-suite-rag-html-markup.ts",
167
168
  "test:skills": "pnpm exec tsx test/continuous-test-suite-skills.ts",
168
169
  "test:decide": "pnpm exec tsx test/continuous-test-suite-decide.ts",
169
170
  "test:decide-catalog": "pnpm exec tsx test/continuous-test-suite-decide-catalog.ts",