docrev 0.6.13 → 0.7.6

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.
@@ -0,0 +1,210 @@
1
+ # PowerShell completion for rev (docrev)
2
+ # Install: Add to $PROFILE:
3
+ # . (rev completions powershell)
4
+ # Or copy output to profile manually
5
+
6
+ $script:revCommands = @(
7
+ 'build', 'new', 'import', 'sections', 'extract', 'review', 'status',
8
+ 'comments', 'resolve', 'reply', 'strip', 'refs', 'migrate', 'config',
9
+ 'install', 'doi', 'citations', 'equations', 'figures', 'response',
10
+ 'anonymize', 'validate', 'merge', 'diff', 'history', 'help', 'init',
11
+ 'split', 'sync', 'word-count', 'wc', 'stats', 'search', 'backup',
12
+ 'archive', 'export', 'preview', 'watch', 'lint', 'grammar', 'spelling',
13
+ 'annotate', 'apply', 'comment', 'completions', 'clean', 'check', 'open',
14
+ 'next', 'prev', 'first', 'last', 'todo', 'accept', 'reject',
15
+ 'pdf-comments', 'install-cli-skill', 'uninstall-cli-skill', 'doctor', 'upgrade'
16
+ )
17
+
18
+ $script:buildFormats = @('pdf', 'docx', 'tex', 'all')
19
+ $script:doiActions = @('check', 'lookup', 'fetch', 'add')
20
+ $script:eqActions = @('list', 'extract', 'convert', 'from-word')
21
+ $script:helpTopics = @('workflow', 'syntax', 'commands')
22
+ $script:previewFormats = @('pdf', 'docx')
23
+ $script:shells = @('bash', 'zsh', 'powershell')
24
+
25
+ Register-ArgumentCompleter -Native -CommandName rev -ScriptBlock {
26
+ param($wordToComplete, $commandAst, $cursorPosition)
27
+
28
+ $tokens = $commandAst.CommandElements
29
+ $command = $null
30
+
31
+ # Find the subcommand (skip 'rev' itself)
32
+ if ($tokens.Count -gt 1) {
33
+ $command = $tokens[1].Extent.Text
34
+ }
35
+
36
+ # Get the current word being completed
37
+ $currentWord = $wordToComplete
38
+
39
+ # If we're completing the first argument (subcommand)
40
+ if ($tokens.Count -le 2 -and -not $currentWord.StartsWith('-')) {
41
+ $script:revCommands | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
42
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
43
+ }
44
+ return
45
+ }
46
+
47
+ # Context-specific completions
48
+ switch ($command) {
49
+ 'build' {
50
+ if ($currentWord.StartsWith('-')) {
51
+ @('--toc', '--show-changes', '--clean', '--dual') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
52
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
53
+ }
54
+ } else {
55
+ $script:buildFormats | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
56
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
57
+ }
58
+ }
59
+ }
60
+ 'new' {
61
+ @('--list', '--template', '-s') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
62
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
63
+ }
64
+ }
65
+ 'doi' {
66
+ $script:doiActions | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
67
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
68
+ }
69
+ }
70
+ { $_ -in @('equations', 'eq') } {
71
+ $script:eqActions | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
72
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
73
+ }
74
+ }
75
+ 'validate' {
76
+ @('--list', '--journal') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
77
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
78
+ }
79
+ }
80
+ 'help' {
81
+ $script:helpTopics | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
82
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
83
+ }
84
+ }
85
+ 'config' {
86
+ @('user', 'sections') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
87
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
88
+ }
89
+ }
90
+ { $_ -in @('word-count', 'wc') } {
91
+ @('--limit', '--journal') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
92
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
93
+ }
94
+ }
95
+ 'preview' {
96
+ $script:previewFormats | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
97
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
98
+ }
99
+ }
100
+ 'watch' {
101
+ if ($currentWord.StartsWith('-')) {
102
+ @('--no-open') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
103
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
104
+ }
105
+ } else {
106
+ @('pdf', 'docx', 'all') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
107
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
108
+ }
109
+ }
110
+ }
111
+ 'completions' {
112
+ $script:shells | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
113
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
114
+ }
115
+ }
116
+ 'comments' {
117
+ @('--pending', '-p', '--resolved', '-r', '--export', '-e', '--author') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
118
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
119
+ }
120
+ # Also complete .md files
121
+ Get-ChildItem -Filter "*.md" -ErrorAction SilentlyContinue | ForEach-Object {
122
+ if ($_.Name -like "$currentWord*") {
123
+ [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ProviderItem', $_.Name)
124
+ }
125
+ }
126
+ }
127
+ { $_ -in @('resolve', 'reply') } {
128
+ @('-n', '-m', '--number', '--message', '--author', '-a') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
129
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
130
+ }
131
+ Get-ChildItem -Filter "*.md" -ErrorAction SilentlyContinue | ForEach-Object {
132
+ if ($_.Name -like "$currentWord*") {
133
+ [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ProviderItem', $_.Name)
134
+ }
135
+ }
136
+ }
137
+ { $_ -in @('accept', 'reject') } {
138
+ @('-n', '-a', '--all') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
139
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
140
+ }
141
+ Get-ChildItem -Filter "*.md" -ErrorAction SilentlyContinue | ForEach-Object {
142
+ if ($_.Name -like "$currentWord*") {
143
+ [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ProviderItem', $_.Name)
144
+ }
145
+ }
146
+ }
147
+ 'pdf-comments' {
148
+ @('--append', '-a', '--json', '--by-page', '--by-author') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
149
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
150
+ }
151
+ Get-ChildItem -Filter "*.pdf" -ErrorAction SilentlyContinue | ForEach-Object {
152
+ if ($_.Name -like "$currentWord*") {
153
+ [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ProviderItem', $_.Name)
154
+ }
155
+ }
156
+ }
157
+ 'grammar' {
158
+ @('--learn', '--forget', '--list', '--rules', '--no-scientific', '--severity') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
159
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
160
+ }
161
+ }
162
+ 'spelling' {
163
+ @('--british', '--learn', '--learn-project', '--list') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
164
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
165
+ }
166
+ }
167
+ 'sync' {
168
+ Get-ChildItem -Filter "*.docx" -ErrorAction SilentlyContinue | ForEach-Object {
169
+ if ($_.Name -like "$currentWord*") {
170
+ [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ProviderItem', $_.Name)
171
+ }
172
+ }
173
+ Get-ChildItem -Filter "*.pdf" -ErrorAction SilentlyContinue | ForEach-Object {
174
+ if ($_.Name -like "$currentWord*") {
175
+ [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ProviderItem', $_.Name)
176
+ }
177
+ }
178
+ }
179
+ 'archive' {
180
+ @('--by', '--dry-run') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
181
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
182
+ }
183
+ }
184
+ 'backup' {
185
+ @('--name', '--output') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
186
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
187
+ }
188
+ }
189
+ 'todo' {
190
+ @('--by-author') | Where-Object { $_ -like "$currentWord*" } | ForEach-Object {
191
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
192
+ }
193
+ }
194
+ default {
195
+ # Default file completion for most commands that take files
196
+ if (-not $currentWord.StartsWith('-')) {
197
+ Get-ChildItem -Filter "*.md" -ErrorAction SilentlyContinue | ForEach-Object {
198
+ if ($_.Name -like "$currentWord*") {
199
+ [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ProviderItem', $_.Name)
200
+ }
201
+ }
202
+ Get-ChildItem -Filter "*.docx" -ErrorAction SilentlyContinue | ForEach-Object {
203
+ if ($_.Name -like "$currentWord*") {
204
+ [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ProviderItem', $_.Name)
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
@@ -20,45 +20,75 @@ const PATTERNS = {
20
20
 
21
21
  /**
22
22
  * Check if a potential comment is actually a false positive
23
- * (e.g., figure caption, nested inside other annotation, etc.)
23
+ * (e.g., figure caption, nested inside other annotation, code block, etc.)
24
24
  * @param {string} commentContent - The content inside {>>...<<}
25
25
  * @param {string} fullText - The full document text
26
26
  * @param {number} position - Position of the comment in the text
27
27
  * @returns {boolean} true if this is a false positive (not a real comment)
28
28
  */
29
29
  function isCommentFalsePositive(commentContent, fullText, position) {
30
+ // Check if inside a code block (fenced or indented)
31
+ const textBefore = fullText.slice(Math.max(0, position - 2000), position);
32
+ const textAfter = fullText.slice(position, Math.min(fullText.length, position + 2000));
33
+
34
+ // Count unclosed fenced code blocks (``` or ~~~)
35
+ const fenceOpens = (textBefore.match(/^```|^~~~/gm) || []).length;
36
+ const fenceCloses = (textBefore.match(/```$|~~~$/gm) || []).length;
37
+ if (fenceOpens > fenceCloses) return true; // Inside code block
38
+
39
+ // Check if on an indented line (4+ spaces or tab at line start = code)
40
+ const lineStart = textBefore.lastIndexOf('\n') + 1;
41
+ const linePrefix = fullText.slice(lineStart, position);
42
+ if (/^(\t| )/.test(linePrefix)) return true; // Indented code
43
+
44
+ // Check if inside inline code backticks
45
+ const backticksBefore = (linePrefix.match(/`/g) || []).length;
46
+ if (backticksBefore % 2 === 1) return true; // Inside inline code
47
+
30
48
  // Check if nested inside a deletion or insertion block
31
- // Look backwards for unclosed {-- or {++ before this position
32
- const textBefore = fullText.slice(Math.max(0, position - 500), position);
49
+ const nearTextBefore = fullText.slice(Math.max(0, position - 500), position);
33
50
 
34
51
  // Count unclosed deletion markers
35
- const delOpens = (textBefore.match(/\{--/g) || []).length;
36
- const delCloses = (textBefore.match(/--\}/g) || []).length;
52
+ const delOpens = (nearTextBefore.match(/\{--/g) || []).length;
53
+ const delCloses = (nearTextBefore.match(/--\}/g) || []).length;
37
54
  if (delOpens > delCloses) return true; // Nested inside deletion
38
55
 
39
56
  // Count unclosed insertion markers
40
- const insOpens = (textBefore.match(/\{\+\+/g) || []).length;
41
- const insCloses = (textBefore.match(/\+\+\}/g) || []).length;
57
+ const insOpens = (nearTextBefore.match(/\{\+\+/g) || []).length;
58
+ const insCloses = (nearTextBefore.match(/\+\+\}/g) || []).length;
42
59
  if (insOpens > insCloses) return true; // Nested inside insertion
43
60
 
44
61
  // Heuristics for figure captions and other false positives:
45
62
 
46
63
  // Contains image/figure path patterns
47
- if (/\(figures?\/|\(images?\/|\.png|\.jpg|\.pdf/i.test(commentContent)) return true;
64
+ if (/\(figures?\/|\(images?\/|\.png|\.jpg|\.jpeg|\.gif|\.svg|\.pdf/i.test(commentContent)) return true;
48
65
 
49
66
  // Contains markdown figure reference syntax
50
67
  if (/\{#fig:|!\[/.test(commentContent)) return true;
51
68
 
69
+ // Contains URL patterns (likely a link, not a comment)
70
+ if (/https?:\/\/|www\./i.test(commentContent) && commentContent.length < 150) return true;
71
+
72
+ // Looks like code (contains programming patterns)
73
+ if (/function\s*\(|=>|import\s+|export\s+|const\s+|let\s+|var\s+/.test(commentContent)) return true;
74
+
52
75
  // Very long without clear author pattern (likely caption, not comment)
53
76
  // Real comments typically have "Author:" at start and are shorter
54
- const hasAuthorPrefix = /^[A-Za-z][A-Za-z\s]{0,20}:/.test(commentContent.trim());
55
- if (!hasAuthorPrefix && commentContent.length > 200) return true;
77
+ const hasAuthorPrefix = /^[A-Za-z][A-Za-z\s]{0,20}:\s/.test(commentContent.trim());
78
+ const hasResolvedMark = /^[✓✔]\s/.test(commentContent.trim());
79
+ if (!hasAuthorPrefix && !hasResolvedMark && commentContent.length > 200) return true;
56
80
 
57
81
  // Looks like a figure caption (starts with "Fig" or contains typical caption words)
58
- if (/^(Fig\.?|Figure|Table|Sankey|Diagram|Proportion|Distribution)/i.test(commentContent.trim())) {
82
+ if (/^(Fig\.?|Figure|Table|Sankey|Diagram|Proportion|Distribution|Map|Chart|Graph|Plot|Panel)/i.test(commentContent.trim())) {
59
83
  return true;
60
84
  }
61
85
 
86
+ // Contains LaTeX-like patterns (likely equation, not comment)
87
+ if (/\\[a-z]+\{|\\frac|\\sum|\\int|\\begin\{/.test(commentContent)) return true;
88
+
89
+ // Looks like BibTeX entry (not a comment)
90
+ if (/@article\{|@book\{|@inproceedings\{/i.test(commentContent)) return true;
91
+
62
92
  return false;
63
93
  }
64
94
 
package/lib/build.js CHANGED
@@ -11,7 +11,7 @@
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
13
  import { execSync, spawn } from 'child_process';
14
- import yaml from 'js-yaml';
14
+ import YAML from 'yaml';
15
15
  import { stripAnnotations } from './annotations.js';
16
16
  import { buildRegistry, labelToDisplay, detectDynamicRefs } from './crossref.js';
17
17
  import { processVariables, hasVariables } from './variables.js';
@@ -66,7 +66,7 @@ export function loadConfig(directory) {
66
66
 
67
67
  try {
68
68
  const content = fs.readFileSync(configPath, 'utf-8');
69
- const userConfig = yaml.load(content) || {};
69
+ const userConfig = YAML.parse(content) || {};
70
70
 
71
71
  // Deep merge with defaults
72
72
  const config = {
@@ -110,7 +110,7 @@ export function findSections(directory, configSections = []) {
110
110
  const sectionsYamlPath = path.join(directory, 'sections.yaml');
111
111
  if (fs.existsSync(sectionsYamlPath)) {
112
112
  try {
113
- const sectionsConfig = yaml.load(fs.readFileSync(sectionsYamlPath, 'utf-8'));
113
+ const sectionsConfig = YAML.parse(fs.readFileSync(sectionsYamlPath, 'utf-8'));
114
114
  if (sectionsConfig.sections) {
115
115
  return Object.entries(sectionsConfig.sections)
116
116
  .sort((a, b) => (a[1].order ?? 999) - (b[1].order ?? 999))
@@ -153,7 +153,7 @@ export function combineSections(directory, config, options = {}) {
153
153
  // Add YAML frontmatter
154
154
  const frontmatter = buildFrontmatter(config);
155
155
  parts.push('---');
156
- parts.push(yaml.dump(frontmatter).trim());
156
+ parts.push(YAML.stringify(frontmatter).trim());
157
157
  parts.push('---');
158
158
  parts.push('');
159
159
 
@@ -371,6 +371,80 @@ export function hasPandoc() {
371
371
  }
372
372
  }
373
373
 
374
+ /**
375
+ * Check if LaTeX is available (for PDF generation)
376
+ * @returns {boolean}
377
+ */
378
+ export function hasLatex() {
379
+ try {
380
+ execSync('pdflatex --version', { stdio: 'ignore' });
381
+ return true;
382
+ } catch {
383
+ try {
384
+ execSync('xelatex --version', { stdio: 'ignore' });
385
+ return true;
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Get installation instructions for missing dependencies
394
+ * @param {string} dependency - 'pandoc', 'latex', 'pandoc-crossref'
395
+ * @returns {string}
396
+ */
397
+ export function getInstallInstructions(dependency) {
398
+ const platform = process.platform;
399
+ const instructions = {
400
+ pandoc: {
401
+ darwin: 'brew install pandoc',
402
+ win32: 'winget install JohnMacFarlane.Pandoc',
403
+ linux: 'sudo apt install pandoc',
404
+ },
405
+ latex: {
406
+ darwin: 'brew install --cask mactex-no-gui',
407
+ win32: 'Install MiKTeX from https://miktex.org/download',
408
+ linux: 'sudo apt install texlive-latex-base texlive-fonts-recommended',
409
+ },
410
+ 'pandoc-crossref': {
411
+ darwin: 'brew install pandoc-crossref',
412
+ win32: 'Download from https://github.com/lierdakil/pandoc-crossref/releases',
413
+ linux: 'Download from https://github.com/lierdakil/pandoc-crossref/releases',
414
+ },
415
+ };
416
+
417
+ const platformInstructions = instructions[dependency];
418
+ if (!platformInstructions) return '';
419
+
420
+ return platformInstructions[platform] || platformInstructions.linux;
421
+ }
422
+
423
+ /**
424
+ * Check dependencies and return status
425
+ * @returns {{ pandoc: boolean, latex: boolean, crossref: boolean, messages: string[] }}
426
+ */
427
+ export function checkDependencies() {
428
+ const status = {
429
+ pandoc: hasPandoc(),
430
+ latex: hasLatex(),
431
+ crossref: hasPandocCrossref(),
432
+ messages: [],
433
+ };
434
+
435
+ if (!status.pandoc) {
436
+ status.messages.push(`Pandoc not found. Install with: ${getInstallInstructions('pandoc')}`);
437
+ }
438
+ if (!status.latex) {
439
+ status.messages.push(`LaTeX not found (required for PDF). Install with: ${getInstallInstructions('latex')}`);
440
+ }
441
+ if (!status.crossref) {
442
+ status.messages.push(`pandoc-crossref not found (optional, for figure/table refs). Install with: ${getInstallInstructions('pandoc-crossref')}`);
443
+ }
444
+
445
+ return status;
446
+ }
447
+
374
448
  /**
375
449
  * Write crossref.yaml if needed
376
450
  * @param {string} directory
@@ -380,7 +454,7 @@ function ensureCrossrefConfig(directory, config) {
380
454
  const crossrefPath = path.join(directory, 'crossref.yaml');
381
455
 
382
456
  if (!fs.existsSync(crossrefPath) && hasPandocCrossref()) {
383
- fs.writeFileSync(crossrefPath, yaml.dump(config.crossref), 'utf-8');
457
+ fs.writeFileSync(crossrefPath, YAML.stringify(config.crossref), 'utf-8');
384
458
  }
385
459
  }
386
460
 
@@ -446,12 +520,25 @@ export async function runPandoc(inputPath, format, config, options = {}) {
446
520
  * @param {string} directory
447
521
  * @param {string[]} formats - ['pdf', 'docx', 'tex'] or ['all']
448
522
  * @param {object} options
449
- * @returns {Promise<{results: object[], paperPath: string}>}
523
+ * @returns {Promise<{results: object[], paperPath: string, warnings: string[]}>}
450
524
  */
451
525
  export async function build(directory, formats = ['pdf', 'docx'], options = {}) {
526
+ const warnings = [];
527
+
452
528
  // Check pandoc
453
529
  if (!hasPandoc()) {
454
- throw new Error('pandoc not found. Run `rev install` to install dependencies.');
530
+ const instruction = getInstallInstructions('pandoc');
531
+ throw new Error(`Pandoc not found. Install with: ${instruction}\nOr run: rev doctor`);
532
+ }
533
+
534
+ // Check LaTeX if PDF is requested
535
+ if ((formats.includes('pdf') || formats.includes('all')) && !hasLatex()) {
536
+ warnings.push(`LaTeX not found - PDF generation may fail. Install with: ${getInstallInstructions('latex')}`);
537
+ }
538
+
539
+ // Check pandoc-crossref
540
+ if (!hasPandocCrossref()) {
541
+ warnings.push('pandoc-crossref not found - figure/table numbering will not work');
455
542
  }
456
543
 
457
544
  // Load config (use passed config if provided, otherwise load from file)
@@ -483,7 +570,7 @@ export async function build(directory, formats = ['pdf', 'docx'], options = {})
483
570
  }
484
571
  }
485
572
 
486
- return { results, paperPath };
573
+ return { results, paperPath, warnings };
487
574
  }
488
575
 
489
576
  /**