docrev 0.8.1 → 0.8.5
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/.claude/settings.local.json +9 -0
- package/PLAN-tables-and-postprocess.md +850 -0
- package/README.md +33 -0
- package/bin/rev.js +12 -131
- package/bin/rev.ts +145 -0
- package/dist/bin/rev.d.ts +9 -0
- package/dist/bin/rev.d.ts.map +1 -0
- package/dist/bin/rev.js +118 -0
- package/dist/bin/rev.js.map +1 -0
- package/dist/lib/annotations.d.ts +91 -0
- package/dist/lib/annotations.d.ts.map +1 -0
- package/dist/lib/annotations.js +554 -0
- package/dist/lib/annotations.js.map +1 -0
- package/dist/lib/build.d.ts +171 -0
- package/dist/lib/build.d.ts.map +1 -0
- package/dist/lib/build.js +755 -0
- package/dist/lib/build.js.map +1 -0
- package/dist/lib/citations.d.ts +34 -0
- package/dist/lib/citations.d.ts.map +1 -0
- package/dist/lib/citations.js +140 -0
- package/dist/lib/citations.js.map +1 -0
- package/dist/lib/commands/build.d.ts +13 -0
- package/dist/lib/commands/build.d.ts.map +1 -0
- package/dist/lib/commands/build.js +678 -0
- package/dist/lib/commands/build.js.map +1 -0
- package/dist/lib/commands/citations.d.ts +11 -0
- package/dist/lib/commands/citations.d.ts.map +1 -0
- package/dist/lib/commands/citations.js +428 -0
- package/dist/lib/commands/citations.js.map +1 -0
- package/dist/lib/commands/comments.d.ts +11 -0
- package/dist/lib/commands/comments.d.ts.map +1 -0
- package/dist/lib/commands/comments.js +883 -0
- package/dist/lib/commands/comments.js.map +1 -0
- package/dist/lib/commands/context.d.ts +35 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/context.js +59 -0
- package/dist/lib/commands/context.js.map +1 -0
- package/dist/lib/commands/core.d.ts +11 -0
- package/dist/lib/commands/core.d.ts.map +1 -0
- package/dist/lib/commands/core.js +246 -0
- package/dist/lib/commands/core.js.map +1 -0
- package/dist/lib/commands/doi.d.ts +11 -0
- package/dist/lib/commands/doi.d.ts.map +1 -0
- package/dist/lib/commands/doi.js +373 -0
- package/dist/lib/commands/doi.js.map +1 -0
- package/dist/lib/commands/history.d.ts +11 -0
- package/dist/lib/commands/history.d.ts.map +1 -0
- package/dist/lib/commands/history.js +245 -0
- package/dist/lib/commands/history.js.map +1 -0
- package/dist/lib/commands/index.d.ts +28 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/index.js +35 -0
- package/dist/lib/commands/index.js.map +1 -0
- package/dist/lib/commands/init.d.ts +11 -0
- package/dist/lib/commands/init.d.ts.map +1 -0
- package/dist/lib/commands/init.js +209 -0
- package/dist/lib/commands/init.js.map +1 -0
- package/dist/lib/commands/response.d.ts +11 -0
- package/dist/lib/commands/response.d.ts.map +1 -0
- package/dist/lib/commands/response.js +317 -0
- package/dist/lib/commands/response.js.map +1 -0
- package/dist/lib/commands/sections.d.ts +11 -0
- package/dist/lib/commands/sections.d.ts.map +1 -0
- package/dist/lib/commands/sections.js +1071 -0
- package/dist/lib/commands/sections.js.map +1 -0
- package/dist/lib/commands/utilities.d.ts +19 -0
- package/dist/lib/commands/utilities.d.ts.map +1 -0
- package/dist/lib/commands/utilities.js +2009 -0
- package/dist/lib/commands/utilities.js.map +1 -0
- package/dist/lib/comment-realign.d.ts +50 -0
- package/dist/lib/comment-realign.d.ts.map +1 -0
- package/dist/lib/comment-realign.js +372 -0
- package/dist/lib/comment-realign.js.map +1 -0
- package/dist/lib/config.d.ts +41 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +76 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/crossref.d.ts +108 -0
- package/dist/lib/crossref.d.ts.map +1 -0
- package/dist/lib/crossref.js +597 -0
- package/dist/lib/crossref.js.map +1 -0
- package/dist/lib/dependencies.d.ts +30 -0
- package/dist/lib/dependencies.d.ts.map +1 -0
- package/dist/lib/dependencies.js +95 -0
- package/dist/lib/dependencies.js.map +1 -0
- package/dist/lib/doi-cache.d.ts +29 -0
- package/dist/lib/doi-cache.d.ts.map +1 -0
- package/dist/lib/doi-cache.js +104 -0
- package/dist/lib/doi-cache.js.map +1 -0
- package/dist/lib/doi.d.ts +65 -0
- package/dist/lib/doi.d.ts.map +1 -0
- package/dist/lib/doi.js +710 -0
- package/dist/lib/doi.js.map +1 -0
- package/dist/lib/equations.d.ts +61 -0
- package/dist/lib/equations.d.ts.map +1 -0
- package/dist/lib/equations.js +445 -0
- package/dist/lib/equations.js.map +1 -0
- package/dist/lib/errors.d.ts +60 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +303 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/format.d.ts +104 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +416 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/git.d.ts +88 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +304 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/grammar.d.ts +62 -0
- package/dist/lib/grammar.d.ts.map +1 -0
- package/dist/lib/grammar.js +244 -0
- package/dist/lib/grammar.js.map +1 -0
- package/dist/lib/image-registry.d.ts +68 -0
- package/dist/lib/image-registry.d.ts.map +1 -0
- package/dist/lib/image-registry.js +112 -0
- package/dist/lib/image-registry.js.map +1 -0
- package/dist/lib/import.d.ts +184 -0
- package/dist/lib/import.d.ts.map +1 -0
- package/dist/lib/import.js +1581 -0
- package/dist/lib/import.js.map +1 -0
- package/dist/lib/journals.d.ts +55 -0
- package/dist/lib/journals.d.ts.map +1 -0
- package/dist/lib/journals.js +417 -0
- package/dist/lib/journals.js.map +1 -0
- package/dist/lib/merge.d.ts +138 -0
- package/dist/lib/merge.d.ts.map +1 -0
- package/dist/lib/merge.js +603 -0
- package/dist/lib/merge.js.map +1 -0
- package/dist/lib/orcid.d.ts +36 -0
- package/dist/lib/orcid.d.ts.map +1 -0
- package/dist/lib/orcid.js +117 -0
- package/dist/lib/orcid.js.map +1 -0
- package/dist/lib/pdf-comments.d.ts +95 -0
- package/dist/lib/pdf-comments.d.ts.map +1 -0
- package/dist/lib/pdf-comments.js +192 -0
- package/dist/lib/pdf-comments.js.map +1 -0
- package/dist/lib/pdf-import.d.ts +118 -0
- package/dist/lib/pdf-import.d.ts.map +1 -0
- package/dist/lib/pdf-import.js +397 -0
- package/dist/lib/pdf-import.js.map +1 -0
- package/dist/lib/plugins.d.ts +76 -0
- package/dist/lib/plugins.d.ts.map +1 -0
- package/dist/lib/plugins.js +235 -0
- package/dist/lib/plugins.js.map +1 -0
- package/dist/lib/postprocess.d.ts +42 -0
- package/dist/lib/postprocess.d.ts.map +1 -0
- package/dist/lib/postprocess.js +138 -0
- package/dist/lib/postprocess.js.map +1 -0
- package/dist/lib/pptx-template.d.ts +59 -0
- package/dist/lib/pptx-template.d.ts.map +1 -0
- package/dist/lib/pptx-template.js +613 -0
- package/dist/lib/pptx-template.js.map +1 -0
- package/dist/lib/pptx-themes.d.ts +80 -0
- package/dist/lib/pptx-themes.d.ts.map +1 -0
- package/dist/lib/pptx-themes.js +818 -0
- package/dist/lib/pptx-themes.js.map +1 -0
- package/dist/lib/protect-restore.d.ts +137 -0
- package/dist/lib/protect-restore.d.ts.map +1 -0
- package/dist/lib/protect-restore.js +394 -0
- package/dist/lib/protect-restore.js.map +1 -0
- package/dist/lib/rate-limiter.d.ts +27 -0
- package/dist/lib/rate-limiter.d.ts.map +1 -0
- package/dist/lib/rate-limiter.js +79 -0
- package/dist/lib/rate-limiter.js.map +1 -0
- package/dist/lib/response.d.ts +41 -0
- package/dist/lib/response.d.ts.map +1 -0
- package/dist/lib/response.js +150 -0
- package/dist/lib/response.js.map +1 -0
- package/dist/lib/review.d.ts +35 -0
- package/dist/lib/review.d.ts.map +1 -0
- package/dist/lib/review.js +263 -0
- package/dist/lib/review.js.map +1 -0
- package/dist/lib/schema.d.ts +66 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +339 -0
- package/dist/lib/schema.js.map +1 -0
- package/dist/lib/scientific-words.d.ts +6 -0
- package/dist/lib/scientific-words.d.ts.map +1 -0
- package/dist/lib/scientific-words.js +66 -0
- package/dist/lib/scientific-words.js.map +1 -0
- package/dist/lib/sections.d.ts +40 -0
- package/dist/lib/sections.d.ts.map +1 -0
- package/dist/lib/sections.js +288 -0
- package/dist/lib/sections.js.map +1 -0
- package/dist/lib/slides.d.ts +86 -0
- package/dist/lib/slides.d.ts.map +1 -0
- package/dist/lib/slides.js +676 -0
- package/dist/lib/slides.js.map +1 -0
- package/dist/lib/spelling.d.ts +76 -0
- package/dist/lib/spelling.d.ts.map +1 -0
- package/dist/lib/spelling.js +272 -0
- package/dist/lib/spelling.js.map +1 -0
- package/dist/lib/templates.d.ts +30 -0
- package/dist/lib/templates.d.ts.map +1 -0
- package/dist/lib/templates.js +504 -0
- package/dist/lib/templates.js.map +1 -0
- package/dist/lib/themes.d.ts +85 -0
- package/dist/lib/themes.d.ts.map +1 -0
- package/dist/lib/themes.js +652 -0
- package/dist/lib/themes.js.map +1 -0
- package/dist/lib/trackchanges.d.ts +51 -0
- package/dist/lib/trackchanges.d.ts.map +1 -0
- package/dist/lib/trackchanges.js +202 -0
- package/dist/lib/trackchanges.js.map +1 -0
- package/dist/lib/tui.d.ts +76 -0
- package/dist/lib/tui.d.ts.map +1 -0
- package/dist/lib/tui.js +377 -0
- package/dist/lib/tui.js.map +1 -0
- package/dist/lib/types.d.ts +447 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +6 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/undo.d.ts +57 -0
- package/dist/lib/undo.d.ts.map +1 -0
- package/dist/lib/undo.js +185 -0
- package/dist/lib/undo.js.map +1 -0
- package/dist/lib/utils.d.ts +16 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +40 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/variables.d.ts +42 -0
- package/dist/lib/variables.d.ts.map +1 -0
- package/dist/lib/variables.js +141 -0
- package/dist/lib/variables.js.map +1 -0
- package/dist/lib/word.d.ts +80 -0
- package/dist/lib/word.d.ts.map +1 -0
- package/dist/lib/word.js +360 -0
- package/dist/lib/word.js.map +1 -0
- package/dist/lib/wordcomments.d.ts +51 -0
- package/dist/lib/wordcomments.d.ts.map +1 -0
- package/dist/lib/wordcomments.js +587 -0
- package/dist/lib/wordcomments.js.map +1 -0
- package/eslint.config.js +27 -0
- package/lib/annotations.ts +622 -0
- package/lib/apply-buildup-colors.py +88 -0
- package/lib/build.ts +1013 -0
- package/lib/{citations.js → citations.ts} +38 -27
- package/lib/commands/{build.js → build.ts} +80 -27
- package/lib/commands/{citations.js → citations.ts} +36 -18
- package/lib/commands/{comments.js → comments.ts} +187 -54
- package/lib/commands/{context.js → context.ts} +18 -8
- package/lib/commands/{core.js → core.ts} +34 -20
- package/lib/commands/{doi.js → doi.ts} +32 -16
- package/lib/commands/{history.js → history.ts} +25 -12
- package/lib/commands/{index.js → index.ts} +9 -5
- package/lib/commands/{init.js → init.ts} +20 -8
- package/lib/commands/{response.js → response.ts} +47 -20
- package/lib/commands/{sections.js → sections.ts} +273 -68
- package/lib/commands/{utilities.js → utilities.ts} +338 -158
- package/lib/{comment-realign.js → comment-realign.ts} +117 -45
- package/lib/config.ts +84 -0
- package/lib/{crossref.js → crossref.ts} +213 -138
- package/lib/dependencies.ts +106 -0
- package/lib/doi-cache.ts +115 -0
- package/lib/{doi.js → doi.ts} +115 -281
- package/lib/{equations.js → equations.ts} +60 -64
- package/lib/{errors.js → errors.ts} +56 -48
- package/lib/{format.js → format.ts} +137 -63
- package/lib/{git.js → git.ts} +66 -63
- package/lib/{grammar.js → grammar.ts} +45 -32
- package/lib/image-registry.ts +180 -0
- package/lib/import.ts +2060 -0
- package/lib/journals.ts +505 -0
- package/lib/{merge.js → merge.ts} +185 -135
- package/lib/{orcid.js → orcid.ts} +17 -22
- package/lib/{pdf-comments.js → pdf-comments.ts} +76 -18
- package/lib/{pdf-import.js → pdf-import.ts} +148 -70
- package/lib/{plugins.js → plugins.ts} +82 -39
- package/lib/postprocess.ts +188 -0
- package/lib/pptx-color-filter.lua +37 -0
- package/lib/pptx-template.ts +625 -0
- package/lib/pptx-themes/academic.pptx +0 -0
- package/lib/pptx-themes/corporate.pptx +0 -0
- package/lib/pptx-themes/dark.pptx +0 -0
- package/lib/pptx-themes/default.pptx +0 -0
- package/lib/pptx-themes/minimal.pptx +0 -0
- package/lib/pptx-themes/plant.pptx +0 -0
- package/lib/pptx-themes.ts +896 -0
- package/lib/protect-restore.ts +516 -0
- package/lib/rate-limiter.ts +94 -0
- package/lib/{response.js → response.ts} +36 -21
- package/lib/{review.js → review.ts} +53 -43
- package/lib/{schema.js → schema.ts} +70 -25
- package/lib/{sections.js → sections.ts} +71 -76
- package/lib/slides.ts +793 -0
- package/lib/{spelling.js → spelling.ts} +43 -59
- package/lib/{templates.js → templates.ts} +20 -17
- package/lib/themes.ts +742 -0
- package/lib/{trackchanges.js → trackchanges.ts} +52 -23
- package/lib/types.ts +509 -0
- package/lib/{undo.js → undo.ts} +75 -52
- package/lib/utils.ts +41 -0
- package/lib/{variables.js → variables.ts} +60 -54
- package/lib/word.ts +428 -0
- package/lib/{wordcomments.js → wordcomments.ts} +94 -40
- package/package.json +15 -5
- package/skill/REFERENCE.md +67 -0
- package/tsconfig.json +26 -0
- package/lib/annotations.js +0 -414
- package/lib/build.js +0 -639
- package/lib/config.js +0 -79
- package/lib/import.js +0 -1145
- package/lib/journals.js +0 -629
- package/lib/word.js +0 -225
- /package/lib/{scientific-words.js → scientific-words.ts} +0 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build system - combines sections → paper.md → PDF/DOCX/TEX
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Reads rev.yaml config
|
|
6
|
+
* - Combines section files into paper.md (persisted)
|
|
7
|
+
* - Strips annotations appropriately per output format
|
|
8
|
+
* - Runs pandoc with crossref filter
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import YAML from 'yaml';
|
|
14
|
+
import { stripAnnotations } from './annotations.js';
|
|
15
|
+
import { buildRegistry, labelToDisplay, detectDynamicRefs, resolveForwardRefs } from './crossref.js';
|
|
16
|
+
import { processVariables, hasVariables } from './variables.js';
|
|
17
|
+
import { processSlideMarkdown, hasSlideSyntax } from './slides.js';
|
|
18
|
+
import { injectMediaIntoPptx, injectSlideNumbers, applyBuildupColors } from './pptx-template.js';
|
|
19
|
+
import { getThemePath } from './pptx-themes.js';
|
|
20
|
+
import { runPostprocess } from './postprocess.js';
|
|
21
|
+
import { hasPandoc, hasPandocCrossref, hasLatex } from './dependencies.js';
|
|
22
|
+
import { buildImageRegistry, writeImageRegistry } from './image-registry.js';
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Constants
|
|
25
|
+
// =============================================================================
|
|
26
|
+
/** Supported output formats */
|
|
27
|
+
const SUPPORTED_FORMATS = ['pdf', 'docx', 'tex', 'beamer', 'pptx'];
|
|
28
|
+
/** Maximum title length for output filename */
|
|
29
|
+
const MAX_TITLE_FILENAME_LENGTH = 50;
|
|
30
|
+
/**
|
|
31
|
+
* Default rev.yaml configuration
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_CONFIG = {
|
|
34
|
+
title: 'Untitled Document',
|
|
35
|
+
authors: [],
|
|
36
|
+
sections: [],
|
|
37
|
+
bibliography: null,
|
|
38
|
+
csl: null,
|
|
39
|
+
crossref: {
|
|
40
|
+
figureTitle: 'Figure',
|
|
41
|
+
tableTitle: 'Table',
|
|
42
|
+
figPrefix: ['Fig.', 'Figs.'],
|
|
43
|
+
tblPrefix: ['Table', 'Tables'],
|
|
44
|
+
secPrefix: ['Section', 'Sections'],
|
|
45
|
+
linkReferences: true,
|
|
46
|
+
},
|
|
47
|
+
pdf: {
|
|
48
|
+
template: null,
|
|
49
|
+
documentclass: 'article',
|
|
50
|
+
fontsize: '12pt',
|
|
51
|
+
geometry: 'margin=1in',
|
|
52
|
+
linestretch: 1.5,
|
|
53
|
+
numbersections: false,
|
|
54
|
+
toc: false,
|
|
55
|
+
},
|
|
56
|
+
docx: {
|
|
57
|
+
reference: null,
|
|
58
|
+
keepComments: true,
|
|
59
|
+
toc: false,
|
|
60
|
+
},
|
|
61
|
+
tex: {
|
|
62
|
+
standalone: true,
|
|
63
|
+
},
|
|
64
|
+
// Slide formats
|
|
65
|
+
beamer: {
|
|
66
|
+
theme: 'default',
|
|
67
|
+
colortheme: null,
|
|
68
|
+
fonttheme: null,
|
|
69
|
+
aspectratio: null, // '169' for 16:9, '43' for 4:3
|
|
70
|
+
navigation: null, // 'horizontal', 'vertical', 'frame', 'empty'
|
|
71
|
+
section: true, // section divider slides
|
|
72
|
+
notes: 'show', // 'show' (presenter view), 'only' (notes only), 'hide', or false
|
|
73
|
+
fit_images: true, // scale images to fit within slide bounds
|
|
74
|
+
},
|
|
75
|
+
pptx: {
|
|
76
|
+
theme: 'default', // Built-in theme: default, dark, academic, minimal, corporate
|
|
77
|
+
reference: null, // Custom reference-doc (overrides theme)
|
|
78
|
+
media: null, // directory with logo images (e.g., logo-left.png, logo-right.png)
|
|
79
|
+
},
|
|
80
|
+
// Table formatting
|
|
81
|
+
tables: {
|
|
82
|
+
nowrap: [], // Column headers to apply nowrap formatting (converts Normal() → $\mathcal{N}()$ etc.)
|
|
83
|
+
},
|
|
84
|
+
// Postprocess scripts
|
|
85
|
+
postprocess: {
|
|
86
|
+
pdf: null,
|
|
87
|
+
docx: null,
|
|
88
|
+
tex: null,
|
|
89
|
+
pptx: null,
|
|
90
|
+
beamer: null,
|
|
91
|
+
all: null, // Runs after any format
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// Public API
|
|
96
|
+
// =============================================================================
|
|
97
|
+
/**
|
|
98
|
+
* Load rev.yaml config from directory
|
|
99
|
+
* @param directory - Project directory path
|
|
100
|
+
* @returns Merged config with defaults
|
|
101
|
+
* @throws {TypeError} If directory is not a string
|
|
102
|
+
* @throws {Error} If rev.yaml exists but cannot be parsed
|
|
103
|
+
*/
|
|
104
|
+
export function loadConfig(directory) {
|
|
105
|
+
if (typeof directory !== 'string') {
|
|
106
|
+
throw new TypeError(`directory must be a string, got ${typeof directory}`);
|
|
107
|
+
}
|
|
108
|
+
const configPath = path.join(directory, 'rev.yaml');
|
|
109
|
+
if (!fs.existsSync(configPath)) {
|
|
110
|
+
return { ...DEFAULT_CONFIG, _configPath: null };
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
114
|
+
const userConfig = YAML.parse(content) || {};
|
|
115
|
+
// Deep merge with defaults
|
|
116
|
+
const config = {
|
|
117
|
+
...DEFAULT_CONFIG,
|
|
118
|
+
...userConfig,
|
|
119
|
+
crossref: { ...DEFAULT_CONFIG.crossref, ...userConfig.crossref },
|
|
120
|
+
pdf: { ...DEFAULT_CONFIG.pdf, ...userConfig.pdf },
|
|
121
|
+
docx: { ...DEFAULT_CONFIG.docx, ...userConfig.docx },
|
|
122
|
+
tex: { ...DEFAULT_CONFIG.tex, ...userConfig.tex },
|
|
123
|
+
beamer: { ...DEFAULT_CONFIG.beamer, ...userConfig.beamer },
|
|
124
|
+
pptx: { ...DEFAULT_CONFIG.pptx, ...userConfig.pptx },
|
|
125
|
+
tables: { ...DEFAULT_CONFIG.tables, ...userConfig.tables },
|
|
126
|
+
postprocess: { ...DEFAULT_CONFIG.postprocess, ...userConfig.postprocess },
|
|
127
|
+
_configPath: configPath,
|
|
128
|
+
};
|
|
129
|
+
return config;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const error = err;
|
|
133
|
+
throw new Error(`Failed to parse rev.yaml: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Find section files in directory
|
|
138
|
+
* @param directory - Project directory path
|
|
139
|
+
* @param configSections - Sections from rev.yaml (optional)
|
|
140
|
+
* @returns Ordered list of section file names
|
|
141
|
+
* @throws {TypeError} If directory is not a string
|
|
142
|
+
*/
|
|
143
|
+
export function findSections(directory, configSections = []) {
|
|
144
|
+
if (typeof directory !== 'string') {
|
|
145
|
+
throw new TypeError(`directory must be a string, got ${typeof directory}`);
|
|
146
|
+
}
|
|
147
|
+
// If sections specified in config, use that order
|
|
148
|
+
if (configSections.length > 0) {
|
|
149
|
+
const sections = [];
|
|
150
|
+
for (const section of configSections) {
|
|
151
|
+
const filePath = path.join(directory, section);
|
|
152
|
+
if (fs.existsSync(filePath)) {
|
|
153
|
+
sections.push(section);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.warn(`Warning: Section file not found: ${section}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return sections;
|
|
160
|
+
}
|
|
161
|
+
// Try sections.yaml
|
|
162
|
+
const sectionsYamlPath = path.join(directory, 'sections.yaml');
|
|
163
|
+
if (fs.existsSync(sectionsYamlPath)) {
|
|
164
|
+
try {
|
|
165
|
+
const sectionsConfig = YAML.parse(fs.readFileSync(sectionsYamlPath, 'utf-8'));
|
|
166
|
+
if (sectionsConfig.sections) {
|
|
167
|
+
return Object.entries(sectionsConfig.sections)
|
|
168
|
+
.sort((a, b) => (a[1].order ?? 999) - (b[1].order ?? 999))
|
|
169
|
+
.map(([file]) => file)
|
|
170
|
+
.filter((f) => fs.existsSync(path.join(directory, f)));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (process.env.DEBUG) {
|
|
175
|
+
const error = e;
|
|
176
|
+
console.warn('build: YAML parse error in sections.yaml:', error.message);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Default: find all .md files except special ones
|
|
181
|
+
const exclude = ['paper.md', 'readme.md', 'claude.md'];
|
|
182
|
+
const files = fs.readdirSync(directory).filter((f) => {
|
|
183
|
+
if (!f.endsWith('.md'))
|
|
184
|
+
return false;
|
|
185
|
+
if (exclude.includes(f.toLowerCase()))
|
|
186
|
+
return false;
|
|
187
|
+
return true;
|
|
188
|
+
});
|
|
189
|
+
// Sort alphabetically as fallback
|
|
190
|
+
return files.sort();
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Combine section files into paper.md
|
|
194
|
+
*/
|
|
195
|
+
export function combineSections(directory, config, options = {}) {
|
|
196
|
+
const sections = findSections(directory, config.sections);
|
|
197
|
+
if (sections.length === 0) {
|
|
198
|
+
throw new Error('No section files found. Create .md files or specify sections in rev.yaml');
|
|
199
|
+
}
|
|
200
|
+
const parts = [];
|
|
201
|
+
// Add YAML frontmatter
|
|
202
|
+
const frontmatter = buildFrontmatter(config);
|
|
203
|
+
parts.push('---');
|
|
204
|
+
parts.push(YAML.stringify(frontmatter).trim());
|
|
205
|
+
parts.push('---');
|
|
206
|
+
parts.push('');
|
|
207
|
+
// Read all section contents for variable processing
|
|
208
|
+
const sectionContents = [];
|
|
209
|
+
// Check if we need to auto-inject references before supplementary
|
|
210
|
+
// Pandoc places refs at the end by default, which breaks when supplementary follows
|
|
211
|
+
const hasRefsSection = sections.some(s => s.toLowerCase().includes('reference') || s.toLowerCase().includes('refs'));
|
|
212
|
+
const suppIndex = sections.findIndex(s => s.toLowerCase().includes('supp') || s.toLowerCase().includes('appendix'));
|
|
213
|
+
const hasBibliography = config.bibliography && fs.existsSync(path.join(directory, config.bibliography));
|
|
214
|
+
// Track if we find an explicit refs div in any section
|
|
215
|
+
let hasExplicitRefsDiv = false;
|
|
216
|
+
// Combine sections
|
|
217
|
+
for (let i = 0; i < sections.length; i++) {
|
|
218
|
+
const section = sections[i];
|
|
219
|
+
if (!section)
|
|
220
|
+
continue;
|
|
221
|
+
const filePath = path.join(directory, section);
|
|
222
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
223
|
+
// Remove any existing frontmatter from section files
|
|
224
|
+
content = stripFrontmatter(content);
|
|
225
|
+
sectionContents.push(content);
|
|
226
|
+
// Check if this section has an explicit refs div
|
|
227
|
+
if (content.includes('::: {#refs}') || content.includes('::: {#refs}')) {
|
|
228
|
+
hasExplicitRefsDiv = true;
|
|
229
|
+
}
|
|
230
|
+
// Auto-inject references before supplementary if needed
|
|
231
|
+
if (i === suppIndex && hasBibliography && !hasRefsSection && !hasExplicitRefsDiv) {
|
|
232
|
+
parts.push('# References\n');
|
|
233
|
+
parts.push('::: {#refs}');
|
|
234
|
+
parts.push(':::');
|
|
235
|
+
parts.push('');
|
|
236
|
+
parts.push('');
|
|
237
|
+
options._refsAutoInjected = true;
|
|
238
|
+
}
|
|
239
|
+
parts.push(content.trim());
|
|
240
|
+
parts.push('');
|
|
241
|
+
parts.push(''); // Double newline between sections
|
|
242
|
+
}
|
|
243
|
+
let paperContent = parts.join('\n');
|
|
244
|
+
// Process template variables if any exist
|
|
245
|
+
if (hasVariables(paperContent)) {
|
|
246
|
+
paperContent = processVariables(paperContent, config, { sectionContents });
|
|
247
|
+
}
|
|
248
|
+
// Resolve forward references (refs that appear before their anchor definition)
|
|
249
|
+
// This fixes pandoc-crossref limitation with multi-file documents
|
|
250
|
+
if (hasPandocCrossref()) {
|
|
251
|
+
const registry = buildRegistry(directory, sections);
|
|
252
|
+
const { text, resolved } = resolveForwardRefs(paperContent, registry);
|
|
253
|
+
if (resolved.length > 0) {
|
|
254
|
+
paperContent = text;
|
|
255
|
+
// Store resolved count for optional reporting
|
|
256
|
+
options._forwardRefsResolved = resolved.length;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const paperPath = path.join(directory, 'paper.md');
|
|
260
|
+
fs.writeFileSync(paperPath, paperContent, 'utf-8');
|
|
261
|
+
return paperPath;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Build YAML frontmatter from config
|
|
265
|
+
*/
|
|
266
|
+
function buildFrontmatter(config) {
|
|
267
|
+
const fm = {};
|
|
268
|
+
if (config.title)
|
|
269
|
+
fm.title = config.title;
|
|
270
|
+
if (config.authors && config.authors.length > 0) {
|
|
271
|
+
fm.author = config.authors;
|
|
272
|
+
}
|
|
273
|
+
if (config.bibliography) {
|
|
274
|
+
fm.bibliography = config.bibliography;
|
|
275
|
+
}
|
|
276
|
+
if (config.csl) {
|
|
277
|
+
fm.csl = config.csl;
|
|
278
|
+
}
|
|
279
|
+
return fm;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Strip YAML frontmatter from content
|
|
283
|
+
*/
|
|
284
|
+
function stripFrontmatter(content) {
|
|
285
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
286
|
+
if (match) {
|
|
287
|
+
return content.slice(match[0].length);
|
|
288
|
+
}
|
|
289
|
+
return content;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Process markdown tables to apply nowrap formatting to specified columns.
|
|
293
|
+
* Converts distribution notation (Normal, Student-t, Gamma) to LaTeX math.
|
|
294
|
+
* @param content - Markdown content
|
|
295
|
+
* @param tablesConfig - tables config from rev.yaml
|
|
296
|
+
* @param format - output format (pdf, docx, etc.)
|
|
297
|
+
* @returns processed content
|
|
298
|
+
*/
|
|
299
|
+
export function processTablesForFormat(content, tablesConfig, format) {
|
|
300
|
+
// Only process for PDF/TeX output
|
|
301
|
+
if (format !== 'pdf' && format !== 'tex') {
|
|
302
|
+
return content;
|
|
303
|
+
}
|
|
304
|
+
// Check if we have nowrap columns configured
|
|
305
|
+
if (!tablesConfig?.nowrap?.length) {
|
|
306
|
+
return content;
|
|
307
|
+
}
|
|
308
|
+
const nowrapPatterns = tablesConfig.nowrap.map((p) => p.toLowerCase());
|
|
309
|
+
// Match pipe tables: header row, separator row, body rows
|
|
310
|
+
// Header: | Col1 | Col2 | Col3 |
|
|
311
|
+
// Separator: |:-----|:-----|:-----|
|
|
312
|
+
// Body: | val1 | val2 | val3 |
|
|
313
|
+
const tableRegex = /^(\|[^\n]+\|\r?\n\|[-:| ]+\|\r?\n)((?:\|[^\n]+\|\r?\n?)+)/gm;
|
|
314
|
+
return content.replace(tableRegex, (match, headerAndSep, body) => {
|
|
315
|
+
// Split header from separator
|
|
316
|
+
const lines = headerAndSep.split(/\r?\n/);
|
|
317
|
+
const headerLine = lines[0] ?? '';
|
|
318
|
+
// Parse header cells to find nowrap column indices
|
|
319
|
+
const headerCells = headerLine
|
|
320
|
+
.split('|')
|
|
321
|
+
.slice(1, -1)
|
|
322
|
+
.map((c) => c.trim().toLowerCase());
|
|
323
|
+
const nowrapCols = [];
|
|
324
|
+
headerCells.forEach((cell, i) => {
|
|
325
|
+
if (nowrapPatterns.some((p) => cell.includes(p))) {
|
|
326
|
+
nowrapCols.push(i);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
// If no nowrap columns found in this table, return unchanged
|
|
330
|
+
if (nowrapCols.length === 0) {
|
|
331
|
+
return match;
|
|
332
|
+
}
|
|
333
|
+
// Process body rows
|
|
334
|
+
const bodyLines = body.split(/\r?\n/).filter((l) => l.trim());
|
|
335
|
+
const processedBody = bodyLines
|
|
336
|
+
.map((row) => {
|
|
337
|
+
// Split row into cells, keeping the pipe structure
|
|
338
|
+
const cells = row.split('|');
|
|
339
|
+
// cells[0] is empty (before first |), cells[last] is empty (after last |)
|
|
340
|
+
nowrapCols.forEach((colIdx) => {
|
|
341
|
+
const cellIdx = colIdx + 1; // Account for empty first element
|
|
342
|
+
if (cells[cellIdx] !== undefined) {
|
|
343
|
+
const cellContent = cells[cellIdx].trim();
|
|
344
|
+
// Skip if empty, already math, or already has LaTeX commands
|
|
345
|
+
if (!cellContent || cellContent.startsWith('$') || cellContent.startsWith('\\')) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Convert distribution notation to LaTeX math
|
|
349
|
+
// Order matters: compound names (Half-Normal) must come before simple names (Normal)
|
|
350
|
+
let processed = cellContent;
|
|
351
|
+
// Half-Normal(x) → $\text{Half-Normal}(x)$ (must come before Normal)
|
|
352
|
+
processed = processed.replace(/Half-Normal\(([^)]+)\)/g, '$\\text{Half-Normal}($1)$');
|
|
353
|
+
// Normal(x, y) → $\mathcal{N}(x, y)$
|
|
354
|
+
processed = processed.replace(/Normal\(([^)]+)\)/g, '$\\mathcal{N}($1)$');
|
|
355
|
+
// Student-t(df, loc, scale) → $t_{df}(loc, scale)$
|
|
356
|
+
processed = processed.replace(/Student-t\((\d+),\s*([^)]+)\)/g, '$t_{$1}($2)$');
|
|
357
|
+
// Gamma(a, b) → $\text{Gamma}(a, b)$
|
|
358
|
+
processed = processed.replace(/Gamma\(([^)]+)\)/g, '$\\text{Gamma}($1)$');
|
|
359
|
+
// Exponential(x) → $\text{Exp}(x)$
|
|
360
|
+
processed = processed.replace(/Exponential\(([^)]+)\)/g, '$\\text{Exp}($1)$');
|
|
361
|
+
// Update cell with padding
|
|
362
|
+
cells[cellIdx] = ` ${processed} `;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
return cells.join('|');
|
|
366
|
+
})
|
|
367
|
+
.join('\n');
|
|
368
|
+
return headerAndSep + processedBody + '\n';
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Prepare paper.md for specific output format
|
|
373
|
+
*/
|
|
374
|
+
export function prepareForFormat(paperPath, format, config, options = {}) {
|
|
375
|
+
const directory = path.dirname(paperPath);
|
|
376
|
+
let content = fs.readFileSync(paperPath, 'utf-8');
|
|
377
|
+
// Build crossref registry for reference conversion
|
|
378
|
+
// Pass sections from config to ensure correct file ordering
|
|
379
|
+
const registry = buildRegistry(directory, config.sections);
|
|
380
|
+
if (format === 'pdf' || format === 'tex') {
|
|
381
|
+
// Strip all annotations for clean output
|
|
382
|
+
content = stripAnnotations(content);
|
|
383
|
+
// Process tables for nowrap columns (convert Normal() → $\mathcal{N}()$ etc.)
|
|
384
|
+
content = processTablesForFormat(content, config.tables, format);
|
|
385
|
+
}
|
|
386
|
+
else if (format === 'docx') {
|
|
387
|
+
// Strip track changes, optionally keep comments
|
|
388
|
+
content = stripAnnotations(content, { keepComments: config.docx.keepComments });
|
|
389
|
+
// Convert @fig:label to "Figure 1" for Word readers
|
|
390
|
+
content = convertDynamicRefsToDisplay(content, registry);
|
|
391
|
+
}
|
|
392
|
+
else if (format === 'beamer' || format === 'pptx') {
|
|
393
|
+
// Strip annotations for slide output
|
|
394
|
+
content = stripAnnotations(content);
|
|
395
|
+
// Process slide syntax (::: step, ::: notes)
|
|
396
|
+
if (hasSlideSyntax(content)) {
|
|
397
|
+
content = processSlideMarkdown(content, format);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Write to temporary file
|
|
401
|
+
const preparedPath = path.join(directory, `.paper-${format}.md`);
|
|
402
|
+
fs.writeFileSync(preparedPath, content, 'utf-8');
|
|
403
|
+
return preparedPath;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Convert @fig:label references to display format (Figure 1)
|
|
407
|
+
*/
|
|
408
|
+
function convertDynamicRefsToDisplay(text, registry) {
|
|
409
|
+
const refs = detectDynamicRefs(text);
|
|
410
|
+
// Process in reverse order to preserve positions
|
|
411
|
+
let result = text;
|
|
412
|
+
for (let i = refs.length - 1; i >= 0; i--) {
|
|
413
|
+
const ref = refs[i];
|
|
414
|
+
if (!ref)
|
|
415
|
+
continue;
|
|
416
|
+
const display = labelToDisplay(ref.type, ref.label, registry);
|
|
417
|
+
if (display) {
|
|
418
|
+
result = result.slice(0, ref.position) + display + result.slice(ref.position + ref.match.length);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Build pandoc arguments for format
|
|
425
|
+
*/
|
|
426
|
+
export function buildPandocArgs(format, config, outputPath) {
|
|
427
|
+
const args = [];
|
|
428
|
+
// Output format
|
|
429
|
+
if (format === 'tex') {
|
|
430
|
+
args.push('-t', 'latex');
|
|
431
|
+
if (config.tex.standalone) {
|
|
432
|
+
args.push('-s');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else if (format === 'pdf') {
|
|
436
|
+
args.push('-t', 'pdf');
|
|
437
|
+
}
|
|
438
|
+
else if (format === 'docx') {
|
|
439
|
+
args.push('-t', 'docx');
|
|
440
|
+
}
|
|
441
|
+
else if (format === 'beamer') {
|
|
442
|
+
args.push('-t', 'beamer');
|
|
443
|
+
}
|
|
444
|
+
else if (format === 'pptx') {
|
|
445
|
+
args.push('-t', 'pptx');
|
|
446
|
+
}
|
|
447
|
+
// Output file (use basename since we set cwd to directory in runPandoc)
|
|
448
|
+
args.push('-o', path.basename(outputPath));
|
|
449
|
+
// Crossref filter (if available) - skip for slides
|
|
450
|
+
if (hasPandocCrossref() && format !== 'beamer' && format !== 'pptx') {
|
|
451
|
+
args.push('--filter', 'pandoc-crossref');
|
|
452
|
+
}
|
|
453
|
+
// Bibliography
|
|
454
|
+
if (config.bibliography) {
|
|
455
|
+
args.push('--citeproc');
|
|
456
|
+
}
|
|
457
|
+
// Format-specific options
|
|
458
|
+
if (format === 'pdf') {
|
|
459
|
+
if (config.pdf.template) {
|
|
460
|
+
args.push('--template', config.pdf.template);
|
|
461
|
+
}
|
|
462
|
+
args.push('-V', `documentclass=${config.pdf.documentclass}`);
|
|
463
|
+
args.push('-V', `fontsize=${config.pdf.fontsize}`);
|
|
464
|
+
args.push('-V', `geometry:${config.pdf.geometry}`);
|
|
465
|
+
if (config.pdf.linestretch !== 1) {
|
|
466
|
+
args.push('-V', `linestretch=${config.pdf.linestretch}`);
|
|
467
|
+
}
|
|
468
|
+
if (config.pdf.numbersections) {
|
|
469
|
+
args.push('--number-sections');
|
|
470
|
+
}
|
|
471
|
+
if (config.pdf.toc) {
|
|
472
|
+
args.push('--toc');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else if (format === 'docx') {
|
|
476
|
+
if (config.docx.reference) {
|
|
477
|
+
args.push('--reference-doc', config.docx.reference);
|
|
478
|
+
}
|
|
479
|
+
if (config.docx.toc) {
|
|
480
|
+
args.push('--toc');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
else if (format === 'beamer') {
|
|
484
|
+
// Beamer slide options
|
|
485
|
+
const beamer = config.beamer || {};
|
|
486
|
+
if (beamer.theme) {
|
|
487
|
+
args.push('-V', `theme=${beamer.theme}`);
|
|
488
|
+
}
|
|
489
|
+
if (beamer.colortheme) {
|
|
490
|
+
args.push('-V', `colortheme=${beamer.colortheme}`);
|
|
491
|
+
}
|
|
492
|
+
if (beamer.fonttheme) {
|
|
493
|
+
args.push('-V', `fonttheme=${beamer.fonttheme}`);
|
|
494
|
+
}
|
|
495
|
+
if (beamer.aspectratio) {
|
|
496
|
+
args.push('-V', `aspectratio=${beamer.aspectratio}`);
|
|
497
|
+
}
|
|
498
|
+
if (beamer.navigation) {
|
|
499
|
+
args.push('-V', `navigation=${beamer.navigation}`);
|
|
500
|
+
}
|
|
501
|
+
// Speaker notes - default to 'show' which creates presenter view PDF
|
|
502
|
+
// Options: 'show' (dual screen), 'only' (notes only), 'hide' (no notes), false (disabled)
|
|
503
|
+
const notesMode = beamer.notes !== undefined ? beamer.notes : 'show';
|
|
504
|
+
if (notesMode && notesMode !== 'hide') {
|
|
505
|
+
args.push('-V', `classoption=notes=${notesMode}`);
|
|
506
|
+
}
|
|
507
|
+
// Fit images within slide bounds (default: true)
|
|
508
|
+
if (beamer.fit_images !== false) {
|
|
509
|
+
const fitImagesHeader = `\\makeatletter
|
|
510
|
+
\\def\\maxwidth{\\ifdim\\Gin@nat@width>\\linewidth\\linewidth\\else\\Gin@nat@width\\fi}
|
|
511
|
+
\\def\\maxheight{\\ifdim\\Gin@nat@height>0.75\\textheight 0.75\\textheight\\else\\Gin@nat@height\\fi}
|
|
512
|
+
\\makeatother
|
|
513
|
+
\\setkeys{Gin}{width=\\maxwidth,height=\\maxheight,keepaspectratio}`;
|
|
514
|
+
args.push('-V', `header-includes=${fitImagesHeader}`);
|
|
515
|
+
}
|
|
516
|
+
// Slides need standalone
|
|
517
|
+
args.push('-s');
|
|
518
|
+
}
|
|
519
|
+
else if (format === 'pptx') {
|
|
520
|
+
// PowerPoint options - handled separately in preparePptxTemplate
|
|
521
|
+
// Reference doc is set by caller after template generation
|
|
522
|
+
}
|
|
523
|
+
return args;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Write crossref.yaml if needed
|
|
527
|
+
*/
|
|
528
|
+
function ensureCrossrefConfig(directory, config) {
|
|
529
|
+
const crossrefPath = path.join(directory, 'crossref.yaml');
|
|
530
|
+
if (!fs.existsSync(crossrefPath) && hasPandocCrossref()) {
|
|
531
|
+
fs.writeFileSync(crossrefPath, YAML.stringify(config.crossref), 'utf-8');
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Get install instructions for missing dependency
|
|
536
|
+
*/
|
|
537
|
+
function getInstallInstructions(tool) {
|
|
538
|
+
const instructions = {
|
|
539
|
+
pandoc: 'https://pandoc.org/installing.html',
|
|
540
|
+
latex: 'https://www.latex-project.org/get/',
|
|
541
|
+
};
|
|
542
|
+
return instructions[tool] || 'Check documentation';
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Run pandoc build
|
|
546
|
+
*/
|
|
547
|
+
export async function runPandoc(inputPath, format, config, options = {}) {
|
|
548
|
+
const directory = path.dirname(inputPath);
|
|
549
|
+
const baseName = config.title
|
|
550
|
+
? config.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50)
|
|
551
|
+
: 'paper';
|
|
552
|
+
// Map format to file extension
|
|
553
|
+
const extMap = {
|
|
554
|
+
tex: '.tex',
|
|
555
|
+
pdf: '.pdf',
|
|
556
|
+
docx: '.docx',
|
|
557
|
+
beamer: '.pdf', // beamer outputs PDF
|
|
558
|
+
pptx: '.pptx',
|
|
559
|
+
};
|
|
560
|
+
const ext = extMap[format] || '.pdf';
|
|
561
|
+
// For beamer, use -slides suffix to distinguish from regular PDF
|
|
562
|
+
const suffix = format === 'beamer' ? '-slides' : '';
|
|
563
|
+
// Allow custom output path via options
|
|
564
|
+
const outputPath = options.outputPath || path.join(directory, `${baseName}${suffix}${ext}`);
|
|
565
|
+
// Ensure crossref.yaml exists
|
|
566
|
+
ensureCrossrefConfig(directory, config);
|
|
567
|
+
const args = buildPandocArgs(format, config, outputPath);
|
|
568
|
+
// Handle PPTX reference template and themes
|
|
569
|
+
let pptxMediaDir = null;
|
|
570
|
+
if (format === 'pptx') {
|
|
571
|
+
const pptx = config.pptx || {};
|
|
572
|
+
// Determine media directory (default: pptx/media or slides/media)
|
|
573
|
+
let mediaDir = pptx.media;
|
|
574
|
+
if (!mediaDir) {
|
|
575
|
+
if (fs.existsSync(path.join(directory, 'pptx', 'media'))) {
|
|
576
|
+
mediaDir = path.join(directory, 'pptx', 'media');
|
|
577
|
+
}
|
|
578
|
+
else if (fs.existsSync(path.join(directory, 'slides', 'media'))) {
|
|
579
|
+
mediaDir = path.join(directory, 'slides', 'media');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
else if (!path.isAbsolute(mediaDir)) {
|
|
583
|
+
mediaDir = path.join(directory, mediaDir);
|
|
584
|
+
}
|
|
585
|
+
pptxMediaDir = mediaDir || null;
|
|
586
|
+
// Determine reference doc: custom reference overrides theme
|
|
587
|
+
let referenceDoc = null;
|
|
588
|
+
if (pptx.reference && fs.existsSync(path.join(directory, pptx.reference))) {
|
|
589
|
+
// Custom reference doc takes precedence
|
|
590
|
+
referenceDoc = path.join(directory, pptx.reference);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
// Use built-in theme (default: 'default')
|
|
594
|
+
const themeName = pptx.theme || 'default';
|
|
595
|
+
const themePath = getThemePath(themeName);
|
|
596
|
+
if (themePath && fs.existsSync(themePath)) {
|
|
597
|
+
referenceDoc = themePath;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (referenceDoc) {
|
|
601
|
+
args.push('--reference-doc', referenceDoc);
|
|
602
|
+
}
|
|
603
|
+
// Add color filter for PPTX (handles [text]{color=#RRGGBB} syntax)
|
|
604
|
+
const colorFilterPath = path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), 'pptx-color-filter.lua');
|
|
605
|
+
if (fs.existsSync(colorFilterPath)) {
|
|
606
|
+
args.push('--lua-filter', colorFilterPath);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Add crossref metadata file if exists (skip for slides - they don't use crossref)
|
|
610
|
+
if (format !== 'beamer' && format !== 'pptx') {
|
|
611
|
+
const crossrefPath = path.join(directory, 'crossref.yaml');
|
|
612
|
+
if (fs.existsSync(crossrefPath) && hasPandocCrossref()) {
|
|
613
|
+
// Use basename since we set cwd to directory
|
|
614
|
+
args.push('--metadata-file', 'crossref.yaml');
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Input file (use basename since we set cwd to directory)
|
|
618
|
+
args.push(path.basename(inputPath));
|
|
619
|
+
return new Promise((resolve) => {
|
|
620
|
+
const pandoc = spawn('pandoc', args, {
|
|
621
|
+
cwd: directory,
|
|
622
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
623
|
+
});
|
|
624
|
+
let stderr = '';
|
|
625
|
+
pandoc.stderr?.on('data', (data) => {
|
|
626
|
+
stderr += data.toString();
|
|
627
|
+
});
|
|
628
|
+
pandoc.on('close', async (code) => {
|
|
629
|
+
if (code === 0) {
|
|
630
|
+
// For PPTX, post-process to add slide numbers, buildup colors, and logos
|
|
631
|
+
if (format === 'pptx') {
|
|
632
|
+
try {
|
|
633
|
+
// Inject slide numbers into content slides only
|
|
634
|
+
await injectSlideNumbers(outputPath);
|
|
635
|
+
}
|
|
636
|
+
catch (e) {
|
|
637
|
+
// Slide number injection failed but output was created
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
// Apply colors (default text color, title color, buildup greying)
|
|
641
|
+
const pptxConfig = config.pptx || {};
|
|
642
|
+
const colorsConfig = pptxConfig.colors || {};
|
|
643
|
+
const buildupConfig = pptxConfig.buildup || {};
|
|
644
|
+
// Merge colors and buildup config for applyBuildupColors
|
|
645
|
+
const colorConfig = {
|
|
646
|
+
default: colorsConfig.default,
|
|
647
|
+
title: colorsConfig.title,
|
|
648
|
+
grey: buildupConfig.grey,
|
|
649
|
+
accent: buildupConfig.accent,
|
|
650
|
+
enabled: buildupConfig.enabled
|
|
651
|
+
};
|
|
652
|
+
await applyBuildupColors(outputPath, colorConfig);
|
|
653
|
+
}
|
|
654
|
+
catch (e) {
|
|
655
|
+
// Color application failed but output was created
|
|
656
|
+
}
|
|
657
|
+
// Inject logos into cover slide (if media dir configured)
|
|
658
|
+
if (pptxMediaDir) {
|
|
659
|
+
try {
|
|
660
|
+
await injectMediaIntoPptx(outputPath, pptxMediaDir);
|
|
661
|
+
}
|
|
662
|
+
catch (e) {
|
|
663
|
+
// Logo injection failed but output was created
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// Run user postprocess scripts
|
|
668
|
+
const postResult = await runPostprocess(outputPath, format, config, options);
|
|
669
|
+
if (!postResult.success && options.verbose) {
|
|
670
|
+
console.error(`Postprocess warning: ${postResult.error}`);
|
|
671
|
+
}
|
|
672
|
+
resolve({ outputPath, success: true });
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
resolve({ outputPath, success: false, error: stderr || `Exit code ${code}` });
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
pandoc.on('error', (err) => {
|
|
679
|
+
resolve({ outputPath, success: false, error: err.message });
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Full build pipeline
|
|
685
|
+
*/
|
|
686
|
+
export async function build(directory, formats = ['pdf', 'docx'], options = {}) {
|
|
687
|
+
const warnings = [];
|
|
688
|
+
let forwardRefsResolved = 0;
|
|
689
|
+
// Check pandoc
|
|
690
|
+
if (!hasPandoc()) {
|
|
691
|
+
const instruction = getInstallInstructions('pandoc');
|
|
692
|
+
throw new Error(`Pandoc not found. Install with: ${instruction}\nOr run: rev doctor`);
|
|
693
|
+
}
|
|
694
|
+
// Check LaTeX if PDF is requested
|
|
695
|
+
if ((formats.includes('pdf') || formats.includes('all')) && !hasLatex()) {
|
|
696
|
+
warnings.push(`LaTeX not found - PDF generation may fail. Install with: ${getInstallInstructions('latex')}`);
|
|
697
|
+
}
|
|
698
|
+
// Check pandoc-crossref
|
|
699
|
+
if (!hasPandocCrossref()) {
|
|
700
|
+
warnings.push('pandoc-crossref not found - figure/table numbering will not work');
|
|
701
|
+
}
|
|
702
|
+
// Load config (use passed config if provided, otherwise load from file)
|
|
703
|
+
const config = options.config || loadConfig(directory);
|
|
704
|
+
// Combine sections → paper.md
|
|
705
|
+
const buildOptions = { ...options };
|
|
706
|
+
const paperPath = combineSections(directory, config, buildOptions);
|
|
707
|
+
forwardRefsResolved = buildOptions._forwardRefsResolved || 0;
|
|
708
|
+
const refsAutoInjected = buildOptions._refsAutoInjected || false;
|
|
709
|
+
// Expand 'all' to all formats
|
|
710
|
+
if (formats.includes('all')) {
|
|
711
|
+
formats = ['pdf', 'docx', 'tex'];
|
|
712
|
+
}
|
|
713
|
+
// Build and save image registry when DOCX is being built
|
|
714
|
+
// This allows import to restore proper image syntax from Word documents
|
|
715
|
+
if (formats.includes('docx')) {
|
|
716
|
+
const paperContent = fs.readFileSync(paperPath, 'utf-8');
|
|
717
|
+
const crossrefReg = buildRegistry(directory, config.sections);
|
|
718
|
+
const imageReg = buildImageRegistry(paperContent, crossrefReg);
|
|
719
|
+
if (imageReg.figures?.length > 0) {
|
|
720
|
+
writeImageRegistry(directory, imageReg);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const results = [];
|
|
724
|
+
for (const format of formats) {
|
|
725
|
+
// Prepare format-specific version
|
|
726
|
+
const preparedPath = prepareForFormat(paperPath, format, config, options);
|
|
727
|
+
// Run pandoc
|
|
728
|
+
const result = await runPandoc(preparedPath, format, config, options);
|
|
729
|
+
results.push({ format, ...result });
|
|
730
|
+
// Clean up temp file
|
|
731
|
+
try {
|
|
732
|
+
fs.unlinkSync(preparedPath);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// Ignore cleanup errors
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return { results, paperPath, warnings, forwardRefsResolved, refsAutoInjected };
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Get build status summary
|
|
742
|
+
*/
|
|
743
|
+
export function formatBuildResults(results) {
|
|
744
|
+
const lines = [];
|
|
745
|
+
for (const r of results) {
|
|
746
|
+
if (r.success) {
|
|
747
|
+
lines.push(` ${r.format.toUpperCase()}: ${path.basename(r.outputPath)}`);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
lines.push(` ${r.format.toUpperCase()}: FAILED - ${r.error}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return lines.join('\n');
|
|
754
|
+
}
|
|
755
|
+
//# sourceMappingURL=build.js.map
|