@ansonlai/docx-redline-js 0.6.0 → 0.6.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.
@@ -183,6 +183,80 @@ function collectDocumentCommentAnchors(paragraphNodes, revisionView) {
183
183
  return anchors;
184
184
  }
185
185
 
186
+ function selectInspectionParagraphs(allParagraphs, options) {
187
+ let matches = allParagraphs;
188
+ if (options.revisedOnly) matches = matches.filter(item => item.hasRevisions);
189
+ if (options.inTable != null) matches = matches.filter(item => item.inTable === !!options.inTable);
190
+ if (options.skipEmpty) matches = matches.filter(item => item.text.length > 0);
191
+ if (options.search) {
192
+ const needle = String(options.search).toLowerCase();
193
+ matches = matches.filter(item => item.text.toLowerCase().includes(needle));
194
+ }
195
+ if (Array.isArray(options.indexes)) {
196
+ const indexes = new Set(options.indexes);
197
+ matches = matches.filter(item => indexes.has(item.index));
198
+ }
199
+ let rangeStart = 1;
200
+ let rangeEnd = allParagraphs.length;
201
+ if (options.range) {
202
+ rangeStart = Number(options.range.start ?? options.range[0]);
203
+ rangeEnd = Number(options.range.end ?? options.range[1]);
204
+ matches = matches.filter(item => item.index >= rangeStart && item.index <= rangeEnd);
205
+ }
206
+
207
+ const totalMatches = matches.length;
208
+ const after = Number.isInteger(options.after) && options.after > 0 ? options.after : null;
209
+ const remaining = after == null ? matches : matches.filter(item => item.index > after);
210
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : null;
211
+ const selectedMatches = limit == null ? remaining : remaining.slice(0, limit);
212
+ const truncated = selectedMatches.length < remaining.length;
213
+ const around = Number.isInteger(options.around) && options.around > 0 ? options.around : 0;
214
+ const exposeSelection = !!options.search || around > 0 || limit != null || after != null;
215
+
216
+ let paragraphs = selectedMatches;
217
+ if (around > 0 && options.search) {
218
+ const directIndexes = new Set(selectedMatches.map(item => item.index));
219
+ const contextFor = new Map();
220
+ for (const match of selectedMatches) {
221
+ const start = Math.max(rangeStart, match.index - around);
222
+ const end = Math.min(rangeEnd, match.index + around);
223
+ for (let index = start; index <= end; index++) {
224
+ if (directIndexes.has(index)) continue;
225
+ const owners = contextFor.get(index) || [];
226
+ owners.push(match.index);
227
+ contextFor.set(index, owners);
228
+ }
229
+ }
230
+ const returnedIndexes = new Set([...directIndexes, ...contextFor.keys()]);
231
+ paragraphs = allParagraphs
232
+ .filter(item => returnedIndexes.has(item.index))
233
+ .map(item => directIndexes.has(item.index)
234
+ ? { ...item, selectionRole: 'match' }
235
+ : { ...item, selectionRole: 'context', contextFor: contextFor.get(item.index) || [] });
236
+ } else if (exposeSelection) {
237
+ paragraphs = selectedMatches.map(item => ({ ...item, selectionRole: 'match' }));
238
+ }
239
+
240
+ return {
241
+ paragraphs,
242
+ ...(exposeSelection ? {
243
+ selection: {
244
+ ...(options.search ? { search: String(options.search), caseSensitive: false } : {}),
245
+ totalMatches,
246
+ returnedMatches: selectedMatches.length,
247
+ returnedParagraphs: paragraphs.length,
248
+ truncated,
249
+ nextAfter: truncated && selectedMatches.length > 0
250
+ ? selectedMatches[selectedMatches.length - 1].index
251
+ : null,
252
+ ...(limit != null ? { limit } : {}),
253
+ ...(after != null ? { after } : {}),
254
+ ...(around > 0 ? { around } : {})
255
+ }
256
+ } : {})
257
+ };
258
+ }
259
+
186
260
  /** Read-only, stable document-parts inspection for agents and package adapters. */
