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.
- package/CHANGELOG.md +32 -0
- package/README.md +191 -133
- package/bin/rev.js +113 -5059
- package/completions/rev.ps1 +210 -0
- package/lib/annotations.js +41 -11
- package/lib/build.js +95 -8
- package/lib/commands/build.js +708 -0
- package/lib/commands/citations.js +497 -0
- package/lib/commands/comments.js +922 -0
- package/lib/commands/context.js +165 -0
- package/lib/commands/core.js +295 -0
- package/lib/commands/doi.js +419 -0
- package/lib/commands/history.js +307 -0
- package/lib/commands/index.js +56 -0
- package/lib/commands/init.js +247 -0
- package/lib/commands/response.js +374 -0
- package/lib/commands/sections.js +862 -0
- package/lib/commands/utilities.js +2272 -0
- package/lib/config.js +19 -0
- package/lib/crossref.js +17 -2
- package/lib/doi.js +279 -43
- package/lib/errors.js +338 -0
- package/lib/format.js +53 -6
- package/lib/git.js +92 -0
- package/lib/import.js +24 -3
- package/lib/journals.js +28 -4
- package/lib/orcid.js +149 -0
- package/lib/pdf-comments.js +217 -0
- package/lib/pdf-import.js +446 -0
- package/lib/plugins.js +285 -0
- package/lib/review.js +109 -0
- package/lib/schema.js +368 -0
- package/lib/sections.js +3 -8
- package/lib/templates.js +218 -0
- package/lib/tui.js +437 -0
- package/lib/undo.js +236 -0
- package/lib/wordcomments.js +15 -20
- package/package.json +5 -3
- package/skill/REFERENCE.md +76 -18
- package/skill/SKILL.md +122 -27
- package/.rev-dictionary +0 -4
|
@@ -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
|
+
}
|
package/lib/annotations.js
CHANGED
|
@@ -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
|
-
|
|
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 = (
|
|
36
|
-
const delCloses = (
|
|
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 = (
|
|
41
|
-
const insCloses = (
|
|
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}
|
|
55
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
/**
|