docrev 0.9.11 → 0.9.14
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/.claude/settings.local.json +9 -9
- package/.gitattributes +1 -1
- package/CHANGELOG.md +149 -149
- package/PLAN-tables-and-postprocess.md +850 -850
- package/README.md +391 -391
- package/bin/rev.js +11 -11
- package/bin/rev.ts +145 -145
- package/completions/rev.bash +127 -127
- package/completions/rev.ps1 +210 -210
- package/completions/rev.zsh +207 -207
- package/dev_notes/stress2/build_adversarial.ts +186 -186
- package/dev_notes/stress2/drift_matcher.ts +62 -62
- package/dev_notes/stress2/probe_anchors.ts +35 -35
- package/dev_notes/stress2/project/discussion.before.md +3 -3
- package/dev_notes/stress2/project/discussion.md +3 -3
- package/dev_notes/stress2/project/methods.before.md +20 -20
- package/dev_notes/stress2/project/methods.md +20 -20
- package/dev_notes/stress2/project/rev.yaml +5 -5
- package/dev_notes/stress2/project/sections.yaml +4 -4
- package/dev_notes/stress2/sections.yaml +5 -5
- package/dev_notes/stress2/trace_placement.ts +50 -50
- package/dev_notes/stresstest_boundaries.ts +27 -27
- package/dev_notes/stresstest_drift_apply.ts +43 -43
- package/dev_notes/stresstest_drift_compare.ts +43 -43
- package/dev_notes/stresstest_drift_v2.ts +54 -54
- package/dev_notes/stresstest_inspect.ts +54 -54
- package/dev_notes/stresstest_pstyle.ts +55 -55
- package/dev_notes/stresstest_section_debug.ts +23 -23
- package/dev_notes/stresstest_split.ts +70 -70
- package/dev_notes/stresstest_trace.ts +19 -19
- package/dev_notes/stresstest_verify_no_overwrite.ts +40 -40
- package/dist/lib/build.d.ts +50 -1
- package/dist/lib/build.d.ts.map +1 -1
- package/dist/lib/build.js +80 -30
- package/dist/lib/build.js.map +1 -1
- package/dist/lib/commands/build.d.ts.map +1 -1
- package/dist/lib/commands/build.js +38 -5
- package/dist/lib/commands/build.js.map +1 -1
- package/dist/lib/commands/utilities.js +164 -164
- package/dist/lib/commands/word-tools.js +8 -8
- package/dist/lib/grammar.js +3 -3
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/import.js +146 -24
- package/dist/lib/import.js.map +1 -1
- package/dist/lib/pdf-comments.js +44 -44
- package/dist/lib/plugins.js +57 -57
- package/dist/lib/pptx-themes.js +115 -115
- package/dist/lib/spelling.js +2 -2
- package/dist/lib/templates.js +387 -387
- package/dist/lib/themes.js +51 -51
- package/dist/lib/types.d.ts +20 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/word-extraction.d.ts +6 -0
- package/dist/lib/word-extraction.d.ts.map +1 -1
- package/dist/lib/word-extraction.js +46 -3
- package/dist/lib/word-extraction.js.map +1 -1
- package/dist/lib/wordcomments.d.ts.map +1 -1
- package/dist/lib/wordcomments.js +23 -5
- package/dist/lib/wordcomments.js.map +1 -1
- package/eslint.config.js +27 -27
- package/lib/anchor-match.ts +276 -276
- package/lib/annotations.ts +644 -644
- package/lib/build.ts +1300 -1227
- package/lib/citations.ts +160 -160
- package/lib/commands/build.ts +833 -801
- package/lib/commands/citations.ts +515 -515
- package/lib/commands/comments.ts +1050 -1050
- package/lib/commands/context.ts +174 -174
- package/lib/commands/core.ts +309 -309
- package/lib/commands/doi.ts +435 -435
- package/lib/commands/file-ops.ts +372 -372
- package/lib/commands/history.ts +320 -320
- package/lib/commands/index.ts +87 -87
- package/lib/commands/init.ts +259 -259
- package/lib/commands/merge-resolve.ts +378 -378
- package/lib/commands/preview.ts +178 -178
- package/lib/commands/project-info.ts +244 -244
- package/lib/commands/quality.ts +517 -517
- package/lib/commands/response.ts +454 -454
- package/lib/commands/section-boundaries.ts +82 -82
- package/lib/commands/sections.ts +451 -451
- package/lib/commands/sync.ts +706 -706
- package/lib/commands/text-ops.ts +449 -449
- package/lib/commands/utilities.ts +448 -448
- package/lib/commands/verify-anchors.ts +272 -272
- package/lib/commands/word-tools.ts +340 -340
- package/lib/comment-realign.ts +517 -517
- package/lib/config.ts +84 -84
- package/lib/crossref.ts +781 -781
- package/lib/csl.ts +191 -191
- package/lib/dependencies.ts +98 -98
- package/lib/diff-engine.ts +465 -465
- package/lib/doi-cache.ts +115 -115
- package/lib/doi.ts +897 -897
- package/lib/equations.ts +506 -506
- package/lib/errors.ts +346 -346
- package/lib/format.ts +541 -541
- package/lib/git.ts +326 -326
- package/lib/grammar.ts +303 -303
- package/lib/image-registry.ts +180 -180
- package/lib/import.ts +911 -792
- package/lib/journals.ts +543 -543
- package/lib/merge.ts +633 -633
- package/lib/orcid.ts +144 -144
- package/lib/pdf-comments.ts +263 -263
- package/lib/pdf-import.ts +524 -524
- package/lib/plugins.ts +362 -362
- package/lib/postprocess.ts +188 -188
- package/lib/pptx-color-filter.lua +37 -37
- package/lib/pptx-template.ts +469 -469
- package/lib/pptx-themes.ts +483 -483
- package/lib/protect-restore.ts +520 -520
- package/lib/rate-limiter.ts +94 -94
- package/lib/response.ts +197 -197
- package/lib/restore-references.ts +240 -240
- package/lib/review.ts +327 -327
- package/lib/schema.ts +417 -417
- package/lib/scientific-words.ts +73 -73
- package/lib/sections.ts +335 -335
- package/lib/slides.ts +756 -756
- package/lib/spelling.ts +334 -334
- package/lib/templates.ts +526 -526
- package/lib/themes.ts +742 -742
- package/lib/trackchanges.ts +247 -247
- package/lib/tui.ts +450 -450
- package/lib/types.ts +550 -530
- package/lib/undo.ts +250 -250
- package/lib/utils.ts +69 -69
- package/lib/variables.ts +179 -179
- package/lib/word-extraction.ts +806 -759
- package/lib/word.ts +643 -643
- package/lib/wordcomments.ts +817 -798
- package/package.json +137 -137
- package/scripts/postbuild.js +28 -28
- package/skill/REFERENCE.md +431 -431
- package/skill/SKILL.md +258 -258
- package/tsconfig.json +26 -26
- package/types/index.d.ts +525 -525
|
@@ -229,179 +229,179 @@ export function register(program, pkg) {
|
|
|
229
229
|
}
|
|
230
230
|
// Helper functions for help text
|
|
231
231
|
function showFullHelp(pkg) {
|
|
232
|
-
console.log(`
|
|
233
|
-
${chalk.bold.cyan('rev')} ${chalk.dim(`v${pkg?.version || 'unknown'}`)} - Revision workflow for Word ↔ Markdown round-trips
|
|
234
|
-
|
|
235
|
-
${chalk.bold('DESCRIPTION')}
|
|
236
|
-
Handle reviewer feedback when collaborating on academic papers.
|
|
237
|
-
Import changes from Word, review them interactively, and preserve
|
|
238
|
-
comments for discussion with Claude.
|
|
239
|
-
|
|
240
|
-
${chalk.bold('GLOBAL OPTIONS')}
|
|
241
|
-
|
|
242
|
-
${chalk.bold('--no-color')} Disable colored output
|
|
243
|
-
${chalk.bold('-q, --quiet')} Suppress non-essential output
|
|
244
|
-
${chalk.bold('--json')} Output in JSON format (for scripting)
|
|
245
|
-
|
|
246
|
-
${chalk.bold('TYPICAL WORKFLOW')}
|
|
247
|
-
|
|
248
|
-
${chalk.dim('1.')} Build and send: ${chalk.green('rev build docx')} ${chalk.dim('(or rev b docx)')}
|
|
249
|
-
${chalk.dim('2.')} Reviewers return ${chalk.yellow('reviewed.docx')} with edits and comments
|
|
250
|
-
${chalk.dim('3.')} Sync their feedback: ${chalk.green('rev sync reviewed.docx')}
|
|
251
|
-
${chalk.dim('4.')} Work through comments: ${chalk.green('rev next')} ${chalk.dim('(n)')} / ${chalk.green('rev todo')} ${chalk.dim('(t)')}
|
|
252
|
-
${chalk.dim('5.')} Accept/reject changes: ${chalk.green('rev accept -a')} ${chalk.dim('(a)')} or ${chalk.green('rev review')}
|
|
253
|
-
${chalk.dim('6.')} Rebuild: ${chalk.green('rev build docx')}
|
|
254
|
-
${chalk.dim('7.')} Archive old files: ${chalk.green('rev archive')}
|
|
255
|
-
|
|
256
|
-
${chalk.bold('MORE HELP')}
|
|
257
|
-
|
|
258
|
-
rev help workflow Detailed workflow guide
|
|
259
|
-
rev help syntax Annotation syntax reference
|
|
260
|
-
rev help commands All commands with options
|
|
232
|
+
console.log(`
|
|
233
|
+
${chalk.bold.cyan('rev')} ${chalk.dim(`v${pkg?.version || 'unknown'}`)} - Revision workflow for Word ↔ Markdown round-trips
|
|
234
|
+
|
|
235
|
+
${chalk.bold('DESCRIPTION')}
|
|
236
|
+
Handle reviewer feedback when collaborating on academic papers.
|
|
237
|
+
Import changes from Word, review them interactively, and preserve
|
|
238
|
+
comments for discussion with Claude.
|
|
239
|
+
|
|
240
|
+
${chalk.bold('GLOBAL OPTIONS')}
|
|
241
|
+
|
|
242
|
+
${chalk.bold('--no-color')} Disable colored output
|
|
243
|
+
${chalk.bold('-q, --quiet')} Suppress non-essential output
|
|
244
|
+
${chalk.bold('--json')} Output in JSON format (for scripting)
|
|
245
|
+
|
|
246
|
+
${chalk.bold('TYPICAL WORKFLOW')}
|
|
247
|
+
|
|
248
|
+
${chalk.dim('1.')} Build and send: ${chalk.green('rev build docx')} ${chalk.dim('(or rev b docx)')}
|
|
249
|
+
${chalk.dim('2.')} Reviewers return ${chalk.yellow('reviewed.docx')} with edits and comments
|
|
250
|
+
${chalk.dim('3.')} Sync their feedback: ${chalk.green('rev sync reviewed.docx')}
|
|
251
|
+
${chalk.dim('4.')} Work through comments: ${chalk.green('rev next')} ${chalk.dim('(n)')} / ${chalk.green('rev todo')} ${chalk.dim('(t)')}
|
|
252
|
+
${chalk.dim('5.')} Accept/reject changes: ${chalk.green('rev accept -a')} ${chalk.dim('(a)')} or ${chalk.green('rev review')}
|
|
253
|
+
${chalk.dim('6.')} Rebuild: ${chalk.green('rev build docx')}
|
|
254
|
+
${chalk.dim('7.')} Archive old files: ${chalk.green('rev archive')}
|
|
255
|
+
|
|
256
|
+
${chalk.bold('MORE HELP')}
|
|
257
|
+
|
|
258
|
+
rev help workflow Detailed workflow guide
|
|
259
|
+
rev help syntax Annotation syntax reference
|
|
260
|
+
rev help commands All commands with options
|
|
261
261
|
`);
|
|
262
262
|
}
|
|
263
263
|
function showWorkflowHelp() {
|
|
264
|
-
console.log(`
|
|
265
|
-
${chalk.bold.cyan('rev')} ${chalk.dim('- Workflow Guide')}
|
|
266
|
-
|
|
267
|
-
${chalk.bold('OVERVIEW')}
|
|
268
|
-
|
|
269
|
-
The rev workflow solves a common problem: you write in Markdown,
|
|
270
|
-
but collaborators review in Word. When they return edited documents,
|
|
271
|
-
you need to merge their changes back into your source files.
|
|
272
|
-
|
|
273
|
-
${chalk.bold('STEP 1: BUILD & SEND')}
|
|
274
|
-
|
|
275
|
-
${chalk.green('rev build docx')}
|
|
276
|
-
${chalk.dim('# Send the .docx to reviewers')}
|
|
277
|
-
|
|
278
|
-
${chalk.bold('STEP 2: RECEIVE FEEDBACK')}
|
|
279
|
-
|
|
280
|
-
Reviewers edit the document, adding:
|
|
281
|
-
${chalk.dim('•')} Track changes (insertions, deletions)
|
|
282
|
-
${chalk.dim('•')} Comments (questions, suggestions)
|
|
283
|
-
|
|
284
|
-
${chalk.bold('STEP 3: SYNC CHANGES')}
|
|
285
|
-
|
|
286
|
-
${chalk.green('rev sync reviewed.docx')}
|
|
287
|
-
${chalk.dim('# Or just: rev sync (auto-detects most recent .docx)')}
|
|
288
|
-
|
|
289
|
-
Your markdown files now contain their feedback as annotations.
|
|
290
|
-
|
|
291
|
-
${chalk.bold('STEP 4: WORK THROUGH COMMENTS')}
|
|
292
|
-
|
|
293
|
-
${chalk.green('rev todo')} ${chalk.dim('# See all pending comments')}
|
|
294
|
-
${chalk.green('rev next')} ${chalk.dim('# Show next pending comment')}
|
|
295
|
-
${chalk.green('rev reply file.md -n 1 -m "Done"')}
|
|
296
|
-
${chalk.green('rev resolve file.md -n 1')}
|
|
297
|
-
|
|
298
|
-
${chalk.bold('STEP 5: ACCEPT/REJECT CHANGES')}
|
|
299
|
-
|
|
300
|
-
${chalk.green('rev accept file.md -a')} ${chalk.dim('# Accept all changes')}
|
|
301
|
-
${chalk.green('rev reject file.md -n 2')} ${chalk.dim('# Reject specific change')}
|
|
302
|
-
${chalk.dim('# Or use interactive mode:')}
|
|
303
|
-
${chalk.green('rev review file.md')}
|
|
304
|
-
|
|
305
|
-
${chalk.bold('STEP 6: REBUILD')}
|
|
306
|
-
|
|
307
|
-
${chalk.green('rev build docx')}
|
|
308
|
-
${chalk.green('rev build docx --dual')} ${chalk.dim('# Clean + comments version')}
|
|
309
|
-
|
|
310
|
-
${chalk.bold('STEP 7: ARCHIVE & REPEAT')}
|
|
311
|
-
|
|
312
|
-
${chalk.green('rev archive')} ${chalk.dim('# Move reviewer files to archive/')}
|
|
313
|
-
${chalk.dim('# Send new .docx, repeat cycle')}
|
|
264
|
+
console.log(`
|
|
265
|
+
${chalk.bold.cyan('rev')} ${chalk.dim('- Workflow Guide')}
|
|
266
|
+
|
|
267
|
+
${chalk.bold('OVERVIEW')}
|
|
268
|
+
|
|
269
|
+
The rev workflow solves a common problem: you write in Markdown,
|
|
270
|
+
but collaborators review in Word. When they return edited documents,
|
|
271
|
+
you need to merge their changes back into your source files.
|
|
272
|
+
|
|
273
|
+
${chalk.bold('STEP 1: BUILD & SEND')}
|
|
274
|
+
|
|
275
|
+
${chalk.green('rev build docx')}
|
|
276
|
+
${chalk.dim('# Send the .docx to reviewers')}
|
|
277
|
+
|
|
278
|
+
${chalk.bold('STEP 2: RECEIVE FEEDBACK')}
|
|
279
|
+
|
|
280
|
+
Reviewers edit the document, adding:
|
|
281
|
+
${chalk.dim('•')} Track changes (insertions, deletions)
|
|
282
|
+
${chalk.dim('•')} Comments (questions, suggestions)
|
|
283
|
+
|
|
284
|
+
${chalk.bold('STEP 3: SYNC CHANGES')}
|
|
285
|
+
|
|
286
|
+
${chalk.green('rev sync reviewed.docx')}
|
|
287
|
+
${chalk.dim('# Or just: rev sync (auto-detects most recent .docx)')}
|
|
288
|
+
|
|
289
|
+
Your markdown files now contain their feedback as annotations.
|
|
290
|
+
|
|
291
|
+
${chalk.bold('STEP 4: WORK THROUGH COMMENTS')}
|
|
292
|
+
|
|
293
|
+
${chalk.green('rev todo')} ${chalk.dim('# See all pending comments')}
|
|
294
|
+
${chalk.green('rev next')} ${chalk.dim('# Show next pending comment')}
|
|
295
|
+
${chalk.green('rev reply file.md -n 1 -m "Done"')}
|
|
296
|
+
${chalk.green('rev resolve file.md -n 1')}
|
|
297
|
+
|
|
298
|
+
${chalk.bold('STEP 5: ACCEPT/REJECT CHANGES')}
|
|
299
|
+
|
|
300
|
+
${chalk.green('rev accept file.md -a')} ${chalk.dim('# Accept all changes')}
|
|
301
|
+
${chalk.green('rev reject file.md -n 2')} ${chalk.dim('# Reject specific change')}
|
|
302
|
+
${chalk.dim('# Or use interactive mode:')}
|
|
303
|
+
${chalk.green('rev review file.md')}
|
|
304
|
+
|
|
305
|
+
${chalk.bold('STEP 6: REBUILD')}
|
|
306
|
+
|
|
307
|
+
${chalk.green('rev build docx')}
|
|
308
|
+
${chalk.green('rev build docx --dual')} ${chalk.dim('# Clean + comments version')}
|
|
309
|
+
|
|
310
|
+
${chalk.bold('STEP 7: ARCHIVE & REPEAT')}
|
|
311
|
+
|
|
312
|
+
${chalk.green('rev archive')} ${chalk.dim('# Move reviewer files to archive/')}
|
|
313
|
+
${chalk.dim('# Send new .docx, repeat cycle')}
|
|
314
314
|
`);
|
|
315
315
|
}
|
|
316
316
|
function showSyntaxHelp() {
|
|
317
|
-
console.log(`
|
|
318
|
-
${chalk.bold.cyan('rev')} ${chalk.dim('- Annotation Syntax (CriticMarkup)')}
|
|
319
|
-
|
|
320
|
-
${chalk.bold('INSERTIONS')}
|
|
321
|
-
|
|
322
|
-
Syntax: ${chalk.green('{++inserted text++}')}
|
|
323
|
-
Meaning: This text was added by the reviewer
|
|
324
|
-
|
|
325
|
-
Example:
|
|
326
|
-
We ${chalk.green('{++specifically++}')} focused on neophytes.
|
|
327
|
-
→ Reviewer added the word "specifically"
|
|
328
|
-
|
|
329
|
-
${chalk.bold('DELETIONS')}
|
|
330
|
-
|
|
331
|
-
Syntax: ${chalk.red('{--deleted text--}')}
|
|
332
|
-
Meaning: This text was removed by the reviewer
|
|
333
|
-
|
|
334
|
-
Example:
|
|
335
|
-
We focused on ${chalk.red('{--recent--}')} neophytes.
|
|
336
|
-
→ Reviewer removed the word "recent"
|
|
337
|
-
|
|
338
|
-
${chalk.bold('SUBSTITUTIONS')}
|
|
339
|
-
|
|
340
|
-
Syntax: ${chalk.yellow('{~~old text~>new text~~}')}
|
|
341
|
-
Meaning: Text was changed from old to new
|
|
342
|
-
|
|
343
|
-
Example:
|
|
344
|
-
The effect was ${chalk.yellow('{~~significant~>substantial~~}')}.
|
|
345
|
-
→ Reviewer changed "significant" to "substantial"
|
|
346
|
-
|
|
347
|
-
${chalk.bold('COMMENTS')}
|
|
348
|
-
|
|
349
|
-
Syntax: ${chalk.blue('{>>Author: comment text<<}')}
|
|
350
|
-
Meaning: Reviewer left a comment at this location
|
|
351
|
-
|
|
352
|
-
Example:
|
|
353
|
-
The results were significant. ${chalk.blue('{>>Dr. Smith: Add p-value<<}')}
|
|
354
|
-
→ Dr. Smith commented asking for a p-value
|
|
355
|
-
|
|
356
|
-
Comments are placed ${chalk.bold('after')} the text they reference.
|
|
317
|
+
console.log(`
|
|
318
|
+
${chalk.bold.cyan('rev')} ${chalk.dim('- Annotation Syntax (CriticMarkup)')}
|
|
319
|
+
|
|
320
|
+
${chalk.bold('INSERTIONS')}
|
|
321
|
+
|
|
322
|
+
Syntax: ${chalk.green('{++inserted text++}')}
|
|
323
|
+
Meaning: This text was added by the reviewer
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
We ${chalk.green('{++specifically++}')} focused on neophytes.
|
|
327
|
+
→ Reviewer added the word "specifically"
|
|
328
|
+
|
|
329
|
+
${chalk.bold('DELETIONS')}
|
|
330
|
+
|
|
331
|
+
Syntax: ${chalk.red('{--deleted text--}')}
|
|
332
|
+
Meaning: This text was removed by the reviewer
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
We focused on ${chalk.red('{--recent--}')} neophytes.
|
|
336
|
+
→ Reviewer removed the word "recent"
|
|
337
|
+
|
|
338
|
+
${chalk.bold('SUBSTITUTIONS')}
|
|
339
|
+
|
|
340
|
+
Syntax: ${chalk.yellow('{~~old text~>new text~~}')}
|
|
341
|
+
Meaning: Text was changed from old to new
|
|
342
|
+
|
|
343
|
+
Example:
|
|
344
|
+
The effect was ${chalk.yellow('{~~significant~>substantial~~}')}.
|
|
345
|
+
→ Reviewer changed "significant" to "substantial"
|
|
346
|
+
|
|
347
|
+
${chalk.bold('COMMENTS')}
|
|
348
|
+
|
|
349
|
+
Syntax: ${chalk.blue('{>>Author: comment text<<}')}
|
|
350
|
+
Meaning: Reviewer left a comment at this location
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
The results were significant. ${chalk.blue('{>>Dr. Smith: Add p-value<<}')}
|
|
354
|
+
→ Dr. Smith commented asking for a p-value
|
|
355
|
+
|
|
356
|
+
Comments are placed ${chalk.bold('after')} the text they reference.
|
|
357
357
|
`);
|
|
358
358
|
}
|
|
359
359
|
function showCommandsHelp() {
|
|
360
|
-
console.log(`
|
|
361
|
-
${chalk.bold.cyan('rev')} ${chalk.dim('- Command Reference')}
|
|
362
|
-
|
|
363
|
-
${chalk.bold('rev import')} <docx> <original-md>
|
|
364
|
-
|
|
365
|
-
Import changes from a Word document by comparing against your
|
|
366
|
-
original Markdown source.
|
|
367
|
-
|
|
368
|
-
${chalk.bold('Arguments:')}
|
|
369
|
-
docx Word document from reviewer
|
|
370
|
-
original-md Your original Markdown file
|
|
371
|
-
|
|
372
|
-
${chalk.bold('Options:')}
|
|
373
|
-
-o, --output <file> Write to different file (default: overwrites original)
|
|
374
|
-
-a, --author <name> Author name for changes (default: "Reviewer")
|
|
375
|
-
--dry-run Preview changes without saving
|
|
376
|
-
|
|
377
|
-
${chalk.bold('rev review')} <file>
|
|
378
|
-
|
|
379
|
-
Interactively review and accept/reject track changes.
|
|
380
|
-
Comments are preserved; only track changes are processed.
|
|
381
|
-
|
|
382
|
-
${chalk.bold('Keys:')}
|
|
383
|
-
a Accept this change
|
|
384
|
-
r Reject this change
|
|
385
|
-
s Skip (decide later)
|
|
386
|
-
A Accept all remaining changes
|
|
387
|
-
L Reject all remaining changes
|
|
388
|
-
q Quit without saving
|
|
389
|
-
|
|
390
|
-
${chalk.bold('rev strip')} <file>
|
|
391
|
-
|
|
392
|
-
Remove annotations, outputting clean Markdown.
|
|
393
|
-
Track changes are applied (insertions kept, deletions removed).
|
|
394
|
-
|
|
395
|
-
${chalk.bold('Options:')}
|
|
396
|
-
-o, --output <file> Write to file (default: stdout)
|
|
397
|
-
-c, --keep-comments Keep comment annotations
|
|
398
|
-
|
|
399
|
-
${chalk.bold('rev help')} [topic]
|
|
400
|
-
|
|
401
|
-
Show help. Optional topics:
|
|
402
|
-
workflow Step-by-step workflow guide
|
|
403
|
-
syntax Annotation syntax reference
|
|
404
|
-
commands This command reference
|
|
360
|
+
console.log(`
|
|
361
|
+
${chalk.bold.cyan('rev')} ${chalk.dim('- Command Reference')}
|
|
362
|
+
|
|
363
|
+
${chalk.bold('rev import')} <docx> <original-md>
|
|
364
|
+
|
|
365
|
+
Import changes from a Word document by comparing against your
|
|
366
|
+
original Markdown source.
|
|
367
|
+
|
|
368
|
+
${chalk.bold('Arguments:')}
|
|
369
|
+
docx Word document from reviewer
|
|
370
|
+
original-md Your original Markdown file
|
|
371
|
+
|
|
372
|
+
${chalk.bold('Options:')}
|
|
373
|
+
-o, --output <file> Write to different file (default: overwrites original)
|
|
374
|
+
-a, --author <name> Author name for changes (default: "Reviewer")
|
|
375
|
+
--dry-run Preview changes without saving
|
|
376
|
+
|
|
377
|
+
${chalk.bold('rev review')} <file>
|
|
378
|
+
|
|
379
|
+
Interactively review and accept/reject track changes.
|
|
380
|
+
Comments are preserved; only track changes are processed.
|
|
381
|
+
|
|
382
|
+
${chalk.bold('Keys:')}
|
|
383
|
+
a Accept this change
|
|
384
|
+
r Reject this change
|
|
385
|
+
s Skip (decide later)
|
|
386
|
+
A Accept all remaining changes
|
|
387
|
+
L Reject all remaining changes
|
|
388
|
+
q Quit without saving
|
|
389
|
+
|
|
390
|
+
${chalk.bold('rev strip')} <file>
|
|
391
|
+
|
|
392
|
+
Remove annotations, outputting clean Markdown.
|
|
393
|
+
Track changes are applied (insertions kept, deletions removed).
|
|
394
|
+
|
|
395
|
+
${chalk.bold('Options:')}
|
|
396
|
+
-o, --output <file> Write to file (default: stdout)
|
|
397
|
+
-c, --keep-comments Keep comment annotations
|
|
398
|
+
|
|
399
|
+
${chalk.bold('rev help')} [topic]
|
|
400
|
+
|
|
401
|
+
Show help. Optional topics:
|
|
402
|
+
workflow Step-by-step workflow guide
|
|
403
|
+
syntax Annotation syntax reference
|
|
404
|
+
commands This command reference
|
|
405
405
|
`);
|
|
406
406
|
}
|
|
407
407
|
//# sourceMappingURL=utilities.js.map
|
|
@@ -50,16 +50,16 @@ export function register(program) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
else {
|
|
53
|
-
commentsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
54
|
-
<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
53
|
+
commentsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
54
|
+
<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
55
55
|
</w:comments>`;
|
|
56
56
|
}
|
|
57
57
|
const author = options.author || getUserName() || 'Claude';
|
|
58
58
|
const date = new Date().toISOString();
|
|
59
59
|
const commentId = nextCommentId;
|
|
60
60
|
// Add comment to comments.xml
|
|
61
|
-
const newComment = `<w:comment w:id="${commentId}" w:author="${author}" w:date="${date}">
|
|
62
|
-
<w:p><w:r><w:t>${options.message}</w:t></w:r></w:p>
|
|
61
|
+
const newComment = `<w:comment w:id="${commentId}" w:author="${author}" w:date="${date}">
|
|
62
|
+
<w:p><w:r><w:t>${options.message}</w:t></w:r></w:p>
|
|
63
63
|
</w:comment>`;
|
|
64
64
|
commentsXml = commentsXml.replace('</w:comments>', `${newComment}\n</w:comments>`);
|
|
65
65
|
// Find text and add comment markers
|
|
@@ -200,15 +200,15 @@ export function register(program) {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
else {
|
|
203
|
-
commentsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
204
|
-
<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
203
|
+
commentsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
204
|
+
<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
205
205
|
</w:comments>`;
|
|
206
206
|
}
|
|
207
207
|
const date = new Date().toISOString();
|
|
208
208
|
const commentId = nextCommentId;
|
|
209
209
|
// Add comment to comments.xml
|
|
210
|
-
const newComment = `<w:comment w:id="${commentId}" w:author="${author}" w:date="${date}">
|
|
211
|
-
<w:p><w:r><w:t>${message}</w:t></w:r></w:p>
|
|
210
|
+
const newComment = `<w:comment w:id="${commentId}" w:author="${author}" w:date="${date}">
|
|
211
|
+
<w:p><w:r><w:t>${message}</w:t></w:r></w:p>
|
|
212
212
|
</w:comment>`;
|
|
213
213
|
commentsXml = commentsXml.replace('</w:comments>', `${newComment}\n</w:comments>`);
|
|
214
214
|
// Find text and add comment markers
|
package/dist/lib/grammar.js
CHANGED
|
@@ -125,9 +125,9 @@ export function loadDictionary(directory = '.') {
|
|
|
125
125
|
*/
|
|
126
126
|
export function saveDictionary(words, directory = '.') {
|
|
127
127
|
const dictPath = path.join(directory, DEFAULT_DICT_NAME);
|
|
128
|
-
const header = `# Custom dictionary for docrev
|
|
129
|
-
# Add one word per line
|
|
130
|
-
# Lines starting with # are comments
|
|
128
|
+
const header = `# Custom dictionary for docrev
|
|
129
|
+
# Add one word per line
|
|
130
|
+
# Lines starting with # are comments
|
|
131
131
|
`;
|
|
132
132
|
const content = header + [...words].sort().join('\n') + '\n';
|
|
133
133
|
fs.writeFileSync(dictPath, content, 'utf-8');
|
package/dist/lib/import.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../lib/import.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../lib/import.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgBH,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,SAAS,EAEV,MAAM,sBAAsB,CAAC;AA0E9B,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,WAAW,EACX,QAAQ,EACR,iBAAiB,EACjB,oBAAoB,EACpB,WAAW,EACX,SAAS,EACT,SAAS,EACT,sBAAsB,EACtB,cAAc,EACd,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,yBAAyB,CAAC;AAQjC,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxD;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACnE;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gCAAgC;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,eAAe,EAAE,OAAO,CAAC;QACzB,gBAAgB,EAAE;YAAE,UAAU,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;KAC7D,CAAC;IACF,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAuED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAAC,EAChD,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAuTR;AAED;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,gCAAgC,CAAC,CA4F3C;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CA8I/B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAiB,GACxB,wBAAwB,CAuB1B"}
|
package/dist/lib/import.js
CHANGED
|
@@ -103,6 +103,44 @@ function pushPastSectionHeading(text, pos) {
|
|
|
103
103
|
}
|
|
104
104
|
return after;
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Snap a position to the nearest whitespace boundary within ±50 chars so a
|
|
108
|
+
* proportional fallback insertion never lands mid-word.
|
|
109
|
+
*/
|
|
110
|
+
function snapToWordBoundary(text, pos) {
|
|
111
|
+
if (pos <= 0)
|
|
112
|
+
return 0;
|
|
113
|
+
if (pos >= text.length)
|
|
114
|
+
return text.length;
|
|
115
|
+
if (/\s/.test(text[pos] ?? ''))
|
|
116
|
+
return pos;
|
|
117
|
+
for (let d = 1; d <= 50; d++) {
|
|
118
|
+
if (pos + d < text.length && /\s/.test(text[pos + d] ?? ''))
|
|
119
|
+
return pos + d;
|
|
120
|
+
if (pos - d >= 0 && /\s/.test(text[pos - d] ?? ''))
|
|
121
|
+
return pos - d;
|
|
122
|
+
}
|
|
123
|
+
return pos;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Final-resort placement when every text-matching strategy failed. The docx
|
|
127
|
+
* carries a real `<w:commentRangeStart w:id="N">` marker at a known offset
|
|
128
|
+
* inside its body text — that's a structural anchor, even if the anchored
|
|
129
|
+
* span itself is empty and the surrounding context drifted in the target.
|
|
130
|
+
*
|
|
131
|
+
* Map docPosition into the target markdown proportionally and snap to a word
|
|
132
|
+
* boundary. This is approximate when the document was heavily restructured,
|
|
133
|
+
* but it's strictly better than silently dropping a reviewer's comment: the
|
|
134
|
+
* comment lands in roughly the right neighborhood and the reviewer can
|
|
135
|
+
* relocate it during their next pass.
|
|
136
|
+
*/
|
|
137
|
+
function proportionalFallback(anchorData, target) {
|
|
138
|
+
if (anchorData.docLength <= 0)
|
|
139
|
+
return null;
|
|
140
|
+
const proportion = Math.min(anchorData.docPosition / anchorData.docLength, 1.0);
|
|
141
|
+
const rawPos = Math.floor(proportion * target.length);
|
|
142
|
+
return pushPastSectionHeading(target, snapToWordBoundary(target, rawPos));
|
|
143
|
+
}
|
|
106
144
|
/**
|
|
107
145
|
* Insert comments into markdown text based on anchor texts with context
|
|
108
146
|
*/
|
|
@@ -113,10 +151,42 @@ export function insertCommentsIntoMarkdown(markdown, comments, anchors, options
|
|
|
113
151
|
let placedCount = 0;
|
|
114
152
|
const duplicateWarnings = [];
|
|
115
153
|
const usedPositions = new Set(); // For tie-breaking: track used positions
|
|
154
|
+
// Resolve threading: replies share their parent's anchor in Word, so they
|
|
155
|
+
// must inherit the parent's position and ride alongside it as one cluster.
|
|
156
|
+
// Letting each reply run through anchor scoring scatters the cluster (the
|
|
157
|
+
// same docPosition forces `usedPositions` to push later replies onto a
|
|
158
|
+
// different occurrence), which on re-build looks like independent comments
|
|
159
|
+
// and loses the paraIdParent threading. See gcol33/docrev issue #2.
|
|
160
|
+
const inputById = new Map();
|
|
161
|
+
for (const c of comments)
|
|
162
|
+
inputById.set(c.id, c);
|
|
163
|
+
function rootIdOf(c) {
|
|
164
|
+
let cur = c;
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
while (cur.parentId && !seen.has(cur.id)) {
|
|
167
|
+
seen.add(cur.id);
|
|
168
|
+
const parent = inputById.get(cur.parentId);
|
|
169
|
+
if (!parent || parent === cur)
|
|
170
|
+
break;
|
|
171
|
+
cur = parent;
|
|
172
|
+
}
|
|
173
|
+
return cur.id;
|
|
174
|
+
}
|
|
175
|
+
const replyRootId = new Map();
|
|
176
|
+
for (const c of comments) {
|
|
177
|
+
const root = rootIdOf(c);
|
|
178
|
+
if (root !== c.id)
|
|
179
|
+
replyRootId.set(c.id, root);
|
|
180
|
+
}
|
|
116
181
|
// Anchor matching primitives live in lib/anchor-match.ts so that
|
|
117
182
|
// `rev verify-anchors` can use the same strategies for drift reporting.
|
|
118
|
-
// Get all positions in order (for sequential tie-breaking)
|
|
183
|
+
// Get all positions in order (for sequential tie-breaking).
|
|
184
|
+
// Replies skip scoring entirely — they piggyback on their root's position
|
|
185
|
+
// in the emit pass below.
|
|
119
186
|
const commentsWithPositions = comments.map((c) => {
|
|
187
|
+
if (replyRootId.has(c.id)) {
|
|
188
|
+
return { ...c, pos: -1, anchorText: null, strategy: 'reply' };
|
|
189
|
+
}
|
|
120
190
|
const anchorData = anchors.get(c.id);
|
|
121
191
|
if (!anchorData) {
|
|
122
192
|
unmatchedCount++;
|
|
@@ -212,12 +282,28 @@ export function insertCommentsIntoMarkdown(markdown, comments, anchors, options
|
|
|
212
282
|
return { ...c, pos: occurrences[0], anchorText: null, isEmpty: true };
|
|
213
283
|
}
|
|
214
284
|
}
|
|
285
|
+
// Last resort: docx carried a structural marker at docPosition; map
|
|
286
|
+
// it proportionally into the target so the comment isn't dropped.
|
|
287
|
+
if (typeof anchorData === 'object') {
|
|
288
|
+
const fallback = proportionalFallback(anchorData, result);
|
|
289
|
+
if (fallback !== null) {
|
|
290
|
+
return { ...c, pos: fallback, anchorText: null, isEmpty: true, strategy: 'proportional-fallback' };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
215
293
|
unmatchedCount++;
|
|
216
294
|
return { ...c, pos: -1, anchorText: null, isEmpty: true };
|
|
217
295
|
}
|
|
218
296
|
// Text-based matching strategies
|
|
219
297
|
const { occurrences, matchedAnchor, strategy, stripped } = findAnchorInText(anchor, result, before, after);
|
|
220
298
|
if (occurrences.length === 0) {
|
|
299
|
+
// Same last-resort as the empty-anchor path: anchor text is gone from
|
|
300
|
+
// the target, but the marker's text-offset survived extraction.
|
|
301
|
+
if (typeof anchorData === 'object') {
|
|
302
|
+
const fallback = proportionalFallback(anchorData, result);
|
|
303
|
+
if (fallback !== null) {
|
|
304
|
+
return { ...c, pos: fallback, anchorText: null, strategy: 'proportional-fallback' };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
221
307
|
unmatchedCount++;
|
|
222
308
|
return { ...c, pos: -1, anchorText: null };
|
|
223
309
|
}
|
|
@@ -244,51 +330,87 @@ export function insertCommentsIntoMarkdown(markdown, comments, anchors, options
|
|
|
244
330
|
return { ...c, pos: finalIdx, anchorText: null };
|
|
245
331
|
}
|
|
246
332
|
});
|
|
247
|
-
//
|
|
248
|
-
|
|
333
|
+
// Group comments into clusters (root + ordered replies). The root carries
|
|
334
|
+
// the resolved position; replies inherit it and ride along in input order
|
|
335
|
+
// so the rebuilt CriticMarkup looks like `{>>p<<}{>>r1<<}{>>r2<<}[anchor]`
|
|
336
|
+
// and adjacency-based reply detection picks the cluster up again.
|
|
337
|
+
const byId = new Map();
|
|
338
|
+
for (const cwp of commentsWithPositions)
|
|
339
|
+
byId.set(cwp.id, cwp);
|
|
340
|
+
const repliesByRoot = new Map();
|
|
341
|
+
for (const c of comments) {
|
|
342
|
+
const rootId = replyRootId.get(c.id);
|
|
343
|
+
if (!rootId)
|
|
344
|
+
continue;
|
|
345
|
+
const cwp = byId.get(c.id);
|
|
346
|
+
if (!cwp)
|
|
347
|
+
continue;
|
|
348
|
+
const list = repliesByRoot.get(rootId);
|
|
349
|
+
if (list)
|
|
350
|
+
list.push(cwp);
|
|
351
|
+
else
|
|
352
|
+
repliesByRoot.set(rootId, [cwp]);
|
|
353
|
+
}
|
|
354
|
+
// Replies whose root never resolved (parent missing from the input slice or
|
|
355
|
+
// parent unmatched) count as unmatched too — there's no position to attach
|
|
356
|
+
// them to.
|
|
357
|
+
for (const [rootId, replies] of repliesByRoot) {
|
|
358
|
+
const root = byId.get(rootId);
|
|
359
|
+
if (!root || root.pos < 0) {
|
|
360
|
+
unmatchedCount += replies.length;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Roots only — replies attach during emission.
|
|
364
|
+
const rootsWithPos = commentsWithPositions.filter(c => !replyRootId.has(c.id));
|
|
365
|
+
// Log any unmatched roots for debugging
|
|
366
|
+
const unmatched = rootsWithPos.filter((c) => c.pos < 0);
|
|
249
367
|
if (process.env.DEBUG) {
|
|
250
|
-
console.log(`[DEBUG] insertComments: ${comments.length} input, ${
|
|
368
|
+
console.log(`[DEBUG] insertComments: ${comments.length} input, ${rootsWithPos.length} roots, ${unmatched.length} unmatched roots, ${replyRootId.size} replies`);
|
|
251
369
|
if (unmatched.length > 0) {
|
|
252
370
|
unmatched.forEach(c => console.log(`[DEBUG] Unmatched ID=${c.id}: anchor="${(c.anchorText || 'none').slice(0, 30)}"`));
|
|
253
371
|
}
|
|
254
372
|
}
|
|
255
|
-
const
|
|
373
|
+
const matchedRoots = rootsWithPos.filter((c) => c.pos >= 0);
|
|
256
374
|
// Sort by position descending (insert from end to avoid offset issues)
|
|
257
|
-
|
|
258
|
-
// Insert each
|
|
375
|
+
matchedRoots.sort((a, b) => b.pos - a.pos);
|
|
376
|
+
// Insert each cluster. With `wrapAnchor` (the default), the anchor text
|
|
259
377
|
// gets wrapped in `[anchor]{.mark}` so the rebuilt docx restores the
|
|
260
378
|
// original Word comment range. Without it, the comment block is inserted
|
|
261
379
|
// adjacent to the anchor and prose stays untouched — required for
|
|
262
380
|
// comments-only sync where multiple comments may share one anchor.
|
|
263
|
-
// Skip insertion when
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
// away.
|
|
381
|
+
// Skip insertion when the parent's CriticMarkup already lives near the
|
|
382
|
+
// target — re-running sync against the same docx would otherwise stack
|
|
383
|
+
// duplicates. A 200-char window catches both wrapped
|
|
384
|
+
// (`{>>...<<}[anchor]{.mark}`) and bare (`{>>...<<}anchor`) forms while
|
|
385
|
+
// ignoring incidental matches farther away.
|
|
269
386
|
let dedupedCount = 0;
|
|
270
|
-
for (const c of
|
|
271
|
-
const
|
|
387
|
+
for (const c of matchedRoots) {
|
|
388
|
+
const parentBlock = `{>>${c.author}: ${c.text}<<}`;
|
|
389
|
+
const replies = repliesByRoot.get(c.id) ?? [];
|
|
272
390
|
const windowStart = Math.max(0, c.pos - 200);
|
|
273
391
|
const windowEnd = Math.min(result.length, c.pos + 200);
|
|
274
|
-
if (result.slice(windowStart, windowEnd).includes(
|
|
275
|
-
|
|
392
|
+
if (result.slice(windowStart, windowEnd).includes(parentBlock)) {
|
|
393
|
+
// Cluster already synced; treat all members as deduped.
|
|
394
|
+
dedupedCount += 1 + replies.length;
|
|
276
395
|
continue;
|
|
277
396
|
}
|
|
397
|
+
// Replies carry an explicit `↪ ` author prefix so the round-trip does not
|
|
398
|
+
// depend on positional adjacency in the markdown. On dense reviewer docs
|
|
399
|
+
// distinct clusters frequently land at the same anchor position; without
|
|
400
|
+
// the prefix the re-parse would misthread them. The injection side strips
|
|
401
|
+
// `↪ ` back off the author so Word renders the original name.
|
|
402
|
+
const replyBlocks = replies.map(r => `{>>↪ ${r.author}: ${r.text}<<}`);
|
|
403
|
+
const combined = parentBlock + replyBlocks.join('');
|
|
278
404
|
if (wrapAnchor && c.anchorText && c.anchorEnd) {
|
|
279
405
|
const before = result.slice(0, c.pos);
|
|
280
406
|
const anchor = result.slice(c.pos, c.anchorEnd);
|
|
281
407
|
const after = result.slice(c.anchorEnd);
|
|
282
|
-
result = before +
|
|
408
|
+
result = before + combined + `[${anchor}]{.mark}` + after;
|
|
283
409
|
}
|
|
284
410
|
else {
|
|
285
|
-
|
|
286
|
-
// tweaks; CriticMarkup blocks are invisible to readers, and adding a
|
|
287
|
-
// leading space would shift prose byte-for-byte (relevant when callers
|
|
288
|
-
// verify that --comments-only didn't touch the original).
|
|
289
|
-
result = result.slice(0, c.pos) + comment + result.slice(c.pos);
|
|
411
|
+
result = result.slice(0, c.pos) + combined + result.slice(c.pos);
|
|
290
412
|
}
|
|
291
|
-
placedCount
|
|
413
|
+
placedCount += 1 + replies.length;
|
|
292
414
|
}
|
|
293
415
|
if (outStats) {
|
|
294
416
|
outStats.placed = placedCount;
|