docrev 0.5.0 → 0.5.1

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.
@@ -173,6 +173,7 @@ export function parseAnnotations(text) {
173
173
 
174
174
  /**
175
175
  * Strip annotations from text, applying changes
176
+ * Handles nested annotations by iterating until stable
176
177
  * @param {string} text
177
178
  * @param {{keepComments?: boolean}} options
178
179
  * @returns {string}
@@ -180,26 +181,88 @@ export function parseAnnotations(text) {
180
181
  export function stripAnnotations(text, options = {}) {
181
182
  const { keepComments = false } = options;
182
183
 
183
- // Apply substitutions: {~~old~>new~~} new
184
- text = text.replace(PATTERNS.substitute, '$2');
184
+ // Iterate until no more changes (handles nested annotations)
185
+ let prev;
186
+ let iterations = 0;
187
+ const maxIterations = 20; // Safety limit
185
188
 
186
- // Apply insertions: {++text++} → text
187
- text = text.replace(PATTERNS.insert, '$1');
189
+ do {
190
+ prev = text;
188
191
 
189
- // Apply deletions: {--text--} → nothing
190
- text = text.replace(PATTERNS.delete, '');
192
+ // Apply substitutions: {~~old~>new~~} → new
193
+ text = text.replace(PATTERNS.substitute, '$2');
191
194
 
192
- // Remove highlights: {==text==} → text
193
- text = text.replace(PATTERNS.highlight, '$1');
195
+ // Apply insertions: {++text++} → text
196
+ text = text.replace(PATTERNS.insert, '$1');
194
197
 
195
- // Remove comments unless keeping
196
- if (!keepComments) {
197
- text = text.replace(PATTERNS.comment, '');
198
- }
198
+ // Apply deletions: {--text--} → nothing
199
+ text = text.replace(PATTERNS.delete, '');
200
+
201
+ // Remove highlights: {==text==} → text
202
+ text = text.replace(PATTERNS.highlight, '$1');
203
+
204
+ // Remove comments unless keeping
205
+ if (!keepComments) {
206
+ text = text.replace(PATTERNS.comment, '');
207
+ }
208
+
209
+ // Clean up partial/orphaned markers within the loop
210
+ // This handles cases where nested annotations leave behind fragments
211
+
212
+ // Empty annotations (from nested stripping)
213
+ text = text.replace(/\{----\}/g, '');
214
+ text = text.replace(/\{\+\+\+\+\}/g, '');
215
+ text = text.replace(/\{--\s*--\}/g, '');
216
+ text = text.replace(/\{\+\+\s*\+\+\}/g, '');
217
+
218
+ // Orphaned substitution fragments: ~>text~~} or {~~text (no proper pairs)
219
+ text = text.replace(/~>[^{]*?~~\}/g, '');
220
+ text = text.replace(/\{~~[^~}]*$/gm, '');
221
+
222
+ // Handle malformed substitution from nested: {~~{~~old → just strip the {~~
223
+ text = text.replace(/\{~~\{~~/g, '{~~');
224
+ text = text.replace(/~~\}~~\}/g, '~~}');
225
+
226
+ iterations++;
227
+ } while (text !== prev && iterations < maxIterations);
228
+
229
+ // Final cleanup of any remaining orphaned markers
230
+ // Orphaned closing markers
231
+ text = text.replace(/--\}(?:--\})+/g, '');
232
+ text = text.replace(/\+\+\}(?:\+\+\})+/g, '');
233
+ text = text.replace(/~~\}(?:~~\})+/g, '');
234
+ text = text.replace(/--\}/g, '');
235
+ text = text.replace(/\+\+\}/g, '');
236
+ text = text.replace(/~~\}/g, '');
237
+
238
+ // Orphaned opening markers
239
+ text = text.replace(/\{--(?:\{--)+/g, '');
240
+ text = text.replace(/\{\+\+(?:\{\+\+)+/g, '');
241
+ text = text.replace(/\{~~(?:\{~~)+/g, '');
242
+ text = text.replace(/\{--/g, '');
243
+ text = text.replace(/\{\+\+/g, '');
244
+ text = text.replace(/\{~~/g, '');
245
+ text = text.replace(/~>/g, '');
246
+
247
+ // Clean up multiple spaces (but preserve structure like newlines)
248
+ text = text.replace(/ +/g, ' ');
199
249
 
200
250
  return text;
201
251
  }
202
252
 
253
+ /**
254
+ * Check if text contains any CriticMarkup annotations
255
+ * @param {string} text
256
+ * @returns {boolean}
257
+ */
258
+ export function hasAnnotations(text) {
259
+ return PATTERNS.insert.test(text) ||
260
+ PATTERNS.delete.test(text) ||
261
+ PATTERNS.substitute.test(text) ||
262
+ PATTERNS.comment.test(text) ||
263
+ PATTERNS.highlight.test(text);
264
+ }
265
+
203
266
  /**
204
267
  * Apply a decision to a single annotation
205
268
  * @param {string} text
package/lib/import.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import { diffWords } from 'diff';
8
+ import { stripAnnotations } from './annotations.js';
8
9
 
9
10
  /**
10
11
  * Extract comments directly from Word docx comments.xml
@@ -985,7 +986,11 @@ export async function importFromWord(docxPath, originalMdPath, options = {}) {
985
986
  }
986
987
 
987
988
  // Read original markdown
988
- const originalMd = fs.readFileSync(originalMdPath, 'utf-8');
989
+ let originalMd = fs.readFileSync(originalMdPath, 'utf-8');
990
+
991
+ // IMPORTANT: Strip any existing annotations to prevent nested annotations
992
+ // This ensures we always diff clean text against Word text
993
+ originalMd = stripAnnotations(originalMd, { keepComments: false });
989
994
 
990
995
  // Generate diff
991
996
  let annotated = generateSmartDiff(originalMd, wordText, author);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docrev",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Academic paper revision workflow: Word ↔ Markdown round-trips, DOI validation, reviewer comments",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",