docrev 0.11.1 → 0.11.3

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.
Files changed (144) hide show
  1. package/.gitattributes +1 -1
  2. package/CHANGELOG.md +207 -197
  3. package/PLAN-tables-and-postprocess.md +850 -850
  4. package/README.md +433 -433
  5. package/bin/rev.js +11 -11
  6. package/bin/rev.ts +145 -145
  7. package/completions/rev.bash +127 -127
  8. package/completions/rev.ps1 +210 -210
  9. package/completions/rev.zsh +207 -207
  10. package/dist/lib/annotations.d.ts.map +1 -1
  11. package/dist/lib/annotations.js +28 -0
  12. package/dist/lib/annotations.js.map +1 -1
  13. package/dist/lib/build.d.ts +28 -0
  14. package/dist/lib/build.d.ts.map +1 -1
  15. package/dist/lib/build.js +81 -4
  16. package/dist/lib/build.js.map +1 -1
  17. package/dist/lib/commands/comments.d.ts.map +1 -1
  18. package/dist/lib/commands/comments.js +58 -28
  19. package/dist/lib/commands/comments.js.map +1 -1
  20. package/dist/lib/commands/context.d.ts +1 -0
  21. package/dist/lib/commands/context.d.ts.map +1 -1
  22. package/dist/lib/commands/context.js +1 -0
  23. package/dist/lib/commands/context.js.map +1 -1
  24. package/dist/lib/commands/core.d.ts.map +1 -1
  25. package/dist/lib/commands/core.js +25 -8
  26. package/dist/lib/commands/core.js.map +1 -1
  27. package/dist/lib/commands/response.js +1 -1
  28. package/dist/lib/commands/response.js.map +1 -1
  29. package/dist/lib/commands/utilities.js +164 -164
  30. package/dist/lib/commands/word-tools.js +8 -8
  31. package/dist/lib/grammar.js +3 -3
  32. package/dist/lib/import.d.ts +14 -0
  33. package/dist/lib/import.d.ts.map +1 -1
  34. package/dist/lib/import.js +23 -0
  35. package/dist/lib/import.js.map +1 -1
  36. package/dist/lib/input.d.ts +67 -0
  37. package/dist/lib/input.d.ts.map +1 -0
  38. package/dist/lib/input.js +164 -0
  39. package/dist/lib/input.js.map +1 -0
  40. package/dist/lib/macro-filter.lua +201 -201
  41. package/dist/lib/pdf-comments.js +44 -44
  42. package/dist/lib/plugins.js +57 -57
  43. package/dist/lib/pptx-color-filter.lua +37 -37
  44. package/dist/lib/pptx-themes.js +115 -115
  45. package/dist/lib/response.d.ts +1 -1
  46. package/dist/lib/response.d.ts.map +1 -1
  47. package/dist/lib/response.js +5 -2
  48. package/dist/lib/response.js.map +1 -1
  49. package/dist/lib/schema.d.ts.map +1 -1
  50. package/dist/lib/schema.js +8 -2
  51. package/dist/lib/schema.js.map +1 -1
  52. package/dist/lib/spelling.js +2 -2
  53. package/dist/lib/templates.js +387 -387
  54. package/dist/lib/themes.js +51 -51
  55. package/eslint.config.js +27 -27
  56. package/lib/anchor-match.ts +307 -307
  57. package/lib/annotations.ts +695 -664
  58. package/lib/build.ts +2047 -1956
  59. package/lib/citations.ts +160 -160
  60. package/lib/commands/build.ts +885 -885
  61. package/lib/commands/citations.ts +515 -515
  62. package/lib/commands/comments.ts +1077 -1050
  63. package/lib/commands/context.ts +185 -176
  64. package/lib/commands/core.ts +328 -309
  65. package/lib/commands/doi.ts +451 -451
  66. package/lib/commands/file-ops.ts +372 -372
  67. package/lib/commands/history.ts +320 -320
  68. package/lib/commands/index.ts +87 -87
  69. package/lib/commands/init.ts +259 -259
  70. package/lib/commands/merge-resolve.ts +378 -378
  71. package/lib/commands/preview.ts +178 -178
  72. package/lib/commands/project-info.ts +244 -244
  73. package/lib/commands/quality.ts +517 -517
  74. package/lib/commands/response.ts +454 -454
  75. package/lib/commands/section-boundaries.ts +82 -82
  76. package/lib/commands/sections.ts +451 -451
  77. package/lib/commands/sync.ts +689 -689
  78. package/lib/commands/text-ops.ts +449 -449
  79. package/lib/commands/utilities.ts +448 -448
  80. package/lib/commands/verify-anchors.ts +272 -272
  81. package/lib/commands/word-tools.ts +340 -340
  82. package/lib/comment-realign.ts +99 -99
  83. package/lib/config.ts +84 -84
  84. package/lib/crossref.ts +781 -781
  85. package/lib/csl.ts +191 -191
  86. package/lib/dependencies.ts +132 -132
  87. package/lib/diff-engine.ts +465 -465
  88. package/lib/doi-cache.ts +115 -115
  89. package/lib/doi.ts +879 -879
  90. package/lib/equations.ts +506 -506
  91. package/lib/errors.ts +350 -350
  92. package/lib/format.ts +541 -541
  93. package/lib/git.ts +326 -326
  94. package/lib/grammar.ts +303 -303
  95. package/lib/image-registry.ts +180 -180
  96. package/lib/import.ts +932 -906
  97. package/lib/input.ts +174 -0
  98. package/lib/journals.ts +543 -543
  99. package/lib/macro-filter.lua +201 -201
  100. package/lib/macros.ts +273 -273
  101. package/lib/merge.ts +633 -633
  102. package/lib/ooxml.ts +768 -768
  103. package/lib/orcid.ts +144 -144
  104. package/lib/pdf-comments.ts +263 -263
  105. package/lib/pdf-import.ts +524 -524
  106. package/lib/plugins.ts +362 -362
  107. package/lib/postprocess.ts +188 -188
  108. package/lib/pptx-color-filter.lua +37 -37
  109. package/lib/pptx-template.ts +469 -469
  110. package/lib/pptx-themes.ts +483 -483
  111. package/lib/protect-restore.ts +520 -520
  112. package/lib/rate-limiter.ts +127 -127
  113. package/lib/response.ts +200 -197
  114. package/lib/restore-references.ts +240 -240
  115. package/lib/review.ts +327 -327
  116. package/lib/schema.ts +494 -488
  117. package/lib/scientific-words.ts +73 -73
  118. package/lib/sections.ts +428 -428
  119. package/lib/slides.ts +756 -756
  120. package/lib/spelling.ts +334 -334
  121. package/lib/templates.ts +526 -526
  122. package/lib/themes.ts +742 -742
  123. package/lib/trackchanges.ts +166 -166
  124. package/lib/tui.ts +450 -450
  125. package/lib/types.ts +546 -546
  126. package/lib/undo.ts +250 -250
  127. package/lib/utils.ts +69 -69
  128. package/lib/variables.ts +179 -179
  129. package/lib/word-extraction.ts +525 -525
  130. package/lib/word.ts +526 -526
  131. package/lib/wordcomments.ts +789 -789
  132. package/package.json +1 -1
  133. package/scripts/postbuild.js +47 -47
  134. package/skill/REFERENCE.md +550 -550
  135. package/skill/SKILL.md +302 -302
  136. package/tsconfig.json +26 -26
  137. package/types/index.d.ts +531 -531
  138. package/issues.md +0 -180
  139. package/site/assets/extra.css +0 -208
  140. package/site/commands.html +0 -926
  141. package/site/configuration.html +0 -469
  142. package/site/index.html +0 -288
  143. package/site/troubleshooting.html +0 -461
  144. package/site/workflow.html +0 -518
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Input routing: read a command's `[file]` argument as CriticMarkup Markdown,
3
+ * transparently handling Word documents.
4
+ *
5
+ * `rev status`/`rev comments` and friends were written to `readFileSync(file,
6
+ * 'utf-8')` every argument and regex it for CriticMarkup. A `.docx` is a binary
7
+ * ZIP; decoded as UTF-8 it occasionally yields a stray `{~~..~>..~~}` byte
8
+ * sequence, so the tool reported a small, plausible, wrong count with no error
9
+ * (gcol33/docrev#8). This module is the single front door that both:
10
+ *
11
+ * - detects a Word document by extension AND by ZIP magic + `word/document.xml`
12
+ * (so a mis-extensioned `.docx` renamed to `.md`/`.txt` is still caught), and
13
+ * - converts it to the same annotated Markdown `rev import` produces, so every
14
+ * downstream reader (`countAnnotations`, `getComments`, ...) sees real tags.
15
+ *
16
+ * Commands that only read report through `readAnnotatedInput`; commands that
17
+ * edit in place call `assertEditableMarkdown` first and refuse a `.docx` with a
18
+ * pointer to `rev import`, since CriticMarkup cannot be written back into a
19
+ * binary ZIP.
20
+ */
21
+ /**
22
+ * A file cannot be read as (or converted to) CriticMarkup Markdown. Carries
23
+ * actionable suggestions so the command layer can render a helpful error.
24
+ */
25
+ export declare class InputError extends Error {
26
+ suggestions: string[];
27
+ constructor(message: string, suggestions?: string[]);
28
+ }
29
+ /** True when the file begins with the ZIP local-file-header magic. */
30
+ export declare function looksLikeZip(file: string): boolean;
31
+ /**
32
+ * True when `file` is a Word document: it carries a Word extension, or it is a
33
+ * ZIP whose package contains `word/document.xml`. The content sniff catches a
34
+ * `.docx` renamed to `.md`/`.txt`, so those never get regexed as CriticMarkup.
35
+ */
36
+ export declare function isWordDocument(file: string): boolean;
37
+ /**
38
+ * Read a file as the CriticMarkup Markdown a command expects. A Word document
39
+ * is converted through the existing OOXML/pandoc reader (real insertions,
40
+ * deletions, substitutions, and comments); a Markdown/text file is read as
41
+ * UTF-8. A non-Word binary (image, PDF, unknown ZIP) is rejected rather than
42
+ * silently miscounted.
43
+ *
44
+ * @throws InputError if the file is a binary that is not a Word document, or a
45
+ * Word document that cannot be parsed.
46
+ */
47
+ export declare function readAnnotatedInput(file: string): Promise<string>;
48
+ /**
49
+ * Guard for commands that edit the file in place (accept/reject/resolve/reply/
50
+ * review). A `.docx` cannot hold CriticMarkup, so refuse it with a pointer to
51
+ * `rev import` instead of corrupting the document.
52
+ *
53
+ * @throws InputError if `file` is a Word document.
54
+ */
55
+ export declare function assertEditableMarkdown(file: string): void;
56
+ /**
57
+ * Command front door: verify the file exists, then read it as annotated
58
+ * Markdown (converting a `.docx` on the way). On a bad input, print a friendly
59
+ * error and exit — matching how the rest of the CLI reports failures.
60
+ */
61
+ export declare function loadAnnotated(file: string, fileType?: string): Promise<string>;
62
+ /**
63
+ * Command front door for editing commands: verify existence and refuse a Word
64
+ * document (which cannot be edited in place), printing guidance and exiting.
65
+ */
66
+ export declare function requireEditableMarkdown(file: string, fileType?: string): void;
67
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../lib/input.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAaH;;;GAGG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,WAAW,EAAE,MAAM,EAAE,CAAC;gBACV,OAAO,EAAE,MAAM,EAAE,WAAW,GAAE,MAAM,EAAO;CAKxD;AAiBD,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQpD;AAQD;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBtE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAWzD;AAMD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ7F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAkB,GAAG,IAAI,CAQtF"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Input routing: read a command's `[file]` argument as CriticMarkup Markdown,
3
+ * transparently handling Word documents.
4
+ *
5
+ * `rev status`/`rev comments` and friends were written to `readFileSync(file,
6
+ * 'utf-8')` every argument and regex it for CriticMarkup. A `.docx` is a binary
7
+ * ZIP; decoded as UTF-8 it occasionally yields a stray `{~~..~>..~~}` byte
8
+ * sequence, so the tool reported a small, plausible, wrong count with no error
9
+ * (gcol33/docrev#8). This module is the single front door that both:
10
+ *
11
+ * - detects a Word document by extension AND by ZIP magic + `word/document.xml`
12
+ * (so a mis-extensioned `.docx` renamed to `.md`/`.txt` is still caught), and
13
+ * - converts it to the same annotated Markdown `rev import` produces, so every
14
+ * downstream reader (`countAnnotations`, `getComments`, ...) sees real tags.
15
+ *
16
+ * Commands that only read report through `readAnnotatedInput`; commands that
17
+ * edit in place call `assertEditableMarkdown` first and refuse a `.docx` with a
18
+ * pointer to `rev import`, since CriticMarkup cannot be written back into a
19
+ * binary ZIP.
20
+ */
21
+ import * as fs from 'fs';
22
+ import * as path from 'path';
23
+ import { openDocx } from './ooxml.js';
24
+ import { exitWithError, requireFile } from './errors.js';
25
+ /** ZIP local-file-header magic: the first four bytes of any `.docx`. */
26
+ const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
27
+ /** OOXML Word extensions (all ZIP-backed, none Markdown). */
28
+ const WORD_EXTENSIONS = new Set(['.docx', '.docm', '.dotx', '.dotm']);
29
+ /**
30
+ * A file cannot be read as (or converted to) CriticMarkup Markdown. Carries
31
+ * actionable suggestions so the command layer can render a helpful error.
32
+ */
33
+ export class InputError extends Error {
34
+ suggestions;
35
+ constructor(message, suggestions = []) {
36
+ super(message);
37
+ this.name = 'InputError';
38
+ this.suggestions = suggestions;
39
+ }
40
+ }
41
+ /** Read the first bytes of a file, or an empty buffer if unreadable. */
42
+ function readMagic(file, length = 4) {
43
+ let fd;
44
+ try {
45
+ fd = fs.openSync(file, 'r');
46
+ const buf = Buffer.alloc(length);
47
+ const n = fs.readSync(fd, buf, 0, length, 0);
48
+ return buf.subarray(0, n);
49
+ }
50
+ catch {
51
+ return Buffer.alloc(0);
52
+ }
53
+ finally {
54
+ if (fd !== undefined)
55
+ fs.closeSync(fd);
56
+ }
57
+ }
58
+ /** True when the file begins with the ZIP local-file-header magic. */
59
+ export function looksLikeZip(file) {
60
+ return readMagic(file).equals(ZIP_MAGIC);
61
+ }
62
+ /**
63
+ * True when `file` is a Word document: it carries a Word extension, or it is a
64
+ * ZIP whose package contains `word/document.xml`. The content sniff catches a
65
+ * `.docx` renamed to `.md`/`.txt`, so those never get regexed as CriticMarkup.
66
+ */
67
+ export function isWordDocument(file) {
68
+ if (WORD_EXTENSIONS.has(path.extname(file).toLowerCase()))
69
+ return true;
70
+ if (!looksLikeZip(file))
71
+ return false;
72
+ try {
73
+ return openDocx(file).getEntry('word/document.xml') !== null;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ /** A NUL byte in the head reliably marks binary; UTF-8 Markdown never has one. */
80
+ function isBinaryBuffer(buf) {
81
+ if (buf.length >= 4 && buf.subarray(0, 4).equals(ZIP_MAGIC))
82
+ return true;
83
+ return buf.subarray(0, Math.min(buf.length, 8192)).includes(0x00);
84
+ }
85
+ /**
86
+ * Read a file as the CriticMarkup Markdown a command expects. A Word document
87
+ * is converted through the existing OOXML/pandoc reader (real insertions,
88
+ * deletions, substitutions, and comments); a Markdown/text file is read as
89
+ * UTF-8. A non-Word binary (image, PDF, unknown ZIP) is rejected rather than
90
+ * silently miscounted.
91
+ *
92
+ * @throws InputError if the file is a binary that is not a Word document, or a
93
+ * Word document that cannot be parsed.
94
+ */
95
+ export async function readAnnotatedInput(file) {
96
+ if (isWordDocument(file)) {
97
+ const { readDocxAsAnnotatedMarkdown } = await import('./import.js');
98
+ try {
99
+ return await readDocxAsAnnotatedMarkdown(file);
100
+ }
101
+ catch (err) {
102
+ throw new InputError(`Failed to read Word document ${path.basename(file)}: ${err.message}`, ['Run "rev import <docx>" to convert it to annotated Markdown first']);
103
+ }
104
+ }
105
+ const buf = fs.readFileSync(file);
106
+ if (isBinaryBuffer(buf)) {
107
+ throw new InputError(`${path.basename(file)} is not a text or Markdown file (binary content).`, [
108
+ 'Word documents: run "rev import <docx>" first, or pass the imported .md',
109
+ 'This command reads CriticMarkup Markdown, not binary files',
110
+ ]);
111
+ }
112
+ return buf.toString('utf-8');
113
+ }
114
+ /**
115
+ * Guard for commands that edit the file in place (accept/reject/resolve/reply/
116
+ * review). A `.docx` cannot hold CriticMarkup, so refuse it with a pointer to
117
+ * `rev import` instead of corrupting the document.
118
+ *
119
+ * @throws InputError if `file` is a Word document.
120
+ */
121
+ export function assertEditableMarkdown(file) {
122
+ if (isWordDocument(file)) {
123
+ const base = path.basename(file);
124
+ throw new InputError(`${base} is a Word document; this command edits Markdown in place.`, [
125
+ `Run "rev import ${base}" to get an editable Markdown file, then edit that`,
126
+ 'Read-only inspection works directly: "rev status" / "rev comments"',
127
+ ]);
128
+ }
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // Command-facing wrappers: translate InputError into the CLI's error surface.
132
+ // ---------------------------------------------------------------------------
133
+ /**
134
+ * Command front door: verify the file exists, then read it as annotated
135
+ * Markdown (converting a `.docx` on the way). On a bad input, print a friendly
136
+ * error and exit — matching how the rest of the CLI reports failures.
137
+ */
138
+ export async function loadAnnotated(file, fileType = 'Markdown file') {
139
+ requireFile(file, fileType);
140
+ try {
141
+ return await readAnnotatedInput(file);
142
+ }
143
+ catch (err) {
144
+ if (err instanceof InputError)
145
+ exitWithError(err.message, err.suggestions);
146
+ throw err;
147
+ }
148
+ }
149
+ /**
150
+ * Command front door for editing commands: verify existence and refuse a Word
151
+ * document (which cannot be edited in place), printing guidance and exiting.
152
+ */
153
+ export function requireEditableMarkdown(file, fileType = 'Markdown file') {
154
+ requireFile(file, fileType);
155
+ try {
156
+ assertEditableMarkdown(file);
157
+ }
158
+ catch (err) {
159
+ if (err instanceof InputError)
160
+ exitWithError(err.message, err.suggestions);
161
+ throw err;
162
+ }
163
+ }
164
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../../lib/input.ts"],"names":[],"mappings":"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"}
@@ -1,201 +1,201 @@
1
- --[[
2
- docrev macro filter.
3
-
4
- Reads a JSON sidecar describing one-argument LaTeX-style macros and expands
5
- them per output FORMAT. Used for the built-in \tofill{X} (bold orange [X]
6
- placeholder) and any user-declared macros from rev.yaml.
7
-
8
- Sidecar path is passed via the DOCREV_MACROS_FILE environment variable, set
9
- by build.ts before spawning pandoc. Env vars (not metadata) because pandoc's
10
- filter traversal runs RawInline/RawBlock BEFORE Meta, so by the time we'd
11
- read metadata the inline expansions have already happened.
12
-
13
- Why raw OpenXML for docx? Pandoc 3.x's docx writer does NOT honor
14
- `Span{style="color: #..."}` — those spans render as plain text with no
15
- <w:color> run property. So for docx we emit raw <w:r> nodes directly. Same
16
- reasoning for the pptx-color-filter.
17
-
18
- For latex/pdf/beamer the markdown source already contains \tofill{X} as a raw
19
- LaTeX inline; we leave it alone because build.ts injects a \providecommand
20
- into header-includes. For html we emit a raw <span> with inline style. For
21
- everything else (markdown, gfm, plain) we degrade to **bold [X]** so the
22
- placeholder never silently disappears.
23
- ]]
24
-
25
- local json = require('pandoc.json')
26
-
27
- local macros_by_name = {}
28
-
29
- local function load_sidecar()
30
- local path = os.getenv('DOCREV_MACROS_FILE')
31
- if not path or path == '' then
32
- return
33
- end
34
- local fh = io.open(path, 'r')
35
- if not fh then
36
- io.stderr:write('docrev macro-filter: cannot read sidecar: ' .. path .. '\n')
37
- return
38
- end
39
- local content = fh:read('*a')
40
- fh:close()
41
- local ok, parsed = pcall(json.decode, content)
42
- if not ok or type(parsed) ~= 'table' or type(parsed.macros) ~= 'table' then
43
- io.stderr:write('docrev macro-filter: malformed sidecar JSON\n')
44
- return
45
- end
46
- for _, m in ipairs(parsed.macros) do
47
- if type(m) == 'table' and type(m.name) == 'string' then
48
- macros_by_name[m.name] = m
49
- end
50
- end
51
- end
52
-
53
- load_sidecar()
54
-
55
- local function xml_escape(s)
56
- return (s:gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;'))
57
- end
58
-
59
- local function html_escape(s)
60
- return (s
61
- :gsub('&', '&amp;')
62
- :gsub('<', '&lt;')
63
- :gsub('>', '&gt;')
64
- :gsub('"', '&quot;'))
65
- end
66
-
67
- -- Resolve effective style for a macro in the current pandoc format.
68
- -- Per-format entry wins over `default` (replacement, not merge — matches
69
- -- macros.ts semantics).
70
- local function pick_style(macro, format)
71
- if macro.formats and macro.formats[format] then
72
- return macro.formats[format]
73
- end
74
- return macro.default or {}
75
- end
76
-
77
- -- Build the inside of the bracket: [prefix][arg][suffix], optionally without
78
- -- brackets when style.bracket == false.
79
- local function compose_text(style, arg)
80
- local prefix = style.prefix or ''
81
- local suffix = style.suffix or ''
82
- local inner = prefix .. arg .. suffix
83
- if style.bracket == false then
84
- return inner
85
- end
86
- return '[' .. inner .. ']'
87
- end
88
-
89
- local function render_docx_run(style, arg)
90
- local rpr = {}
91
- if style.color then
92
- table.insert(rpr, '<w:color w:val="' .. style.color .. '"/>')
93
- end
94
- if style.bold then
95
- table.insert(rpr, '<w:b/>')
96
- end
97
- if style.italic then
98
- table.insert(rpr, '<w:i/>')
99
- end
100
- local rpr_xml = ''
101
- if #rpr > 0 then
102
- rpr_xml = '<w:rPr>' .. table.concat(rpr) .. '</w:rPr>'
103
- end
104
- local text = xml_escape(compose_text(style, arg))
105
- return '<w:r>' .. rpr_xml ..
106
- '<w:t xml:space="preserve">' .. text .. '</w:t></w:r>'
107
- end
108
-
109
- local function render_html(style, arg)
110
- local css = {}
111
- if style.color then
112
- table.insert(css, 'color:#' .. style.color)
113
- end
114
- if style.bold then
115
- table.insert(css, 'font-weight:bold')
116
- end
117
- if style.italic then
118
- table.insert(css, 'font-style:italic')
119
- end
120
- local text = html_escape(compose_text(style, arg))
121
- if #css == 0 then
122
- return '<span>' .. text .. '</span>'
123
- end
124
- return '<span style="' .. table.concat(css, ';') .. '">' .. text .. '</span>'
125
- end
126
-
127
- -- Fallback path: produce native pandoc inlines so the macro never silently
128
- -- disappears in markdown/gfm/plain output. Used when the current format has
129
- -- no native rich-text path (or we couldn't open the sidecar).
130
- local function fallback_inlines(style, arg)
131
- local doc = pandoc.read(compose_text(style, arg), 'markdown')
132
- local inlines = pandoc.utils.blocks_to_inlines(doc.blocks)
133
- if style.bold then
134
- inlines = { pandoc.Strong(inlines) }
135
- end
136
- if style.italic then
137
- inlines = { pandoc.Emph(inlines) }
138
- end
139
- return inlines
140
- end
141
-
142
- -- Match `\NAME{...}` (with balanced braces inside the argument is NOT
143
- -- supported — the use case is plain placeholder text, mirroring the reference
144
- -- filter; users who need nested braces should use a different mechanism).
145
- local function parse_call(text)
146
- local name, arg = text:match('^\\([A-Za-z][A-Za-z0-9]*)%s*{(.*)}%s*$')
147
- if name and arg and macros_by_name[name] then
148
- return name, arg
149
- end
150
- return nil, nil
151
- end
152
-
153
- local function expand_inline(el)
154
- if el.format ~= 'tex' and el.format ~= 'latex' then
155
- return nil
156
- end
157
- local name, arg = parse_call(el.text)
158
- if not name then return nil end
159
- local macro = macros_by_name[name]
160
- local style = pick_style(macro, FORMAT)
161
-
162
- if FORMAT == 'docx' then
163
- return pandoc.RawInline('openxml', render_docx_run(style, arg))
164
- elseif FORMAT == 'html' or FORMAT == 'html4' or FORMAT == 'html5' or FORMAT == 'chunkedhtml' then
165
- return pandoc.RawInline('html', render_html(style, arg))
166
- elseif FORMAT == 'latex' or FORMAT == 'beamer' or FORMAT == 'context' then
167
- -- Leave the raw LaTeX as-is. build.ts injects \providecommand into
168
- -- header-includes, so the LaTeX engine renders it directly.
169
- return nil
170
- else
171
- return fallback_inlines(style, arg)
172
- end
173
- end
174
-
175
- local function expand_block(el)
176
- if el.format ~= 'tex' and el.format ~= 'latex' then
177
- return nil
178
- end
179
- local name, arg = parse_call(el.text)
180
- if not name then return nil end
181
- local macro = macros_by_name[name]
182
- local style = pick_style(macro, FORMAT)
183
-
184
- if FORMAT == 'docx' then
185
- return pandoc.RawBlock('openxml', '<w:p>' .. render_docx_run(style, arg) .. '</w:p>')
186
- elseif FORMAT == 'html' or FORMAT == 'html4' or FORMAT == 'html5' or FORMAT == 'chunkedhtml' then
187
- return pandoc.RawBlock('html', '<p>' .. render_html(style, arg) .. '</p>')
188
- elseif FORMAT == 'latex' or FORMAT == 'beamer' or FORMAT == 'context' then
189
- return nil
190
- else
191
- return pandoc.Para(fallback_inlines(style, arg))
192
- end
193
- end
194
-
195
- function RawInline(el)
196
- return expand_inline(el)
197
- end
198
-
199
- function RawBlock(el)
200
- return expand_block(el)
201
- end
1
+ --[[
2
+ docrev macro filter.
3
+
4
+ Reads a JSON sidecar describing one-argument LaTeX-style macros and expands
5
+ them per output FORMAT. Used for the built-in \tofill{X} (bold orange [X]
6
+ placeholder) and any user-declared macros from rev.yaml.
7
+
8
+ Sidecar path is passed via the DOCREV_MACROS_FILE environment variable, set
9
+ by build.ts before spawning pandoc. Env vars (not metadata) because pandoc's
10
+ filter traversal runs RawInline/RawBlock BEFORE Meta, so by the time we'd
11
+ read metadata the inline expansions have already happened.
12
+
13
+ Why raw OpenXML for docx? Pandoc 3.x's docx writer does NOT honor
14
+ `Span{style="color: #..."}` — those spans render as plain text with no
15
+ <w:color> run property. So for docx we emit raw <w:r> nodes directly. Same
16
+ reasoning for the pptx-color-filter.
17
+
18
+ For latex/pdf/beamer the markdown source already contains \tofill{X} as a raw
19
+ LaTeX inline; we leave it alone because build.ts injects a \providecommand
20
+ into header-includes. For html we emit a raw <span> with inline style. For
21
+ everything else (markdown, gfm, plain) we degrade to **bold [X]** so the
22
+ placeholder never silently disappears.
23
+ ]]
24
+
25
+ local json = require('pandoc.json')
26
+
27
+ local macros_by_name = {}
28
+
29
+ local function load_sidecar()
30
+ local path = os.getenv('DOCREV_MACROS_FILE')
31
+ if not path or path == '' then
32
+ return
33
+ end
34
+ local fh = io.open(path, 'r')
35
+ if not fh then
36
+ io.stderr:write('docrev macro-filter: cannot read sidecar: ' .. path .. '\n')
37
+ return
38
+ end
39
+ local content = fh:read('*a')
40
+ fh:close()
41
+ local ok, parsed = pcall(json.decode, content)
42
+ if not ok or type(parsed) ~= 'table' or type(parsed.macros) ~= 'table' then
43
+ io.stderr:write('docrev macro-filter: malformed sidecar JSON\n')
44
+ return
45
+ end
46
+ for _, m in ipairs(parsed.macros) do
47
+ if type(m) == 'table' and type(m.name) == 'string' then
48
+ macros_by_name[m.name] = m
49
+ end
50
+ end
51
+ end
52
+
53
+ load_sidecar()
54
+
55
+ local function xml_escape(s)
56
+ return (s:gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;'))
57
+ end
58
+
59
+ local function html_escape(s)
60
+ return (s
61
+ :gsub('&', '&amp;')
62
+ :gsub('<', '&lt;')
63
+ :gsub('>', '&gt;')
64
+ :gsub('"', '&quot;'))
65
+ end
66
+
67
+ -- Resolve effective style for a macro in the current pandoc format.
68
+ -- Per-format entry wins over `default` (replacement, not merge — matches
69
+ -- macros.ts semantics).
70
+ local function pick_style(macro, format)
71
+ if macro.formats and macro.formats[format] then
72
+ return macro.formats[format]
73
+ end
74
+ return macro.default or {}
75
+ end
76
+
77
+ -- Build the inside of the bracket: [prefix][arg][suffix], optionally without
78
+ -- brackets when style.bracket == false.
79
+ local function compose_text(style, arg)
80
+ local prefix = style.prefix or ''
81
+ local suffix = style.suffix or ''
82
+ local inner = prefix .. arg .. suffix
83
+ if style.bracket == false then
84
+ return inner
85
+ end
86
+ return '[' .. inner .. ']'
87
+ end
88
+
89
+ local function render_docx_run(style, arg)
90
+ local rpr = {}
91
+ if style.color then
92
+ table.insert(rpr, '<w:color w:val="' .. style.color .. '"/>')
93
+ end
94
+ if style.bold then
95
+ table.insert(rpr, '<w:b/>')
96
+ end
97
+ if style.italic then
98
+ table.insert(rpr, '<w:i/>')
99
+ end
100
+ local rpr_xml = ''
101
+ if #rpr > 0 then
102
+ rpr_xml = '<w:rPr>' .. table.concat(rpr) .. '</w:rPr>'
103
+ end
104
+ local text = xml_escape(compose_text(style, arg))
105
+ return '<w:r>' .. rpr_xml ..
106
+ '<w:t xml:space="preserve">' .. text .. '</w:t></w:r>'
107
+ end
108
+
109
+ local function render_html(style, arg)
110
+ local css = {}
111
+ if style.color then
112
+ table.insert(css, 'color:#' .. style.color)
113
+ end
114
+ if style.bold then
115
+ table.insert(css, 'font-weight:bold')
116
+ end
117
+ if style.italic then
118
+ table.insert(css, 'font-style:italic')
119
+ end
120
+ local text = html_escape(compose_text(style, arg))
121
+ if #css == 0 then
122
+ return '<span>' .. text .. '</span>'
123
+ end
124
+ return '<span style="' .. table.concat(css, ';') .. '">' .. text .. '</span>'
125
+ end
126
+
127
+ -- Fallback path: produce native pandoc inlines so the macro never silently
128
+ -- disappears in markdown/gfm/plain output. Used when the current format has
129
+ -- no native rich-text path (or we couldn't open the sidecar).
130
+ local function fallback_inlines(style, arg)
131
+ local doc = pandoc.read(compose_text(style, arg), 'markdown')
132
+ local inlines = pandoc.utils.blocks_to_inlines(doc.blocks)
133
+ if style.bold then
134
+ inlines = { pandoc.Strong(inlines) }
135
+ end
136
+ if style.italic then
137
+ inlines = { pandoc.Emph(inlines) }
138
+ end
139
+ return inlines
140
+ end
141
+
142
+ -- Match `\NAME{...}` (with balanced braces inside the argument is NOT
143
+ -- supported — the use case is plain placeholder text, mirroring the reference
144
+ -- filter; users who need nested braces should use a different mechanism).
145
+ local function parse_call(text)
146
+ local name, arg = text:match('^\\([A-Za-z][A-Za-z0-9]*)%s*{(.*)}%s*$')
147
+ if name and arg and macros_by_name[name] then
148
+ return name, arg
149
+ end
150
+ return nil, nil
151
+ end
152
+
153
+ local function expand_inline(el)
154
+ if el.format ~= 'tex' and el.format ~= 'latex' then
155
+ return nil
156
+ end
157
+ local name, arg = parse_call(el.text)
158
+ if not name then return nil end
159
+ local macro = macros_by_name[name]
160
+ local style = pick_style(macro, FORMAT)
161
+
162
+ if FORMAT == 'docx' then
163
+ return pandoc.RawInline('openxml', render_docx_run(style, arg))
164
+ elseif FORMAT == 'html' or FORMAT == 'html4' or FORMAT == 'html5' or FORMAT == 'chunkedhtml' then
165
+ return pandoc.RawInline('html', render_html(style, arg))
166
+ elseif FORMAT == 'latex' or FORMAT == 'beamer' or FORMAT == 'context' then
167
+ -- Leave the raw LaTeX as-is. build.ts injects \providecommand into
168
+ -- header-includes, so the LaTeX engine renders it directly.
169
+ return nil
170
+ else
171
+ return fallback_inlines(style, arg)
172
+ end
173
+ end
174
+
175
+ local function expand_block(el)
176
+ if el.format ~= 'tex' and el.format ~= 'latex' then
177
+ return nil
178
+ end
179
+ local name, arg = parse_call(el.text)
180
+ if not name then return nil end
181
+ local macro = macros_by_name[name]
182
+ local style = pick_style(macro, FORMAT)
183
+
184
+ if FORMAT == 'docx' then
185
+ return pandoc.RawBlock('openxml', '<w:p>' .. render_docx_run(style, arg) .. '</w:p>')
186
+ elseif FORMAT == 'html' or FORMAT == 'html4' or FORMAT == 'html5' or FORMAT == 'chunkedhtml' then
187
+ return pandoc.RawBlock('html', '<p>' .. render_html(style, arg) .. '</p>')
188
+ elseif FORMAT == 'latex' or FORMAT == 'beamer' or FORMAT == 'context' then
189
+ return nil
190
+ else
191
+ return pandoc.Para(fallback_inlines(style, arg))
192
+ end
193
+ end
194
+
195
+ function RawInline(el)
196
+ return expand_inline(el)
197
+ end
198
+
199
+ function RawBlock(el)
200
+ return expand_block(el)
201
+ end