docrev 0.9.13 → 0.9.15
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 +411 -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 +38 -1
- package/dist/lib/build.d.ts.map +1 -1
- package/dist/lib/build.js +68 -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/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/eslint.config.js +27 -27
- package/lib/anchor-match.ts +276 -276
- package/lib/annotations.ts +644 -644
- package/lib/build.ts +1300 -1251
- 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 -911
- 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 -550
- package/lib/undo.ts +250 -250
- package/lib/utils.ts +69 -69
- package/lib/variables.ts +179 -179
- package/lib/word-extraction.ts +806 -806
- package/lib/word.ts +643 -643
- package/lib/wordcomments.ts +817 -817
- package/package.json +137 -137
- package/scripts/postbuild.js +28 -28
- package/skill/REFERENCE.md +473 -431
- package/skill/SKILL.md +274 -258
- package/tsconfig.json +26 -26
- package/types/index.d.ts +525 -525
package/lib/sections.ts
CHANGED
|
@@ -1,335 +1,335 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Section handling - map between section .md files and combined documents
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
import YAML from 'yaml';
|
|
8
|
-
import type { SectionConfig, SectionsConfig, ExtractedSection } from './types.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Default section order (common academic paper structure)
|
|
12
|
-
*/
|
|
13
|
-
const DEFAULT_ORDER = [
|
|
14
|
-
'abstract',
|
|
15
|
-
'introduction',
|
|
16
|
-
'background',
|
|
17
|
-
'literature',
|
|
18
|
-
'theory',
|
|
19
|
-
'methods',
|
|
20
|
-
'materials',
|
|
21
|
-
'data',
|
|
22
|
-
'results',
|
|
23
|
-
'analysis',
|
|
24
|
-
'discussion',
|
|
25
|
-
'conclusion',
|
|
26
|
-
'references',
|
|
27
|
-
'appendix',
|
|
28
|
-
'supplementary',
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Extract header from a markdown file
|
|
33
|
-
*/
|
|
34
|
-
export function extractHeader(filePath: string): string | null {
|
|
35
|
-
if (!fs.existsSync(filePath)) return null;
|
|
36
|
-
|
|
37
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
-
const lines = content.split('\n');
|
|
39
|
-
|
|
40
|
-
for (const line of lines) {
|
|
41
|
-
const match = line.match(/^#\s+(.+)$/);
|
|
42
|
-
if (match && match[1]) {
|
|
43
|
-
return match[1].trim();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Generate sections.yaml from existing .md files
|
|
52
|
-
*/
|
|
53
|
-
export function generateConfig(
|
|
54
|
-
directory: string,
|
|
55
|
-
excludePatterns: string[] = ['paper.md', 'README.md', 'CLAUDE.md']
|
|
56
|
-
): SectionsConfig {
|
|
57
|
-
const files = fs.readdirSync(directory).filter((f) => {
|
|
58
|
-
if (!f.endsWith('.md')) return false;
|
|
59
|
-
if (excludePatterns.some((p) => f.toLowerCase().includes(p.toLowerCase()))) return false;
|
|
60
|
-
return true;
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const sections: Record<string, SectionConfig> = {};
|
|
64
|
-
|
|
65
|
-
for (const file of files) {
|
|
66
|
-
const filePath = path.join(directory, file);
|
|
67
|
-
const header = extractHeader(filePath);
|
|
68
|
-
const baseName = path.basename(file, '.md').toLowerCase();
|
|
69
|
-
|
|
70
|
-
// Determine order based on common patterns
|
|
71
|
-
let order = DEFAULT_ORDER.findIndex((s) => baseName.includes(s));
|
|
72
|
-
if (order === -1) order = 999;
|
|
73
|
-
|
|
74
|
-
sections[file] = {
|
|
75
|
-
header: header || titleCase(baseName),
|
|
76
|
-
aliases: [],
|
|
77
|
-
order: order,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Sort by order
|
|
82
|
-
const sorted = Object.entries(sections)
|
|
83
|
-
.sort((a, b) => (a[1].order ?? 999) - (b[1].order ?? 999))
|
|
84
|
-
.reduce((acc, [k, v]) => {
|
|
85
|
-
acc[k] = v;
|
|
86
|
-
return acc;
|
|
87
|
-
}, {} as Record<string, SectionConfig>);
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
version: 1,
|
|
91
|
-
description: 'Section configuration for rev import/split',
|
|
92
|
-
sections: sorted,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Convert string to title case
|
|
98
|
-
*/
|
|
99
|
-
function titleCase(str: string): string {
|
|
100
|
-
return str
|
|
101
|
-
.split(/[-_\s]+/)
|
|
102
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
103
|
-
.join(' ');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Load sections config from yaml file
|
|
108
|
-
*/
|
|
109
|
-
export function loadConfig(configPath: string): SectionsConfig {
|
|
110
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
111
|
-
const config = YAML.parse(content);
|
|
112
|
-
|
|
113
|
-
// Normalize: convert string values to full config objects
|
|
114
|
-
const normalized: SectionsConfig = {
|
|
115
|
-
version: config.version || 1,
|
|
116
|
-
description: config.description,
|
|
117
|
-
sections: {},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
for (const [file, value] of Object.entries(config.sections || {})) {
|
|
121
|
-
if (typeof value === 'string') {
|
|
122
|
-
normalized.sections[file] = {
|
|
123
|
-
header: value,
|
|
124
|
-
aliases: [],
|
|
125
|
-
};
|
|
126
|
-
} else {
|
|
127
|
-
const typedValue = value as SectionConfig;
|
|
128
|
-
normalized.sections[file] = {
|
|
129
|
-
header: typedValue.header,
|
|
130
|
-
aliases: typedValue.aliases || [],
|
|
131
|
-
order: typedValue.order,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return normalized;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Save sections config to yaml file
|
|
141
|
-
*/
|
|
142
|
-
export function saveConfig(configPath: string, config: SectionsConfig): void {
|
|
143
|
-
const yamlStr = YAML.stringify(config, { indent: 2, lineWidth: 100 });
|
|
144
|
-
fs.writeFileSync(configPath, yamlStr, 'utf-8');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Match a heading to a section file
|
|
149
|
-
*/
|
|
150
|
-
export function matchHeading(
|
|
151
|
-
heading: string,
|
|
152
|
-
sections: Record<string, SectionConfig>
|
|
153
|
-
): { file: string; config: SectionConfig } | null {
|
|
154
|
-
// Strip markdown header prefix (# or ##, etc.) before matching
|
|
155
|
-
const normalizedHeading = heading.replace(/^#{1,6}\s+/, '').toLowerCase().trim();
|
|
156
|
-
|
|
157
|
-
for (const [file, config] of Object.entries(sections)) {
|
|
158
|
-
// Check primary header
|
|
159
|
-
if (config.header.toLowerCase().trim() === normalizedHeading) {
|
|
160
|
-
return { file, config };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Check aliases
|
|
164
|
-
if (config.aliases) {
|
|
165
|
-
for (const alias of config.aliases) {
|
|
166
|
-
if (alias.toLowerCase().trim() === normalizedHeading) {
|
|
167
|
-
return { file, config };
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Fuzzy match: check if heading contains the key words
|
|
173
|
-
const headerWords = config.header.toLowerCase().split(/\s+/);
|
|
174
|
-
const headingWords = normalizedHeading.split(/\s+/);
|
|
175
|
-
const matchCount = headerWords.filter((w) => headingWords.includes(w)).length;
|
|
176
|
-
if (matchCount >= headerWords.length * 0.7) {
|
|
177
|
-
return { file, config };
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Extract sections from Word document text
|
|
186
|
-
*/
|
|
187
|
-
export function extractSectionsFromText(
|
|
188
|
-
text: string,
|
|
189
|
-
sections: Record<string, SectionConfig>
|
|
190
|
-
): ExtractedSection[] {
|
|
191
|
-
const result: ExtractedSection[] = [];
|
|
192
|
-
|
|
193
|
-
// Process line by line to detect markdown headers
|
|
194
|
-
const lines = text.split('\n');
|
|
195
|
-
let currentSection: { file: string; header: string } | null = null;
|
|
196
|
-
let currentContent: string[] = [];
|
|
197
|
-
|
|
198
|
-
for (let i = 0; i < lines.length; i++) {
|
|
199
|
-
const line = lines[i];
|
|
200
|
-
if (!line) continue;
|
|
201
|
-
const trimmed = line.trim();
|
|
202
|
-
|
|
203
|
-
// Explicitly check for markdown headers (# Header)
|
|
204
|
-
const headerMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
205
|
-
|
|
206
|
-
let matchedSection: { file: string; config: SectionConfig } | null = null;
|
|
207
|
-
if (headerMatch) {
|
|
208
|
-
// This is a markdown header - try to match it to a section
|
|
209
|
-
matchedSection = matchHeading(trimmed, sections);
|
|
210
|
-
} else if (trimmed.length > 0 && trimmed.length < 100 && !trimmed.includes('.')) {
|
|
211
|
-
// Fallback: check if short text without periods matches a section (for plain text headings)
|
|
212
|
-
matchedSection = matchHeading(trimmed, sections);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (matchedSection) {
|
|
216
|
-
// Save previous section
|
|
217
|
-
if (currentSection) {
|
|
218
|
-
// Include header in content for proper diffing
|
|
219
|
-
const fullContent = currentSection.header + '\n\n' + currentContent.join('\n').trim();
|
|
220
|
-
result.push({
|
|
221
|
-
file: currentSection.file,
|
|
222
|
-
header: currentSection.header,
|
|
223
|
-
content: fullContent.trim(),
|
|
224
|
-
matched: true,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
currentSection = {
|
|
229
|
-
file: matchedSection.file,
|
|
230
|
-
header: trimmed,
|
|
231
|
-
};
|
|
232
|
-
currentContent = [];
|
|
233
|
-
} else {
|
|
234
|
-
currentContent.push(line);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Save last section
|
|
239
|
-
if (currentSection) {
|
|
240
|
-
// Include header in content for proper diffing
|
|
241
|
-
const fullContent = currentSection.header + '\n\n' + currentContent.join('\n').trim();
|
|
242
|
-
result.push({
|
|
243
|
-
file: currentSection.file,
|
|
244
|
-
header: currentSection.header,
|
|
245
|
-
content: fullContent.trim(),
|
|
246
|
-
matched: true,
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return result;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Parse annotated paper.md and split back to section files
|
|
255
|
-
*/
|
|
256
|
-
export function splitAnnotatedPaper(
|
|
257
|
-
paperContent: string,
|
|
258
|
-
sections: Record<string, SectionConfig>
|
|
259
|
-
): Map<string, string> {
|
|
260
|
-
const result = new Map<string, string>();
|
|
261
|
-
|
|
262
|
-
// Look for section markers: <!-- @section:filename.md -->
|
|
263
|
-
const markerPattern = /<!--\s*@section:(\S+\.md)\s*-->/g;
|
|
264
|
-
const markers = [...paperContent.matchAll(markerPattern)];
|
|
265
|
-
|
|
266
|
-
if (markers.length > 0) {
|
|
267
|
-
// Use markers
|
|
268
|
-
for (let i = 0; i < markers.length; i++) {
|
|
269
|
-
const marker = markers[i];
|
|
270
|
-
if (!marker || !marker[1]) continue;
|
|
271
|
-
const file = marker[1];
|
|
272
|
-
const start = (marker.index || 0) + marker[0].length;
|
|
273
|
-
const end = markers[i + 1]?.index || paperContent.length;
|
|
274
|
-
|
|
275
|
-
let content = paperContent.slice(start, end).trim();
|
|
276
|
-
|
|
277
|
-
// Remove trailing marker if present
|
|
278
|
-
content = content.replace(/<!--\s*@section:\S+\.md\s*-->$/, '').trim();
|
|
279
|
-
|
|
280
|
-
result.set(file, content);
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
// Fall back to header detection
|
|
284
|
-
const lines = paperContent.split('\n');
|
|
285
|
-
let currentFile: string | null = null;
|
|
286
|
-
let currentContent: string[] = [];
|
|
287
|
-
|
|
288
|
-
for (const line of lines) {
|
|
289
|
-
const headerMatch = line.match(/^#\s+(.+)$/);
|
|
290
|
-
|
|
291
|
-
if (headerMatch && headerMatch[1]) {
|
|
292
|
-
// Save previous section
|
|
293
|
-
if (currentFile) {
|
|
294
|
-
result.set(currentFile, currentContent.join('\n').trim());
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Find matching section file
|
|
298
|
-
const heading = headerMatch[1].trim();
|
|
299
|
-
const match = matchHeading(heading, sections);
|
|
300
|
-
|
|
301
|
-
if (match) {
|
|
302
|
-
currentFile = match.file;
|
|
303
|
-
currentContent = [line];
|
|
304
|
-
} else {
|
|
305
|
-
// Unknown section - keep accumulating to previous
|
|
306
|
-
currentContent.push(line);
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
currentContent.push(line);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Save last section
|
|
314
|
-
if (currentFile) {
|
|
315
|
-
result.set(currentFile, currentContent.join('\n').trim());
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return result;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Get ordered list of section files from config
|
|
324
|
-
*/
|
|
325
|
-
export function getOrderedSections(config: SectionsConfig): string[] {
|
|
326
|
-
const entries = Object.entries(config.sections || {});
|
|
327
|
-
|
|
328
|
-
return entries
|
|
329
|
-
.sort((a, b) => {
|
|
330
|
-
const orderA = a[1].order ?? 999;
|
|
331
|
-
const orderB = b[1].order ?? 999;
|
|
332
|
-
return orderA - orderB;
|
|
333
|
-
})
|
|
334
|
-
.map(([file]) => file);
|
|
335
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Section handling - map between section .md files and combined documents
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import YAML from 'yaml';
|
|
8
|
+
import type { SectionConfig, SectionsConfig, ExtractedSection } from './types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default section order (common academic paper structure)
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_ORDER = [
|
|
14
|
+
'abstract',
|
|
15
|
+
'introduction',
|
|
16
|
+
'background',
|
|
17
|
+
'literature',
|
|
18
|
+
'theory',
|
|
19
|
+
'methods',
|
|
20
|
+
'materials',
|
|
21
|
+
'data',
|
|
22
|
+
'results',
|
|
23
|
+
'analysis',
|
|
24
|
+
'discussion',
|
|
25
|
+
'conclusion',
|
|
26
|
+
'references',
|
|
27
|
+
'appendix',
|
|
28
|
+
'supplementary',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract header from a markdown file
|
|
33
|
+
*/
|
|
34
|
+
export function extractHeader(filePath: string): string | null {
|
|
35
|
+
if (!fs.existsSync(filePath)) return null;
|
|
36
|
+
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
+
const lines = content.split('\n');
|
|
39
|
+
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const match = line.match(/^#\s+(.+)$/);
|
|
42
|
+
if (match && match[1]) {
|
|
43
|
+
return match[1].trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate sections.yaml from existing .md files
|
|
52
|
+
*/
|
|
53
|
+
export function generateConfig(
|
|
54
|
+
directory: string,
|
|
55
|
+
excludePatterns: string[] = ['paper.md', 'README.md', 'CLAUDE.md']
|
|
56
|
+
): SectionsConfig {
|
|
57
|
+
const files = fs.readdirSync(directory).filter((f) => {
|
|
58
|
+
if (!f.endsWith('.md')) return false;
|
|
59
|
+
if (excludePatterns.some((p) => f.toLowerCase().includes(p.toLowerCase()))) return false;
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const sections: Record<string, SectionConfig> = {};
|
|
64
|
+
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
const filePath = path.join(directory, file);
|
|
67
|
+
const header = extractHeader(filePath);
|
|
68
|
+
const baseName = path.basename(file, '.md').toLowerCase();
|
|
69
|
+
|
|
70
|
+
// Determine order based on common patterns
|
|
71
|
+
let order = DEFAULT_ORDER.findIndex((s) => baseName.includes(s));
|
|
72
|
+
if (order === -1) order = 999;
|
|
73
|
+
|
|
74
|
+
sections[file] = {
|
|
75
|
+
header: header || titleCase(baseName),
|
|
76
|
+
aliases: [],
|
|
77
|
+
order: order,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Sort by order
|
|
82
|
+
const sorted = Object.entries(sections)
|
|
83
|
+
.sort((a, b) => (a[1].order ?? 999) - (b[1].order ?? 999))
|
|
84
|
+
.reduce((acc, [k, v]) => {
|
|
85
|
+
acc[k] = v;
|
|
86
|
+
return acc;
|
|
87
|
+
}, {} as Record<string, SectionConfig>);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
version: 1,
|
|
91
|
+
description: 'Section configuration for rev import/split',
|
|
92
|
+
sections: sorted,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convert string to title case
|
|
98
|
+
*/
|
|
99
|
+
function titleCase(str: string): string {
|
|
100
|
+
return str
|
|
101
|
+
.split(/[-_\s]+/)
|
|
102
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
103
|
+
.join(' ');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load sections config from yaml file
|
|
108
|
+
*/
|
|
109
|
+
export function loadConfig(configPath: string): SectionsConfig {
|
|
110
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
111
|
+
const config = YAML.parse(content);
|
|
112
|
+
|
|
113
|
+
// Normalize: convert string values to full config objects
|
|
114
|
+
const normalized: SectionsConfig = {
|
|
115
|
+
version: config.version || 1,
|
|
116
|
+
description: config.description,
|
|
117
|
+
sections: {},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (const [file, value] of Object.entries(config.sections || {})) {
|
|
121
|
+
if (typeof value === 'string') {
|
|
122
|
+
normalized.sections[file] = {
|
|
123
|
+
header: value,
|
|
124
|
+
aliases: [],
|
|
125
|
+
};
|
|
126
|
+
} else {
|
|
127
|
+
const typedValue = value as SectionConfig;
|
|
128
|
+
normalized.sections[file] = {
|
|
129
|
+
header: typedValue.header,
|
|
130
|
+
aliases: typedValue.aliases || [],
|
|
131
|
+
order: typedValue.order,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return normalized;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Save sections config to yaml file
|
|
141
|
+
*/
|
|
142
|
+
export function saveConfig(configPath: string, config: SectionsConfig): void {
|
|
143
|
+
const yamlStr = YAML.stringify(config, { indent: 2, lineWidth: 100 });
|
|
144
|
+
fs.writeFileSync(configPath, yamlStr, 'utf-8');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Match a heading to a section file
|
|
149
|
+
*/
|
|
150
|
+
export function matchHeading(
|
|
151
|
+
heading: string,
|
|
152
|
+
sections: Record<string, SectionConfig>
|
|
153
|
+
): { file: string; config: SectionConfig } | null {
|
|
154
|
+
// Strip markdown header prefix (# or ##, etc.) before matching
|
|
155
|
+
const normalizedHeading = heading.replace(/^#{1,6}\s+/, '').toLowerCase().trim();
|
|
156
|
+
|
|
157
|
+
for (const [file, config] of Object.entries(sections)) {
|
|
158
|
+
// Check primary header
|
|
159
|
+
if (config.header.toLowerCase().trim() === normalizedHeading) {
|
|
160
|
+
return { file, config };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check aliases
|
|
164
|
+
if (config.aliases) {
|
|
165
|
+
for (const alias of config.aliases) {
|
|
166
|
+
if (alias.toLowerCase().trim() === normalizedHeading) {
|
|
167
|
+
return { file, config };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fuzzy match: check if heading contains the key words
|
|
173
|
+
const headerWords = config.header.toLowerCase().split(/\s+/);
|
|
174
|
+
const headingWords = normalizedHeading.split(/\s+/);
|
|
175
|
+
const matchCount = headerWords.filter((w) => headingWords.includes(w)).length;
|
|
176
|
+
if (matchCount >= headerWords.length * 0.7) {
|
|
177
|
+
return { file, config };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Extract sections from Word document text
|
|
186
|
+
*/
|
|
187
|
+
export function extractSectionsFromText(
|
|
188
|
+
text: string,
|
|
189
|
+
sections: Record<string, SectionConfig>
|
|
190
|
+
): ExtractedSection[] {
|
|
191
|
+
const result: ExtractedSection[] = [];
|
|
192
|
+
|
|
193
|
+
// Process line by line to detect markdown headers
|
|
194
|
+
const lines = text.split('\n');
|
|
195
|
+
let currentSection: { file: string; header: string } | null = null;
|
|
196
|
+
let currentContent: string[] = [];
|
|
197
|
+
|
|
198
|
+
for (let i = 0; i < lines.length; i++) {
|
|
199
|
+
const line = lines[i];
|
|
200
|
+
if (!line) continue;
|
|
201
|
+
const trimmed = line.trim();
|
|
202
|
+
|
|
203
|
+
// Explicitly check for markdown headers (# Header)
|
|
204
|
+
const headerMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
205
|
+
|
|
206
|
+
let matchedSection: { file: string; config: SectionConfig } | null = null;
|
|
207
|
+
if (headerMatch) {
|
|
208
|
+
// This is a markdown header - try to match it to a section
|
|
209
|
+
matchedSection = matchHeading(trimmed, sections);
|
|
210
|
+
} else if (trimmed.length > 0 && trimmed.length < 100 && !trimmed.includes('.')) {
|
|
211
|
+
// Fallback: check if short text without periods matches a section (for plain text headings)
|
|
212
|
+
matchedSection = matchHeading(trimmed, sections);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (matchedSection) {
|
|
216
|
+
// Save previous section
|
|
217
|
+
if (currentSection) {
|
|
218
|
+
// Include header in content for proper diffing
|
|
219
|
+
const fullContent = currentSection.header + '\n\n' + currentContent.join('\n').trim();
|
|
220
|
+
result.push({
|
|
221
|
+
file: currentSection.file,
|
|
222
|
+
header: currentSection.header,
|
|
223
|
+
content: fullContent.trim(),
|
|
224
|
+
matched: true,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
currentSection = {
|
|
229
|
+
file: matchedSection.file,
|
|
230
|
+
header: trimmed,
|
|
231
|
+
};
|
|
232
|
+
currentContent = [];
|
|
233
|
+
} else {
|
|
234
|
+
currentContent.push(line);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Save last section
|
|
239
|
+
if (currentSection) {
|
|
240
|
+
// Include header in content for proper diffing
|
|
241
|
+
const fullContent = currentSection.header + '\n\n' + currentContent.join('\n').trim();
|
|
242
|
+
result.push({
|
|
243
|
+
file: currentSection.file,
|
|
244
|
+
header: currentSection.header,
|
|
245
|
+
content: fullContent.trim(),
|
|
246
|
+
matched: true,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parse annotated paper.md and split back to section files
|
|
255
|
+
*/
|
|
256
|
+
export function splitAnnotatedPaper(
|
|
257
|
+
paperContent: string,
|
|
258
|
+
sections: Record<string, SectionConfig>
|
|
259
|
+
): Map<string, string> {
|
|
260
|
+
const result = new Map<string, string>();
|
|
261
|
+
|
|
262
|
+
// Look for section markers: <!-- @section:filename.md -->
|
|
263
|
+
const markerPattern = /<!--\s*@section:(\S+\.md)\s*-->/g;
|
|
264
|
+
const markers = [...paperContent.matchAll(markerPattern)];
|
|
265
|
+
|
|
266
|
+
if (markers.length > 0) {
|
|
267
|
+
// Use markers
|
|
268
|
+
for (let i = 0; i < markers.length; i++) {
|
|
269
|
+
const marker = markers[i];
|
|
270
|
+
if (!marker || !marker[1]) continue;
|
|
271
|
+
const file = marker[1];
|
|
272
|
+
const start = (marker.index || 0) + marker[0].length;
|
|
273
|
+
const end = markers[i + 1]?.index || paperContent.length;
|
|
274
|
+
|
|
275
|
+
let content = paperContent.slice(start, end).trim();
|
|
276
|
+
|
|
277
|
+
// Remove trailing marker if present
|
|
278
|
+
content = content.replace(/<!--\s*@section:\S+\.md\s*-->$/, '').trim();
|
|
279
|
+
|
|
280
|
+
result.set(file, content);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
// Fall back to header detection
|
|
284
|
+
const lines = paperContent.split('\n');
|
|
285
|
+
let currentFile: string | null = null;
|
|
286
|
+
let currentContent: string[] = [];
|
|
287
|
+
|
|
288
|
+
for (const line of lines) {
|
|
289
|
+
const headerMatch = line.match(/^#\s+(.+)$/);
|
|
290
|
+
|
|
291
|
+
if (headerMatch && headerMatch[1]) {
|
|
292
|
+
// Save previous section
|
|
293
|
+
if (currentFile) {
|
|
294
|
+
result.set(currentFile, currentContent.join('\n').trim());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Find matching section file
|
|
298
|
+
const heading = headerMatch[1].trim();
|
|
299
|
+
const match = matchHeading(heading, sections);
|
|
300
|
+
|
|
301
|
+
if (match) {
|
|
302
|
+
currentFile = match.file;
|
|
303
|
+
currentContent = [line];
|
|
304
|
+
} else {
|
|
305
|
+
// Unknown section - keep accumulating to previous
|
|
306
|
+
currentContent.push(line);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
currentContent.push(line);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Save last section
|
|
314
|
+
if (currentFile) {
|
|
315
|
+
result.set(currentFile, currentContent.join('\n').trim());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get ordered list of section files from config
|
|
324
|
+
*/
|
|
325
|
+
export function getOrderedSections(config: SectionsConfig): string[] {
|
|
326
|
+
const entries = Object.entries(config.sections || {});
|
|
327
|
+
|
|
328
|
+
return entries
|
|
329
|
+
.sort((a, b) => {
|
|
330
|
+
const orderA = a[1].order ?? 999;
|
|
331
|
+
const orderB = b[1].order ?? 999;
|
|
332
|
+
return orderA - orderB;
|
|
333
|
+
})
|
|
334
|
+
.map(([file]) => file);
|
|
335
|
+
}
|