docrev 0.7.7 → 0.7.8
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/README.md +17 -2
- package/lib/build.js +50 -5
- package/lib/commands/build.js +10 -3
- package/lib/crossref.js +82 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,10 +157,25 @@ my-report/
|
|
|
157
157
|
Write your content in the markdown files. When ready to share:
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
|
-
rev build docx
|
|
160
|
+
rev build docx pdf
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
After building, your project structure looks like:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
my-report/
|
|
167
|
+
├── intro.md
|
|
168
|
+
├── methods.md
|
|
169
|
+
├── results.md
|
|
170
|
+
├── discussion.md
|
|
171
|
+
├── references.bib
|
|
172
|
+
├── rev.yaml
|
|
173
|
+
├── paper.md ← combined sections (auto-generated)
|
|
174
|
+
├── my-report.docx ← output for collaborators
|
|
175
|
+
└── my-report.pdf ← output for journals
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The output filename is derived from your project title in `rev.yaml`. Citations are resolved, equations rendered, and cross-references numbered.
|
|
164
179
|
|
|
165
180
|
### Starting from an Existing Word Document
|
|
166
181
|
|
package/lib/build.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as path from 'path';
|
|
|
13
13
|
import { execSync, spawn } from 'child_process';
|
|
14
14
|
import YAML from 'yaml';
|
|
15
15
|
import { stripAnnotations } from './annotations.js';
|
|
16
|
-
import { buildRegistry, labelToDisplay, detectDynamicRefs } from './crossref.js';
|
|
16
|
+
import { buildRegistry, labelToDisplay, detectDynamicRefs, resolveForwardRefs } from './crossref.js';
|
|
17
17
|
import { processVariables, hasVariables } from './variables.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -160,8 +160,22 @@ export function combineSections(directory, config, options = {}) {
|
|
|
160
160
|
// Read all section contents for variable processing
|
|
161
161
|
const sectionContents = [];
|
|
162
162
|
|
|
163
|
+
// Check if we need to auto-inject references before supplementary
|
|
164
|
+
// Pandoc places refs at the end by default, which breaks when supplementary follows
|
|
165
|
+
const hasRefsSection = sections.some(s =>
|
|
166
|
+
s.toLowerCase().includes('reference') || s.toLowerCase().includes('refs')
|
|
167
|
+
);
|
|
168
|
+
const suppIndex = sections.findIndex(s =>
|
|
169
|
+
s.toLowerCase().includes('supp') || s.toLowerCase().includes('appendix')
|
|
170
|
+
);
|
|
171
|
+
const hasBibliography = config.bibliography && fs.existsSync(path.join(directory, config.bibliography));
|
|
172
|
+
|
|
173
|
+
// Track if we find an explicit refs div in any section
|
|
174
|
+
let hasExplicitRefsDiv = false;
|
|
175
|
+
|
|
163
176
|
// Combine sections
|
|
164
|
-
for (
|
|
177
|
+
for (let i = 0; i < sections.length; i++) {
|
|
178
|
+
const section = sections[i];
|
|
165
179
|
const filePath = path.join(directory, section);
|
|
166
180
|
let content = fs.readFileSync(filePath, 'utf-8');
|
|
167
181
|
|
|
@@ -169,6 +183,21 @@ export function combineSections(directory, config, options = {}) {
|
|
|
169
183
|
content = stripFrontmatter(content);
|
|
170
184
|
sectionContents.push(content);
|
|
171
185
|
|
|
186
|
+
// Check if this section has an explicit refs div
|
|
187
|
+
if (content.includes('::: {#refs}') || content.includes('::: {#refs}')) {
|
|
188
|
+
hasExplicitRefsDiv = true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Auto-inject references before supplementary if needed
|
|
192
|
+
if (i === suppIndex && hasBibliography && !hasRefsSection && !hasExplicitRefsDiv) {
|
|
193
|
+
parts.push('# References\n');
|
|
194
|
+
parts.push('::: {#refs}');
|
|
195
|
+
parts.push(':::');
|
|
196
|
+
parts.push('');
|
|
197
|
+
parts.push('');
|
|
198
|
+
options._refsAutoInjected = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
172
201
|
parts.push(content.trim());
|
|
173
202
|
parts.push('');
|
|
174
203
|
parts.push(''); // Double newline between sections
|
|
@@ -181,6 +210,18 @@ export function combineSections(directory, config, options = {}) {
|
|
|
181
210
|
paperContent = processVariables(paperContent, config, { sectionContents });
|
|
182
211
|
}
|
|
183
212
|
|
|
213
|
+
// Resolve forward references (refs that appear before their anchor definition)
|
|
214
|
+
// This fixes pandoc-crossref limitation with multi-file documents
|
|
215
|
+
if (hasPandocCrossref()) {
|
|
216
|
+
const registry = buildRegistry(directory);
|
|
217
|
+
const { text, resolved } = resolveForwardRefs(paperContent, registry);
|
|
218
|
+
if (resolved.length > 0) {
|
|
219
|
+
paperContent = text;
|
|
220
|
+
// Store resolved count for optional reporting
|
|
221
|
+
options._forwardRefsResolved = resolved.length;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
184
225
|
const paperPath = path.join(directory, 'paper.md');
|
|
185
226
|
|
|
186
227
|
fs.writeFileSync(paperPath, paperContent, 'utf-8');
|
|
@@ -520,10 +561,11 @@ export async function runPandoc(inputPath, format, config, options = {}) {
|
|
|
520
561
|
* @param {string} directory
|
|
521
562
|
* @param {string[]} formats - ['pdf', 'docx', 'tex'] or ['all']
|
|
522
563
|
* @param {object} options
|
|
523
|
-
* @returns {Promise<{results: object[], paperPath: string, warnings: string[]}>}
|
|
564
|
+
* @returns {Promise<{results: object[], paperPath: string, warnings: string[], forwardRefsResolved: number}>}
|
|
524
565
|
*/
|
|
525
566
|
export async function build(directory, formats = ['pdf', 'docx'], options = {}) {
|
|
526
567
|
const warnings = [];
|
|
568
|
+
let forwardRefsResolved = 0;
|
|
527
569
|
|
|
528
570
|
// Check pandoc
|
|
529
571
|
if (!hasPandoc()) {
|
|
@@ -545,7 +587,10 @@ export async function build(directory, formats = ['pdf', 'docx'], options = {})
|
|
|
545
587
|
const config = options.config || loadConfig(directory);
|
|
546
588
|
|
|
547
589
|
// Combine sections → paper.md
|
|
548
|
-
const
|
|
590
|
+
const buildOptions = { ...options };
|
|
591
|
+
const paperPath = combineSections(directory, config, buildOptions);
|
|
592
|
+
forwardRefsResolved = buildOptions._forwardRefsResolved || 0;
|
|
593
|
+
const refsAutoInjected = buildOptions._refsAutoInjected || false;
|
|
549
594
|
|
|
550
595
|
// Expand 'all' to all formats
|
|
551
596
|
if (formats.includes('all')) {
|
|
@@ -570,7 +615,7 @@ export async function build(directory, formats = ['pdf', 'docx'], options = {})
|
|
|
570
615
|
}
|
|
571
616
|
}
|
|
572
617
|
|
|
573
|
-
return { results, paperPath, warnings };
|
|
618
|
+
return { results, paperPath, warnings, forwardRefsResolved, refsAutoInjected };
|
|
574
619
|
}
|
|
575
620
|
|
|
576
621
|
/**
|
package/lib/commands/build.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
getRefStatus,
|
|
18
18
|
formatRegistry,
|
|
19
19
|
build,
|
|
20
|
-
|
|
20
|
+
loadBuildConfig,
|
|
21
21
|
hasPandoc,
|
|
22
22
|
hasPandocCrossref,
|
|
23
23
|
formatBuildResults,
|
|
@@ -555,7 +555,7 @@ export function register(program, pkg) {
|
|
|
555
555
|
const spin = fmt.spinner('Building...').start();
|
|
556
556
|
|
|
557
557
|
try {
|
|
558
|
-
const { results, paperPath } = await build(dir, targetFormats, {
|
|
558
|
+
const { results, paperPath, forwardRefsResolved, refsAutoInjected } = await build(dir, targetFormats, {
|
|
559
559
|
crossref: options.crossref,
|
|
560
560
|
config,
|
|
561
561
|
});
|
|
@@ -563,7 +563,14 @@ export function register(program, pkg) {
|
|
|
563
563
|
spin.stop();
|
|
564
564
|
|
|
565
565
|
console.log(chalk.cyan('Combined sections → paper.md'));
|
|
566
|
-
console.log(chalk.dim(` ${paperPath}
|
|
566
|
+
console.log(chalk.dim(` ${paperPath}`));
|
|
567
|
+
if (forwardRefsResolved > 0) {
|
|
568
|
+
console.log(chalk.dim(` ${forwardRefsResolved} forward reference(s) pre-resolved`));
|
|
569
|
+
}
|
|
570
|
+
if (refsAutoInjected) {
|
|
571
|
+
console.log(chalk.dim(` References section auto-injected before supplementary`));
|
|
572
|
+
}
|
|
573
|
+
console.log('');
|
|
567
574
|
|
|
568
575
|
console.log(chalk.cyan('Output:'));
|
|
569
576
|
console.log(formatBuildResults(results));
|
package/lib/crossref.js
CHANGED
|
@@ -489,6 +489,88 @@ export function getRefStatus(text, registry) {
|
|
|
489
489
|
};
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Detect forward references in combined text
|
|
494
|
+
* A forward reference is a @ref that appears before its {#anchor} definition
|
|
495
|
+
*
|
|
496
|
+
* @param {string} text - Combined document text
|
|
497
|
+
* @returns {{
|
|
498
|
+
* forwardRefs: Array<{type: string, label: string, match: string, position: number}>,
|
|
499
|
+
* anchorPositions: Map<string, number>
|
|
500
|
+
* }}
|
|
501
|
+
*/
|
|
502
|
+
export function detectForwardRefs(text) {
|
|
503
|
+
// Build map of anchor positions: "fig:label" -> position
|
|
504
|
+
const anchorPositions = new Map();
|
|
505
|
+
ANCHOR_PATTERN.lastIndex = 0;
|
|
506
|
+
let match;
|
|
507
|
+
while ((match = ANCHOR_PATTERN.exec(text)) !== null) {
|
|
508
|
+
const key = `${match[1]}:${match[2]}`;
|
|
509
|
+
// Only store first occurrence (in case of duplicates)
|
|
510
|
+
if (!anchorPositions.has(key)) {
|
|
511
|
+
anchorPositions.set(key, match.index);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Find all references
|
|
516
|
+
const refs = detectDynamicRefs(text);
|
|
517
|
+
|
|
518
|
+
// Filter to only forward references
|
|
519
|
+
const forwardRefs = refs.filter((ref) => {
|
|
520
|
+
const key = `${ref.type}:${ref.label}`;
|
|
521
|
+
const anchorPos = anchorPositions.get(key);
|
|
522
|
+
// Forward ref if anchor doesn't exist or appears after the reference
|
|
523
|
+
return anchorPos === undefined || ref.position < anchorPos;
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
return { forwardRefs, anchorPositions };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Resolve forward references to display format
|
|
531
|
+
* Only resolves refs that appear before their anchor definition
|
|
532
|
+
* Leaves other refs for pandoc-crossref to handle (preserves clickable links)
|
|
533
|
+
*
|
|
534
|
+
* @param {string} text - Combined document text
|
|
535
|
+
* @param {object} registry - Registry from buildRegistry()
|
|
536
|
+
* @returns {{
|
|
537
|
+
* text: string,
|
|
538
|
+
* resolved: Array<{from: string, to: string, position: number}>,
|
|
539
|
+
* unresolved: Array<{ref: string, position: number}>
|
|
540
|
+
* }}
|
|
541
|
+
*/
|
|
542
|
+
export function resolveForwardRefs(text, registry) {
|
|
543
|
+
const { forwardRefs } = detectForwardRefs(text);
|
|
544
|
+
const resolved = [];
|
|
545
|
+
const unresolved = [];
|
|
546
|
+
|
|
547
|
+
// Process in reverse order to preserve positions
|
|
548
|
+
let result = text;
|
|
549
|
+
for (let i = forwardRefs.length - 1; i >= 0; i--) {
|
|
550
|
+
const ref = forwardRefs[i];
|
|
551
|
+
const display = labelToDisplay(ref.type, ref.label, registry);
|
|
552
|
+
|
|
553
|
+
if (display) {
|
|
554
|
+
result =
|
|
555
|
+
result.slice(0, ref.position) +
|
|
556
|
+
display +
|
|
557
|
+
result.slice(ref.position + ref.match.length);
|
|
558
|
+
resolved.push({
|
|
559
|
+
from: ref.match,
|
|
560
|
+
to: display,
|
|
561
|
+
position: ref.position,
|
|
562
|
+
});
|
|
563
|
+
} else {
|
|
564
|
+
unresolved.push({
|
|
565
|
+
ref: ref.match,
|
|
566
|
+
position: ref.position,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return { text: result, resolved, unresolved };
|
|
572
|
+
}
|
|
573
|
+
|
|
492
574
|
/**
|
|
493
575
|
* Format registry for display
|
|
494
576
|
* @param {object} registry
|