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.
- package/CHANGELOG.md +5 -0
- package/dist/lib/annotations.d.ts.map +1 -1
- package/dist/lib/annotations.js +28 -0
- package/dist/lib/annotations.js.map +1 -1
- package/dist/lib/commands/comments.d.ts.map +1 -1
- package/dist/lib/commands/comments.js +58 -28
- package/dist/lib/commands/comments.js.map +1 -1
- package/dist/lib/commands/context.d.ts +1 -0
- package/dist/lib/commands/context.d.ts.map +1 -1
- package/dist/lib/commands/context.js +1 -0
- package/dist/lib/commands/context.js.map +1 -1
- package/dist/lib/commands/core.d.ts.map +1 -1
- package/dist/lib/commands/core.js +25 -8
- package/dist/lib/commands/core.js.map +1 -1
- package/dist/lib/commands/response.js +1 -1
- package/dist/lib/commands/response.js.map +1 -1
- package/dist/lib/import.d.ts +14 -0
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/import.js +23 -0
- package/dist/lib/import.js.map +1 -1
- package/dist/lib/input.d.ts +67 -0
- package/dist/lib/input.d.ts.map +1 -0
- package/dist/lib/input.js +164 -0
- package/dist/lib/input.js.map +1 -0
- package/dist/lib/response.d.ts +1 -1
- package/dist/lib/response.d.ts.map +1 -1
- package/dist/lib/response.js +5 -2
- package/dist/lib/response.js.map +1 -1
- package/lib/annotations.ts +31 -0
- package/lib/commands/comments.ts +56 -29
- package/lib/commands/context.ts +9 -0
- package/lib/commands/core.ts +28 -9
- package/lib/commands/response.ts +1 -1
- package/lib/import.ts +26 -0
- package/lib/input.ts +174 -0
- package/lib/response.ts +5 -2
- package/package.json +1 -1
package/lib/input.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import { openDocx } from './ooxml.js';
|
|
25
|
+
import { exitWithError, requireFile } from './errors.js';
|
|
26
|
+
|
|
27
|
+
/** ZIP local-file-header magic: the first four bytes of any `.docx`. */
|
|
28
|
+
const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
29
|
+
|
|
30
|
+
/** OOXML Word extensions (all ZIP-backed, none Markdown). */
|
|
31
|
+
const WORD_EXTENSIONS = new Set(['.docx', '.docm', '.dotx', '.dotm']);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A file cannot be read as (or converted to) CriticMarkup Markdown. Carries
|
|
35
|
+
* actionable suggestions so the command layer can render a helpful error.
|
|
36
|
+
*/
|
|
37
|
+
export class InputError extends Error {
|
|
38
|
+
suggestions: string[];
|
|
39
|
+
constructor(message: string, suggestions: string[] = []) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'InputError';
|
|
42
|
+
this.suggestions = suggestions;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Read the first bytes of a file, or an empty buffer if unreadable. */
|
|
47
|
+
function readMagic(file: string, length = 4): Buffer {
|
|
48
|
+
let fd: number | undefined;
|
|
49
|
+
try {
|
|
50
|
+
fd = fs.openSync(file, 'r');
|
|
51
|
+
const buf = Buffer.alloc(length);
|
|
52
|
+
const n = fs.readSync(fd, buf, 0, length, 0);
|
|
53
|
+
return buf.subarray(0, n);
|
|
54
|
+
} catch {
|
|
55
|
+
return Buffer.alloc(0);
|
|
56
|
+
} finally {
|
|
57
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when the file begins with the ZIP local-file-header magic. */
|
|
62
|
+
export function looksLikeZip(file: string): boolean {
|
|
63
|
+
return readMagic(file).equals(ZIP_MAGIC);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* True when `file` is a Word document: it carries a Word extension, or it is a
|
|
68
|
+
* ZIP whose package contains `word/document.xml`. The content sniff catches a
|
|
69
|
+
* `.docx` renamed to `.md`/`.txt`, so those never get regexed as CriticMarkup.
|
|
70
|
+
*/
|
|
71
|
+
export function isWordDocument(file: string): boolean {
|
|
72
|
+
if (WORD_EXTENSIONS.has(path.extname(file).toLowerCase())) return true;
|
|
73
|
+
if (!looksLikeZip(file)) return false;
|
|
74
|
+
try {
|
|
75
|
+
return openDocx(file).getEntry('word/document.xml') !== null;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A NUL byte in the head reliably marks binary; UTF-8 Markdown never has one. */
|
|
82
|
+
function isBinaryBuffer(buf: Buffer): boolean {
|
|
83
|
+
if (buf.length >= 4 && buf.subarray(0, 4).equals(ZIP_MAGIC)) return true;
|
|
84
|
+
return buf.subarray(0, Math.min(buf.length, 8192)).includes(0x00);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read a file as the CriticMarkup Markdown a command expects. A Word document
|
|
89
|
+
* is converted through the existing OOXML/pandoc reader (real insertions,
|
|
90
|
+
* deletions, substitutions, and comments); a Markdown/text file is read as
|
|
91
|
+
* UTF-8. A non-Word binary (image, PDF, unknown ZIP) is rejected rather than
|
|
92
|
+
* silently miscounted.
|
|
93
|
+
*
|
|
94
|
+
* @throws InputError if the file is a binary that is not a Word document, or a
|
|
95
|
+
* Word document that cannot be parsed.
|
|
96
|
+
*/
|
|
97
|
+
export async function readAnnotatedInput(file: string): Promise<string> {
|
|
98
|
+
if (isWordDocument(file)) {
|
|
99
|
+
const { readDocxAsAnnotatedMarkdown } = await import('./import.js');
|
|
100
|
+
try {
|
|
101
|
+
return await readDocxAsAnnotatedMarkdown(file);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw new InputError(
|
|
104
|
+
`Failed to read Word document ${path.basename(file)}: ${(err as Error).message}`,
|
|
105
|
+
['Run "rev import <docx>" to convert it to annotated Markdown first'],
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const buf = fs.readFileSync(file);
|
|
111
|
+
if (isBinaryBuffer(buf)) {
|
|
112
|
+
throw new InputError(
|
|
113
|
+
`${path.basename(file)} is not a text or Markdown file (binary content).`,
|
|
114
|
+
[
|
|
115
|
+
'Word documents: run "rev import <docx>" first, or pass the imported .md',
|
|
116
|
+
'This command reads CriticMarkup Markdown, not binary files',
|
|
117
|
+
],
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return buf.toString('utf-8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Guard for commands that edit the file in place (accept/reject/resolve/reply/
|
|
125
|
+
* review). A `.docx` cannot hold CriticMarkup, so refuse it with a pointer to
|
|
126
|
+
* `rev import` instead of corrupting the document.
|
|
127
|
+
*
|
|
128
|
+
* @throws InputError if `file` is a Word document.
|
|
129
|
+
*/
|
|
130
|
+
export function assertEditableMarkdown(file: string): void {
|
|
131
|
+
if (isWordDocument(file)) {
|
|
132
|
+
const base = path.basename(file);
|
|
133
|
+
throw new InputError(
|
|
134
|
+
`${base} is a Word document; this command edits Markdown in place.`,
|
|
135
|
+
[
|
|
136
|
+
`Run "rev import ${base}" to get an editable Markdown file, then edit that`,
|
|
137
|
+
'Read-only inspection works directly: "rev status" / "rev comments"',
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Command-facing wrappers: translate InputError into the CLI's error surface.
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Command front door: verify the file exists, then read it as annotated
|
|
149
|
+
* Markdown (converting a `.docx` on the way). On a bad input, print a friendly
|
|
150
|
+
* error and exit — matching how the rest of the CLI reports failures.
|
|
151
|
+
*/
|
|
152
|
+
export async function loadAnnotated(file: string, fileType = 'Markdown file'): Promise<string> {
|
|
153
|
+
requireFile(file, fileType);
|
|
154
|
+
try {
|
|
155
|
+
return await readAnnotatedInput(file);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err instanceof InputError) exitWithError(err.message, err.suggestions);
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Command front door for editing commands: verify existence and refuse a Word
|
|
164
|
+
* document (which cannot be edited in place), printing guidance and exiting.
|
|
165
|
+
*/
|
|
166
|
+
export function requireEditableMarkdown(file: string, fileType = 'Markdown file'): void {
|
|
167
|
+
requireFile(file, fileType);
|
|
168
|
+
try {
|
|
169
|
+
assertEditableMarkdown(file);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err instanceof InputError) exitWithError(err.message, err.suggestions);
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/lib/response.ts
CHANGED
|
@@ -182,13 +182,16 @@ export function generateResponseLetter(comments: CommentWithReplies[], options:
|
|
|
182
182
|
/**
|
|
183
183
|
* Collect comments from multiple files
|
|
184
184
|
*/
|
|
185
|
-
export function collectComments(files: string[]): CommentWithReplies[] {
|
|
185
|
+
export async function collectComments(files: string[]): Promise<CommentWithReplies[]> {
|
|
186
|
+
const { readAnnotatedInput } = await import('./input.js');
|
|
186
187
|
const allComments: CommentWithReplies[] = [];
|
|
187
188
|
|
|
188
189
|
for (const file of files) {
|
|
189
190
|
if (!fs.existsSync(file)) continue;
|
|
190
191
|
|
|
191
|
-
|
|
192
|
+
// Route each file through the shared reader so a returned `.docx` is
|
|
193
|
+
// converted to CriticMarkup first, rather than regexing the binary ZIP.
|
|
194
|
+
const text = await readAnnotatedInput(file);
|
|
192
195
|
const comments = parseCommentsWithReplies(text, path.basename(file));
|
|
193
196
|
allComments.push(...comments);
|
|
194
197
|
}
|