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.
@@ -10,7 +10,8 @@ import {
10
10
  path,
11
11
  fmt,
12
12
  findFiles,
13
- loadConfig,
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 configPath = path.resolve(options.dir, options.config);
117
- if (fs.existsSync(configPath) && !options.dryRun) {
118
- const config = loadConfig(configPath);
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 && typeof mainSection === 'string') {
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
- const configPath = path.resolve(options.dir, options.config);
139
- if (!fs.existsSync(configPath)) {
140
- console.error(fmt.status('error', `Config not found: ${configPath}`));
141
- console.error(chalk.dim(' Run "rev init" first to generate sections.yaml'));
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, configPath);
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 = loadConfig(configPath);
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
- // Calculate section boundaries in the XML document text for comment filtering
287
- // Comment positions (docPosition) are relative to xmlDocText, NOT wordText
288
- // So we must find section headers in xmlDocText to get matching boundaries
289
- const sectionBoundaries: Array<{ file: string; start: number; end: number }> = [];
290
- const xmlLower = xmlDocText.toLowerCase();
291
-
292
- // Standard section header keywords to search for in XML
293
- // Map from file name pattern to search terms
294
- const sectionKeywords: Record<string, string[]> = {
295
- 'abstract': ['abstract', 'summary'],
296
- 'introduction': ['introduction', 'background'],
297
- 'methods': ['methods', 'materials and methods', 'methodology'],
298
- 'results': ['results'],
299
- 'discussion': ['discussion'],
300
- 'conclusion': ['conclusion', 'conclusions'],
301
- };
302
-
303
- // Helper: find section header (skip labels like "Methods:" in structured abstracts)
304
- // Real section headers are NOT followed by ":" immediately
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 fileBase = section.file.replace(/\.md$/i, '').toLowerCase();
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
- sectionBoundaries.push({
314
+ if (!fs.existsSync(sectionPath)) {
315
+ sectionResults.push({
352
316
  file: section.file,
353
- start: headerIdx,
354
- end: nextHeaderIdx
317
+ header: section.header,
318
+ status: 'skipped',
319
+ stats: undefined,
355
320
  });
356
-
321
+ continue;
357
322
  }
358
- }
359
323
 
360
- // Document length is the XML text length (same coordinate system as docPosition)
361
- const docLength = xmlDocText.length;
362
-
363
- for (const section of wordSections) {
364
- const sectionPath = path.join(options.dir, section.file);
365
-
366
- if (!fs.existsSync(sectionPath)) {
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: 'skipped',
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 only those that belong to this section
395
- // Use exact position matching: docPosition is in xmlDocText coordinates,
396
- // and sectionBoundaries are also in xmlDocText coordinates (same source!)
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 isFirstSection = wordSections.indexOf(section) === 0;
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: any) => {
364
+ const sectionComments = comments.filter((c: { id: string }) => {
402
365
  const anchorData = anchors.get(c.id);
403
- if (!anchorData) return false;
404
-
405
- // Use exact position - no scaling needed since both are in xmlDocText coordinates
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
- // Use a more robust pattern that handles < in comment text
426
- const commentPattern = /\{>>.*?<<\}/gs;
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 // Pass section boundary for position-based insertion
381
+ sectionBoundary: boundary,
382
+ outStats: cstats,
431
383
  });
432
- const afterCount = (annotated.match(commentPattern) || []).length;
433
- commentsInserted = afterCount - beforeCount;
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}: inserted ${commentsInserted} of ${sectionComments.length} comments`);
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('skipped'),
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 (comments.length > 0) summaryLines.push(`${chalk.bold(comments.length)} comments placed`);
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
- configPath: string,
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
- loadConfig,
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 configPath = path.resolve(options.dir, options.config);
62
- if (!fs.existsSync(configPath)) {
63
- console.error(fmt.status('error', `Config not found: ${configPath}`));
64
- console.error(chalk.dim(' Run "rev init" first to generate sections.yaml'));
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 = loadConfig(configPath);
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
- const header = extractHeader(filePath);
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(/^#\s+(.+)$/);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docrev",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Academic paper revision workflow: Word ↔ Markdown round-trips, DOI validation, reviewer comments",
5
5
  "type": "module",
6
6
  "types": "dist/lib/types.d.ts",