docrev 0.10.0 → 0.10.2
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 +20 -0
- package/dist/lib/commands/context.d.ts +1 -1
- package/dist/lib/commands/context.d.ts.map +1 -1
- package/dist/lib/commands/context.js +1 -1
- package/dist/lib/commands/context.js.map +1 -1
- package/dist/lib/commands/sections.js +7 -7
- package/dist/lib/commands/sections.js.map +1 -1
- package/dist/lib/commands/sync.d.ts.map +1 -1
- package/dist/lib/commands/sync.js +93 -114
- package/dist/lib/commands/sync.js.map +1 -1
- package/dist/lib/commands/verify-anchors.js +6 -6
- package/dist/lib/commands/verify-anchors.js.map +1 -1
- package/dist/lib/sections.d.ts +35 -0
- package/dist/lib/sections.d.ts.map +1 -1
- package/dist/lib/sections.js +86 -2
- package/dist/lib/sections.js.map +1 -1
- package/docs-src/build.py +113 -113
- package/docs-src/extra.css +208 -208
- package/docs-src/md-to-html.lua +6 -6
- package/docs-src/template.html +116 -116
- package/issues.md +180 -0
- package/lib/commands/context.ts +2 -0
- package/lib/commands/sections.ts +7 -7
- package/lib/commands/sync.ts +95 -125
- package/lib/commands/verify-anchors.ts +6 -6
- package/lib/sections.ts +95 -2
- package/mkdocs.yml +64 -64
- package/package.json +1 -1
- package/site/assets/extra.css +208 -0
- package/site/commands.html +926 -0
- package/site/configuration.html +469 -0
- package/site/index.html +288 -0
- package/site/troubleshooting.html +461 -0
- package/site/workflow.html +518 -0
package/lib/commands/sync.ts
CHANGED
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
path,
|
|
11
11
|
fmt,
|
|
12
12
|
findFiles,
|
|
13
|
-
|
|
13
|
+
resolveSectionsConfig,
|
|
14
|
+
getOrderedSections,
|
|
14
15
|
extractSectionsFromText,
|
|
15
16
|
countAnnotations,
|
|
16
17
|
buildRegistry,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
inlineDiffPreview,
|
|
19
20
|
} from './context.js';
|
|
20
21
|
import type { Command } from 'commander';
|
|
22
|
+
import type { SectionsConfig } from '../types.js';
|
|
21
23
|
import * as readline from 'readline';
|
|
22
24
|
|
|
23
25
|
interface ImportStats {
|
|
@@ -113,12 +115,11 @@ export function register(program: Command): void {
|
|
|
113
115
|
console.log(chalk.dim(`Total: ${stats.total} comments from ${authorList}`));
|
|
114
116
|
console.log();
|
|
115
117
|
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
const
|
|
119
|
-
const mainSection = config.sections?.[0];
|
|
118
|
+
const resolved = resolveSectionsConfig(options.dir, options.config);
|
|
119
|
+
if (resolved && !options.dryRun) {
|
|
120
|
+
const mainSection = getOrderedSections(resolved.config)[0];
|
|
120
121
|
|
|
121
|
-
if (mainSection
|
|
122
|
+
if (mainSection) {
|
|
122
123
|
const mainPath = path.join(options.dir, mainSection);
|
|
123
124
|
if (fs.existsSync(mainPath)) {
|
|
124
125
|
console.log(chalk.dim(`Use 'rev pdf-comments ${docx} --append ${mainSection}' to add comments to markdown.`));
|
|
@@ -135,18 +136,21 @@ export function register(program: Command): void {
|
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
// Resolve the section config: an explicit sections.yaml if present,
|
|
140
|
+
// otherwise the `sections:` list in rev.yaml (single source of truth).
|
|
141
|
+
const resolved = resolveSectionsConfig(options.dir, options.config);
|
|
142
|
+
if (!resolved) {
|
|
143
|
+
console.error(fmt.status('error', `No section config found in ${path.resolve(options.dir)}`));
|
|
144
|
+
console.error(chalk.dim(' Add a `sections:` list to rev.yaml, or run "rev init" to generate sections.yaml.'));
|
|
142
145
|
process.exit(1);
|
|
143
146
|
}
|
|
147
|
+
const sectionsConfig = resolved.config;
|
|
144
148
|
|
|
145
149
|
// --comments-only: import comments only, never modify existing prose.
|
|
146
150
|
// Use this when the markdown has been revised since the docx was sent
|
|
147
151
|
// out — track changes from a stale draft would clobber newer edits.
|
|
148
152
|
if (options.commentsOnly) {
|
|
149
|
-
await syncCommentsOnly(docx, sections, options,
|
|
153
|
+
await syncCommentsOnly(docx, sections, options, sectionsConfig);
|
|
150
154
|
return;
|
|
151
155
|
}
|
|
152
156
|
|
|
@@ -161,8 +165,9 @@ export function register(program: Command): void {
|
|
|
161
165
|
const spin = fmt.spinner(`Importing ${path.basename(docx)}...`).start();
|
|
162
166
|
|
|
163
167
|
try {
|
|
164
|
-
const config =
|
|
165
|
-
const { importFromWord, extractWordComments, extractCommentAnchors, insertCommentsIntoMarkdown, extractFromWord } = await import('../import.js');
|
|
168
|
+
const config = sectionsConfig;
|
|
169
|
+
const { importFromWord, extractWordComments, extractCommentAnchors, insertCommentsIntoMarkdown, extractFromWord, extractHeadings } = await import('../import.js');
|
|
170
|
+
const { computeSectionBoundaries } = await import('./section-boundaries.js');
|
|
166
171
|
|
|
167
172
|
let registry = null;
|
|
168
173
|
let totalRefConversions = 0;
|
|
@@ -283,91 +288,50 @@ export function register(program: Command): void {
|
|
|
283
288
|
}> = [];
|
|
284
289
|
let totalChanges = 0;
|
|
285
290
|
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
function findSectionHeader(text: string, keyword: string, startFrom: number = 0): number {
|
|
306
|
-
const lower = text.toLowerCase();
|
|
307
|
-
let idx = startFrom;
|
|
308
|
-
while ((idx = lower.indexOf(keyword, idx)) !== -1) {
|
|
309
|
-
// Check what follows the keyword
|
|
310
|
-
const afterKeyword = text.slice(idx + keyword.length, idx + keyword.length + 5);
|
|
311
|
-
// Skip if followed by ":" (this is a label, not a section header)
|
|
312
|
-
// Real headers are followed by text content, a newline, or a subheading
|
|
313
|
-
if (!afterKeyword.startsWith(':') && !afterKeyword.startsWith(' :')) {
|
|
314
|
-
return idx;
|
|
315
|
-
}
|
|
316
|
-
idx++;
|
|
317
|
-
}
|
|
318
|
-
return -1;
|
|
319
|
-
}
|
|
291
|
+
// Route comments to sections using boundaries from the document's real
|
|
292
|
+
// heading paragraphs. docPosition (from extractCommentAnchors) and each
|
|
293
|
+
// heading's docPosition share one coordinate system over xmlDocText, so
|
|
294
|
+
// every configured section gets a boundary — not only the handful that
|
|
295
|
+
// matched a hardcoded keyword list. Sections named "Objectives" or
|
|
296
|
+
// "Annex 2" used to get no boundary, so their comments were silently
|
|
297
|
+
// dropped while the summary still claimed every comment was placed.
|
|
298
|
+
// This is the same routing path as `sync --comments-only`.
|
|
299
|
+
const headings = await extractHeadings(docx);
|
|
300
|
+
const sectionBoundaries = computeSectionBoundaries(config.sections, headings, xmlDocText.length);
|
|
301
|
+
const firstBoundaryStart = sectionBoundaries.length > 0 ? sectionBoundaries[0].start : 0;
|
|
302
|
+
|
|
303
|
+
// Truthful comment accounting: track which comments were routed to a
|
|
304
|
+
// synced section and how many were actually written, so the summary
|
|
305
|
+
// reports placements rather than the raw extracted count.
|
|
306
|
+
const routedCommentIds = new Set<string>();
|
|
307
|
+
let totalCommentsPlaced = 0;
|
|
308
|
+
let totalCommentsDeduped = 0;
|
|
309
|
+
let totalCommentsUnmatched = 0;
|
|
320
310
|
|
|
321
311
|
for (const section of wordSections) {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
// Get keywords for this section
|
|
325
|
-
const keywords = sectionKeywords[fileBase] || [fileBase];
|
|
326
|
-
|
|
327
|
-
// Find the first valid keyword that exists in XML (not a label)
|
|
328
|
-
let headerIdx = -1;
|
|
329
|
-
for (const kw of keywords) {
|
|
330
|
-
const idx = findSectionHeader(xmlDocText, kw, 0);
|
|
331
|
-
if (idx >= 0 && (headerIdx < 0 || idx < headerIdx)) {
|
|
332
|
-
headerIdx = idx;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (headerIdx >= 0) {
|
|
337
|
-
// Find the next section's start to determine end boundary
|
|
338
|
-
let nextHeaderIdx = xmlDocText.length;
|
|
339
|
-
const sectionIdx = wordSections.indexOf(section);
|
|
340
|
-
if (sectionIdx < wordSections.length - 1) {
|
|
341
|
-
const nextFileBase = wordSections[sectionIdx + 1].file.replace(/\.md$/i, '').toLowerCase();
|
|
342
|
-
const nextKeywords = sectionKeywords[nextFileBase] || [nextFileBase];
|
|
343
|
-
for (const nkw of nextKeywords) {
|
|
344
|
-
const foundNext = findSectionHeader(xmlDocText, nkw, headerIdx + 10);
|
|
345
|
-
if (foundNext >= 0 && foundNext < nextHeaderIdx) {
|
|
346
|
-
nextHeaderIdx = foundNext;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
312
|
+
const sectionPath = path.join(options.dir, section.file);
|
|
350
313
|
|
|
351
|
-
|
|
314
|
+
if (!fs.existsSync(sectionPath)) {
|
|
315
|
+
sectionResults.push({
|
|
352
316
|
file: section.file,
|
|
353
|
-
|
|
354
|
-
|
|
317
|
+
header: section.header,
|
|
318
|
+
status: 'skipped',
|
|
319
|
+
stats: undefined,
|
|
355
320
|
});
|
|
356
|
-
|
|
321
|
+
continue;
|
|
357
322
|
}
|
|
358
|
-
}
|
|
359
323
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (
|
|
324
|
+
// A section that appears in the reviewed document as a bare heading
|
|
325
|
+
// with no body was not really part of this build (e.g. a "no-annex"
|
|
326
|
+
// export synced against the full project). Importing it would diff
|
|
327
|
+
// real prose against an empty body and rewrite the file to near-empty.
|
|
328
|
+
// Leave it untouched.
|
|
329
|
+
const bodyEmpty = section.content.trim() === section.header.trim();
|
|
330
|
+
if (bodyEmpty) {
|
|
367
331
|
sectionResults.push({
|
|
368
332
|
file: section.file,
|
|
369
333
|
header: section.header,
|
|
370
|
-
status: '
|
|
334
|
+
status: 'untouched',
|
|
371
335
|
stats: undefined,
|
|
372
336
|
});
|
|
373
337
|
continue;
|
|
@@ -389,31 +353,19 @@ export function register(program: Command): void {
|
|
|
389
353
|
totalRefConversions += refConversions.length;
|
|
390
354
|
}
|
|
391
355
|
|
|
392
|
-
let commentsInserted = 0;
|
|
393
356
|
if (comments.length > 0 && anchors.size > 0) {
|
|
394
|
-
// Filter comments to
|
|
395
|
-
//
|
|
396
|
-
//
|
|
357
|
+
// Filter comments to those whose docPosition falls in this section's
|
|
358
|
+
// boundary (docPosition and boundaries share xmlDocText coordinates).
|
|
359
|
+
// The section owning the first boundary also catches comments placed
|
|
360
|
+
// before any heading.
|
|
397
361
|
const boundary = sectionBoundaries.find(b => b.file === section.file);
|
|
398
|
-
const
|
|
399
|
-
const firstBoundaryStart = sectionBoundaries.length > 0 ? Math.min(...sectionBoundaries.map(b => b.start)) : 0;
|
|
362
|
+
const ownsFirstBoundary = !!boundary && boundary.start === firstBoundaryStart;
|
|
400
363
|
|
|
401
|
-
const sectionComments = comments.filter((c:
|
|
364
|
+
const sectionComments = comments.filter((c: { id: string }) => {
|
|
402
365
|
const anchorData = anchors.get(c.id);
|
|
403
|
-
if (!anchorData) return false;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (anchorData.docPosition !== undefined && boundary) {
|
|
407
|
-
// Include comments within section boundaries
|
|
408
|
-
if (anchorData.docPosition >= boundary.start && anchorData.docPosition < boundary.end) {
|
|
409
|
-
return true;
|
|
410
|
-
}
|
|
411
|
-
// Also include "outside" comments (before first section) in the first section file
|
|
412
|
-
if (isFirstSection && anchorData.docPosition < firstBoundaryStart) {
|
|
413
|
-
return true;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
366
|
+
if (!anchorData || anchorData.docPosition === undefined || !boundary) return false;
|
|
367
|
+
if (anchorData.docPosition >= boundary.start && anchorData.docPosition < boundary.end) return true;
|
|
368
|
+
if (ownsFirstBoundary && anchorData.docPosition < firstBoundaryStart) return true;
|
|
417
369
|
return false;
|
|
418
370
|
});
|
|
419
371
|
|
|
@@ -422,22 +374,20 @@ export function register(program: Command): void {
|
|
|
422
374
|
}
|
|
423
375
|
|
|
424
376
|
if (sectionComments.length > 0) {
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
const beforeCount = (annotated.match(commentPattern) || []).length;
|
|
377
|
+
for (const c of sectionComments) routedCommentIds.add(c.id);
|
|
378
|
+
const cstats = { placed: 0, deduped: 0, unmatched: 0 };
|
|
428
379
|
annotated = insertCommentsIntoMarkdown(annotated, sectionComments, anchors, {
|
|
429
380
|
quiet: !process.env.DEBUG,
|
|
430
|
-
sectionBoundary: boundary
|
|
381
|
+
sectionBoundary: boundary,
|
|
382
|
+
outStats: cstats,
|
|
431
383
|
});
|
|
432
|
-
|
|
433
|
-
|
|
384
|
+
stats.comments = (stats.comments || 0) + cstats.placed;
|
|
385
|
+
totalCommentsPlaced += cstats.placed;
|
|
386
|
+
totalCommentsDeduped += cstats.deduped;
|
|
387
|
+
totalCommentsUnmatched += cstats.unmatched;
|
|
434
388
|
|
|
435
389
|
if (process.env.DEBUG) {
|
|
436
|
-
console.log(`[DEBUG] ${section.file}:
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (commentsInserted > 0) {
|
|
440
|
-
stats.comments = (stats.comments || 0) + commentsInserted;
|
|
390
|
+
console.log(`[DEBUG] ${section.file}: placed ${cstats.placed}, deduped ${cstats.deduped}, unmatched ${cstats.unmatched} of ${sectionComments.length}`);
|
|
441
391
|
}
|
|
442
392
|
}
|
|
443
393
|
}
|
|
@@ -471,11 +421,11 @@ export function register(program: Command): void {
|
|
|
471
421
|
}
|
|
472
422
|
|
|
473
423
|
const tableRows = sectionResults.map((r) => {
|
|
474
|
-
if (r.status === 'skipped') {
|
|
424
|
+
if (r.status === 'skipped' || r.status === 'untouched') {
|
|
475
425
|
return [
|
|
476
426
|
chalk.dim(r.file),
|
|
477
427
|
chalk.dim(r.header.slice(0, 25)),
|
|
478
|
-
chalk.yellow(
|
|
428
|
+
chalk.yellow(r.status),
|
|
479
429
|
'',
|
|
480
430
|
'',
|
|
481
431
|
'',
|
|
@@ -520,17 +470,38 @@ export function register(program: Command): void {
|
|
|
520
470
|
}
|
|
521
471
|
}
|
|
522
472
|
|
|
473
|
+
// Comments carried by the document but never routed to a synced section
|
|
474
|
+
// (they fell in a skipped/untouched/absent section). Surfacing these
|
|
475
|
+
// keeps the summary honest instead of reporting every extracted comment
|
|
476
|
+
// as placed.
|
|
477
|
+
const unroutedComments = comments.length - routedCommentIds.size;
|
|
478
|
+
|
|
523
479
|
if (options.dryRun) {
|
|
524
480
|
console.log(fmt.box(chalk.yellow('Dry run - no files written'), { padding: 0 }));
|
|
525
481
|
} else if (totalChanges > 0 || totalRefConversions > 0 || comments.length > 0) {
|
|
526
482
|
const summaryLines: string[] = [];
|
|
527
483
|
summaryLines.push(`${chalk.bold(wordSections.length)} sections processed`);
|
|
528
484
|
if (totalChanges > 0) summaryLines.push(`${chalk.bold(totalChanges)} annotations imported`);
|
|
529
|
-
if (
|
|
485
|
+
if (totalCommentsPlaced > 0) {
|
|
486
|
+
summaryLines.push(`${chalk.bold(totalCommentsPlaced)} of ${comments.length} comments placed`);
|
|
487
|
+
}
|
|
488
|
+
if (totalCommentsDeduped > 0) {
|
|
489
|
+
summaryLines.push(`${chalk.cyan(totalCommentsDeduped)} already present (skipped)`);
|
|
490
|
+
}
|
|
491
|
+
if (totalCommentsUnmatched > 0) {
|
|
492
|
+
summaryLines.push(`${chalk.yellow(totalCommentsUnmatched)} unmatched (anchor not in current prose)`);
|
|
493
|
+
}
|
|
494
|
+
if (unroutedComments > 0) {
|
|
495
|
+
summaryLines.push(`${chalk.yellow(unroutedComments)} not routed to any synced section`);
|
|
496
|
+
}
|
|
530
497
|
if (totalRefConversions > 0) summaryLines.push(`${chalk.bold(totalRefConversions)} refs converted to @-syntax`);
|
|
531
498
|
|
|
532
499
|
console.log(fmt.box(summaryLines.join('\n'), { title: 'Summary', padding: 0 }));
|
|
533
500
|
console.log();
|
|
501
|
+
if (totalCommentsUnmatched > 0 || unroutedComments > 0) {
|
|
502
|
+
console.log(chalk.yellow(` ${totalCommentsUnmatched + unroutedComments} comment(s) were not written. Run "rev verify-anchors" or re-sync with the full document.`));
|
|
503
|
+
console.log();
|
|
504
|
+
}
|
|
534
505
|
console.log(chalk.dim('Next steps:'));
|
|
535
506
|
console.log(chalk.dim(' 1. rev review <section.md> - Accept/reject changes'));
|
|
536
507
|
console.log(chalk.dim(' 2. rev comments <section.md> - View/address comments'));
|
|
@@ -560,9 +531,8 @@ async function syncCommentsOnly(
|
|
|
560
531
|
docx: string,
|
|
561
532
|
sectionFilter: string[] | undefined,
|
|
562
533
|
options: SyncOptions,
|
|
563
|
-
|
|
534
|
+
config: SectionsConfig,
|
|
564
535
|
): Promise<void> {
|
|
565
|
-
const config = loadConfig(configPath);
|
|
566
536
|
const { extractWordComments, extractCommentAnchors, extractHeadings, insertCommentsIntoMarkdown } = await import('../import.js');
|
|
567
537
|
const { computeSectionBoundaries } = await import('./section-boundaries.js');
|
|
568
538
|
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
fs,
|
|
19
19
|
path,
|
|
20
20
|
fmt,
|
|
21
|
-
|
|
21
|
+
resolveSectionsConfig,
|
|
22
22
|
jsonMode,
|
|
23
23
|
jsonOutput,
|
|
24
24
|
} from './context.js';
|
|
@@ -58,14 +58,14 @@ export function register(program: Command): void {
|
|
|
58
58
|
process.exit(1);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
63
|
-
console.error(fmt.status('error', `
|
|
64
|
-
console.error(chalk.dim('
|
|
61
|
+
const resolved = resolveSectionsConfig(options.dir, options.config);
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
console.error(fmt.status('error', `No section config found in ${path.resolve(options.dir)}`));
|
|
64
|
+
console.error(chalk.dim(' Add a `sections:` list to rev.yaml, or run "rev init" to generate sections.yaml.'));
|
|
65
65
|
process.exit(1);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const config =
|
|
68
|
+
const config = resolved.config;
|
|
69
69
|
const { extractWordComments, extractCommentAnchors, extractHeadings } = await import('../import.js');
|
|
70
70
|
|
|
71
71
|
let comments;
|
package/lib/sections.ts
CHANGED
|
@@ -47,6 +47,28 @@ export function extractHeader(filePath: string): string | null {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Extract the first markdown heading of ANY level (`#`–`######`) from a file.
|
|
52
|
+
*
|
|
53
|
+
* Unlike {@link extractHeader} (which is H1-only by contract), this is used to
|
|
54
|
+
* derive a section's header from files that lead with a subsection — e.g.
|
|
55
|
+
* `02_objectives.md` starting with `## 1.2 Objectives`. Using the real first
|
|
56
|
+
* heading lets the derived header match the corresponding docx heading.
|
|
57
|
+
*/
|
|
58
|
+
export function extractFirstHeading(filePath: string): string | null {
|
|
59
|
+
if (!fs.existsSync(filePath)) return null;
|
|
60
|
+
|
|
61
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
62
|
+
for (const line of content.split('\n')) {
|
|
63
|
+
const match = line.match(/^#{1,6}\s+(.+)$/);
|
|
64
|
+
if (match && match[1]) {
|
|
65
|
+
return match[1].trim();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
/**
|
|
51
73
|
* Generate sections.yaml from existing .md files
|
|
52
74
|
*/
|
|
@@ -64,7 +86,10 @@ export function generateConfig(
|
|
|
64
86
|
|
|
65
87
|
for (const file of files) {
|
|
66
88
|
const filePath = path.join(directory, file);
|
|
67
|
-
|
|
89
|
+
// Use the first heading at any level: a section file may be headed by a
|
|
90
|
+
// subsection (## 1.2 Objectives). H1-only extraction left such files with a
|
|
91
|
+
// filename-derived header that never matched the reviewed document.
|
|
92
|
+
const header = extractFirstHeading(filePath);
|
|
68
93
|
const baseName = path.basename(file, '.md').toLowerCase();
|
|
69
94
|
|
|
70
95
|
// Determine order based on common patterns
|
|
@@ -144,6 +169,74 @@ export function saveConfig(configPath: string, config: SectionsConfig): void {
|
|
|
144
169
|
fs.writeFileSync(configPath, yamlStr, 'utf-8');
|
|
145
170
|
}
|
|
146
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Derive a SectionsConfig from the `sections:` list in rev.yaml.
|
|
174
|
+
*
|
|
175
|
+
* Each listed file's header is its first markdown H1 (falling back to a
|
|
176
|
+
* title-cased file name); order follows the list order in rev.yaml. This is
|
|
177
|
+
* the same section list that `build` consumes, so a project that only has a
|
|
178
|
+
* `rev.yaml` needs no separate `sections.yaml`.
|
|
179
|
+
*
|
|
180
|
+
* Returns null when rev.yaml is absent, unparseable, or has no `sections` list.
|
|
181
|
+
*/
|
|
182
|
+
export function deriveSectionsFromRev(directory: string): SectionsConfig | null {
|
|
183
|
+
const revPath = path.join(directory, 'rev.yaml');
|
|
184
|
+
if (!fs.existsSync(revPath)) return null;
|
|
185
|
+
|
|
186
|
+
let parsed: { sections?: unknown };
|
|
187
|
+
try {
|
|
188
|
+
parsed = YAML.parse(fs.readFileSync(revPath, 'utf-8')) || {};
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const list = parsed.sections;
|
|
194
|
+
if (!Array.isArray(list) || list.length === 0) return null;
|
|
195
|
+
|
|
196
|
+
const sections: Record<string, SectionConfig> = {};
|
|
197
|
+
list.forEach((entry, index) => {
|
|
198
|
+
if (typeof entry !== 'string') return;
|
|
199
|
+
const header = extractFirstHeading(path.join(directory, entry)) || titleCase(path.basename(entry, '.md'));
|
|
200
|
+
sections[entry] = { header, aliases: [], order: index };
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (Object.keys(sections).length === 0) return null;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
version: 1,
|
|
207
|
+
description: 'Derived from rev.yaml sections list',
|
|
208
|
+
sections,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resolve the effective sections config for a project directory.
|
|
214
|
+
*
|
|
215
|
+
* Precedence (single source of truth, with optional override):
|
|
216
|
+
* 1. An explicit sections config file (default `sections.yaml`) when it
|
|
217
|
+
* exists — lets users override headers/aliases/order.
|
|
218
|
+
* 2. Otherwise the `sections:` list in `rev.yaml`, via {@link deriveSectionsFromRev}.
|
|
219
|
+
*
|
|
220
|
+
* Returns null only when neither source yields any sections; callers turn that
|
|
221
|
+
* into a user-facing error.
|
|
222
|
+
*/
|
|
223
|
+
export function resolveSectionsConfig(
|
|
224
|
+
directory: string,
|
|
225
|
+
configFileName = 'sections.yaml'
|
|
226
|
+
): { config: SectionsConfig; source: string } | null {
|
|
227
|
+
const explicitPath = path.resolve(directory, configFileName);
|
|
228
|
+
if (fs.existsSync(explicitPath)) {
|
|
229
|
+
return { config: loadConfig(explicitPath), source: explicitPath };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const derived = deriveSectionsFromRev(directory);
|
|
233
|
+
if (derived) {
|
|
234
|
+
return { config: derived, source: path.resolve(directory, 'rev.yaml') };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
147
240
|
/**
|
|
148
241
|
* Match a heading to a section file
|
|
149
242
|
*/
|
|
@@ -286,7 +379,7 @@ export function splitAnnotatedPaper(
|
|
|
286
379
|
let currentContent: string[] = [];
|
|
287
380
|
|
|
288
381
|
for (const line of lines) {
|
|
289
|
-
const headerMatch = line.match(
|
|
382
|
+
const headerMatch = line.match(/^#{1,6}\s+(.+)$/);
|
|
290
383
|
|
|
291
384
|
if (headerMatch && headerMatch[1]) {
|
|
292
385
|
// Save previous section
|
package/mkdocs.yml
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
site_name: docrev
|
|
2
|
-
site_url: https://gillescolling.com/docrev
|
|
3
|
-
site_description: CLI for writing documents in Markdown while collaborating with Word users.
|
|
4
|
-
site_author: Gilles Colling
|
|
5
|
-
repo_url: https://github.com/gcol33/docrev
|
|
6
|
-
repo_name: gcol33/docrev
|
|
7
|
-
|
|
8
|
-
theme:
|
|
9
|
-
name: material
|
|
10
|
-
palette:
|
|
11
|
-
- scheme: default
|
|
12
|
-
primary: custom
|
|
13
|
-
accent: custom
|
|
14
|
-
toggle:
|
|
15
|
-
icon: material/brightness-7
|
|
16
|
-
name: Switch to dark mode
|
|
17
|
-
- scheme: slate
|
|
18
|
-
primary: custom
|
|
19
|
-
accent: custom
|
|
20
|
-
toggle:
|
|
21
|
-
icon: material/brightness-4
|
|
22
|
-
name: Switch to light mode
|
|
23
|
-
font:
|
|
24
|
-
text: Roboto
|
|
25
|
-
code: Roboto Mono
|
|
26
|
-
features:
|
|
27
|
-
- navigation.tabs
|
|
28
|
-
- navigation.top
|
|
29
|
-
- navigation.instant
|
|
30
|
-
- search.highlight
|
|
31
|
-
- content.code.copy
|
|
32
|
-
icon:
|
|
33
|
-
repo: fontawesome/brands/github
|
|
34
|
-
|
|
35
|
-
nav:
|
|
36
|
-
- Home: index.md
|
|
37
|
-
- Get Started: workflow.md
|
|
38
|
-
- Commands: commands.md
|
|
39
|
-
- Configuration: configuration.md
|
|
40
|
-
- Troubleshooting: troubleshooting.md
|
|
41
|
-
|
|
42
|
-
markdown_extensions:
|
|
43
|
-
- pymdownx.highlight:
|
|
44
|
-
anchor_linenums: true
|
|
45
|
-
- pymdownx.superfences
|
|
46
|
-
- pymdownx.inlinehilite
|
|
47
|
-
- pymdownx.tabbed:
|
|
48
|
-
alternate_style: true
|
|
49
|
-
- admonition
|
|
50
|
-
- pymdownx.details
|
|
51
|
-
- attr_list
|
|
52
|
-
- md_in_html
|
|
53
|
-
- toc:
|
|
54
|
-
permalink: true
|
|
55
|
-
|
|
56
|
-
extra_css:
|
|
57
|
-
- stylesheets/extra.css
|
|
58
|
-
|
|
59
|
-
extra:
|
|
60
|
-
social:
|
|
61
|
-
- icon: fontawesome/brands/github
|
|
62
|
-
link: https://github.com/gcol33/docrev
|
|
63
|
-
- icon: fontawesome/brands/npm
|
|
64
|
-
link: https://www.npmjs.com/package/docrev
|
|
1
|
+
site_name: docrev
|
|
2
|
+
site_url: https://gillescolling.com/docrev
|
|
3
|
+
site_description: CLI for writing documents in Markdown while collaborating with Word users.
|
|
4
|
+
site_author: Gilles Colling
|
|
5
|
+
repo_url: https://github.com/gcol33/docrev
|
|
6
|
+
repo_name: gcol33/docrev
|
|
7
|
+
|
|
8
|
+
theme:
|
|
9
|
+
name: material
|
|
10
|
+
palette:
|
|
11
|
+
- scheme: default
|
|
12
|
+
primary: custom
|
|
13
|
+
accent: custom
|
|
14
|
+
toggle:
|
|
15
|
+
icon: material/brightness-7
|
|
16
|
+
name: Switch to dark mode
|
|
17
|
+
- scheme: slate
|
|
18
|
+
primary: custom
|
|
19
|
+
accent: custom
|
|
20
|
+
toggle:
|
|
21
|
+
icon: material/brightness-4
|
|
22
|
+
name: Switch to light mode
|
|
23
|
+
font:
|
|
24
|
+
text: Roboto
|
|
25
|
+
code: Roboto Mono
|
|
26
|
+
features:
|
|
27
|
+
- navigation.tabs
|
|
28
|
+
- navigation.top
|
|
29
|
+
- navigation.instant
|
|
30
|
+
- search.highlight
|
|
31
|
+
- content.code.copy
|
|
32
|
+
icon:
|
|
33
|
+
repo: fontawesome/brands/github
|
|
34
|
+
|
|
35
|
+
nav:
|
|
36
|
+
- Home: index.md
|
|
37
|
+
- Get Started: workflow.md
|
|
38
|
+
- Commands: commands.md
|
|
39
|
+
- Configuration: configuration.md
|
|
40
|
+
- Troubleshooting: troubleshooting.md
|
|
41
|
+
|
|
42
|
+
markdown_extensions:
|
|
43
|
+
- pymdownx.highlight:
|
|
44
|
+
anchor_linenums: true
|
|
45
|
+
- pymdownx.superfences
|
|
46
|
+
- pymdownx.inlinehilite
|
|
47
|
+
- pymdownx.tabbed:
|
|
48
|
+
alternate_style: true
|
|
49
|
+
- admonition
|
|
50
|
+
- pymdownx.details
|
|
51
|
+
- attr_list
|
|
52
|
+
- md_in_html
|
|
53
|
+
- toc:
|
|
54
|
+
permalink: true
|
|
55
|
+
|
|
56
|
+
extra_css:
|
|
57
|
+
- stylesheets/extra.css
|
|
58
|
+
|
|
59
|
+
extra:
|
|
60
|
+
social:
|
|
61
|
+
- icon: fontawesome/brands/github
|
|
62
|
+
link: https://github.com/gcol33/docrev
|
|
63
|
+
- icon: fontawesome/brands/npm
|
|
64
|
+
link: https://www.npmjs.com/package/docrev
|