docrev 0.9.16 → 0.9.18
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/dist/lib/build.d.ts +112 -1
- package/dist/lib/build.d.ts.map +1 -1
- package/dist/lib/build.js +360 -25
- package/dist/lib/build.js.map +1 -1
- package/dist/lib/commands/build.d.ts.map +1 -1
- package/dist/lib/commands/build.js +25 -10
- package/dist/lib/commands/build.js.map +1 -1
- package/dist/lib/schema.d.ts.map +1 -1
- package/dist/lib/schema.js +37 -0
- package/dist/lib/schema.js.map +1 -1
- package/issues.md +180 -0
- package/lib/build.ts +420 -26
- package/lib/commands/build.ts +32 -10
- package/lib/schema.ts +37 -0
- package/package.json +1 -1
- package/skill/REFERENCE.md +66 -0
- package/skill/SKILL.md +25 -4
- package/.claude/settings.local.json +0 -9
package/dist/lib/build.d.ts
CHANGED
|
@@ -37,14 +37,26 @@ export interface PdfConfig {
|
|
|
37
37
|
sansfont?: string;
|
|
38
38
|
/** Monospace font (xelatex/lualatex only). */
|
|
39
39
|
monofont?: string;
|
|
40
|
+
/** Extra pandoc args appended for this format (after top-level pandocArgs). */
|
|
41
|
+
pandocArgs?: string[];
|
|
40
42
|
}
|
|
41
43
|
export interface DocxConfig {
|
|
42
44
|
reference?: string | null;
|
|
43
45
|
keepComments?: boolean;
|
|
46
|
+
affiliationNewline?: boolean;
|
|
44
47
|
toc?: boolean;
|
|
48
|
+
pandocArgs?: string[];
|
|
49
|
+
/**
|
|
50
|
+
* Auto-translate the common-shape raw `\begin{figure}...\end{figure}` block
|
|
51
|
+
* to portable `{#fig:label width=N%}` markdown so figures
|
|
52
|
+
* survive the docx build (pandoc otherwise drops raw LaTeX silently).
|
|
53
|
+
* Default true. Set false to opt out — blocks then warn and are left alone.
|
|
54
|
+
*/
|
|
55
|
+
translateRawFigures?: boolean;
|
|
45
56
|
}
|
|
46
57
|
export interface TexConfig {
|
|
47
58
|
standalone?: boolean;
|
|
59
|
+
pandocArgs?: string[];
|
|
48
60
|
}
|
|
49
61
|
export interface BeamerConfig {
|
|
50
62
|
theme?: string;
|
|
@@ -55,6 +67,7 @@ export interface BeamerConfig {
|
|
|
55
67
|
section?: boolean;
|
|
56
68
|
notes?: string | false;
|
|
57
69
|
fit_images?: boolean;
|
|
70
|
+
pandocArgs?: string[];
|
|
58
71
|
}
|
|
59
72
|
export interface PptxConfig {
|
|
60
73
|
theme?: string;
|
|
@@ -69,6 +82,7 @@ export interface PptxConfig {
|
|
|
69
82
|
accent?: string;
|
|
70
83
|
enabled?: boolean;
|
|
71
84
|
};
|
|
85
|
+
pandocArgs?: string[];
|
|
72
86
|
}
|
|
73
87
|
export interface TablesConfig {
|
|
74
88
|
nowrap?: string[];
|
|
@@ -103,6 +117,19 @@ export interface BuildConfig {
|
|
|
103
117
|
* behavior).
|
|
104
118
|
*/
|
|
105
119
|
outputDir?: string | null;
|
|
120
|
+
/**
|
|
121
|
+
* Per-format output filenames. Keys are format names (pdf/docx/tex/beamer/
|
|
122
|
+
* pptx); values are paths. Relative paths resolve under outputDir; absolute
|
|
123
|
+
* paths are honored as-is. Extension is added if missing. CLI `-o` wins
|
|
124
|
+
* over this map.
|
|
125
|
+
*/
|
|
126
|
+
output?: Record<string, string>;
|
|
127
|
+
/**
|
|
128
|
+
* Extra pandoc args applied to every format. Format-specific args
|
|
129
|
+
* (e.g. docx.pandocArgs) are appended *after* these, and CLI --pandoc-arg
|
|
130
|
+
* values are appended last.
|
|
131
|
+
*/
|
|
132
|
+
pandocArgs?: string[];
|
|
106
133
|
_configPath?: string | null;
|
|
107
134
|
}
|
|
108
135
|
export interface BuildResult {
|
|
@@ -114,8 +141,20 @@ export interface BuildResult {
|
|
|
114
141
|
interface BuildOptions {
|
|
115
142
|
verbose?: boolean;
|
|
116
143
|
config?: BuildConfig;
|
|
144
|
+
/**
|
|
145
|
+
* Internal: forces the exact output path. Used by dual-mode/temp builds that
|
|
146
|
+
* route to specific temp files. Bypasses the `output:` resolver.
|
|
147
|
+
*/
|
|
117
148
|
outputPath?: string;
|
|
149
|
+
/**
|
|
150
|
+
* CLI override (`-o, --output <path>`). Beats `config.output[format]` but
|
|
151
|
+
* loses to `options.outputPath`. Relative paths resolve under outputDir;
|
|
152
|
+
* absolute paths bypass outputDir.
|
|
153
|
+
*/
|
|
154
|
+
output?: string;
|
|
118
155
|
crossref?: boolean;
|
|
156
|
+
/** Extra pandoc args from CLI (--pandoc-arg). Appended after config args. */
|
|
157
|
+
pandocArgs?: string[];
|
|
119
158
|
_refsAutoInjected?: boolean;
|
|
120
159
|
_forwardRefsResolved?: number;
|
|
121
160
|
}
|
|
@@ -203,14 +242,86 @@ export declare function applyFormatTransforms(content: string, format: string, c
|
|
|
203
242
|
*/
|
|
204
243
|
export declare function prepareForFormat(paperPath: string, format: string, config: BuildConfig, _options?: BuildOptions): string;
|
|
205
244
|
/**
|
|
206
|
-
*
|
|
245
|
+
* A raw LaTeX `\begin{figure}...\end{figure}` block found in source markdown.
|
|
246
|
+
* `exotic` blocks contain features we don't auto-translate (multiple
|
|
247
|
+
* `\includegraphics`, `\subfloat`, `\rotatebox`, unrecognised width units);
|
|
248
|
+
* pandoc strips raw LaTeX silently in docx output, so users get warned about
|
|
249
|
+
* anything that won't be translated.
|
|
250
|
+
*/
|
|
251
|
+
export interface RawLatexFigure {
|
|
252
|
+
file?: string;
|
|
253
|
+
line: number;
|
|
254
|
+
block: string;
|
|
255
|
+
exotic: boolean;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Find raw LaTeX figure blocks containing `\includegraphics` in markdown.
|
|
259
|
+
* `file`, if given, is attached to each result. `line` is 1-based within the
|
|
260
|
+
* supplied content (the line where `\begin{figure}` sits).
|
|
261
|
+
*/
|
|
262
|
+
export declare function detectRawLatexFigures(content: string, file?: string): RawLatexFigure[];
|
|
263
|
+
/**
|
|
264
|
+
* Translate the 80% case: single `\includegraphics` figure with optional
|
|
265
|
+
* `\caption{...}` and `\label{...}`, wrapped in `\begin{figure}...\end{figure}`,
|
|
266
|
+
* to portable `{#fig:label width=N%}` markdown. Exotic blocks
|
|
267
|
+
* (see `isExoticFigureBlock`) are left untouched.
|
|
268
|
+
*/
|
|
269
|
+
export declare function translateRawLatexFigures(content: string): {
|
|
270
|
+
translated: string;
|
|
271
|
+
translatedCount: number;
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Walk section files and gather a warning for any raw LaTeX figure blocks that
|
|
275
|
+
* won't survive the docx build. Returns null when there's nothing to warn about.
|
|
276
|
+
*/
|
|
277
|
+
export declare function collectRawLatexFigureWarning(directory: string, config: BuildConfig): string | null;
|
|
278
|
+
/**
|
|
279
|
+
* Build pandoc arguments for format.
|
|
280
|
+
*
|
|
281
|
+
* Returns only the built-in args derived from config. Passthrough args
|
|
282
|
+
* (config.pandocArgs, config[format].pandocArgs, CLI --pandoc-arg) are
|
|
283
|
+
* appended later in runPandoc so they win against pptx/crossref defaults
|
|
284
|
+
* added there.
|
|
207
285
|
*/
|
|
208
286
|
export declare function buildPandocArgs(format: string, config: BuildConfig, outputPath: string): string[];
|
|
287
|
+
/**
|
|
288
|
+
* Collect passthrough pandoc args for a format in the canonical order:
|
|
289
|
+
* top-level config → format-specific config → CLI extras. Later wins for
|
|
290
|
+
* repeated flags.
|
|
291
|
+
*/
|
|
292
|
+
export declare function collectPandocPassthroughArgs(format: string, config: BuildConfig, extraArgs?: string[]): string[];
|
|
209
293
|
/**
|
|
210
294
|
* Resolve the absolute directory where final outputs should land.
|
|
211
295
|
* Honors config.outputDir; falls back to the project directory when null/empty.
|
|
212
296
|
*/
|
|
213
297
|
export declare function resolveOutputDir(directory: string, config: BuildConfig): string;
|
|
298
|
+
/** Get file extension for a format, defaulting to `.pdf`. */
|
|
299
|
+
export declare function getFormatExtension(format: string): string;
|
|
300
|
+
/**
|
|
301
|
+
* Slugify a title for use as a default output filename. Lowercases, replaces
|
|
302
|
+
* non-alphanumeric runs with `-`, and truncates at the last `-` boundary
|
|
303
|
+
* at-or-before MAX_TITLE_FILENAME_LENGTH so words stay whole (the old blind
|
|
304
|
+
* `.slice` cut mid-word).
|
|
305
|
+
*/
|
|
306
|
+
export declare function slugifyTitle(title: string): string;
|
|
307
|
+
/**
|
|
308
|
+
* Resolve the final output path for a build.
|
|
309
|
+
*
|
|
310
|
+
* Priority: `options.outputPath` (internal force) > `cliOverride` (-o flag) >
|
|
311
|
+
* `config.output[format]` > slugified title fallback.
|
|
312
|
+
*
|
|
313
|
+
* Relative paths from `cliOverride`/`config.output` resolve under outputDir;
|
|
314
|
+
* absolute paths bypass outputDir. The fallback path always lives under
|
|
315
|
+
* outputDir.
|
|
316
|
+
*
|
|
317
|
+
* @param suffix - Appended before the extension (e.g. "-changes", "-slides").
|
|
318
|
+
* Suppressed when user supplied an explicit name via CLI or
|
|
319
|
+
* config — they pick their own suffix.
|
|
320
|
+
*/
|
|
321
|
+
export declare function resolveOutputPath(directory: string, config: BuildConfig, format: string, options?: {
|
|
322
|
+
cliOverride?: string;
|
|
323
|
+
suffix?: string;
|
|
324
|
+
}): string;
|
|
214
325
|
/**
|
|
215
326
|
* Run pandoc build
|
|
216
327
|
*/
|
package/dist/lib/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../lib/build.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../lib/build.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/build.js
CHANGED
|
@@ -27,8 +27,13 @@ import { resolveCSL } from './csl.js';
|
|
|
27
27
|
// =============================================================================
|
|
28
28
|
/** Supported output formats */
|
|
29
29
|
const SUPPORTED_FORMATS = ['pdf', 'docx', 'tex', 'beamer', 'pptx'];
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Maximum length for slugified-title output filenames. Only used when no
|
|
32
|
+
* explicit `output:` filename is configured. Long titles are truncated at the
|
|
33
|
+
* last `-` boundary at-or-before this length so words stay intact (the old
|
|
34
|
+
* blind `.slice(0, 50)` cut mid-word).
|
|
35
|
+
*/
|
|
36
|
+
const MAX_TITLE_FILENAME_LENGTH = 80;
|
|
32
37
|
/**
|
|
33
38
|
* Default rev.yaml configuration
|
|
34
39
|
*/
|
|
@@ -58,8 +63,10 @@ export const DEFAULT_CONFIG = {
|
|
|
58
63
|
},
|
|
59
64
|
docx: {
|
|
60
65
|
reference: null,
|
|
61
|
-
keepComments:
|
|
66
|
+
keepComments: false,
|
|
67
|
+
affiliationNewline: true,
|
|
62
68
|
toc: false,
|
|
69
|
+
translateRawFigures: true,
|
|
63
70
|
},
|
|
64
71
|
tex: {
|
|
65
72
|
standalone: true,
|
|
@@ -157,6 +164,21 @@ export function mergeJournalFormatting(config, formatting, directory) {
|
|
|
157
164
|
}
|
|
158
165
|
return merged;
|
|
159
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* In-place: copy `pandoc-args` → `pandocArgs` on an object (if not already set).
|
|
169
|
+
* Idempotent. Coerces a single string into a one-element array.
|
|
170
|
+
*/
|
|
171
|
+
function normalizePandocArgsKey(obj) {
|
|
172
|
+
if (!obj || typeof obj !== 'object')
|
|
173
|
+
return;
|
|
174
|
+
const hy = obj['pandoc-args'];
|
|
175
|
+
if (hy === undefined)
|
|
176
|
+
return;
|
|
177
|
+
if (obj.pandocArgs === undefined) {
|
|
178
|
+
obj.pandocArgs = Array.isArray(hy) ? hy : [hy];
|
|
179
|
+
}
|
|
180
|
+
delete obj['pandoc-args'];
|
|
181
|
+
}
|
|
160
182
|
/**
|
|
161
183
|
* Load rev.yaml config from directory
|
|
162
184
|
* @param directory - Project directory path
|
|
@@ -175,6 +197,15 @@ export function loadConfig(directory) {
|
|
|
175
197
|
try {
|
|
176
198
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
177
199
|
const userConfig = YAML.parse(content) || {};
|
|
200
|
+
// Accept hyphenated `pandoc-args` (the form pandoc itself uses) in addition
|
|
201
|
+
// to camelCase `pandocArgs`. Hyphenated is what we document; camelCase is
|
|
202
|
+
// accepted for users who already prefer that convention.
|
|
203
|
+
normalizePandocArgsKey(userConfig);
|
|
204
|
+
for (const fmt of ['pdf', 'docx', 'tex', 'beamer', 'pptx']) {
|
|
205
|
+
if (userConfig[fmt] && typeof userConfig[fmt] === 'object') {
|
|
206
|
+
normalizePandocArgsKey(userConfig[fmt]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
178
209
|
// Deep merge with defaults
|
|
179
210
|
let config = {
|
|
180
211
|
...DEFAULT_CONFIG,
|
|
@@ -451,12 +482,16 @@ function generateMarkdownAuthorBlock(config) {
|
|
|
451
482
|
lines.push(authorParts.join(', '));
|
|
452
483
|
lines.push('');
|
|
453
484
|
// Affiliation lines: ^1^ Department of ...
|
|
454
|
-
|
|
485
|
+
const affiliationEntries = Object.entries(config.affiliations);
|
|
486
|
+
const useLineBreaks = config.docx.affiliationNewline !== false;
|
|
487
|
+
affiliationEntries.forEach(([key, text], idx) => {
|
|
455
488
|
const num = keyToNum.get(key);
|
|
456
489
|
if (num !== undefined) {
|
|
457
|
-
|
|
490
|
+
const isLast = idx === affiliationEntries.length - 1;
|
|
491
|
+
const suffix = useLineBreaks && !isLast ? '\\' : '';
|
|
492
|
+
lines.push(`^${num}^ ${text}${suffix}`);
|
|
458
493
|
}
|
|
459
|
-
}
|
|
494
|
+
});
|
|
460
495
|
// Corresponding author footnote
|
|
461
496
|
const corresponding = config.authors.find(a => typeof a !== 'string' && a.corresponding);
|
|
462
497
|
if (corresponding?.email) {
|
|
@@ -571,6 +606,13 @@ export function applyFormatTransforms(content, format, config, registry) {
|
|
|
571
606
|
}
|
|
572
607
|
else if (format === 'docx') {
|
|
573
608
|
content = convertDynamicRefsToDisplay(content, registry);
|
|
609
|
+
// Pandoc strips raw LaTeX in docx output. Translate the common
|
|
610
|
+
// `\begin{figure}...\end{figure}` shape to portable markdown so figures
|
|
611
|
+
// actually appear; exotic blocks are left alone (warned about in build()).
|
|
612
|
+
if (config.docx?.translateRawFigures !== false) {
|
|
613
|
+
const { translated } = translateRawLatexFigures(content);
|
|
614
|
+
content = translated;
|
|
615
|
+
}
|
|
574
616
|
if (hasNumberedAffiliations(config)) {
|
|
575
617
|
const mdBlock = generateMarkdownAuthorBlock(config);
|
|
576
618
|
content = content.replace(/^(---\r?\n[\s\S]*?---\r?\n)/, `$1\n${mdBlock}\n`);
|
|
@@ -624,8 +666,202 @@ function convertDynamicRefsToDisplay(text, registry) {
|
|
|
624
666
|
}
|
|
625
667
|
return result;
|
|
626
668
|
}
|
|
669
|
+
/** Match `\begin{figure}` / `\begin{figure*}` … `\end{figure}` blocks. */
|
|
670
|
+
function makeRawFigureRegex() {
|
|
671
|
+
return /\\begin\{figure\*?\}(?:\[[^\]]*\])?[\s\S]*?\\end\{figure\*?\}/g;
|
|
672
|
+
}
|
|
627
673
|
/**
|
|
628
|
-
*
|
|
674
|
+
* Convert a LaTeX width spec to a markdown image attribute value.
|
|
675
|
+
* - `0.8\textwidth` → `80%`
|
|
676
|
+
* - `\linewidth` → `100%`
|
|
677
|
+
* - `8cm`, `2in`, `12pt` → kept verbatim
|
|
678
|
+
* Returns null for anything we don't translate (block stays "exotic").
|
|
679
|
+
*/
|
|
680
|
+
function convertLatexWidth(raw) {
|
|
681
|
+
const trimmed = raw.trim();
|
|
682
|
+
// Coefficient × relative length
|
|
683
|
+
const rel = trimmed.match(/^([\d.]+)\s*\\(textwidth|linewidth|columnwidth)$/);
|
|
684
|
+
if (rel) {
|
|
685
|
+
const pct = Math.round(parseFloat(rel[1]) * 100);
|
|
686
|
+
if (!isFinite(pct) || pct <= 0)
|
|
687
|
+
return null;
|
|
688
|
+
return `${pct}%`;
|
|
689
|
+
}
|
|
690
|
+
// Bare relative length
|
|
691
|
+
if (/^\\(textwidth|linewidth|columnwidth)$/.test(trimmed))
|
|
692
|
+
return '100%';
|
|
693
|
+
// Absolute units
|
|
694
|
+
if (/^[\d.]+\s*(cm|mm|in|pt|px|em|ex)$/.test(trimmed))
|
|
695
|
+
return trimmed.replace(/\s+/g, '');
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
/** Extract a balanced `{...}` argument that follows `command` in `text`. */
|
|
699
|
+
function extractBracedArg(text, command) {
|
|
700
|
+
const idx = text.indexOf(command);
|
|
701
|
+
if (idx === -1)
|
|
702
|
+
return null;
|
|
703
|
+
let i = idx + command.length;
|
|
704
|
+
while (i < text.length && /\s/.test(text[i]))
|
|
705
|
+
i++;
|
|
706
|
+
if (text[i] !== '{')
|
|
707
|
+
return null;
|
|
708
|
+
i++;
|
|
709
|
+
const start = i;
|
|
710
|
+
let depth = 1;
|
|
711
|
+
while (i < text.length) {
|
|
712
|
+
const ch = text[i];
|
|
713
|
+
if (ch === '\\' && i + 1 < text.length) {
|
|
714
|
+
i += 2;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (ch === '{')
|
|
718
|
+
depth++;
|
|
719
|
+
else if (ch === '}') {
|
|
720
|
+
depth--;
|
|
721
|
+
if (depth === 0)
|
|
722
|
+
return text.slice(start, i);
|
|
723
|
+
}
|
|
724
|
+
i++;
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
/** True if a `\begin{figure}` block contains features we don't auto-translate. */
|
|
729
|
+
function isExoticFigureBlock(block) {
|
|
730
|
+
if (/\\subfloat\b/.test(block))
|
|
731
|
+
return true;
|
|
732
|
+
if (/\\rotatebox\b/.test(block))
|
|
733
|
+
return true;
|
|
734
|
+
const includes = (block.match(/\\includegraphics\b/g) || []).length;
|
|
735
|
+
if (includes !== 1)
|
|
736
|
+
return true;
|
|
737
|
+
const m = block.match(/\\includegraphics\s*(?:\[([^\]]*)\])?\s*\{([^}]+)\}/);
|
|
738
|
+
if (!m)
|
|
739
|
+
return true;
|
|
740
|
+
const opts = m[1] || '';
|
|
741
|
+
const widthMatch = opts.match(/(?:^|,)\s*width\s*=\s*([^,]+)/);
|
|
742
|
+
if (widthMatch && !convertLatexWidth(widthMatch[1]))
|
|
743
|
+
return true;
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Find raw LaTeX figure blocks containing `\includegraphics` in markdown.
|
|
748
|
+
* `file`, if given, is attached to each result. `line` is 1-based within the
|
|
749
|
+
* supplied content (the line where `\begin{figure}` sits).
|
|
750
|
+
*/
|
|
751
|
+
export function detectRawLatexFigures(content, file) {
|
|
752
|
+
const figures = [];
|
|
753
|
+
const re = makeRawFigureRegex();
|
|
754
|
+
let m;
|
|
755
|
+
while ((m = re.exec(content)) !== null) {
|
|
756
|
+
const block = m[0];
|
|
757
|
+
if (!block.includes('\\includegraphics'))
|
|
758
|
+
continue;
|
|
759
|
+
const line = content.slice(0, m.index).split(/\r?\n/).length;
|
|
760
|
+
figures.push({ file, line, block, exotic: isExoticFigureBlock(block) });
|
|
761
|
+
}
|
|
762
|
+
return figures;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Translate the 80% case: single `\includegraphics` figure with optional
|
|
766
|
+
* `\caption{...}` and `\label{...}`, wrapped in `\begin{figure}...\end{figure}`,
|
|
767
|
+
* to portable `{#fig:label width=N%}` markdown. Exotic blocks
|
|
768
|
+
* (see `isExoticFigureBlock`) are left untouched.
|
|
769
|
+
*/
|
|
770
|
+
export function translateRawLatexFigures(content) {
|
|
771
|
+
let translatedCount = 0;
|
|
772
|
+
const re = makeRawFigureRegex();
|
|
773
|
+
const translated = content.replace(re, (block) => {
|
|
774
|
+
if (!block.includes('\\includegraphics'))
|
|
775
|
+
return block;
|
|
776
|
+
if (isExoticFigureBlock(block))
|
|
777
|
+
return block;
|
|
778
|
+
const inc = block.match(/\\includegraphics\s*(?:\[([^\]]*)\])?\s*\{([^}]+)\}/);
|
|
779
|
+
if (!inc)
|
|
780
|
+
return block;
|
|
781
|
+
const optsStr = inc[1] || '';
|
|
782
|
+
const imgPath = inc[2].trim();
|
|
783
|
+
let width;
|
|
784
|
+
const widthMatch = optsStr.match(/(?:^|,)\s*width\s*=\s*([^,]+)/);
|
|
785
|
+
if (widthMatch) {
|
|
786
|
+
const w = convertLatexWidth(widthMatch[1]);
|
|
787
|
+
if (!w)
|
|
788
|
+
return block; // already filtered by isExoticFigureBlock, defensive
|
|
789
|
+
width = w;
|
|
790
|
+
}
|
|
791
|
+
const caption = (extractBracedArg(block, '\\caption') ?? '').trim();
|
|
792
|
+
const labelRaw = extractBracedArg(block, '\\label');
|
|
793
|
+
const attrs = [];
|
|
794
|
+
if (labelRaw) {
|
|
795
|
+
const label = labelRaw.trim();
|
|
796
|
+
const labelWithPrefix = /^[a-z]+:/i.test(label) ? label : `fig:${label}`;
|
|
797
|
+
attrs.push(`#${labelWithPrefix}`);
|
|
798
|
+
}
|
|
799
|
+
if (width)
|
|
800
|
+
attrs.push(`width=${width}`);
|
|
801
|
+
translatedCount++;
|
|
802
|
+
const attrStr = attrs.length > 0 ? ` {${attrs.join(' ')}}` : '';
|
|
803
|
+
return `${attrStr}`;
|
|
804
|
+
});
|
|
805
|
+
return { translated, translatedCount };
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Format the warning surfaced for raw LaTeX figure blocks that won't render
|
|
809
|
+
* in docx. `translateEnabled` reflects whether auto-translate ran (true = the
|
|
810
|
+
* listed blocks are exotic leftovers; false = no translation was attempted).
|
|
811
|
+
*/
|
|
812
|
+
function formatRawLatexFigureWarning(figs, translateEnabled) {
|
|
813
|
+
const reason = translateEnabled ? 'too complex to auto-translate' : 'translateRawFigures: false';
|
|
814
|
+
const lines = [
|
|
815
|
+
`${figs.length} raw LaTeX figure block(s) won't render in docx (${reason}).`,
|
|
816
|
+
];
|
|
817
|
+
for (const f of figs) {
|
|
818
|
+
const loc = f.file ? `${f.file}:${f.line}` : `line ${f.line}`;
|
|
819
|
+
const pathMatch = f.block.match(/\\includegraphics\s*(?:\[[^\]]*\])?\s*\{([^}]+)\}/);
|
|
820
|
+
const pathInfo = pathMatch ? ` ${pathMatch[1].trim()}` : '';
|
|
821
|
+
lines.push(` ${loc}${pathInfo}`);
|
|
822
|
+
}
|
|
823
|
+
lines.push(' Hint: use {#fig:label width=80%} for format-portable figures,');
|
|
824
|
+
lines.push(' or pass --pandoc-arg=--lua-filter=<your.lua> to translate them yourself.');
|
|
825
|
+
return lines.join('\n');
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Walk section files and gather a warning for any raw LaTeX figure blocks that
|
|
829
|
+
* won't survive the docx build. Returns null when there's nothing to warn about.
|
|
830
|
+
*/
|
|
831
|
+
export function collectRawLatexFigureWarning(directory, config) {
|
|
832
|
+
const translateEnabled = config.docx?.translateRawFigures !== false;
|
|
833
|
+
const all = [];
|
|
834
|
+
for (const section of findSections(directory, config.sections)) {
|
|
835
|
+
const sectionPath = path.join(directory, section);
|
|
836
|
+
if (!fs.existsSync(sectionPath))
|
|
837
|
+
continue;
|
|
838
|
+
try {
|
|
839
|
+
const content = fs.readFileSync(sectionPath, 'utf-8');
|
|
840
|
+
const figs = detectRawLatexFigures(content, section);
|
|
841
|
+
for (const f of figs) {
|
|
842
|
+
// When auto-translate is on, non-exotic blocks get rewritten cleanly —
|
|
843
|
+
// only the exotic leftovers need warning. When opted out, everything
|
|
844
|
+
// is at risk and we warn about every block.
|
|
845
|
+
if (translateEnabled && !f.exotic)
|
|
846
|
+
continue;
|
|
847
|
+
all.push(f);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
// ignore unreadable sections
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (all.length === 0)
|
|
855
|
+
return null;
|
|
856
|
+
return formatRawLatexFigureWarning(all, translateEnabled);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Build pandoc arguments for format.
|
|
860
|
+
*
|
|
861
|
+
* Returns only the built-in args derived from config. Passthrough args
|
|
862
|
+
* (config.pandocArgs, config[format].pandocArgs, CLI --pandoc-arg) are
|
|
863
|
+
* appended later in runPandoc so they win against pptx/crossref defaults
|
|
864
|
+
* added there.
|
|
629
865
|
*/
|
|
630
866
|
export function buildPandocArgs(format, config, outputPath) {
|
|
631
867
|
const args = [];
|
|
@@ -743,6 +979,25 @@ export function buildPandocArgs(format, config, outputPath) {
|
|
|
743
979
|
}
|
|
744
980
|
return args;
|
|
745
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* Collect passthrough pandoc args for a format in the canonical order:
|
|
984
|
+
* top-level config → format-specific config → CLI extras. Later wins for
|
|
985
|
+
* repeated flags.
|
|
986
|
+
*/
|
|
987
|
+
export function collectPandocPassthroughArgs(format, config, extraArgs = []) {
|
|
988
|
+
const out = [];
|
|
989
|
+
if (config.pandocArgs && config.pandocArgs.length > 0) {
|
|
990
|
+
out.push(...config.pandocArgs);
|
|
991
|
+
}
|
|
992
|
+
const formatConfig = config[format];
|
|
993
|
+
if (formatConfig?.pandocArgs && formatConfig.pandocArgs.length > 0) {
|
|
994
|
+
out.push(...formatConfig.pandocArgs);
|
|
995
|
+
}
|
|
996
|
+
if (extraArgs.length > 0) {
|
|
997
|
+
out.push(...extraArgs);
|
|
998
|
+
}
|
|
999
|
+
return out;
|
|
1000
|
+
}
|
|
746
1001
|
/**
|
|
747
1002
|
* Write crossref.yaml if needed
|
|
748
1003
|
*/
|
|
@@ -772,31 +1027,98 @@ export function resolveOutputDir(directory, config) {
|
|
|
772
1027
|
return directory;
|
|
773
1028
|
return path.isAbsolute(out) ? out : path.join(directory, out);
|
|
774
1029
|
}
|
|
1030
|
+
/** File extension (with leading dot) for each supported pandoc format. */
|
|
1031
|
+
const FORMAT_EXTENSIONS = {
|
|
1032
|
+
tex: '.tex',
|
|
1033
|
+
pdf: '.pdf',
|
|
1034
|
+
docx: '.docx',
|
|
1035
|
+
beamer: '.pdf',
|
|
1036
|
+
pptx: '.pptx',
|
|
1037
|
+
};
|
|
1038
|
+
/** Get file extension for a format, defaulting to `.pdf`. */
|
|
1039
|
+
export function getFormatExtension(format) {
|
|
1040
|
+
return FORMAT_EXTENSIONS[format] ?? '.pdf';
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Slugify a title for use as a default output filename. Lowercases, replaces
|
|
1044
|
+
* non-alphanumeric runs with `-`, and truncates at the last `-` boundary
|
|
1045
|
+
* at-or-before MAX_TITLE_FILENAME_LENGTH so words stay whole (the old blind
|
|
1046
|
+
* `.slice` cut mid-word).
|
|
1047
|
+
*/
|
|
1048
|
+
export function slugifyTitle(title) {
|
|
1049
|
+
if (!title)
|
|
1050
|
+
return 'paper';
|
|
1051
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1052
|
+
if (!slug)
|
|
1053
|
+
return 'paper';
|
|
1054
|
+
if (slug.length <= MAX_TITLE_FILENAME_LENGTH)
|
|
1055
|
+
return slug;
|
|
1056
|
+
const cut = slug.slice(0, MAX_TITLE_FILENAME_LENGTH);
|
|
1057
|
+
const lastDash = cut.lastIndexOf('-');
|
|
1058
|
+
// Only truncate at a hyphen if it leaves a reasonable amount of content.
|
|
1059
|
+
// Otherwise hard-cut (handles degenerate titles with no spaces at all).
|
|
1060
|
+
if (lastDash >= MAX_TITLE_FILENAME_LENGTH / 2) {
|
|
1061
|
+
return slug.slice(0, lastDash);
|
|
1062
|
+
}
|
|
1063
|
+
return cut;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Ensure `name` ends with `ext` (case-insensitive). If the user already supplied
|
|
1067
|
+
* the correct extension, return unchanged; if they supplied none or a different
|
|
1068
|
+
* one, append the format's canonical extension.
|
|
1069
|
+
*
|
|
1070
|
+
* Different-extension case (e.g. `output.docx` when building tex): we append
|
|
1071
|
+
* rather than replace, since stripping looks like an unsafe guess. The result
|
|
1072
|
+
* `output.docx.tex` is loud enough to flag the misconfiguration.
|
|
1073
|
+
*/
|
|
1074
|
+
function ensureExtension(name, ext) {
|
|
1075
|
+
if (name.toLowerCase().endsWith(ext.toLowerCase()))
|
|
1076
|
+
return name;
|
|
1077
|
+
return name + ext;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Resolve the final output path for a build.
|
|
1081
|
+
*
|
|
1082
|
+
* Priority: `options.outputPath` (internal force) > `cliOverride` (-o flag) >
|
|
1083
|
+
* `config.output[format]` > slugified title fallback.
|
|
1084
|
+
*
|
|
1085
|
+
* Relative paths from `cliOverride`/`config.output` resolve under outputDir;
|
|
1086
|
+
* absolute paths bypass outputDir. The fallback path always lives under
|
|
1087
|
+
* outputDir.
|
|
1088
|
+
*
|
|
1089
|
+
* @param suffix - Appended before the extension (e.g. "-changes", "-slides").
|
|
1090
|
+
* Suppressed when user supplied an explicit name via CLI or
|
|
1091
|
+
* config — they pick their own suffix.
|
|
1092
|
+
*/
|
|
1093
|
+
export function resolveOutputPath(directory, config, format, options = {}) {
|
|
1094
|
+
const { cliOverride, suffix = '' } = options;
|
|
1095
|
+
const ext = getFormatExtension(format);
|
|
1096
|
+
const explicit = cliOverride ?? config.output?.[format];
|
|
1097
|
+
if (explicit) {
|
|
1098
|
+
const baseDir = path.isAbsolute(explicit)
|
|
1099
|
+
? path.dirname(explicit)
|
|
1100
|
+
: resolveOutputDir(directory, config);
|
|
1101
|
+
const baseName = path.basename(explicit);
|
|
1102
|
+
const stem = baseName.replace(/\.[^./\\]+$/, '');
|
|
1103
|
+
return path.join(baseDir, ensureExtension(`${stem}${suffix}`, ext));
|
|
1104
|
+
}
|
|
1105
|
+
const slug = slugifyTitle(config.title);
|
|
1106
|
+
return path.join(resolveOutputDir(directory, config), `${slug}${suffix}${ext}`);
|
|
1107
|
+
}
|
|
775
1108
|
/**
|
|
776
1109
|
* Run pandoc build
|
|
777
1110
|
*/
|
|
778
1111
|
export async function runPandoc(inputPath, format, config, options = {}) {
|
|
779
1112
|
const directory = path.dirname(inputPath);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
// Map format to file extension
|
|
784
|
-
const extMap = {
|
|
785
|
-
tex: '.tex',
|
|
786
|
-
pdf: '.pdf',
|
|
787
|
-
docx: '.docx',
|
|
788
|
-
beamer: '.pdf', // beamer outputs PDF
|
|
789
|
-
pptx: '.pptx',
|
|
790
|
-
};
|
|
791
|
-
const ext = extMap[format] || '.pdf';
|
|
792
|
-
// For beamer, use -slides suffix to distinguish from regular PDF
|
|
1113
|
+
// outputPath (internal force) wins over the resolver. For beamer, we keep
|
|
1114
|
+
// the `-slides` suffix on the slug fallback to distinguish from a regular
|
|
1115
|
+
// PDF build; when the user supplies an explicit name, they pick their own.
|
|
793
1116
|
const suffix = format === 'beamer' ? '-slides' : '';
|
|
794
|
-
// Allow custom output path via options. Auto-named outputs go through the
|
|
795
|
-
// configured outputDir (default 'output/'); explicit paths are honored as-is
|
|
796
|
-
// so callers can route temp/intermediate artefacts where they want.
|
|
797
1117
|
const outputPath = options.outputPath
|
|
798
|
-
|
|
799
|
-
|
|
1118
|
+
?? resolveOutputPath(directory, config, format, {
|
|
1119
|
+
cliOverride: options.output,
|
|
1120
|
+
suffix,
|
|
1121
|
+
});
|
|
800
1122
|
if (!options.outputPath) {
|
|
801
1123
|
const outDir = path.dirname(outputPath);
|
|
802
1124
|
if (!fs.existsSync(outDir)) {
|
|
@@ -856,8 +1178,15 @@ export async function runPandoc(inputPath, format, config, options = {}) {
|
|
|
856
1178
|
args.push('--metadata-file', 'crossref.yaml');
|
|
857
1179
|
}
|
|
858
1180
|
}
|
|
1181
|
+
// Passthrough args go last so they win against built-in defaults.
|
|
1182
|
+
args.push(...collectPandocPassthroughArgs(format, config, options.pandocArgs));
|
|
859
1183
|
// Input file (use basename since we set cwd to directory)
|
|
860
1184
|
args.push(path.basename(inputPath));
|
|
1185
|
+
if (options.verbose) {
|
|
1186
|
+
const quoted = args.map(a => /[\s"'$`]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a).join(' ');
|
|
1187
|
+
console.error(`[pandoc ${format}] (cwd: ${directory})`);
|
|
1188
|
+
console.error(` pandoc ${quoted}`);
|
|
1189
|
+
}
|
|
861
1190
|
return new Promise((resolve) => {
|
|
862
1191
|
const pandoc = spawn('pandoc', args, {
|
|
863
1192
|
cwd: directory,
|
|
@@ -961,6 +1290,12 @@ export async function build(directory, formats = ['pdf', 'docx'], options = {})
|
|
|
961
1290
|
if (imageReg.figures?.length > 0) {
|
|
962
1291
|
writeImageRegistry(directory, imageReg);
|
|
963
1292
|
}
|
|
1293
|
+
// Warn about raw LaTeX figure blocks that won't render in docx (pandoc
|
|
1294
|
+
// drops them silently). With auto-translate on (default), this surfaces
|
|
1295
|
+
// only the exotic leftovers; with it off, every block.
|
|
1296
|
+
const rawFigWarning = collectRawLatexFigureWarning(directory, config);
|
|
1297
|
+
if (rawFigWarning)
|
|
1298
|
+
warnings.push(rawFigWarning);
|
|
964
1299
|
}
|
|
965
1300
|
const results = [];
|
|
966
1301
|
for (const format of formats) {
|