docrev 0.11.3 → 0.12.0
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 +22 -0
- package/LICENSE +21 -0
- package/dist/bin/rev.js +29 -21
- package/dist/bin/rev.js.map +1 -1
- package/dist/lib/build.d.ts +43 -0
- package/dist/lib/build.d.ts.map +1 -1
- package/dist/lib/build.js +175 -5
- package/dist/lib/build.js.map +1 -1
- package/dist/lib/commands/build.d.ts.map +1 -1
- package/dist/lib/commands/build.js +39 -125
- package/dist/lib/commands/build.js.map +1 -1
- package/dist/lib/commands/comments.js +3 -3
- package/dist/lib/commands/comments.js.map +1 -1
- package/dist/lib/commands/context.d.ts +12 -1
- package/dist/lib/commands/context.d.ts.map +1 -1
- package/dist/lib/commands/context.js +34 -1
- package/dist/lib/commands/context.js.map +1 -1
- package/dist/lib/commands/merge-resolve.js +1 -1
- package/dist/lib/commands/merge-resolve.js.map +1 -1
- package/dist/lib/commands/quality.js +2 -2
- package/dist/lib/commands/quality.js.map +1 -1
- package/dist/lib/commands/sections.js +1 -1
- package/dist/lib/commands/sections.js.map +1 -1
- package/dist/lib/commands/text-ops.js +1 -1
- package/dist/lib/commands/text-ops.js.map +1 -1
- package/dist/lib/commands/utilities.d.ts.map +1 -1
- package/dist/lib/commands/utilities.js +5 -2
- package/dist/lib/commands/utilities.js.map +1 -1
- package/dist/lib/commands/word-tools.js +2 -2
- package/dist/lib/commands/word-tools.js.map +1 -1
- package/dist/lib/crossref.d.ts.map +1 -1
- package/dist/lib/crossref.js +12 -41
- package/dist/lib/crossref.js.map +1 -1
- package/dist/lib/csl.d.ts.map +1 -1
- package/dist/lib/csl.js +14 -22
- package/dist/lib/csl.js.map +1 -1
- package/dist/lib/diff-engine.js +1 -1
- package/dist/lib/diff-engine.js.map +1 -1
- package/dist/lib/equations.d.ts.map +1 -1
- package/dist/lib/equations.js +12 -3
- package/dist/lib/equations.js.map +1 -1
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +1 -30
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/image-registry.d.ts.map +1 -1
- package/dist/lib/image-registry.js +4 -0
- package/dist/lib/image-registry.js.map +1 -1
- package/dist/lib/import.d.ts +1 -1
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/import.js +1 -1
- package/dist/lib/import.js.map +1 -1
- package/dist/lib/input.d.ts +0 -11
- package/dist/lib/input.d.ts.map +1 -1
- package/dist/lib/input.js +0 -35
- package/dist/lib/input.js.map +1 -1
- package/dist/lib/merge.d.ts.map +1 -1
- package/dist/lib/merge.js +9 -1
- package/dist/lib/merge.js.map +1 -1
- package/dist/lib/ooxml.d.ts +22 -0
- package/dist/lib/ooxml.d.ts.map +1 -1
- package/dist/lib/ooxml.js +130 -0
- package/dist/lib/ooxml.js.map +1 -1
- package/dist/lib/orcid.d.ts.map +1 -1
- package/dist/lib/orcid.js +16 -7
- package/dist/lib/orcid.js.map +1 -1
- package/dist/lib/plugins.d.ts.map +1 -1
- package/dist/lib/plugins.js +8 -4
- package/dist/lib/plugins.js.map +1 -1
- package/dist/lib/protect-restore.d.ts.map +1 -1
- package/dist/lib/protect-restore.js +7 -5
- package/dist/lib/protect-restore.js.map +1 -1
- package/dist/lib/restore-references.d.ts.map +1 -1
- package/dist/lib/restore-references.js +25 -3
- package/dist/lib/restore-references.js.map +1 -1
- package/dist/lib/schema.d.ts.map +1 -1
- package/dist/lib/schema.js +1 -19
- package/dist/lib/schema.js.map +1 -1
- package/dist/lib/slides.js +1 -1
- package/dist/lib/slides.js.map +1 -1
- package/dist/lib/spelling.js +1 -1
- package/dist/lib/spelling.js.map +1 -1
- package/dist/lib/tui.js +0 -1
- package/dist/lib/tui.js.map +1 -1
- package/dist/lib/utils.d.ts +14 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +41 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/word-extraction.d.ts +5 -5
- package/dist/lib/word-extraction.d.ts.map +1 -1
- package/dist/lib/word-extraction.js +25 -106
- package/dist/lib/word-extraction.js.map +1 -1
- package/dist/lib/word.d.ts +14 -38
- package/dist/lib/word.d.ts.map +1 -1
- package/dist/lib/word.js +24 -179
- package/dist/lib/word.js.map +1 -1
- package/dist/lib/wordcomments.d.ts.map +1 -1
- package/dist/lib/wordcomments.js +9 -7
- package/dist/lib/wordcomments.js.map +1 -1
- package/package.json +17 -6
- package/.gitattributes +0 -1
- package/PLAN-tables-and-postprocess.md +0 -850
- package/bin/rev.ts +0 -145
- package/docs-src/build.py +0 -113
- package/docs-src/extra.css +0 -208
- package/docs-src/md-to-html.lua +0 -6
- package/docs-src/template.html +0 -116
- package/eslint.config.js +0 -27
- package/lib/anchor-match.ts +0 -307
- package/lib/annotations.ts +0 -695
- package/lib/build.ts +0 -2047
- package/lib/citations.ts +0 -160
- package/lib/commands/build.ts +0 -885
- package/lib/commands/citations.ts +0 -515
- package/lib/commands/comments.ts +0 -1077
- package/lib/commands/context.ts +0 -185
- package/lib/commands/core.ts +0 -328
- package/lib/commands/doi.ts +0 -451
- package/lib/commands/file-ops.ts +0 -372
- package/lib/commands/history.ts +0 -320
- package/lib/commands/index.ts +0 -87
- package/lib/commands/init.ts +0 -259
- package/lib/commands/merge-resolve.ts +0 -378
- package/lib/commands/preview.ts +0 -178
- package/lib/commands/project-info.ts +0 -244
- package/lib/commands/quality.ts +0 -517
- package/lib/commands/response.ts +0 -454
- package/lib/commands/section-boundaries.ts +0 -82
- package/lib/commands/sections.ts +0 -451
- package/lib/commands/sync.ts +0 -689
- package/lib/commands/text-ops.ts +0 -449
- package/lib/commands/utilities.ts +0 -448
- package/lib/commands/verify-anchors.ts +0 -272
- package/lib/commands/word-tools.ts +0 -340
- package/lib/comment-realign.ts +0 -99
- package/lib/config.ts +0 -84
- package/lib/crossref.ts +0 -781
- package/lib/csl.ts +0 -191
- package/lib/dependencies.ts +0 -132
- package/lib/diff-engine.ts +0 -465
- package/lib/doi-cache.ts +0 -115
- package/lib/doi.ts +0 -879
- package/lib/equations.ts +0 -506
- package/lib/errors.ts +0 -350
- package/lib/format.ts +0 -541
- package/lib/git.ts +0 -326
- package/lib/grammar.ts +0 -303
- package/lib/image-registry.ts +0 -180
- package/lib/import.ts +0 -932
- package/lib/input.ts +0 -174
- package/lib/journals.ts +0 -543
- package/lib/macro-filter.lua +0 -201
- package/lib/macros.ts +0 -273
- package/lib/merge.ts +0 -633
- package/lib/ooxml.ts +0 -768
- package/lib/orcid.ts +0 -144
- package/lib/pdf-comments.ts +0 -263
- package/lib/pdf-import.ts +0 -524
- package/lib/plugins.ts +0 -362
- package/lib/postprocess.ts +0 -188
- package/lib/pptx-color-filter.lua +0 -37
- package/lib/pptx-template.ts +0 -469
- package/lib/pptx-themes/academic.pptx +0 -0
- package/lib/pptx-themes/corporate.pptx +0 -0
- package/lib/pptx-themes/dark.pptx +0 -0
- package/lib/pptx-themes/default.pptx +0 -0
- package/lib/pptx-themes/minimal.pptx +0 -0
- package/lib/pptx-themes/plant.pptx +0 -0
- package/lib/pptx-themes.ts +0 -483
- package/lib/protect-restore.ts +0 -520
- package/lib/rate-limiter.ts +0 -127
- package/lib/response.ts +0 -200
- package/lib/restore-references.ts +0 -240
- package/lib/review.ts +0 -327
- package/lib/schema.ts +0 -494
- package/lib/scientific-words.ts +0 -73
- package/lib/sections.ts +0 -428
- package/lib/slides.ts +0 -756
- package/lib/spelling.ts +0 -334
- package/lib/templates.ts +0 -526
- package/lib/themes.ts +0 -742
- package/lib/trackchanges.ts +0 -166
- package/lib/tui.ts +0 -450
- package/lib/types.ts +0 -546
- package/lib/undo.ts +0 -250
- package/lib/utils.ts +0 -69
- package/lib/variables.ts +0 -179
- package/lib/word-extraction.ts +0 -525
- package/lib/word.ts +0 -526
- package/lib/wordcomments.ts +0 -789
- package/mkdocs.yml +0 -64
- package/scripts/postbuild.js +0 -47
- package/tsconfig.json +0 -26
- package/types/index.d.ts +0 -531
package/lib/response.ts
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Response letter generator
|
|
3
|
-
* Extract comments and replies from markdown files for journal resubmission
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
|
|
9
|
-
interface Reply {
|
|
10
|
-
author: string;
|
|
11
|
-
text: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface CommentWithReplies {
|
|
15
|
-
author: string;
|
|
16
|
-
text: string;
|
|
17
|
-
replies: Reply[];
|
|
18
|
-
context: string;
|
|
19
|
-
file: string;
|
|
20
|
-
line: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface ResponseOptions {
|
|
24
|
-
title?: string;
|
|
25
|
-
authorName?: string;
|
|
26
|
-
includeContext?: boolean;
|
|
27
|
-
includeLocation?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Parse a comment with potential replies
|
|
32
|
-
* Format: {>>Author: comment<<} {>>Reply Author: reply<<}
|
|
33
|
-
*/
|
|
34
|
-
export function parseCommentsWithReplies(text: string, file: string = ''): CommentWithReplies[] {
|
|
35
|
-
const comments: CommentWithReplies[] = [];
|
|
36
|
-
const lines = text.split('\n');
|
|
37
|
-
|
|
38
|
-
// Pattern for comments: {>>Author: text<<}
|
|
39
|
-
const commentPattern = /\{>>([^:]+):\s*([^<]+)<<\}/g;
|
|
40
|
-
|
|
41
|
-
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
42
|
-
const line = lines[lineNum];
|
|
43
|
-
if (!line) continue;
|
|
44
|
-
const matches = [...line.matchAll(commentPattern)];
|
|
45
|
-
|
|
46
|
-
if (matches.length === 0) continue;
|
|
47
|
-
|
|
48
|
-
// Get context (surrounding text without comments)
|
|
49
|
-
const contextLine = line.replace(/\{>>[\s\S]+?<<\}/g, '').trim();
|
|
50
|
-
const context = contextLine.slice(0, 100) + (contextLine.length > 100 ? '...' : '');
|
|
51
|
-
|
|
52
|
-
// First match is the original comment, rest are replies
|
|
53
|
-
const [first, ...rest] = matches;
|
|
54
|
-
if (!first || !first[1] || !first[2]) continue;
|
|
55
|
-
|
|
56
|
-
comments.push({
|
|
57
|
-
author: first[1].trim(),
|
|
58
|
-
text: first[2].trim(),
|
|
59
|
-
replies: rest.map(m => ({
|
|
60
|
-
author: m[1]?.trim() || '',
|
|
61
|
-
text: m[2]?.trim() || '',
|
|
62
|
-
})),
|
|
63
|
-
context,
|
|
64
|
-
file,
|
|
65
|
-
line: lineNum + 1,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return comments;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Group comments by reviewer
|
|
74
|
-
*/
|
|
75
|
-
export function groupByReviewer(comments: CommentWithReplies[]): Map<string, CommentWithReplies[]> {
|
|
76
|
-
const grouped = new Map<string, CommentWithReplies[]>();
|
|
77
|
-
|
|
78
|
-
for (const comment of comments) {
|
|
79
|
-
const reviewer = comment.author;
|
|
80
|
-
if (!grouped.has(reviewer)) {
|
|
81
|
-
grouped.set(reviewer, []);
|
|
82
|
-
}
|
|
83
|
-
grouped.get(reviewer)!.push(comment);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return grouped;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Generate response letter in Markdown format
|
|
91
|
-
*/
|
|
92
|
-
export function generateResponseLetter(comments: CommentWithReplies[], options: ResponseOptions = {}): string {
|
|
93
|
-
const {
|
|
94
|
-
title = 'Response to Reviewers',
|
|
95
|
-
authorName = 'Author',
|
|
96
|
-
includeContext = true,
|
|
97
|
-
includeLocation = true,
|
|
98
|
-
} = options;
|
|
99
|
-
|
|
100
|
-
const lines: string[] = [];
|
|
101
|
-
lines.push(`# ${title}`);
|
|
102
|
-
lines.push('');
|
|
103
|
-
lines.push(`We thank the reviewers for their constructive feedback. Below we address each comment.`);
|
|
104
|
-
lines.push('');
|
|
105
|
-
|
|
106
|
-
// Group by reviewer
|
|
107
|
-
const grouped = groupByReviewer(comments);
|
|
108
|
-
|
|
109
|
-
// Sort reviewers (put known reviewer names first, then others)
|
|
110
|
-
const reviewers = [...grouped.keys()].sort((a, b) => {
|
|
111
|
-
// "Reviewer" names first, then alphabetical
|
|
112
|
-
const aIsReviewer = a.toLowerCase().includes('reviewer');
|
|
113
|
-
const bIsReviewer = b.toLowerCase().includes('reviewer');
|
|
114
|
-
if (aIsReviewer && !bIsReviewer) return -1;
|
|
115
|
-
if (!aIsReviewer && bIsReviewer) return 1;
|
|
116
|
-
return a.localeCompare(b);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
for (const reviewer of reviewers) {
|
|
120
|
-
// Skip if this is the author's own comments (replies)
|
|
121
|
-
if (reviewer.toLowerCase() === authorName.toLowerCase()) continue;
|
|
122
|
-
if (reviewer.toLowerCase() === 'claude') continue;
|
|
123
|
-
|
|
124
|
-
const reviewerComments = grouped.get(reviewer)!;
|
|
125
|
-
lines.push(`## ${reviewer}`);
|
|
126
|
-
lines.push('');
|
|
127
|
-
|
|
128
|
-
for (let i = 0; i < reviewerComments.length; i++) {
|
|
129
|
-
const c = reviewerComments[i];
|
|
130
|
-
if (!c) continue;
|
|
131
|
-
|
|
132
|
-
lines.push(`### Comment ${i + 1}`);
|
|
133
|
-
if (includeLocation) {
|
|
134
|
-
lines.push(`*${c.file}:${c.line}*`);
|
|
135
|
-
}
|
|
136
|
-
lines.push('');
|
|
137
|
-
|
|
138
|
-
// Original comment
|
|
139
|
-
lines.push(`> **${c.author}:** ${c.text}`);
|
|
140
|
-
lines.push('');
|
|
141
|
-
|
|
142
|
-
// Context if available
|
|
143
|
-
if (includeContext && c.context) {
|
|
144
|
-
lines.push(`*Context:* "${c.context}"`);
|
|
145
|
-
lines.push('');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Replies
|
|
149
|
-
if (c.replies.length > 0) {
|
|
150
|
-
lines.push('**Response:**');
|
|
151
|
-
lines.push('');
|
|
152
|
-
for (const reply of c.replies) {
|
|
153
|
-
lines.push(`${reply.text}`);
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
lines.push('**Response:**');
|
|
157
|
-
lines.push('');
|
|
158
|
-
lines.push('*[TODO: Add response]*');
|
|
159
|
-
}
|
|
160
|
-
lines.push('');
|
|
161
|
-
lines.push('---');
|
|
162
|
-
lines.push('');
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Summary stats
|
|
167
|
-
const totalComments = comments.filter(c =>
|
|
168
|
-
!c.author.toLowerCase().includes('claude') &&
|
|
169
|
-
c.author.toLowerCase() !== authorName.toLowerCase()
|
|
170
|
-
).length;
|
|
171
|
-
const answered = comments.filter(c => c.replies.length > 0).length;
|
|
172
|
-
|
|
173
|
-
lines.push('## Summary');
|
|
174
|
-
lines.push('');
|
|
175
|
-
lines.push(`- Total comments: ${totalComments}`);
|
|
176
|
-
lines.push(`- Addressed: ${answered}`);
|
|
177
|
-
lines.push(`- Pending: ${totalComments - answered}`);
|
|
178
|
-
|
|
179
|
-
return lines.join('\n');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Collect comments from multiple files
|
|
184
|
-
*/
|
|
185
|
-
export async function collectComments(files: string[]): Promise<CommentWithReplies[]> {
|
|
186
|
-
const { readAnnotatedInput } = await import('./input.js');
|
|
187
|
-
const allComments: CommentWithReplies[] = [];
|
|
188
|
-
|
|
189
|
-
for (const file of files) {
|
|
190
|
-
if (!fs.existsSync(file)) continue;
|
|
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);
|
|
195
|
-
const comments = parseCommentsWithReplies(text, path.basename(file));
|
|
196
|
-
allComments.push(...comments);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return allComments;
|
|
200
|
-
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Post-extraction reference restoration and comment parsing
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readImageRegistry } from './image-registry.js';
|
|
6
|
-
|
|
7
|
-
// ============================================
|
|
8
|
-
// Type Definitions
|
|
9
|
-
// ============================================
|
|
10
|
-
|
|
11
|
-
export interface RestoreCrossrefResult {
|
|
12
|
-
text: string;
|
|
13
|
-
restored: number;
|
|
14
|
-
messages: string[];
|
|
15
|
-
restoredLabels: Set<string>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface RestoreImagesResult {
|
|
19
|
-
text: string;
|
|
20
|
-
restored: number;
|
|
21
|
-
messages: string[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ============================================
|
|
25
|
-
// Functions
|
|
26
|
-
// ============================================
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Parse visible comment markers from Word text
|
|
30
|
-
*/
|
|
31
|
-
export function parseVisibleComments(text: string): Array<{ author: string; text: string; position: number }> {
|
|
32
|
-
const comments: Array<{ author: string; text: string; position: number }> = [];
|
|
33
|
-
const pattern = /\[([^\]:]+):\s*([^\]]+)\]/g;
|
|
34
|
-
|
|
35
|
-
let match;
|
|
36
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
37
|
-
comments.push({
|
|
38
|
-
author: match[1].trim(),
|
|
39
|
-
text: match[2].trim(),
|
|
40
|
-
position: match.index,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return comments;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Convert visible comments to CriticMarkup format
|
|
49
|
-
*/
|
|
50
|
-
export function convertVisibleComments(text: string): string {
|
|
51
|
-
return text.replace(/\[([^\]:]+):\s*([^\]]+)\]/g, '{>>$1: $2<<}');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Restore pandoc-crossref figure/table references from Word-rendered format
|
|
56
|
-
*/
|
|
57
|
-
export function restoreCrossrefFromWord(
|
|
58
|
-
text: string,
|
|
59
|
-
projectDir: string,
|
|
60
|
-
restoredLabels: Set<string> | null = null
|
|
61
|
-
): RestoreCrossrefResult {
|
|
62
|
-
const messages: string[] = [];
|
|
63
|
-
let restored = 0;
|
|
64
|
-
let result = text;
|
|
65
|
-
|
|
66
|
-
const registry = readImageRegistry(projectDir);
|
|
67
|
-
|
|
68
|
-
if (!restoredLabels) {
|
|
69
|
-
restoredLabels = new Set<string>();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Pattern 1: [Figure]{.mark} [N]{.mark}
|
|
73
|
-
result = result.replace(/\[(Figure|Table|Fig\.?)\]\{\.mark\}\s*\[(\d+|S\d+)\]\{\.mark\}/gi, (match, type, num) => {
|
|
74
|
-
const prefix = type.toLowerCase().startsWith('tab') ? 'tbl' : 'fig';
|
|
75
|
-
if (registry) {
|
|
76
|
-
const entry = registry.byNumber?.get(`${prefix}:${num}`);
|
|
77
|
-
if (entry && entry.label) {
|
|
78
|
-
restored++;
|
|
79
|
-
return `@${prefix}:${entry.label}`;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
restored++;
|
|
83
|
-
messages.push(`Restored ${type} ${num} (no label found, using placeholder)`);
|
|
84
|
-
return `@${prefix}:fig${num}`;
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Pattern 2: Plain "Figure N" or "Fig. N"
|
|
88
|
-
result = result.replace(/(?<!!)\b(Figure|Fig\.?|Table|Tbl\.?)\s+(\d+|S\d+)\b(?!\s*:)/gi, (match, type, num) => {
|
|
89
|
-
const prefix = type.toLowerCase().startsWith('tab') ? 'tbl' : 'fig';
|
|
90
|
-
if (registry) {
|
|
91
|
-
const entry = registry.byNumber?.get(`${prefix}:${num}`);
|
|
92
|
-
if (entry && entry.label) {
|
|
93
|
-
restored++;
|
|
94
|
-
return `@${prefix}:${entry.label}`;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return match;
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Pattern 3: Remove duplicate plain-text captions
|
|
101
|
-
result = result.replace(/(\!\[[^\]]+\]\([^)]+\)(?:\{[^}]*\})?)\s*\n+\s*(?:Figure|Fig\.?|Table|Tbl\.?)\s+\d+[:\.]?\s*[^\n]+/gi, '$1');
|
|
102
|
-
|
|
103
|
-
// Pattern 4: Clean up image captions that start with "Figure N: "
|
|
104
|
-
result = result.replace(/!\[(Figure|Fig\.?|Table|Tbl\.?)\s+(\d+|S\d+)[:\.]?\s*([^\]]*)\]\(([^)]+)\)(?:\{[^}]*\})?/gi,
|
|
105
|
-
(match, type, num, caption, imgPath) => {
|
|
106
|
-
const prefix = type.toLowerCase().startsWith('tab') ? 'tbl' : 'fig';
|
|
107
|
-
const labelKey = `${prefix}:${num}`;
|
|
108
|
-
|
|
109
|
-
if (registry) {
|
|
110
|
-
const entry = registry.byNumber?.get(labelKey);
|
|
111
|
-
if (entry) {
|
|
112
|
-
if (restoredLabels!.has(labelKey)) {
|
|
113
|
-
messages.push(`Skipped duplicate ${prefix}:${entry.label} (already restored)`);
|
|
114
|
-
return ``;
|
|
115
|
-
}
|
|
116
|
-
restoredLabels!.add(labelKey);
|
|
117
|
-
restored++;
|
|
118
|
-
messages.push(`Restored image ${prefix}:${entry.label} from Figure ${num}`);
|
|
119
|
-
return `{#${prefix}:${entry.label}}`;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const cleanCaption = caption.trim();
|
|
123
|
-
return ``;
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
return { text: result, restored, messages, restoredLabels };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Restore proper markdown image syntax from Word-extracted text using image registry
|
|
131
|
-
*/
|
|
132
|
-
export function restoreImagesFromRegistry(
|
|
133
|
-
text: string,
|
|
134
|
-
projectDir: string,
|
|
135
|
-
restoredLabels: Set<string> | null = null
|
|
136
|
-
): RestoreImagesResult {
|
|
137
|
-
const messages: string[] = [];
|
|
138
|
-
let restored = 0;
|
|
139
|
-
|
|
140
|
-
const registry = readImageRegistry(projectDir);
|
|
141
|
-
if (!registry || !registry.figures || registry.figures.length === 0) {
|
|
142
|
-
return { text, restored: 0, messages: ['No image registry found'] };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (!restoredLabels) {
|
|
146
|
-
restoredLabels = new Set<string>();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
let result = text;
|
|
150
|
-
|
|
151
|
-
// Pattern 1: Caption-like text
|
|
152
|
-
const captionPatterns = [
|
|
153
|
-
/@(fig|tbl):([a-zA-Z0-9_-]+):\s*([^\n]+)/gi,
|
|
154
|
-
/^(Figure|Fig\.?)\s+(\d+|S\d+)[.:]\s*([^\n]+)/gim,
|
|
155
|
-
/\|\s*@(fig|tbl):([a-zA-Z0-9_-]+):\s*([^|]+)\s*\|/gi,
|
|
156
|
-
];
|
|
157
|
-
|
|
158
|
-
// Fix @fig:label: caption patterns
|
|
159
|
-
result = result.replace(captionPatterns[0], (match, type, label, caption) => {
|
|
160
|
-
const key = `${type}:${label}`;
|
|
161
|
-
const entry = registry.byLabel.get(key);
|
|
162
|
-
if (entry) {
|
|
163
|
-
if (restoredLabels!.has(key)) {
|
|
164
|
-
messages.push(`Skipped duplicate ${key} (already restored)`);
|
|
165
|
-
return ``;
|
|
166
|
-
}
|
|
167
|
-
restoredLabels!.add(key);
|
|
168
|
-
restored++;
|
|
169
|
-
messages.push(`Restored ${type}:${label} from registry`);
|
|
170
|
-
return `{#${type}:${label}}`;
|
|
171
|
-
}
|
|
172
|
-
return match;
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Fix table-wrapped captions
|
|
176
|
-
result = result.replace(captionPatterns[2], (match, type, label, caption) => {
|
|
177
|
-
const key = `${type}:${label}`;
|
|
178
|
-
const entry = registry.byLabel.get(key);
|
|
179
|
-
if (entry) {
|
|
180
|
-
if (restoredLabels!.has(key)) {
|
|
181
|
-
messages.push(`Skipped duplicate ${key} from table wrapper`);
|
|
182
|
-
return ``;
|
|
183
|
-
}
|
|
184
|
-
restoredLabels!.add(key);
|
|
185
|
-
restored++;
|
|
186
|
-
messages.push(`Restored ${type}:${label} from table wrapper`);
|
|
187
|
-
return `{#${type}:${label}}`;
|
|
188
|
-
}
|
|
189
|
-
return match;
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// Clean up empty table structures
|
|
193
|
-
result = result.replace(/\|\s*\|\s*\n\|:--:\|\s*\n/g, '');
|
|
194
|
-
|
|
195
|
-
// Fix "Figure N:" standalone lines
|
|
196
|
-
result = result.replace(captionPatterns[1], (match, prefix, num, caption) => {
|
|
197
|
-
const numKey = `fig:${num}`;
|
|
198
|
-
const entry = registry.byNumber.get(numKey);
|
|
199
|
-
if (entry) {
|
|
200
|
-
const labelKey = `fig:${entry.label}`;
|
|
201
|
-
if (restoredLabels!.has(labelKey)) {
|
|
202
|
-
messages.push(`Skipped duplicate Figure ${num} (already restored)`);
|
|
203
|
-
return ``;
|
|
204
|
-
}
|
|
205
|
-
restoredLabels!.add(labelKey);
|
|
206
|
-
restored++;
|
|
207
|
-
messages.push(`Restored Figure ${num} by number lookup`);
|
|
208
|
-
return `{#fig:${entry.label}}`;
|
|
209
|
-
}
|
|
210
|
-
return match;
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Fix generic media paths by matching caption text
|
|
214
|
-
const genericImagePattern = /!\[([^\]]*)\]\(media\/[^)]+\)/g;
|
|
215
|
-
result = result.replace(genericImagePattern, (match, caption) => {
|
|
216
|
-
if (!caption || caption.trim() === '') {
|
|
217
|
-
return match;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const captionKey = caption.slice(0, 50).toLowerCase().trim();
|
|
221
|
-
const entry = registry.byCaption.get(captionKey);
|
|
222
|
-
if (entry) {
|
|
223
|
-
const labelKey = entry.label ? `${entry.type}:${entry.label}` : null;
|
|
224
|
-
if (labelKey && restoredLabels!.has(labelKey)) {
|
|
225
|
-
messages.push(`Skipped duplicate by caption match: ${captionKey.slice(0, 30)}...`);
|
|
226
|
-
return ``;
|
|
227
|
-
}
|
|
228
|
-
if (labelKey) {
|
|
229
|
-
restoredLabels!.add(labelKey);
|
|
230
|
-
}
|
|
231
|
-
restored++;
|
|
232
|
-
messages.push(`Restored image by caption match: ${captionKey.slice(0, 30)}...`);
|
|
233
|
-
const anchor = (entry.label && !restoredLabels!.has(labelKey!)) ? `{#${entry.type}:${entry.label}}` : '';
|
|
234
|
-
return `${anchor}`;
|
|
235
|
-
}
|
|
236
|
-
return match;
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
return { text: result, restored, messages };
|
|
240
|
-
}
|