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 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
- This produces `my-report.docx` with citations resolved, equations rendered, and cross-references numbered. Use `rev build pdf` for PDF output instead.
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 (const section of sections) {
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 paperPath = combineSections(directory, config, options);
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
  /**
@@ -17,7 +17,7 @@ import {
17
17
  getRefStatus,
18
18
  formatRegistry,
19
19
  build,
20
- loadConfig as loadBuildConfig,
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}\n`));
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docrev",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "Academic paper revision workflow: Word ↔ Markdown round-trips, DOI validation, reviewer comments",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",