docrev 0.11.2 → 0.11.3

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,164 @@
1
+ /**
2
+ * Input routing: read a command's `[file]` argument as CriticMarkup Markdown,
3
+ * transparently handling Word documents.
4
+ *
5
+ * `rev status`/`rev comments` and friends were written to `readFileSync(file,
6
+ * 'utf-8')` every argument and regex it for CriticMarkup. A `.docx` is a binary
7
+ * ZIP; decoded as UTF-8 it occasionally yields a stray `{~~..~>..~~}` byte
8
+ * sequence, so the tool reported a small, plausible, wrong count with no error
9
+ * (gcol33/docrev#8). This module is the single front door that both:
10
+ *
11
+ * - detects a Word document by extension AND by ZIP magic + `word/document.xml`
12
+ * (so a mis-extensioned `.docx` renamed to `.md`/`.txt` is still caught), and
13
+ * - converts it to the same annotated Markdown `rev import` produces, so every
14
+ * downstream reader (`countAnnotations`, `getComments`, ...) sees real tags.
15
+ *
16
+ * Commands that only read report through `readAnnotatedInput`; commands that
17
+ * edit in place call `assertEditableMarkdown` first and refuse a `.docx` with a
18
+ * pointer to `rev import`, since CriticMarkup cannot be written back into a
19
+ * binary ZIP.
20
+ */
21
+ import * as fs from 'fs';
22
+ import * as path from 'path';
23
+ import { openDocx } from './ooxml.js';
24
+ import { exitWithError, requireFile } from './errors.js';
25
+ /** ZIP local-file-header magic: the first four bytes of any `.docx`. */
26
+ const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
27
+ /** OOXML Word extensions (all ZIP-backed, none Markdown). */
28
+ const WORD_EXTENSIONS = new Set(['.docx', '.docm', '.dotx', '.dotm']);
29
+ /**
30
+ * A file cannot be read as (or converted to) CriticMarkup Markdown. Carries
31
+ * actionable suggestions so the command layer can render a helpful error.
32
+ */
33
+ export class InputError extends Error {
34
+ suggestions;
35
+ constructor(message, suggestions = []) {
36
+ super(message);
37
+ this.name = 'InputError';
38
+ this.suggestions = suggestions;
39
+ }
40
+ }
41
+ /** Read the first bytes of a file, or an empty buffer if unreadable. */
42
+ function readMagic(file, length = 4) {
43
+ let fd;
44
+ try {
45
+ fd = fs.openSync(file, 'r');
46
+ const buf = Buffer.alloc(length);
47
+ const n = fs.readSync(fd, buf, 0, length, 0);
48
+ return buf.subarray(0, n);
49
+ }
50
+ catch {
51
+ return Buffer.alloc(0);
52
+ }
53
+ finally {
54
+ if (fd !== undefined)
55
+ fs.closeSync(fd);
56
+ }
57
+ }
58
+ /** True when the file begins with the ZIP local-file-header magic. */
59
+ export function looksLikeZip(file) {
60
+ return readMagic(file).equals(ZIP_MAGIC);
61
+ }
62
+ /**
63
+ * True when `file` is a Word document: it carries a Word extension, or it is a
64
+ * ZIP whose package contains `word/document.xml`. The content sniff catches a
65
+ * `.docx` renamed to `.md`/`.txt`, so those never get regexed as CriticMarkup.
66
+ */
67
+ export function isWordDocument(file) {
68
+ if (WORD_EXTENSIONS.has(path.extname(file).toLowerCase()))
69
+ return true;
70
+ if (!looksLikeZip(file))
71
+ return false;
72
+ try {
73
+ return openDocx(file).getEntry('word/document.xml') !== null;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ /** A NUL byte in the head reliably marks binary; UTF-8 Markdown never has one. */
80
+ function isBinaryBuffer(buf) {
81
+ if (buf.length >= 4 && buf.subarray(0, 4).equals(ZIP_MAGIC))
82
+ return true;
83
+ return buf.subarray(0, Math.min(buf.length, 8192)).includes(0x00);
84
+ }
85
+ /**
86
+ * Read a file as the CriticMarkup Markdown a command expects. A Word document
87
+ * is converted through the existing OOXML/pandoc reader (real insertions,
88
+ * deletions, substitutions, and comments); a Markdown/text file is read as
89
+ * UTF-8. A non-Word binary (image, PDF, unknown ZIP) is rejected rather than
90
+ * silently miscounted.
91
+ *
92
+ * @throws InputError if the file is a binary that is not a Word document, or a
93
+ * Word document that cannot be parsed.
94
+ */
95
+ export async function readAnnotatedInput(file) {
96
+ if (isWordDocument(file)) {
97
+ const { readDocxAsAnnotatedMarkdown } = await import('./import.js');
98
+ try {
99
+ return await readDocxAsAnnotatedMarkdown(file);
100
+ }
101
+ catch (err) {
102
+ throw new InputError(`Failed to read Word document ${path.basename(file)}: ${err.message}`, ['Run "rev import <docx>" to convert it to annotated Markdown first']);
103
+ }
104
+ }
105
+ const buf = fs.readFileSync(file);
106
+ if (isBinaryBuffer(buf)) {
107
+ throw new InputError(`${path.basename(file)} is not a text or Markdown file (binary content).`, [
108
+ 'Word documents: run "rev import <docx>" first, or pass the imported .md',
109
+ 'This command reads CriticMarkup Markdown, not binary files',
110
+ ]);
111
+ }
112
+ return buf.toString('utf-8');
113
+ }
114
+ /**
115
+ * Guard for commands that edit the file in place (accept/reject/resolve/reply/
116
+ * review). A `.docx` cannot hold CriticMarkup, so refuse it with a pointer to
117
+ * `rev import` instead of corrupting the document.
118
+ *
119
+ * @throws InputError if `file` is a Word document.
120
+ */
121
+ export function assertEditableMarkdown(file) {
122
+ if (isWordDocument(file)) {
123
+ const base = path.basename(file);
124
+ throw new InputError(`${base} is a Word document; this command edits Markdown in place.`, [
125
+ `Run "rev import ${base}" to get an editable Markdown file, then edit that`,
126
+ 'Read-only inspection works directly: "rev status" / "rev comments"',
127
+ ]);
128
+ }
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // Command-facing wrappers: translate InputError into the CLI's error surface.
132
+ // ---------------------------------------------------------------------------
133
+ /**
134
+ * Command front door: verify the file exists, then read it as annotated
135
+ * Markdown (converting a `.docx` on the way). On a bad input, print a friendly
136
+ * error and exit — matching how the rest of the CLI reports failures.
137
+ */
138
+ export async function loadAnnotated(file, fileType = 'Markdown file') {
139
+ requireFile(file, fileType);
140
+ try {
141
+ return await readAnnotatedInput(file);
142
+ }
143
+ catch (err) {
144
+ if (err instanceof InputError)
145
+ exitWithError(err.message, err.suggestions);
146
+ throw err;
147
+ }
148
+ }
149
+ /**
150
+ * Command front door for editing commands: verify existence and refuse a Word
151
+ * document (which cannot be edited in place), printing guidance and exiting.
152
+ */
153
+ export function requireEditableMarkdown(file, fileType = 'Markdown file') {
154
+ requireFile(file, fileType);
155
+ try {
156
+ assertEditableMarkdown(file);
157
+ }
158
+ catch (err) {
159
+ if (err instanceof InputError)
160
+ exitWithError(err.message, err.suggestions);
161
+ throw err;
162
+ }
163
+ }
164
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../../lib/input.ts"],"names":[],"mappings":"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"}
@@ -36,6 +36,6 @@ export declare function generateResponseLetter(comments: CommentWithReplies[], o
36
36
  /**
37
37
  * Collect comments from multiple files
38
38
  */
39
- export declare function collectComments(files: string[]): CommentWithReplies[];
39
+ export declare function collectComments(files: string[]): Promise<CommentWithReplies[]>;
40
40
  export {};
41
41
  //# sourceMappingURL=response.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../lib/response.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,UAAU,KAAK;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAW,GAAG,kBAAkB,EAAE,CAoC9F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAYjG;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAwF5G;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAYrE"}
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../lib/response.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,UAAU,KAAK;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAW,GAAG,kBAAkB,EAAE,CAoC9F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAYjG;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CAwF5G;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAepF"}
@@ -136,12 +136,15 @@ export function generateResponseLetter(comments, options = {}) {
136
136
  /**
137
137
  * Collect comments from multiple files
138
138
  */
139
- export function collectComments(files) {
139
+ export async function collectComments(files) {
140
+ const { readAnnotatedInput } = await import('./input.js');
140
141
  const allComments = [];
141
142
  for (const file of files) {
142
143
  if (!fs.existsSync(file))
143
144
  continue;
144
- const text = fs.readFileSync(file, 'utf-8');
145
+ // Route each file through the shared reader so a returned `.docx` is
146
+ // converted to CriticMarkup first, rather than regexing the binary ZIP.
147
+ const text = await readAnnotatedInput(file);
145
148
  const comments = parseCommentsWithReplies(text, path.basename(file));
146
149
  allComments.push(...comments);
147
150
  }
@@ -1 +1 @@
1
- {"version":3,"file":"response.js","sourceRoot":"","sources":["../../lib/response.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"response.js","sourceRoot":"","sources":["../../lib/response.ts"],"names":[],"mappings":"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"}
@@ -124,6 +124,34 @@ function isCommentFalsePositive(commentContent: string, fullText: string, positi
124
124
  // Combined pattern for any track change (not comments)
125
125
  const TRACK_CHANGE_PATTERN = /(\{\+\+.+?\+\+\}|\{--.+?--\}|\{~~.+?~>.+?~~\})/gs;
126
126
 
127
+ /**
128
+ * Reject binary content before it reaches the CriticMarkup regexes. A `.docx`
129
+ * is a ZIP; read as UTF-8 it starts with `PK\x03\x04` and is riddled with NUL
130
+ * bytes from the DEFLATE stream. Running the annotation patterns over that
131
+ * yields accidental byte-matches that look like a small, plausible count —
132
+ * silent garbage. This guard turns that whole failure class into a loud error.
133
+ * Callers that legitimately handle Word documents route them through
134
+ * `readAnnotatedInput` (lib/input.ts), which converts the docx to CriticMarkup
135
+ * first, so this only fires on a raw binary that reached a text-only path.
136
+ *
137
+ * UTF-8 Markdown never contains a NUL byte, so the check cannot false-positive
138
+ * on real input; it scans only the head to stay O(1) on large documents.
139
+ * @throws Error if the text is binary rather than text/Markdown.
140
+ */
141
+ function assertNotBinary(text: string): void {
142
+ // ZIP local-file-header magic "PK\x03\x04" (docx/xlsx/pptx read as text),
143
+ // or any NUL byte in the head: a reliable binary marker absent from UTF-8
144
+ // Markdown, so this never false-positives on real input. Scans only the
145
+ // head to stay cheap on large documents.
146
+ if (text.startsWith('PK\u0003\u0004') || text.slice(0, 8192).includes('\u0000')) {
147
+ throw new Error(
148
+ 'Expected text/Markdown but received binary content. If this is a Word ' +
149
+ 'document, convert it first with "rev import <docx>" (rev status/comments ' +
150
+ 'read .docx directly; other commands need the imported Markdown).',
151
+ );
152
+ }
153
+ }
154
+
127
155
  // =============================================================================
128
156
  // Public API
129
157
  // =============================================================================
@@ -138,6 +166,7 @@ export function parseAnnotations(text: string): Annotation[] {
138
166
  if (typeof text !== 'string') {
139
167
  throw new TypeError(`text must be a string, got ${typeof text}`);
140
168
  }
169
+ assertNotBinary(text);
141
170
 
142
171
  const annotations: Annotation[] = [];
143
172
 
@@ -257,6 +286,7 @@ export function stripAnnotations(text: string, options: StripOptions = {}): stri
257
286
  if (typeof text !== 'string') {
258
287
  throw new TypeError(`text must be a string, got ${typeof text}`);
259
288
  }
289
+ assertNotBinary(text);
260
290
 
261
291
  const { keepComments = false } = options;
262
292
 
@@ -427,6 +457,7 @@ export function hasAnnotations(text: string): boolean {
427
457
  if (typeof text !== 'string') {
428
458
  throw new TypeError(`text must be a string, got ${typeof text}`);
429
459
  }
460
+ assertNotBinary(text);
430
461
 
431
462
  return ANNOTATION_TESTERS.some((re) => re.test(text));
432
463
  }
@@ -23,10 +23,31 @@ import {
23
23
  getUserName,
24
24
  exitWithError,
25
25
  getAnnotationSuggestions,
26
- requireFile,
26
+ loadAnnotated,
27
+ readAnnotatedInput,
28
+ requireEditableMarkdown,
29
+ InputError,
27
30
  } from './context.js';
28
31
  import type { Comment, Annotation } from '../types.js';
29
32
 
33
+ /**
34
+ * Read one file for the multi-file navigation commands (next/prev/first/last/
35
+ * todo). Converts a `.docx` to CriticMarkup like every other reader; on an
36
+ * unreadable/binary file it warns and returns null so a scan over many files
37
+ * is not aborted by one bad entry (and never silently under-counts).
38
+ */
39
+ async function readCommentSource(f: string): Promise<string | null> {
40
+ try {
41
+ return await readAnnotatedInput(f);
42
+ } catch (err) {
43
+ if (err instanceof InputError) {
44
+ console.error(chalk.yellow(` Skipping ${f}: ${err.message}`));
45
+ return null;
46
+ }
47
+ throw err;
48
+ }
49
+ }
50
+
30
51
  /**
31
52
  * Add a reply after a comment
32
53
  * @param text - Full document text
@@ -170,9 +191,15 @@ export function register(program: Command): void {
170
191
  .option('-i, --interactive', 'Interactive review mode (reply, resolve, skip)')
171
192
  .option('-t, --tui', 'Visual TUI mode for comment review')
172
193
  .action(async (file: string, options: CommentsOptions) => {
173
- requireFile(file, 'Markdown file');
194
+ // Interactive and TUI modes write replies/resolutions back to the file,
195
+ // so they need editable Markdown; a .docx is refused with a pointer to
196
+ // `rev import`. Plain listing/export is read-only and reads a .docx
197
+ // directly (converted to CriticMarkup first).
198
+ if (options.interactive || options.tui) {
199
+ requireEditableMarkdown(file, 'Markdown file');
200
+ }
174
201
 
175
- const text = fs.readFileSync(file, 'utf-8');
202
+ const text = await loadAnnotated(file, 'Markdown file');
176
203
 
177
204
  // TUI review mode
178
205
  if (options.tui) {
@@ -314,7 +341,8 @@ export function register(program: Command): void {
314
341
  .option('-u, --unresolve', 'Mark as pending (unresolve)')
315
342
  .option('--dry-run', 'Preview without saving')
316
343
  .action((file: string, options: ResolveOptions) => {
317
- requireFile(file, 'Markdown file');
344
+ // Writes resolved markers back to the file; refuse a .docx (see review).
345
+ requireEditableMarkdown(file, 'Markdown file');
318
346
 
319
347
  let text = fs.readFileSync(file, 'utf-8');
320
348
  const comments = getComments(text);
@@ -395,7 +423,7 @@ export function register(program: Command): void {
395
423
  .description('Show next pending comment')
396
424
  .argument('[file]', 'Specific file (default: all markdown files)')
397
425
  .option('-n, --number <n>', 'Skip to nth pending comment', parseInt)
398
- .action((file: string | undefined, options: NextOptions) => {
426
+ .action(async (file: string | undefined, options: NextOptions) => {
399
427
  const files = file ? [file] : findFiles('.md');
400
428
 
401
429
  if (files.length === 0) {
@@ -407,7 +435,8 @@ export function register(program: Command): void {
407
435
  const allPending: Array<Annotation & { file: string; number: number }> = [];
408
436
  for (const f of files) {
409
437
  if (!fs.existsSync(f)) continue;
410
- const text = fs.readFileSync(f, 'utf-8');
438
+ const text = await readCommentSource(f);
439
+ if (text === null) continue;
411
440
  const allComments = getComments(text);
412
441
  const pending = getComments(text, { pendingOnly: true });
413
442
 
@@ -460,7 +489,7 @@ export function register(program: Command): void {
460
489
  .description('Show previous pending comment')
461
490
  .argument('[file]', 'Specific file (default: all markdown files)')
462
491
  .option('-n, --number <n>', 'Skip to nth pending comment from end', parseInt)
463
- .action((file: string | undefined, options: PrevOptions) => {
492
+ .action(async (file: string | undefined, options: PrevOptions) => {
464
493
  const files = file ? [file] : findFiles('.md');
465
494
 
466
495
  if (files.length === 0) {
@@ -472,7 +501,8 @@ export function register(program: Command): void {
472
501
  const allPending: Array<Annotation & { file: string; number: number }> = [];
473
502
  for (const f of files) {
474
503
  if (!fs.existsSync(f)) continue;
475
- const text = fs.readFileSync(f, 'utf-8');
504
+ const text = await readCommentSource(f);
505
+ if (text === null) continue;
476
506
  const allComments = getComments(text);
477
507
  const pending = getComments(text, { pendingOnly: true });
478
508
 
@@ -527,7 +557,7 @@ export function register(program: Command): void {
527
557
  .command('first')
528
558
  .description('Show first comment')
529
559
  .argument('[section]', 'Specific file or section name (default: all markdown files)')
530
- .action((section: string | undefined) => {
560
+ .action(async (section: string | undefined) => {
531
561
  const files = section ? findSectionFile(section) : findFiles('.md');
532
562
 
533
563
  if (files.length === 0) {
@@ -538,7 +568,8 @@ export function register(program: Command): void {
538
568
  // Find first comment across files
539
569
  for (const f of files) {
540
570
  if (!fs.existsSync(f)) continue;
541
- const text = fs.readFileSync(f, 'utf-8');
571
+ const text = await readCommentSource(f);
572
+ if (text === null) continue;
542
573
  const comments = getComments(text);
543
574
 
544
575
  if (comments.length > 0) {
@@ -573,7 +604,7 @@ export function register(program: Command): void {
573
604
  .command('last')
574
605
  .description('Show last comment')
575
606
  .argument('[section]', 'Specific file or section name (default: all markdown files)')
576
- .action((section: string | undefined) => {
607
+ .action(async (section: string | undefined) => {
577
608
  const files = section ? findSectionFile(section) : findFiles('.md').reverse();
578
609
 
579
610
  if (files.length === 0) {
@@ -584,7 +615,8 @@ export function register(program: Command): void {
584
615
  // Find last comment across files (reverse order)
585
616
  for (const f of files) {
586
617
  if (!fs.existsSync(f)) continue;
587
- const text = fs.readFileSync(f, 'utf-8');
618
+ const text = await readCommentSource(f);
619
+ if (text === null) continue;
588
620
  const comments = getComments(text);
589
621
 
590
622
  if (comments.length > 0) {
@@ -622,7 +654,7 @@ export function register(program: Command): void {
622
654
  .description('List all pending comments as a checklist')
623
655
  .argument('[file]', 'Specific file (default: all markdown files)')
624
656
  .option('--by-author', 'Group by author')
625
- .action((file: string | undefined, options: { byAuthor?: boolean }) => {
657
+ .action(async (file: string | undefined, options: { byAuthor?: boolean }) => {
626
658
  const files = file ? [file] : findFiles('.md');
627
659
 
628
660
  if (files.length === 0) {
@@ -640,7 +672,8 @@ export function register(program: Command): void {
640
672
  }> = [];
641
673
  for (const f of files) {
642
674
  if (!fs.existsSync(f)) continue;
643
- const text = fs.readFileSync(f, 'utf-8');
675
+ const text = await readCommentSource(f);
676
+ if (text === null) continue;
644
677
  const allComments = getComments(text);
645
678
  const pending = allComments.filter(c => !c.resolved);
646
679
 
@@ -711,10 +744,8 @@ export function register(program: Command): void {
711
744
  .option('-a, --all', 'Accept all changes')
712
745
  .option('--dry-run', 'Preview without saving')
713
746
  .action((file: string, options: AcceptOptions) => {
714
- if (!fs.existsSync(file)) {
715
- console.error(chalk.red(`Error: File not found: ${file}`));
716
- process.exit(1);
717
- }
747
+ // Writes accepted text back to the file; refuse a .docx (see review).
748
+ requireEditableMarkdown(file, 'Markdown file');
718
749
 
719
750
  let text = fs.readFileSync(file, 'utf-8');
720
751
  const changes = getTrackChanges(text);
@@ -795,10 +826,8 @@ export function register(program: Command): void {
795
826
  .option('-a, --all', 'Reject all changes')
796
827
  .option('--dry-run', 'Preview without saving')
797
828
  .action((file: string, options: RejectOptions) => {
798
- if (!fs.existsSync(file)) {
799
- console.error(chalk.red(`Error: File not found: ${file}`));
800
- process.exit(1);
801
- }
829
+ // Writes reverted text back to the file; refuse a .docx (see review).
830
+ requireEditableMarkdown(file, 'Markdown file');
802
831
 
803
832
  let text = fs.readFileSync(file, 'utf-8');
804
833
  const changes = getTrackChanges(text);
@@ -878,10 +907,8 @@ export function register(program: Command): void {
878
907
  .option('--all', 'Reply to all pending comments with the same message (requires -m)')
879
908
  .option('--dry-run', 'Preview without saving')
880
909
  .action(async (file: string, options: ReplyOptions) => {
881
- if (!fs.existsSync(file)) {
882
- console.error(chalk.red(`File not found: ${file}`));
883
- process.exit(1);
884
- }
910
+ // Writes replies back to the file; refuse a .docx (see review).
911
+ requireEditableMarkdown(file, 'Markdown file');
885
912
 
886
913
  // Get author name
887
914
  let author = options.author || getUserName();
@@ -1001,9 +1028,9 @@ export function register(program: Command): void {
1001
1028
  .option('--no-context', 'Exclude commented text context')
1002
1029
  .option('--force', 'Overwrite existing output file')
1003
1030
  .action(async (file: string, options: ReplyDocOptions) => {
1004
- requireFile(file, 'Markdown file');
1005
-
1006
- const text = fs.readFileSync(file, 'utf-8');
1031
+ // Read-only over the input; the reply template is written to a new file,
1032
+ // so a .docx is read directly (converted to CriticMarkup first).
1033
+ const text = await loadAnnotated(file, 'Markdown file');
1007
1034
  const comments = getComments(text);
1008
1035
 
1009
1036
  if (comments.length === 0) {
@@ -160,6 +160,15 @@ export {
160
160
  requireFile,
161
161
  } from '../errors.js';
162
162
 
163
+ export {
164
+ isWordDocument,
165
+ readAnnotatedInput,
166
+ assertEditableMarkdown,
167
+ requireEditableMarkdown,
168
+ loadAnnotated,
169
+ InputError,
170
+ } from '../input.js';
171
+
163
172
  export {
164
173
  listJournals,
165
174
  getJournalProfile,
@@ -22,7 +22,10 @@ import {
22
22
  interactiveReview,
23
23
  exitWithError,
24
24
  getFileNotFoundSuggestions,
25
- requireFile,
25
+ loadAnnotated,
26
+ readAnnotatedInput,
27
+ requireEditableMarkdown,
28
+ InputError,
26
29
  } from './context.js';
27
30
 
28
31
  interface StripOptions {
@@ -43,7 +46,9 @@ export function register(program: Command): void {
43
46
  .description('Interactively review and accept/reject track changes')
44
47
  .argument('<file>', 'Markdown file to review')
45
48
  .action(async (file: string) => {
46
- requireFile(file, 'Markdown file');
49
+ // Review writes accepted/rejected text back to the same path; a .docx
50
+ // cannot hold CriticMarkup, so refuse it with a pointer to `rev import`.
51
+ requireEditableMarkdown(file, 'Markdown file');
47
52
 
48
53
  const text = fs.readFileSync(file, 'utf-8');
49
54
  const result = await interactiveReview(text);
@@ -78,10 +83,10 @@ export function register(program: Command): void {
78
83
  .argument('<file>', 'Markdown file to strip')
79
84
  .option('-o, --output <file>', 'Output file (default: stdout)')
80
85
  .option('-c, --keep-comments', 'Keep comment annotations')
81
- .action((file: string, options: StripOptions) => {
82
- requireFile(file, 'Markdown file');
83
-
84
- const text = fs.readFileSync(file, 'utf-8');
86
+ .action(async (file: string, options: StripOptions) => {
87
+ // Reads the input (a .docx is converted to CriticMarkup first) and writes
88
+ // clean text to stdout or --output, never back to the input file.
89
+ const text = await loadAnnotated(file, 'Markdown file');
85
90
  const clean = stripAnnotations(text, { keepComments: options.keepComments });
86
91
 
87
92
  if (options.output) {
@@ -107,12 +112,26 @@ export function register(program: Command): void {
107
112
  if (!fs.existsSync(file)) {
108
113
  if (jsonMode) {
109
114
  jsonOutput({ error: `File not found: ${file}` });
110
- } else {
111
- exitWithError(`File not found: ${file}`, getFileNotFoundSuggestions(file));
115
+ return;
112
116
  }
117
+ exitWithError(`File not found: ${file}`, getFileNotFoundSuggestions(file));
113
118
  }
114
119
 
115
- const text = fs.readFileSync(file, 'utf-8');
120
+ // Route .docx through the OOXML reader so real track changes and
121
+ // comments are counted, instead of regexing the binary ZIP (#8).
122
+ let text: string;
123
+ try {
124
+ text = await readAnnotatedInput(file);
125
+ } catch (err) {
126
+ if (err instanceof InputError) {
127
+ if (jsonMode) {
128
+ jsonOutput({ error: err.message });
129
+ return;
130
+ }
131
+ exitWithError(err.message, err.suggestions);
132
+ }
133
+ throw err;
134
+ }
116
135
  const counts = countAnnotations(text);
117
136
  const comments = getComments(text);
118
137
 
@@ -75,7 +75,7 @@ export function register(program: Command): void {
75
75
 
76
76
  const spin = fmt.spinner('Collecting comments...').start();
77
77
 
78
- const comments = collectComments(mdFiles);
78
+ const comments = await collectComments(mdFiles);
79
79
  spin.stop();
80
80
 
81
81
  if (comments.length === 0) {
package/lib/import.ts CHANGED
@@ -722,6 +722,32 @@ export async function importWordWithTrackChanges(
722
722
  };
723
723
  }
724
724
 
725
+ /**
726
+ * Read a `.docx` into the same CriticMarkup-annotated Markdown that
727
+ * `rev import` produces, for read-only inspection (`rev status`, `rev
728
+ * comments`, ...). Track changes become `{++..++}` / `{--..--}` / `{~~..~~}`
729
+ * and comments are placed at their anchors, so `countAnnotations` /
730
+ * `getComments` and the rest operate on it exactly as they do on imported
731
+ * Markdown — the numbers match a subsequent `rev import`.
732
+ *
733
+ * Side-effect free: no media is extracted, nothing is written to disk, and
734
+ * extraction/placement messages are discarded (the caller only wants the
735
+ * annotated text). Works with or without pandoc — the XML fallback emits the
736
+ * same CriticMarkup — so it never reads the binary ZIP as text.
737
+ */
738
+ export async function readDocxAsAnnotatedMarkdown(docxPath: string): Promise<string> {
739
+ const extracted = await extractFromWord(docxPath, { skipMediaExtraction: true });
740
+ let text = extracted.text;
741
+ const comments = extracted.comments || [];
742
+ const anchors = extracted.anchors || new Map();
743
+
744
+ if (comments.length > 0) {
745
+ text = insertCommentsIntoMarkdown(text, comments, anchors, { quiet: true });
746
+ }
747
+
748
+ return cleanupAnnotations(text);
749
+ }
750
+
725
751
  /**
726
752
  * Legacy import function: Word doc → annotated MD via diff
727
753
  */