page-analyzer 1.0.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,168 @@
1
+ import { parse } from 'csv-parse/sync';
2
+ import {
3
+ cleanText,
4
+ wrapInteractiveActionSegments,
5
+ toCsvIdValue,
6
+ parseIntegerOr,
7
+ escapeCsvField
8
+ } from './event-analyzer-utils.js';
9
+
10
+ function parseInputCsvRows(csvData) {
11
+ const raw = String(csvData || '').trim();
12
+ if (!raw) {
13
+ return [];
14
+ }
15
+
16
+ let records = [];
17
+ try {
18
+ records = parse(raw, {
19
+ columns: true,
20
+ skip_empty_lines: true,
21
+ trim: true,
22
+ relax_column_count: true,
23
+ relax_quotes: true
24
+ });
25
+ } catch (error) {
26
+ throw new Error(`Failed to parse input CSV: ${error.message}`);
27
+ }
28
+
29
+ const rows = [];
30
+ for (let index = 0; index < records.length; index += 1) {
31
+ const record = records[index];
32
+ const csvId = toCsvIdValue(record?.csv_id ?? record?.idx);
33
+ if (!csvId) {
34
+ continue;
35
+ }
36
+ rows.push({
37
+ order: index,
38
+ csv_id: csvId,
39
+ blockIdx: parseIntegerOr(record?.blockIdx ?? record?.blockidx, -1),
40
+ tag: String(record?.tag || '').trim(),
41
+ imageAlt: cleanText(record?.imageAlt || '', 300),
42
+ text: cleanText(record?.text || '', 300),
43
+ context: cleanText(record?.context || '', 300),
44
+ href: String(record?.href || '').trim()
45
+ });
46
+ }
47
+
48
+ return rows;
49
+ }
50
+
51
+ function normalizeBlockPosition(position = {}, fallback = {}) {
52
+ const source = position && typeof position === 'object' && !Array.isArray(position)
53
+ ? position
54
+ : {};
55
+ const toNumber = (value) => {
56
+ const parsed = Number(value);
57
+ return Number.isFinite(parsed) ? parsed : 0;
58
+ };
59
+
60
+ return {
61
+ left: toNumber(source.left ?? fallback.left),
62
+ top: toNumber(source.top ?? fallback.top),
63
+ height: Math.max(0, toNumber(source.height ?? fallback.height)),
64
+ width: Math.max(0, toNumber(source.width ?? fallback.width))
65
+ };
66
+ }
67
+
68
+ function formatBlockPosition(position = {}) {
69
+ const normalized = normalizeBlockPosition(position);
70
+ return [
71
+ `left=${Math.round(normalized.left)}`,
72
+ `top=${Math.round(normalized.top)}`,
73
+ `height=${Math.round(normalized.height)}`,
74
+ `width=${Math.round(normalized.width)}`
75
+ ].join(';');
76
+ }
77
+
78
+ function normalizeBlocks(blocks = []) {
79
+ const source = Array.isArray(blocks) ? blocks : [];
80
+ return source.map((item, index) => {
81
+ const childInteractiveCount = Number.isInteger(item?.childInteractiveCount)
82
+ ? item.childInteractiveCount
83
+ : parseIntegerOr(item?.childInteractiveCount, 0);
84
+ const textPreview = cleanText(
85
+ wrapInteractiveActionSegments(item?.textPreview || '', childInteractiveCount),
86
+ 1200
87
+ );
88
+ const blockPosition = normalizeBlockPosition(item?.blockPosition, item);
89
+
90
+ return {
91
+ blockIdx: Number.isInteger(item?.blockIdx) ? item.blockIdx : index,
92
+ branchPath: cleanText(item?.branchPath || '', 120),
93
+ depth: Number.isInteger(item?.depth) ? item.depth : parseIntegerOr(item?.depth, 0),
94
+ domOrder: Number.isInteger(item?.domOrder) ? item.domOrder : parseIntegerOr(item?.domOrder, index + 1),
95
+ tag: String(item?.tag || '').trim().toLowerCase(),
96
+ fixed: Boolean(item?.fixed),
97
+ top: Number(item?.top) || 0,
98
+ left: Number(item?.left) || 0,
99
+ width: Number(item?.width) || 0,
100
+ height: Number(item?.height) || 0,
101
+ blockCssPath: cleanText(item?.blockCssPath || item?.cssPath || '', 500),
102
+ blockPosition,
103
+ textPreview,
104
+ childInteractiveCount,
105
+ order: index
106
+ };
107
+ });
108
+ }
109
+
110
+ function serializeBlocksIndexCsv(blocks = []) {
111
+ const lines = ['blockIdx,branchPath,depth,domOrder,tag,fixed,top,left,width,height,blockCssPath,blockPosition,textPreview,childInteractiveCount'];
112
+ for (const block of blocks) {
113
+ lines.push([
114
+ block.blockIdx,
115
+ escapeCsvField(block.branchPath || ''),
116
+ Number.isInteger(block.depth) ? block.depth : 0,
117
+ Number.isInteger(block.domOrder) ? block.domOrder : 0,
118
+ escapeCsvField(block.tag),
119
+ block.fixed ? 1 : 0,
120
+ Math.round(Number(block.top) || 0),
121
+ Math.round(Number(block.left) || 0),
122
+ Math.round(Number(block.width) || 0),
123
+ Math.round(Number(block.height) || 0),
124
+ escapeCsvField(block.blockCssPath || ''),
125
+ escapeCsvField(formatBlockPosition(block.blockPosition || block)),
126
+ escapeCsvField(block.textPreview),
127
+ Number.isInteger(block.childInteractiveCount) ? block.childInteractiveCount : 0
128
+ ].join(','));
129
+ }
130
+ return lines.join('\n');
131
+ }
132
+
133
+ function serializeBlockAnalysisCsv(blockContexts = []) {
134
+ const lines = ['blockIdx,blockName,blockPossibleEvents'];
135
+ for (const block of blockContexts) {
136
+ lines.push([
137
+ escapeCsvField(String(block.blockIdxKey || block.blockIdx)),
138
+ escapeCsvField(block.blockName),
139
+ escapeCsvField((Array.isArray(block.possibleEvents) ? block.possibleEvents : []).join(','))
140
+ ].join(','));
141
+ }
142
+ return lines.join('\n');
143
+ }
144
+
145
+ function serializeDomCsvRows(rows = []) {
146
+ const lines = ['idx,blockIdx,blockSemantic,tag,imageAlt,text,context,href'];
147
+ for (const row of rows) {
148
+ lines.push([
149
+ row.csv_id,
150
+ escapeCsvField(String(row.logicalBlockIdxKey ?? row.blockIdx)),
151
+ escapeCsvField(row.blockSemantic || ''),
152
+ escapeCsvField(row.tag),
153
+ escapeCsvField(row.imageAlt),
154
+ escapeCsvField(row.text),
155
+ escapeCsvField(row.context),
156
+ escapeCsvField(row.href)
157
+ ].join(','));
158
+ }
159
+ return lines.join('\n');
160
+ }
161
+
162
+ export {
163
+ parseInputCsvRows,
164
+ normalizeBlocks,
165
+ serializeBlocksIndexCsv,
166
+ serializeBlockAnalysisCsv,
167
+ serializeDomCsvRows
168
+ };
@@ -0,0 +1,15 @@
1
+ export function buildRunContextMetadata(runContext = {}, extra = {}) {
2
+ const metadata = { ...extra };
3
+ const safeRunContext = runContext && typeof runContext === 'object' ? runContext : {};
4
+ const domain = String(safeRunContext?.domain || '').trim();
5
+ const nodeId = String(safeRunContext?.nodeId || '').trim();
6
+
7
+ if (domain) {
8
+ metadata.domain = domain;
9
+ }
10
+ if (nodeId) {
11
+ metadata.nodeId = nodeId;
12
+ }
13
+
14
+ return metadata;
15
+ }
@@ -0,0 +1,71 @@
1
+ import { DEFAULT_ATTRIBUTE_KEYS } from './event-analyzer-constants.js';
2
+
3
+ function cleanText(value, maxLength = 2000) {
4
+ return String(value || '')
5
+ .replace(/\s+/g, ' ')
6
+ .trim()
7
+ .slice(0, maxLength);
8
+ }
9
+
10
+ export function buildSpecialBlockPrompt({
11
+ template,
12
+ blocksIndexCsv
13
+ }) {
14
+ let prompt = String(template || '');
15
+ prompt = prompt.replace('<<<BLOCKS_INDEX_CSV>>>', String(blocksIndexCsv || '').trim());
16
+ return prompt;
17
+ }
18
+
19
+ export function buildEventAnalysisPrompt({
20
+ template,
21
+ siteSummary,
22
+ blockAnalysisCsv,
23
+ domCsv
24
+ }) {
25
+ let prompt = String(template || '');
26
+ prompt = prompt.replace('<<<SITE_SUMMARY>>>', cleanText(siteSummary, 500));
27
+ prompt = prompt.replace('<<<BLOCK_ANALYSIS_CSV>>>', String(blockAnalysisCsv || '').trim());
28
+ prompt = prompt.replace('<<<DOM_CSV>>>', String(domCsv || '').trim());
29
+ return prompt;
30
+ }
31
+
32
+ export function buildCsvRepairPrompt({
33
+ rawResponse,
34
+ knownEventTypes = [],
35
+ attributeKeys = [],
36
+ parseErrorMessage = ''
37
+ }) {
38
+ const knownTypesJson = JSON.stringify(knownEventTypes || []);
39
+ const attributeHint = Array.isArray(attributeKeys) && attributeKeys.length > 0
40
+ ? attributeKeys.join(', ')
41
+ : DEFAULT_ATTRIBUTE_KEYS.join(', ');
42
+ const parseError = String(parseErrorMessage || '').trim();
43
+
44
+ const lines = [
45
+ 'You are a strict CSV formatter.',
46
+ 'Reformat the input as plain text CSV only, with no markdown, no prose, no code fence.',
47
+ 'Output must contain section 1 (required): events_by_node CSV.',
48
+ 'Section 1 header must be exactly: csv_id,event_type,attributes_kv',
49
+ 'attributes_kv must use key=value&key2=value2 format.',
50
+ 'event_type must be snake_case.',
51
+ 'CSV quoting rules are mandatory:',
52
+ '- If a field contains comma, quote, or newline, wrap it in double quotes.',
53
+ '- Escape internal double quotes by doubling them ("").',
54
+ '- Never leave unmatched quotes in any row.',
55
+ 'Section 2 (optional): new_event_types CSV with header: name,why_webmasters_care',
56
+ 'If section 2 exists, separate sections with a standalone line containing exactly ---',
57
+ 'If no section 2 rows, do not output ---.',
58
+ 'Do not change semantic meaning. Only fix structure/format.',
59
+ `Known event types: ${knownTypesJson}`,
60
+ `Preferred attribute keys: ${attributeHint}`,
61
+ 'Input to fix:',
62
+ rawResponse,
63
+ 'Output only the corrected CSV response.'
64
+ ];
65
+
66
+ if (parseError) {
67
+ lines.splice(lines.length - 2, 0, `Parser error to fix: ${parseError}`);
68
+ }
69
+
70
+ return lines.join('\n');
71
+ }
@@ -0,0 +1,290 @@
1
+ import {
2
+ deriveEventTypeMetadata,
3
+ parseEventsFromCsv,
4
+ parseNewEventTypesFromCsv
5
+ } from '../../utils/event-csv.js';
6
+ import { DEFAULT_ATTRIBUTE_KEYS } from './event-analyzer-constants.js';
7
+
8
+ class EventAnalyzerResponse {
9
+ constructor(config = {}) {
10
+ this.config = config;
11
+ }
12
+
13
+ parseCsvResponse(response, knownEventTypes = [], options = {}) {
14
+ const logger = options?.logger || console;
15
+ const tolerateNewEventTypesParseError = options?.tolerateNewEventTypesParseError !== false;
16
+ const { eventsSection, newEventTypesSection, hasSeparator } = this.splitCsvSections(response);
17
+ if (!eventsSection) {
18
+ throw new Error('events_by_node CSV section is empty');
19
+ }
20
+ if (this.looksLikeJsonPayload(response)) {
21
+ throw new Error('Response appears to be JSON instead of CSV sections');
22
+ }
23
+
24
+ const { events, invalidRows } = parseEventsFromCsv(eventsSection, {
25
+ sourceLabel: 'llm_events_by_node',
26
+ logger: console,
27
+ skipInvalidRows: true
28
+ });
29
+
30
+ if (invalidRows > 0) {
31
+ logger.warn(`[LLM][CSV] Skipped ${invalidRows} invalid events_by_node row(s)`);
32
+ }
33
+ if (events.length === 0 && invalidRows > 0) {
34
+ throw new Error('events_by_node CSV has no valid rows');
35
+ }
36
+
37
+ const {
38
+ eventTypesSummary,
39
+ newEventTypes: derivedNewEventTypes
40
+ } = deriveEventTypeMetadata(events, knownEventTypes);
41
+
42
+ let parsedNewEventTypes = [];
43
+ let newTypeInvalidRows = 0;
44
+ if (newEventTypesSection) {
45
+ try {
46
+ const parsedResult = parseNewEventTypesFromCsv(newEventTypesSection, {
47
+ sourceLabel: 'llm_new_event_types',
48
+ logger,
49
+ skipInvalidRows: true
50
+ });
51
+ parsedNewEventTypes = parsedResult.newEventTypes;
52
+ newTypeInvalidRows = parsedResult.invalidRows;
53
+
54
+ if (newTypeInvalidRows > 0) {
55
+ logger.warn(`[LLM][CSV] Skipped ${newTypeInvalidRows} invalid new_event_types row(s)`);
56
+ }
57
+ } catch (error) {
58
+ if (!tolerateNewEventTypesParseError) {
59
+ throw error;
60
+ }
61
+ logger.warn(
62
+ `[LLM][CSV] Failed to parse new_event_types section: ${error.message}. ` +
63
+ 'Fallback to derived new_event_types.'
64
+ );
65
+ }
66
+ } else if (hasSeparator) {
67
+ logger.warn('[LLM][CSV] Separator found but new_event_types section is empty; using derived fallback');
68
+ }
69
+
70
+ const normalizedNewEventTypes = this.resolveNewEventTypes({
71
+ parsedNewEventTypes,
72
+ derivedNewEventTypes,
73
+ knownEventTypes
74
+ });
75
+
76
+ return {
77
+ events_by_node: events,
78
+ event_types_summary: eventTypesSummary,
79
+ new_event_types: normalizedNewEventTypes
80
+ };
81
+ }
82
+
83
+ resolveNewEventTypes({ parsedNewEventTypes = [], derivedNewEventTypes = [], knownEventTypes = [] } = {}) {
84
+ const knownSet = new Set(this.normalizeStringList(knownEventTypes, { eventType: true }));
85
+ const derivedByName = new Map();
86
+ for (const item of Array.isArray(derivedNewEventTypes) ? derivedNewEventTypes : []) {
87
+ const name = this.normalizeEventType(item?.name);
88
+ if (!name) {
89
+ continue;
90
+ }
91
+ derivedByName.set(name, item);
92
+ }
93
+
94
+ const mergedByName = new Map();
95
+ for (const item of Array.isArray(parsedNewEventTypes) ? parsedNewEventTypes : []) {
96
+ const name = this.normalizeEventType(item?.name);
97
+ if (!name || knownSet.has(name) || mergedByName.has(name)) {
98
+ continue;
99
+ }
100
+
101
+ const parsedReasons = this.normalizeReasons(item?.why_webmasters_care);
102
+ const derivedReasons = this.normalizeReasons(derivedByName.get(name)?.why_webmasters_care);
103
+ const whyWebmastersCare = parsedReasons.length > 0 ? parsedReasons : derivedReasons;
104
+ const normalizedItem = {
105
+ name,
106
+ why_webmasters_care: whyWebmastersCare
107
+ };
108
+ if (knownSet.size > 0) {
109
+ normalizedItem.not_in_known_event_types_because = `event_type "${name}" was not found in known_event_types`;
110
+ }
111
+
112
+ mergedByName.set(name, normalizedItem);
113
+ }
114
+
115
+ for (const item of Array.isArray(derivedNewEventTypes) ? derivedNewEventTypes : []) {
116
+ const name = this.normalizeEventType(item?.name);
117
+ if (!name || knownSet.has(name) || mergedByName.has(name)) {
118
+ continue;
119
+ }
120
+
121
+ const normalizedItem = {
122
+ name,
123
+ why_webmasters_care: this.normalizeReasons(item?.why_webmasters_care)
124
+ };
125
+ const note = String(item?.not_in_known_event_types_because || '').trim();
126
+ if (note) {
127
+ normalizedItem.not_in_known_event_types_because = note;
128
+ } else if (knownSet.size > 0) {
129
+ normalizedItem.not_in_known_event_types_because = `event_type "${name}" was not found in known_event_types`;
130
+ }
131
+
132
+ mergedByName.set(name, normalizedItem);
133
+ }
134
+
135
+ return Array.from(mergedByName.values());
136
+ }
137
+
138
+ splitCsvSections(response) {
139
+ const normalized = this.unwrapCsvResponseText(response);
140
+ if (!normalized) {
141
+ return {
142
+ eventsSection: '',
143
+ newEventTypesSection: '',
144
+ hasSeparator: false
145
+ };
146
+ }
147
+
148
+ const lines = normalized.replace(/\r\n/g, '\n').split('\n');
149
+ const separatorIndex = lines.findIndex((line) => line.trim() === '---');
150
+ if (separatorIndex === -1) {
151
+ return {
152
+ eventsSection: normalized.trim(),
153
+ newEventTypesSection: '',
154
+ hasSeparator: false
155
+ };
156
+ }
157
+
158
+ const eventsSection = lines.slice(0, separatorIndex).join('\n').trim();
159
+ const newEventTypesSection = lines.slice(separatorIndex + 1).join('\n').trim();
160
+
161
+ return {
162
+ eventsSection,
163
+ newEventTypesSection,
164
+ hasSeparator: true
165
+ };
166
+ }
167
+
168
+ unwrapCsvResponseText(response) {
169
+ const raw = String(response || '').trim();
170
+ if (!raw) {
171
+ return '';
172
+ }
173
+
174
+ const codeBlockRegex = /```(?:csv|text)?\s*\n?([\s\S]*?)\n?```/gi;
175
+ const segments = [];
176
+ let lastIndex = 0;
177
+ let foundCodeBlock = false;
178
+ let match = null;
179
+
180
+ const appendSeparators = (text) => {
181
+ const lines = String(text || '')
182
+ .replace(/\r\n/g, '\n')
183
+ .split('\n')
184
+ .map((line) => line.trim())
185
+ .filter((line) => line === '---');
186
+ if (lines.length > 0) {
187
+ segments.push(...lines);
188
+ }
189
+ };
190
+
191
+ while ((match = codeBlockRegex.exec(raw)) !== null) {
192
+ foundCodeBlock = true;
193
+ appendSeparators(raw.slice(lastIndex, match.index));
194
+
195
+ const block = String(match[1] || '').trim();
196
+ if (block) {
197
+ segments.push(block);
198
+ }
199
+
200
+ lastIndex = codeBlockRegex.lastIndex;
201
+ }
202
+
203
+ if (!foundCodeBlock) {
204
+ return raw;
205
+ }
206
+ appendSeparators(raw.slice(lastIndex));
207
+ return segments.join('\n').trim();
208
+ }
209
+
210
+ looksLikeJsonPayload(response) {
211
+ const raw = String(response || '').trim();
212
+ if (!raw) {
213
+ return false;
214
+ }
215
+
216
+ if ((raw.startsWith('{') && raw.endsWith('}')) || (raw.startsWith('[') && raw.endsWith(']'))) {
217
+ return true;
218
+ }
219
+
220
+ return /"events_by_node"\s*:/.test(raw);
221
+ }
222
+
223
+ normalizeEventType(value) {
224
+ return String(value || '')
225
+ .trim()
226
+ .toLowerCase()
227
+ .replace(/[^a-z0-9_]+/g, '_')
228
+ .replace(/_+/g, '_')
229
+ .replace(/^_+|_+$/g, '');
230
+ }
231
+
232
+ normalizeStringList(values, { eventType = false } = {}) {
233
+ if (!Array.isArray(values)) {
234
+ return [];
235
+ }
236
+
237
+ const seen = new Set();
238
+ const normalized = [];
239
+ for (const item of values) {
240
+ let text = String(item ?? '').trim();
241
+ if (!text) {
242
+ continue;
243
+ }
244
+
245
+ if (eventType) {
246
+ text = this.normalizeEventType(text);
247
+ if (!text) {
248
+ continue;
249
+ }
250
+ }
251
+
252
+ if (seen.has(text)) {
253
+ continue;
254
+ }
255
+ seen.add(text);
256
+ normalized.push(text);
257
+ }
258
+
259
+ return normalized;
260
+ }
261
+
262
+ getAttributeKeys() {
263
+ const configured = this.normalizeStringList(this.config?.attributeKeys);
264
+ if (configured.length > 0) {
265
+ return configured;
266
+ }
267
+
268
+ return [...DEFAULT_ATTRIBUTE_KEYS];
269
+ }
270
+
271
+ normalizeReasons(value) {
272
+ if (Array.isArray(value)) {
273
+ return value
274
+ .map(item => String(item || '').trim())
275
+ .filter(Boolean);
276
+ }
277
+
278
+ const text = String(value || '').trim();
279
+ return text ? [text] : [];
280
+ }
281
+
282
+ logNewEventTypes(result) {
283
+ if (result.new_event_types && result.new_event_types.length > 0) {
284
+ const newTypeNames = result.new_event_types.map(e => e.name).join(', ');
285
+ console.log(`[LLM] Discovered ${result.new_event_types.length} new event types: ${newTypeNames}`);
286
+ }
287
+ }
288
+ }
289
+
290
+ export { EventAnalyzerResponse };
@@ -0,0 +1,96 @@
1
+ function cleanText(value, maxLength = 240) {
2
+ return String(value || '')
3
+ .replace(/\s+/g, ' ')
4
+ .trim()
5
+ .slice(0, maxLength);
6
+ }
7
+
8
+ function wrapInteractiveActionSegments(textPreview, childInteractiveCount = 0) {
9
+ if (!Number.isInteger(childInteractiveCount) || childInteractiveCount <= 0) {
10
+ return String(textPreview || '');
11
+ }
12
+
13
+ const source = String(textPreview || '');
14
+ if (!source) {
15
+ return '';
16
+ }
17
+
18
+ return source.replace(/\b(?:formaction|action)\s*[:=]\s*[^\s|;,<>]+/gi, (match, offset, input) => {
19
+ const before = offset > 0 ? input[offset - 1] : '';
20
+ const afterIndex = offset + match.length;
21
+ const after = afterIndex < input.length ? input[afterIndex] : '';
22
+ if (before === '<' && after === '>') {
23
+ return match;
24
+ }
25
+ return `<${match}>`;
26
+ });
27
+ }
28
+
29
+ function toCsvIdValue(value) {
30
+ return String(value ?? '').trim();
31
+ }
32
+
33
+ function parseIntegerOr(value, fallback = -1) {
34
+ const parsed = Number.parseInt(String(value ?? '').trim(), 10);
35
+ return Number.isInteger(parsed) ? parsed : fallback;
36
+ }
37
+
38
+ function escapeCsvField(value) {
39
+ const text = String(value ?? '');
40
+ if (text.includes('"') || text.includes(',') || text.includes('\n') || text.includes('\r')) {
41
+ return `"${text.replace(/"/g, '""')}"`;
42
+ }
43
+ return text;
44
+ }
45
+
46
+ function normalizeSpecialBlockName(name, fallback = 'ContentBlock') {
47
+ const text = cleanText(name, 80);
48
+ return text || fallback;
49
+ }
50
+
51
+ function normalizeDestination(value) {
52
+ const href = String(value || '').trim();
53
+ if (!href) {
54
+ return '';
55
+ }
56
+
57
+ try {
58
+ const parsed = new URL(href);
59
+ const pathPart = `${parsed.pathname || '/'}${parsed.search || ''}`;
60
+ return cleanText(pathPart || parsed.href, 200);
61
+ } catch {
62
+ return cleanText(href, 200);
63
+ }
64
+ }
65
+
66
+ function parseBlockIdxList(value) {
67
+ const raw = String(value || '').trim();
68
+ if (!raw) {
69
+ return [];
70
+ }
71
+
72
+ const out = [];
73
+ const seen = new Set();
74
+ for (const part of raw.split('.')) {
75
+ const parsed = Number.parseInt(String(part || '').trim(), 10);
76
+ if (!Number.isInteger(parsed) || parsed < 0 || seen.has(parsed)) {
77
+ continue;
78
+ }
79
+ seen.add(parsed);
80
+ out.push(parsed);
81
+ }
82
+
83
+ out.sort((left, right) => left - right);
84
+ return out;
85
+ }
86
+
87
+ export {
88
+ cleanText,
89
+ wrapInteractiveActionSegments,
90
+ toCsvIdValue,
91
+ parseIntegerOr,
92
+ escapeCsvField,
93
+ normalizeSpecialBlockName,
94
+ normalizeDestination,
95
+ parseBlockIdxList
96
+ };