187
261
  export function inspectDocumentParts(parts, options = {}) {
188
262
  const documentPart = parseXml(parts?.documentXml, 'word/document.xml', true);
@@ -213,12 +287,17 @@ export function inspectDocumentParts(parts, options = {}) {
213
287
  const index = zeroIndex + 1;
214
288
  const provision = list?.label && list.format !== 'bullet' ? list.label : null;
215
289
  const headingText = nearestHeading?.text || null;
216
- const humanReference = [provision, headingText, text.slice(0, options.excerptLength || 120)].filter(Boolean).join(' — ');
290
+ const excerpt = text.slice(0, options.excerptLength || 120);
291
+ const humanReference = level
292
+ ? text
293
+ : (provision
294
+ ? [provision, headingText].filter(Boolean).join(' — ')
295
+ : (headingText ? [headingText, excerpt].filter(Boolean).join(' — ') : excerpt));
217
296
  const segments = extractParagraphRevisionSegments(paragraph);
218
297
  return {
219
298
  index, ref: `P${index}`, paragraphId: getParagraphId(paragraph),
220
299
  fingerprint: createParagraphFingerprint(paragraph, { text, index, revisionView }), revisionView,
221
- text, exactText: text, excerpt: text.slice(0, options.excerptLength || 120), humanReference, inTable: hasAncestor(paragraph, 'tc'), table: structure.table,
300
+ text, exactText: text, excerpt, humanReference, provision, inTable: hasAncestor(paragraph, 'tc'), table: structure.table,
222
301
  styleId, headingLevel: level, nearestHeading, list, structuralReferences: structure.references, hasRevisions: authors.length > 0, revisionAuthors: authors, commentIds: ids,
223
302
  segments
224
303
  };
@@ -229,12 +308,38 @@ export function inspectDocumentParts(parts, options = {}) {
229
308
  definition.anchoredText ??= commentAnchors.get(id) || paragraph.text;
230
309
  comments.set(id, definition);
231
310
  }
232
- if (options.revisedOnly) paragraphs = paragraphs.filter(item => item.hasRevisions);
233
- if (options.inTable != null) paragraphs = paragraphs.filter(item => item.inTable === !!options.inTable);
234
- if (options.skipEmpty) paragraphs = paragraphs.filter(item => item.text.length > 0);
235
- if (options.search) { const needle = String(options.search).toLowerCase(); paragraphs = paragraphs.filter(item => item.text.toLowerCase().includes(needle)); }
236
- if (Array.isArray(options.indexes)) { const indexes = new Set(options.indexes); paragraphs = paragraphs.filter(item => indexes.has(item.index)); }
237
- if (options.range) { const start = Number(options.range.start ?? options.range[0]); const end = Number(options.range.end ?? options.range[1]); paragraphs = paragraphs.filter(item => item.index >= start && item.index <= end); }
311
+ const selected = selectInspectionParagraphs(paragraphs, options);
312
+ if (options.search && selected.selection && selected.selection.totalMatches === 0) {
313
+ const needle = String(options.search).toLowerCase();
314
+ const alternateView = revisionView === 'accepted' ? 'rejected' : 'accepted';
315
+ let rangeStart = 1;
316
+ let rangeEnd = paragraphNodes.length;
317
+ if (options.range) {
318
+ rangeStart = Number(options.range.start ?? options.range[0]);
319
+ rangeEnd = Number(options.range.end ?? options.range[1]);
320
+ }
321
+ let alternateMatches = 0;
322
+ for (let i = 0; i < paragraphNodes.length; i++) {
323
+ const index = i + 1;
324
+ if (options.range && (index < rangeStart || index > rangeEnd)) continue;
325
+ if (Array.isArray(options.indexes) && !options.indexes.includes(index)) continue;
326
+ const pNode = paragraphNodes[i];
327
+ if (options.table && !hasAncestor(pNode, 'tc')) continue;
328
+ if (options.body && hasAncestor(pNode, 'tc')) continue;
329
+ if (options.revised && revisionAuthors(pNode).length === 0) continue;
330
+ const altText = extractCanonicalParagraphText(pNode, { revisionView: alternateView });
331
+ if (options.skipEmpty && altText.length === 0) continue;
332
+ if (altText.toLowerCase().includes(needle)) {
333
+ alternateMatches++;
334
+ }
335
+ }
336
+ if (alternateMatches > 0) {
337
+ selected.selection.hint = revisionView === 'accepted'
338
+ ? `0 matches in accepted view, but ${alternateMatches} match${alternateMatches === 1 ? '' : 'es'} found in --view rejected. If searching for deleted text to restore, re-run with --view rejected.`
339
+ : `0 matches in rejected view, but ${alternateMatches} match${alternateMatches === 1 ? '' : 'es'} found in --view accepted. Re-run with --view accepted.`;
340
+ }
341
+ }
342
+ paragraphs = selected.paragraphs;
238
343
  const allRevisionAuthors = [...new Set(paragraphs.flatMap(item => item.revisionAuthors))].sort();
239
344
  const coveredEntries = extractDocumentPartsEntries(parts);
240
345
  const coveredParts = coveredEntries.map(e => e.name).sort();
@@ -251,6 +356,7 @@ export function inspectDocumentParts(parts, options = {}) {
251
356
  revisionToken,
252
357
  coveredParts,
253
358
  paragraphs,
359
+ ...(selected.selection ? { selection: selected.selection } : {}),
254
360
  comments: [...comments.values()],
255
361
  revisionAuthors: allRevisionAuthors,
256
362
  commentAuthors: [...new Set([...comments.values()].map(item => item.author).filter(Boolean))].sort(),