docrev 0.6.7 → 0.6.13
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/README.md +165 -88
- package/lib/import.js +17 -6
- package/lib/wordcomments.js +88 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
# docrev
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/docrev)
|
|
4
|
+
[](https://www.npmjs.com/package/docrev)
|
|
5
|
+
[](https://nodejs.org)
|
|
4
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/gcol33/docrev/actions/workflows/ci.yml)
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
A CLI for writing scientific papers in Markdown while collaborating with Word users.
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
Markdown ──► docrev ──► Word/PDF ──► Collaborators
|
|
10
|
-
▲ │
|
|
11
|
-
└─────────── docrev ◄─────────────────────┘
|
|
12
|
-
(import feedback)
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Scientific papers go through many revision cycles with collaborators and reviewers. Track changes become unreadable, versions multiply, equations break when copying, figures get embedded at wrong resolutions. docrev keeps your source in plain Markdown under version control while generating Word documents for the review cycle. Your collaborators keep using Word as usual. You handle conversion on your end.
|
|
11
|
+
You write in Markdown under version control. Your collaborators use Word. docrev converts between the two, preserving track changes, comments, equations, and cross-references.
|
|
16
12
|
|
|
17
13
|
## Install
|
|
18
14
|
|
|
@@ -21,20 +17,20 @@ npm install -g docrev
|
|
|
21
17
|
brew install pandoc
|
|
22
18
|
```
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
Pandoc is required for document conversion. On Windows use `winget install JohnMacFarlane.Pandoc`, on Linux use `apt install pandoc`.
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
## Getting Started
|
|
23
|
+
|
|
24
|
+
### Starting from a Word Document
|
|
25
|
+
|
|
26
|
+
If you have an existing manuscript in Word:
|
|
31
27
|
|
|
32
|
-
From existing Word document:
|
|
33
28
|
```bash
|
|
34
29
|
rev import manuscript.docx
|
|
35
30
|
```
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
This converts your document to markdown, splitting it into sections:
|
|
33
|
+
|
|
38
34
|
```
|
|
39
35
|
my-paper/
|
|
40
36
|
├── introduction.md
|
|
@@ -45,130 +41,211 @@ my-paper/
|
|
|
45
41
|
└── rev.yaml
|
|
46
42
|
```
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
Track changes and comments from the Word document are preserved as annotations in the markdown files (see below).
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
# Heading
|
|
46
|
+
### Starting from Scratch
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
To start a new paper in markdown:
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
```bash
|
|
51
|
+
rev new my-paper
|
|
52
|
+
cd my-paper
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This creates the same project structure with empty section files. Write your paper in the markdown files, then build Word documents to share with collaborators.
|
|
56
|
+
|
|
57
|
+
## The Revision Cycle
|
|
58
|
+
|
|
59
|
+
### 1. Build and Share
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
Generate a Word document from your markdown:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
rev build docx
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
Send this to your collaborators. They review it in Word, adding comments and track changes as usual.
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
year = {2020}
|
|
71
|
-
}
|
|
69
|
+
### 2. Import Feedback
|
|
70
|
+
|
|
71
|
+
When collaborators return the reviewed document, import their feedback:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
rev sections reviewed.docx
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
This updates your markdown files with their comments and track changes, converted to inline annotations.
|
|
78
|
+
|
|
79
|
+
### 3. Review Track Changes
|
|
80
|
+
|
|
81
|
+
Track changes appear as inline annotations in your markdown:
|
|
82
|
+
|
|
75
83
|
```markdown
|
|
76
|
-
|
|
84
|
+
The sample size was {--100--}{++150++} individuals.
|
|
85
|
+
We collected data {~~monthly~>weekly~~} from each site.
|
|
77
86
|
```
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
- `{++text++}` — inserted text
|
|
89
|
+
- `{--text--}` — deleted text
|
|
90
|
+
- `{~~old~>new~~}` — substitution
|
|
80
91
|
|
|
81
|
-
|
|
92
|
+
To accept a change, keep the new text and delete the markup. To reject it, keep the old text. When you're done, the file is clean markdown.
|
|
93
|
+
|
|
94
|
+
### 4. Respond to Comments
|
|
82
95
|
|
|
83
|
-
|
|
96
|
+
Comments appear inline in your markdown:
|
|
84
97
|
|
|
85
|
-
Display:
|
|
86
98
|
```markdown
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
$$
|
|
99
|
+
We used a random sampling approach.
|
|
100
|
+
{>>Reviewer 2: Please clarify the sampling method.<<}
|
|
90
101
|
```
|
|
91
102
|
|
|
92
|
-
|
|
103
|
+
List all comments in a file:
|
|
93
104
|
|
|
94
|
-
```
|
|
95
|
-
|
|
105
|
+
```bash
|
|
106
|
+
rev comments methods.md
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Reply from the command line:
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
```bash
|
|
112
|
+
rev config user "Your Name" # one-time setup
|
|
113
|
+
rev reply methods.md -n 1 -m "Added clarification in paragraph 2"
|
|
98
114
|
```
|
|
99
115
|
|
|
100
|
-
|
|
116
|
+
Your reply threads beneath the original:
|
|
101
117
|
|
|
102
|
-
|
|
118
|
+
```markdown
|
|
119
|
+
We used a random sampling approach.
|
|
120
|
+
{>>Reviewer 2: Please clarify the sampling method.<<}
|
|
121
|
+
{>>Your Name: Added clarification in paragraph 2.<<}
|
|
122
|
+
```
|
|
103
123
|
|
|
104
|
-
|
|
124
|
+
Mark comments as resolved:
|
|
105
125
|
|
|
106
126
|
```bash
|
|
107
|
-
rev
|
|
108
|
-
rev build pdf # PDF
|
|
109
|
-
rev build --dual # Clean + comments versions
|
|
110
|
-
rev watch docx # Auto-rebuild on save
|
|
127
|
+
rev resolve methods.md -n 1
|
|
111
128
|
```
|
|
112
129
|
|
|
113
|
-
|
|
130
|
+
### 5. Rebuild with Comment Threads
|
|
131
|
+
|
|
132
|
+
Generate both a clean version and one showing the comment threads:
|
|
114
133
|
|
|
115
|
-
Import reviewed document:
|
|
116
134
|
```bash
|
|
117
|
-
rev
|
|
135
|
+
rev build --dual
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This produces:
|
|
139
|
+
- `paper.docx` — clean, for submission
|
|
140
|
+
- `paper_comments.docx` — includes comment threads as Word comments
|
|
141
|
+
|
|
142
|
+
Your collaborators see the full conversation in the comments pane.
|
|
143
|
+
|
|
144
|
+
### 6. Repeat
|
|
145
|
+
|
|
146
|
+
Send the updated Word document. Import new feedback with `rev sections`. Continue until done.
|
|
147
|
+
|
|
148
|
+
## Before Submission
|
|
149
|
+
|
|
150
|
+
### Validate Your Bibliography
|
|
151
|
+
|
|
152
|
+
Check that DOIs in your bibliography resolve correctly:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
rev doi check references.bib
|
|
118
156
|
```
|
|
119
157
|
|
|
120
|
-
|
|
158
|
+
Find DOIs for entries missing them:
|
|
159
|
+
|
|
121
160
|
```bash
|
|
122
|
-
rev
|
|
161
|
+
rev doi lookup references.bib
|
|
123
162
|
```
|
|
124
163
|
|
|
125
|
-
|
|
164
|
+
Add a citation directly from a DOI:
|
|
165
|
+
|
|
126
166
|
```bash
|
|
127
|
-
rev
|
|
128
|
-
rev reply methods.md -n 1 -m "Clarified sampling methodology"
|
|
167
|
+
rev doi add 10.1038/s41586-020-2649-2
|
|
129
168
|
```
|
|
130
169
|
|
|
131
|
-
|
|
170
|
+
### Run Pre-Submission Checks
|
|
171
|
+
|
|
172
|
+
Check for broken references, missing citations, and common issues:
|
|
173
|
+
|
|
132
174
|
```bash
|
|
133
|
-
rev
|
|
175
|
+
rev check
|
|
134
176
|
```
|
|
135
177
|
|
|
136
|
-
|
|
137
|
-
- `paper.docx` (clean)
|
|
138
|
-
- `paper_comments.docx` (with comment threads)
|
|
178
|
+
## Writing in Markdown
|
|
139
179
|
|
|
140
|
-
|
|
180
|
+
### Citations
|
|
181
|
+
|
|
182
|
+
Add references to `references.bib`:
|
|
183
|
+
|
|
184
|
+
```bibtex
|
|
185
|
+
@article{Smith2020,
|
|
186
|
+
author = {Smith, Jane},
|
|
187
|
+
title = {Paper Title},
|
|
188
|
+
journal = {Nature},
|
|
189
|
+
year = {2020},
|
|
190
|
+
doi = {10.1038/example}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Cite in text:
|
|
195
|
+
|
|
196
|
+
```markdown
|
|
197
|
+
Previous work [@Smith2020] established this relationship.
|
|
198
|
+
Multiple sources support this [@Smith2020; @Jones2021].
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Equations
|
|
202
|
+
|
|
203
|
+
Inline equations use single dollar signs: `$E = mc^2$`
|
|
204
|
+
|
|
205
|
+
Display equations use double dollar signs:
|
|
206
|
+
|
|
207
|
+
```markdown
|
|
208
|
+
$$
|
|
209
|
+
\bar{x} = \frac{1}{n} \sum_{i=1}^{n} x_i
|
|
210
|
+
$$
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Figures and Cross-References
|
|
214
|
+
|
|
215
|
+
```markdown
|
|
216
|
+
{#fig:map}
|
|
217
|
+
|
|
218
|
+
Results are shown in @fig:map.
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The reference `@fig:map` becomes "Figure 1" in the output. Numbers update automatically when figures are reordered.
|
|
222
|
+
|
|
223
|
+
Tables and equations work the same way with `@tbl:label` and `@eq:label`.
|
|
224
|
+
|
|
225
|
+
## Useful Commands
|
|
141
226
|
|
|
142
227
|
| Task | Command |
|
|
143
228
|
|------|---------|
|
|
144
|
-
|
|
|
145
|
-
| Import Word | `rev import manuscript.docx` |
|
|
229
|
+
| Start new project | `rev new my-paper` |
|
|
230
|
+
| Import Word document | `rev import manuscript.docx` |
|
|
231
|
+
| Import feedback | `rev sections reviewed.docx` |
|
|
232
|
+
| List comments | `rev comments methods.md` |
|
|
233
|
+
| Reply to comment | `rev reply methods.md -n 1 -m "response"` |
|
|
146
234
|
| Build Word | `rev build docx` |
|
|
147
235
|
| Build PDF | `rev build pdf` |
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
236
|
+
| Build both clean and annotated | `rev build --dual` |
|
|
237
|
+
| Check DOIs | `rev doi check references.bib` |
|
|
238
|
+
| Find missing DOIs | `rev doi lookup references.bib` |
|
|
151
239
|
| Word count | `rev word-count` |
|
|
152
|
-
|
|
|
153
|
-
|
|
|
240
|
+
| Pre-submission check | `rev check` |
|
|
241
|
+
| Watch for changes | `rev watch docx` |
|
|
154
242
|
|
|
155
|
-
Full reference: [docs/commands.md](docs/commands.md)
|
|
243
|
+
Full command reference: [docs/commands.md](docs/commands.md)
|
|
156
244
|
|
|
157
245
|
## Requirements
|
|
158
246
|
|
|
159
247
|
- Node.js 18+
|
|
160
|
-
- Pandoc
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
# macOS
|
|
164
|
-
brew install pandoc
|
|
165
|
-
|
|
166
|
-
# Windows
|
|
167
|
-
winget install JohnMacFarlane.Pandoc
|
|
168
|
-
|
|
169
|
-
# Linux
|
|
170
|
-
sudo apt install pandoc
|
|
171
|
-
```
|
|
248
|
+
- Pandoc 2.11+
|
|
172
249
|
|
|
173
250
|
## License
|
|
174
251
|
|
package/lib/import.js
CHANGED
|
@@ -268,7 +268,8 @@ export function insertCommentsIntoMarkdown(markdown, comments, anchors, options
|
|
|
268
268
|
|
|
269
269
|
if (occurrences.length === 1) {
|
|
270
270
|
// Unique match - easy case
|
|
271
|
-
|
|
271
|
+
// Position at START of anchor (comment goes before, anchor gets marked)
|
|
272
|
+
return { ...c, pos: occurrences[0], anchorText: anchor, anchorEnd: occurrences[0] + anchor.length };
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
// Multiple occurrences - use context for disambiguation
|
|
@@ -321,16 +322,26 @@ export function insertCommentsIntoMarkdown(markdown, comments, anchors, options
|
|
|
321
322
|
// Mark this position as used for tie-breaking subsequent comments
|
|
322
323
|
usedPositions.add(bestIdx);
|
|
323
324
|
|
|
324
|
-
|
|
325
|
-
|
|
325
|
+
// Position at START of anchor (comment goes before, anchor gets marked)
|
|
326
|
+
return { ...c, pos: bestIdx, anchorText: anchor, anchorEnd: bestIdx + anchor.length };
|
|
327
|
+
}).filter((c) => c.pos >= 0);
|
|
326
328
|
|
|
327
329
|
// Sort by position descending (insert from end to avoid offset issues)
|
|
328
330
|
commentsWithPositions.sort((a, b) => b.pos - a.pos);
|
|
329
331
|
|
|
330
|
-
// Insert each comment
|
|
332
|
+
// Insert each comment with anchor marking
|
|
331
333
|
for (const c of commentsWithPositions) {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
+
const comment = `{>>${c.author}: ${c.text}<<}`;
|
|
335
|
+
if (c.anchorText && c.anchorEnd) {
|
|
336
|
+
// Replace anchor text with: {>>comment<<}[anchor]{.mark}
|
|
337
|
+
const before = result.slice(0, c.pos);
|
|
338
|
+
const anchor = result.slice(c.pos, c.anchorEnd);
|
|
339
|
+
const after = result.slice(c.anchorEnd);
|
|
340
|
+
result = before + comment + `[${anchor}]{.mark}` + after;
|
|
341
|
+
} else {
|
|
342
|
+
// No anchor - just insert comment at position
|
|
343
|
+
result = result.slice(0, c.pos) + ` ${comment}` + result.slice(c.pos);
|
|
344
|
+
}
|
|
334
345
|
}
|
|
335
346
|
|
|
336
347
|
// Log warnings unless quiet mode
|
package/lib/wordcomments.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Word comment injection with reply threading
|
|
3
3
|
*
|
|
4
4
|
* Flow:
|
|
5
|
-
* 1. prepareMarkdownWithMarkers() - Parse comments, detect
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
5
|
+
* 1. prepareMarkdownWithMarkers() - Parse comments, detect reply relationships
|
|
6
|
+
* - First comment in a cluster = parent (gets markers: ⟦CMS:n⟧anchor⟦CME:n⟧)
|
|
7
|
+
* - Subsequent adjacent comments = replies (no markers, attach to parent)
|
|
8
8
|
* 2. Pandoc converts to DOCX
|
|
9
9
|
* 3. injectCommentsAtMarkers() - Insert comment ranges for parents only
|
|
10
10
|
* - Replies go in comments.xml with parent reference in commentsExtended.xml
|
|
@@ -71,50 +71,41 @@ export function prepareMarkdownWithMarkers(markdown) {
|
|
|
71
71
|
return { markedMarkdown: markdown, comments: [] };
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// Detect reply relationships
|
|
74
|
+
// Detect reply relationships based on adjacency
|
|
75
|
+
// First comment in a cluster = parent, all subsequent = replies to that parent
|
|
75
76
|
// Comments are "adjacent" if there's only whitespace between them (< 50 chars)
|
|
76
77
|
const ADJACENT_THRESHOLD = 50;
|
|
77
78
|
const comments = [];
|
|
78
|
-
let
|
|
79
|
+
let clusterParentIdx = -1; // Index of first comment in current cluster
|
|
79
80
|
let lastCommentEnd = -1;
|
|
80
81
|
|
|
81
82
|
for (let i = 0; i < rawMatches.length; i++) {
|
|
82
83
|
const m = rawMatches[i];
|
|
83
|
-
const isGuy = m.author === 'Guy Colling';
|
|
84
|
-
const isGilles = m.author === 'Gilles Colling';
|
|
85
84
|
|
|
86
85
|
// Check if this comment is adjacent to the previous one
|
|
87
86
|
const gap = lastCommentEnd >= 0 ? m.start - lastCommentEnd : Infinity;
|
|
88
87
|
const isAdjacent = gap < ADJACENT_THRESHOLD;
|
|
89
88
|
|
|
90
|
-
// Reset
|
|
89
|
+
// Reset cluster if there's a gap (comments not in same cluster)
|
|
91
90
|
if (!isAdjacent) {
|
|
92
|
-
|
|
91
|
+
clusterParentIdx = -1;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
if (
|
|
94
|
+
if (clusterParentIdx === -1) {
|
|
95
|
+
// First comment in cluster = parent (regardless of author)
|
|
96
96
|
comments.push({
|
|
97
97
|
...m,
|
|
98
98
|
isReply: false,
|
|
99
99
|
parentIdx: null,
|
|
100
100
|
commentIdx: comments.length
|
|
101
101
|
});
|
|
102
|
-
|
|
103
|
-
} else if (isGilles && lastGuyIdx >= 0 && isAdjacent) {
|
|
104
|
-
// Gilles immediately following Guy (same cluster) = reply
|
|
105
|
-
comments.push({
|
|
106
|
-
...m,
|
|
107
|
-
isReply: true,
|
|
108
|
-
parentIdx: lastGuyIdx,
|
|
109
|
-
commentIdx: comments.length
|
|
110
|
-
});
|
|
111
|
-
// Don't reset lastGuyIdx - multiple replies could follow
|
|
102
|
+
clusterParentIdx = comments.length - 1;
|
|
112
103
|
} else {
|
|
113
|
-
//
|
|
104
|
+
// Subsequent comment in cluster = reply to first comment
|
|
114
105
|
comments.push({
|
|
115
106
|
...m,
|
|
116
|
-
isReply:
|
|
117
|
-
parentIdx:
|
|
107
|
+
isReply: true,
|
|
108
|
+
parentIdx: clusterParentIdx,
|
|
118
109
|
commentIdx: comments.length
|
|
119
110
|
});
|
|
120
111
|
}
|
|
@@ -122,6 +113,21 @@ export function prepareMarkdownWithMarkers(markdown) {
|
|
|
122
113
|
lastCommentEnd = m.end;
|
|
123
114
|
}
|
|
124
115
|
|
|
116
|
+
// Propagate anchors from replies to parents
|
|
117
|
+
// If a reply has an anchor but its parent doesn't, move the anchor to the parent
|
|
118
|
+
// Track flags for special handling during marker generation
|
|
119
|
+
for (const c of comments) {
|
|
120
|
+
if (c.isReply && c.anchor && c.parentIdx !== null) {
|
|
121
|
+
const parent = comments[c.parentIdx];
|
|
122
|
+
if (!parent.anchor) {
|
|
123
|
+
parent.anchor = c.anchor;
|
|
124
|
+
parent.anchorFromReply = true; // Parent's anchor came from a reply (markers placed by reply)
|
|
125
|
+
c.placesParentMarkers = true; // This reply should place the parent's markers
|
|
126
|
+
c.anchor = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
125
131
|
// Build marked markdown - only parent comments get markers
|
|
126
132
|
// Process from end to start to preserve positions
|
|
127
133
|
let markedMarkdown = markdown;
|
|
@@ -131,12 +137,57 @@ export function prepareMarkdownWithMarkers(markdown) {
|
|
|
131
137
|
|
|
132
138
|
if (c.isReply) {
|
|
133
139
|
// Reply: remove from document entirely (will be in comments.xml only)
|
|
134
|
-
|
|
140
|
+
// Also consume leading whitespace to avoid double spaces
|
|
141
|
+
let removeStart = c.start;
|
|
142
|
+
while (removeStart > 0 && /\s/.test(markedMarkdown[removeStart - 1])) {
|
|
143
|
+
removeStart--;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If this reply places parent's markers (anchor was propagated)
|
|
147
|
+
if (c.placesParentMarkers && c.parentIdx !== null) {
|
|
148
|
+
// Extract anchor text from the original match
|
|
149
|
+
const anchorMatch = c.fullMatch.match(/\[([^\]]+)\]\{\.mark\}$/);
|
|
150
|
+
if (anchorMatch) {
|
|
151
|
+
const anchorText = anchorMatch[1];
|
|
152
|
+
// Output markers with PARENT's index around the anchor text
|
|
153
|
+
const parentIdx = c.parentIdx;
|
|
154
|
+
const replacement = `${MARKER_START_PREFIX}${parentIdx}${MARKER_SUFFIX}${anchorText}${MARKER_END_PREFIX}${parentIdx}${MARKER_SUFFIX}`;
|
|
155
|
+
markedMarkdown = markedMarkdown.slice(0, removeStart) + replacement + markedMarkdown.slice(c.end);
|
|
156
|
+
} else {
|
|
157
|
+
markedMarkdown = markedMarkdown.slice(0, removeStart) + markedMarkdown.slice(c.end);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
markedMarkdown = markedMarkdown.slice(0, removeStart) + markedMarkdown.slice(c.end);
|
|
161
|
+
}
|
|
135
162
|
} else {
|
|
136
|
-
// Parent comment
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
163
|
+
// Parent comment
|
|
164
|
+
if (c.anchorFromReply) {
|
|
165
|
+
// Anchor markers are placed by the reply, just remove this comment
|
|
166
|
+
let removeStart = c.start;
|
|
167
|
+
while (removeStart > 0 && /\s/.test(markedMarkdown[removeStart - 1])) {
|
|
168
|
+
removeStart--;
|
|
169
|
+
}
|
|
170
|
+
markedMarkdown = markedMarkdown.slice(0, removeStart) + markedMarkdown.slice(c.end);
|
|
171
|
+
} else {
|
|
172
|
+
// Normal case: replace with markers
|
|
173
|
+
let anchor = c.anchor || '';
|
|
174
|
+
|
|
175
|
+
// If no anchor, try to use the next word as fallback to avoid empty ranges
|
|
176
|
+
if (!anchor) {
|
|
177
|
+
const afterComment = markedMarkdown.slice(c.end);
|
|
178
|
+
// Match the next word (skip leading whitespace/punctuation)
|
|
179
|
+
const nextWordMatch = afterComment.match(/^\s*([a-zA-Z0-9]+)/);
|
|
180
|
+
if (nextWordMatch) {
|
|
181
|
+
anchor = nextWordMatch[1];
|
|
182
|
+
// Also need to consume the matched text from afterComment
|
|
183
|
+
c.consumeAfter = nextWordMatch[0].length;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const replacement = `${MARKER_START_PREFIX}${i}${MARKER_SUFFIX}${anchor}${MARKER_END_PREFIX}${i}${MARKER_SUFFIX}`;
|
|
188
|
+
const endPos = c.end + (c.consumeAfter || 0);
|
|
189
|
+
markedMarkdown = markedMarkdown.slice(0, c.start) + replacement + markedMarkdown.slice(endPos);
|
|
190
|
+
}
|
|
140
191
|
}
|
|
141
192
|
}
|
|
142
193
|
|
|
@@ -239,11 +290,7 @@ function createCommentsExtensibleXml(comments) {
|
|
|
239
290
|
return xml;
|
|
240
291
|
}
|
|
241
292
|
|
|
242
|
-
//
|
|
243
|
-
const AUTHOR_USER_IDS = {
|
|
244
|
-
'Guy Colling': '9ff4d97962428673',
|
|
245
|
-
'Gilles Colling': '46e930a4c4b85dfd',
|
|
246
|
-
};
|
|
293
|
+
// Generate deterministic user IDs for authors (no hardcoded personal data)
|
|
247
294
|
|
|
248
295
|
function createPeopleXml(comments) {
|
|
249
296
|
// Extract unique authors
|
|
@@ -265,7 +312,7 @@ function createPeopleXml(comments) {
|
|
|
265
312
|
xml += 'mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh">';
|
|
266
313
|
|
|
267
314
|
for (const author of authors) {
|
|
268
|
-
const userId =
|
|
315
|
+
const userId = generateUserId(author);
|
|
269
316
|
xml += `<w15:person w15:author="${escapeXml(author)}">`;
|
|
270
317
|
xml += `<w15:presenceInfo w15:providerId="Windows Live" w15:userId="${userId}"/>`;
|
|
271
318
|
xml += `</w15:person>`;
|
|
@@ -366,9 +413,14 @@ export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
|
|
|
366
413
|
const endInText = fullText.indexOf(endMarker);
|
|
367
414
|
if (startInText === -1 || endInText === -1) continue;
|
|
368
415
|
|
|
369
|
-
|
|
416
|
+
let textBefore = fullText.slice(0, startInText);
|
|
370
417
|
const anchorText = fullText.slice(startInText + startMarker.length, endInText);
|
|
371
|
-
|
|
418
|
+
let textAfter = fullText.slice(endInText + endMarker.length);
|
|
419
|
+
|
|
420
|
+
// When anchor is empty, normalize double spaces to single space
|
|
421
|
+
if (!anchorText && textBefore.endsWith(' ') && textAfter.startsWith(' ')) {
|
|
422
|
+
textAfter = textAfter.slice(1); // Remove leading space from textAfter
|
|
423
|
+
}
|
|
372
424
|
|
|
373
425
|
// Build replacement
|
|
374
426
|
let replacement = '';
|