@geraldmaron/construct 1.4.1 → 1.4.2
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/bin/construct +2 -1
- package/bin/construct-postinstall.mjs +27 -2
- package/config/tag-vocabulary.json +264 -0
- package/lib/config/schema.mjs +1 -1
- package/lib/doctor/diagnosis.mjs +20 -0
- package/lib/doctor/index.mjs +5 -0
- package/lib/embed/worker.mjs +5 -0
- package/lib/env-config.mjs +10 -2
- package/lib/export-validate.mjs +34 -2
- package/lib/host/readiness.mjs +109 -0
- package/lib/host-disposition.mjs +10 -1
- package/lib/install/stage-project.mjs +41 -4
- package/lib/intake/prepare.mjs +2 -0
- package/lib/mcp/destructive-approval.mjs +57 -0
- package/lib/mcp/server.mjs +208 -34
- package/lib/mcp/tool-rate-limit.mjs +47 -0
- package/lib/mcp/tool-safety.mjs +94 -0
- package/lib/mcp/tool-surface-parity.mjs +60 -0
- package/lib/mcp/tools/orchestration-run.mjs +45 -7
- package/lib/mcp/tools/project.mjs +25 -8
- package/lib/mcp/tools/storage.mjs +9 -1
- package/lib/mcp/tools/web-search-governance.mjs +96 -0
- package/lib/mcp/tools/web-search.mjs +5 -76
- package/lib/mcp-platform-config.mjs +27 -18
- package/lib/oracle/daemon-entry.mjs +6 -0
- package/lib/orchestration/runtime.mjs +81 -42
- package/lib/orchestration/web-capability.mjs +59 -0
- package/lib/orchestration/worker.mjs +263 -19
- package/lib/output-quality.mjs +61 -2
- package/lib/path-policy.mjs +56 -0
- package/lib/providers/secret-audit-wiring.mjs +35 -18
- package/lib/providers/secret-resolver.mjs +28 -13
- package/lib/registry/catalog.mjs +6 -0
- package/lib/registry/loader.mjs +7 -1
- package/lib/registry/validate.mjs +3 -3
- package/lib/sandbox.mjs +1 -1
- package/lib/service-manager.mjs +59 -9
- package/lib/storage/admin.mjs +7 -2
- package/package.json +6 -1
- package/registry/agent-manifest.json +117 -0
- package/registry/capabilities.json +1880 -0
- package/schemas/brand-voice.schema.json +24 -0
- package/schemas/capability-registry.schema.json +72 -0
- package/schemas/certification-run.schema.json +130 -0
- package/schemas/demo-recording.schema.json +46 -0
- package/schemas/eval-dataset.schema.json +79 -0
- package/schemas/execution-capability-profile.schema.json +46 -0
- package/schemas/execution-policy.schema.json +114 -0
- package/schemas/improvement-proposal.schema.json +65 -0
- package/schemas/mcp-tool-output.schema.json +61 -0
- package/schemas/platform-capabilities.schema.json +83 -0
- package/schemas/project-config.schema.json +227 -0
- package/schemas/project-demo.schema.json +60 -0
- package/schemas/provider-behavior-matrix.schema.json +91 -0
- package/schemas/scope.schema.json +197 -0
- package/schemas/specialist-trace.schema.json +107 -0
- package/schemas/team.schema.json +99 -0
- package/schemas/unified-registry.schema.json +548 -0
- package/scripts/sync-specialists.mjs +52 -25
- package/specialists/org/specialists/cx-researcher.json +1 -0
- package/specialists/prompts/cx-researcher.md +9 -8
- package/vendor/pandoc-ext/README.md +3 -0
- package/vendor/pandoc-ext/diagram.lua +687 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
diagram – create images and figures from code blocks.
|
|
3
|
+
|
|
4
|
+
See copyright notice in file LICENSE.
|
|
5
|
+
]]
|
|
6
|
+
-- The filter uses the Figure AST element, which was added in pandoc 3.
|
|
7
|
+
PANDOC_VERSION:must_be_at_least '3.0'
|
|
8
|
+
|
|
9
|
+
local version = pandoc.types.Version '1.2.0'
|
|
10
|
+
|
|
11
|
+
-- Report Lua warnings to stderr if the `warn` function is not plugged into
|
|
12
|
+
-- pandoc's logging system.
|
|
13
|
+
if not warn then
|
|
14
|
+
-- fallback
|
|
15
|
+
warn = function(...) io.stderr:write(table.concat({ ... })) end
|
|
16
|
+
elseif PANDOC_VERSION < '3.1.4' then
|
|
17
|
+
-- starting with pandoc 3.1.4, warnings are reported to pandoc's logging
|
|
18
|
+
-- system, so no need to print warnings to stderr.
|
|
19
|
+
warn '@on'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
local io = require 'io'
|
|
23
|
+
local pandoc = require 'pandoc'
|
|
24
|
+
local system = require 'pandoc.system'
|
|
25
|
+
local utils = require 'pandoc.utils'
|
|
26
|
+
local List = require 'pandoc.List'
|
|
27
|
+
local stringify = utils.stringify
|
|
28
|
+
local with_temporary_directory = system.with_temporary_directory
|
|
29
|
+
local with_working_directory = system.with_working_directory
|
|
30
|
+
|
|
31
|
+
--- Returns a filter-specific directory in which cache files can be
|
|
32
|
+
--- stored, or nil if no such directory is available.
|
|
33
|
+
local function cachedir ()
|
|
34
|
+
local cache_home = os.getenv 'XDG_CACHE_HOME'
|
|
35
|
+
if not cache_home or cache_home == '' then
|
|
36
|
+
local user_home = system.os == 'windows'
|
|
37
|
+
and os.getenv 'USERPROFILE'
|
|
38
|
+
or os.getenv 'HOME'
|
|
39
|
+
|
|
40
|
+
if not user_home or user_home == '' then
|
|
41
|
+
return nil
|
|
42
|
+
end
|
|
43
|
+
cache_home = pandoc.path.join{user_home, '.cache'} or nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
-- Create filter cache directory
|
|
47
|
+
return pandoc.path.join{cache_home, 'pandoc-diagram-filter'}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
--- Path holding the image cache, or `nil` if the cache is not used.
|
|
51
|
+
local image_cache = nil
|
|
52
|
+
|
|
53
|
+
local mimetype_for_extension = {
|
|
54
|
+
jpeg = 'image/jpeg',
|
|
55
|
+
jpg = 'image/jpeg',
|
|
56
|
+
pdf = 'application/pdf',
|
|
57
|
+
png = 'image/png',
|
|
58
|
+
svg = 'image/svg+xml',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
local extension_for_mimetype = {
|
|
62
|
+
['application/pdf'] = 'pdf',
|
|
63
|
+
['image/jpeg'] = 'jpg',
|
|
64
|
+
['image/png'] = 'png',
|
|
65
|
+
['image/svg+xml'] = 'svg',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
--- Converts a list of format specifiers to a set of MIME types.
|
|
69
|
+
local function mime_types_set (tbl)
|
|
70
|
+
local set = {}
|
|
71
|
+
local mime_type
|
|
72
|
+
for _, image_format_spec in ipairs(tbl) do
|
|
73
|
+
mime_type = mimetype_for_extension[image_format_spec] or image_format_spec
|
|
74
|
+
set[mime_type] = true
|
|
75
|
+
end
|
|
76
|
+
return set
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
--- Reads the contents of a file.
|
|
80
|
+
local function read_file (filepath)
|
|
81
|
+
local fh = io.open(filepath, 'rb')
|
|
82
|
+
local contents = fh:read('a')
|
|
83
|
+
fh:close()
|
|
84
|
+
return contents
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
--- Writes the contents into a file at the given path.
|
|
88
|
+
local function write_file (filepath, content)
|
|
89
|
+
local fh = io.open(filepath, 'wb')
|
|
90
|
+
fh:write(content)
|
|
91
|
+
fh:close()
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
--- Like `pandoc.pipe`, but allows "multi word" paths:
|
|
95
|
+
-- Supplying a list as the first argument will use the first element as
|
|
96
|
+
-- the executable path and prepend the remaining elements to the list of
|
|
97
|
+
-- arguments.
|
|
98
|
+
local function pipe (command, args, input)
|
|
99
|
+
local cmd
|
|
100
|
+
if pandoc.utils.type(command) == 'List' then
|
|
101
|
+
command = command:map(stringify)
|
|
102
|
+
cmd = command:remove(1)
|
|
103
|
+
args = command .. args
|
|
104
|
+
else
|
|
105
|
+
cmd = stringify(command)
|
|
106
|
+
end
|
|
107
|
+
return pandoc.pipe(cmd, args, input)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
--
|
|
112
|
+
-- Diagram Engines
|
|
113
|
+
--
|
|
114
|
+
|
|
115
|
+
-- PlantUML engine; assumes that there's a `plantuml` binary.
|
|
116
|
+
local plantuml = {
|
|
117
|
+
line_comment_start = [[']],
|
|
118
|
+
mime_types = mime_types_set{'pdf', 'png', 'svg'},
|
|
119
|
+
compile = function (self, puml)
|
|
120
|
+
local mime_type = self.mime_type or 'image/svg+xml'
|
|
121
|
+
-- PlantUML format identifiers correspond to common file extensions.
|
|
122
|
+
local format = extension_for_mimetype[mime_type]
|
|
123
|
+
if not format then
|
|
124
|
+
format, mime_type = 'svg', 'image/svg+xml'
|
|
125
|
+
end
|
|
126
|
+
local args = {'-t' .. format, "-pipe", "-charset", "UTF8"}
|
|
127
|
+
return pipe(self.execpath or 'plantuml', args, puml), mime_type
|
|
128
|
+
end,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
--- GraphViz engine for the dot language
|
|
132
|
+
local graphviz = {
|
|
133
|
+
line_comment_start = '//',
|
|
134
|
+
mime_types = mime_types_set{'jpg', 'pdf', 'png', 'svg'},
|
|
135
|
+
mime_type = 'image/svg+xml',
|
|
136
|
+
compile = function (self, code)
|
|
137
|
+
local mime_type = self.mime_type
|
|
138
|
+
-- GraphViz format identifiers correspond to common file extensions.
|
|
139
|
+
local format = extension_for_mimetype[mime_type]
|
|
140
|
+
if not format then
|
|
141
|
+
format, mime_type = 'svg', 'image/svg+xml'
|
|
142
|
+
end
|
|
143
|
+
return pipe(self.execpath or 'dot', {"-T"..format}, code), mime_type
|
|
144
|
+
end,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
--- Mermaid engine
|
|
148
|
+
local mermaid = {
|
|
149
|
+
line_comment_start = '%%',
|
|
150
|
+
mime_types = mime_types_set{'pdf', 'png', 'svg'},
|
|
151
|
+
compile = function (self, code)
|
|
152
|
+
local override = os.getenv('CONSTRUCT_MERMAID_MIME')
|
|
153
|
+
local mime_type = (override and override ~= '') and override or (self.mime_type or 'image/svg+xml')
|
|
154
|
+
local file_extension = extension_for_mimetype[mime_type]
|
|
155
|
+
return with_temporary_directory("diagram", function (tmpdir)
|
|
156
|
+
return with_working_directory(tmpdir, function ()
|
|
157
|
+
local infile = 'diagram.mmd'
|
|
158
|
+
local outfile = 'diagram.' .. file_extension
|
|
159
|
+
write_file(infile, code)
|
|
160
|
+
local mmdc_args = {"--pdfFit", "--input", infile, "--output", outfile}
|
|
161
|
+
local mermaid_width = os.getenv('CONSTRUCT_MERMAID_WIDTH')
|
|
162
|
+
if mermaid_width and mermaid_width ~= '' then
|
|
163
|
+
table.insert(mmdc_args, 1, '--width=' .. mermaid_width)
|
|
164
|
+
end
|
|
165
|
+
local mermaid_scale = os.getenv('CONSTRUCT_MERMAID_SCALE')
|
|
166
|
+
if mermaid_scale and mermaid_scale ~= '' then
|
|
167
|
+
table.insert(mmdc_args, 1, '--scale=' .. mermaid_scale)
|
|
168
|
+
end
|
|
169
|
+
local pptr_config = os.getenv('CONSTRUCT_MERMAID_PPTR_CONFIG')
|
|
170
|
+
if pptr_config and pptr_config ~= '' then
|
|
171
|
+
table.insert(mmdc_args, 1, pptr_config)
|
|
172
|
+
table.insert(mmdc_args, 1, '--puppeteerConfigFile')
|
|
173
|
+
end
|
|
174
|
+
pipe(
|
|
175
|
+
self.execpath or 'mmdc',
|
|
176
|
+
mmdc_args,
|
|
177
|
+
''
|
|
178
|
+
)
|
|
179
|
+
return read_file(outfile), mime_type
|
|
180
|
+
end)
|
|
181
|
+
end)
|
|
182
|
+
end,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
--- TikZ
|
|
186
|
+
--
|
|
187
|
+
|
|
188
|
+
--- LaTeX template used to compile TikZ images.
|
|
189
|
+
local tikz_template = pandoc.template.compile [[
|
|
190
|
+
\documentclass{standalone}
|
|
191
|
+
\usepackage{tikz}
|
|
192
|
+
$for(header-includes)$
|
|
193
|
+
$it$
|
|
194
|
+
$endfor$
|
|
195
|
+
$additional-packages$
|
|
196
|
+
\begin{document}
|
|
197
|
+
$body$
|
|
198
|
+
\end{document}
|
|
199
|
+
]]
|
|
200
|
+
|
|
201
|
+
--- The TikZ engine uses pdflatex to compile TikZ code to an image
|
|
202
|
+
local tikz = {
|
|
203
|
+
line_comment_start = '%%',
|
|
204
|
+
|
|
205
|
+
mime_types = {
|
|
206
|
+
['application/pdf'] = true,
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
--- Compile LaTeX with TikZ code to an image
|
|
210
|
+
compile = function (self, src, user_opts)
|
|
211
|
+
return with_temporary_directory("tikz", function (tmpdir)
|
|
212
|
+
return with_working_directory(tmpdir, function ()
|
|
213
|
+
-- Define file names:
|
|
214
|
+
local file_template = "%s/tikz-image.%s"
|
|
215
|
+
local tikz_file = file_template:format(tmpdir, "tex")
|
|
216
|
+
local pdf_file = file_template:format(tmpdir, "pdf")
|
|
217
|
+
|
|
218
|
+
-- Treat string values as raw LaTeX
|
|
219
|
+
local meta = {
|
|
220
|
+
['header-includes'] = user_opts['header-includes'],
|
|
221
|
+
['additional-packages'] = {pandoc.RawInline(
|
|
222
|
+
'latex',
|
|
223
|
+
stringify(user_opts['additional-packages'] or '')
|
|
224
|
+
)},
|
|
225
|
+
}
|
|
226
|
+
local tex_code = pandoc.write(
|
|
227
|
+
pandoc.Pandoc({pandoc.RawBlock('latex', src)}, meta),
|
|
228
|
+
'latex',
|
|
229
|
+
{template = tikz_template}
|
|
230
|
+
)
|
|
231
|
+
write_file(tikz_file, tex_code)
|
|
232
|
+
|
|
233
|
+
-- Execute the LaTeX compiler:
|
|
234
|
+
local success, result = pcall(
|
|
235
|
+
pipe,
|
|
236
|
+
self.execpath or 'pdflatex',
|
|
237
|
+
{ '-interaction=nonstopmode', '-output-directory', tmpdir, tikz_file },
|
|
238
|
+
''
|
|
239
|
+
)
|
|
240
|
+
if not success then
|
|
241
|
+
warn(string.format(
|
|
242
|
+
"The call\n%s\nfailed with error code %s. Output:\n%s",
|
|
243
|
+
result.command,
|
|
244
|
+
result.error_code,
|
|
245
|
+
result.output
|
|
246
|
+
))
|
|
247
|
+
end
|
|
248
|
+
return read_file(pdf_file), 'application/pdf'
|
|
249
|
+
end)
|
|
250
|
+
end)
|
|
251
|
+
end
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
--- Asymptote diagram engine
|
|
255
|
+
local asymptote = {
|
|
256
|
+
line_comment_start = '%%',
|
|
257
|
+
mime_types = {
|
|
258
|
+
['application/pdf'] = true,
|
|
259
|
+
},
|
|
260
|
+
compile = function (self, code)
|
|
261
|
+
return with_temporary_directory("asymptote", function(tmpdir)
|
|
262
|
+
return with_working_directory(tmpdir, function ()
|
|
263
|
+
local pdf_file = "pandoc_diagram.pdf"
|
|
264
|
+
local args = {'-tex', 'pdflatex', "-o", "pandoc_diagram", '-'}
|
|
265
|
+
pipe(self.execpath or 'asy', args, code)
|
|
266
|
+
return read_file(pdf_file), 'application/pdf'
|
|
267
|
+
end)
|
|
268
|
+
end)
|
|
269
|
+
end,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
--- Cetz diagram engine
|
|
273
|
+
local cetz = {
|
|
274
|
+
line_comment_start = '%%',
|
|
275
|
+
mime_types = mime_types_set{'jpg', 'pdf', 'png', 'svg'},
|
|
276
|
+
mime_type = 'image/svg+xml',
|
|
277
|
+
compile = function (self, code)
|
|
278
|
+
local mime_type = self.mime_type
|
|
279
|
+
local format = extension_for_mimetype[mime_type]
|
|
280
|
+
if not format then
|
|
281
|
+
format, mime_type = 'svg', 'image/svg+xml'
|
|
282
|
+
end
|
|
283
|
+
local preamble = [[
|
|
284
|
+
#import "@preview/cetz:0.3.4"
|
|
285
|
+
#set page(width: auto, height: auto, margin: .5cm)
|
|
286
|
+
]]
|
|
287
|
+
|
|
288
|
+
local typst_code = preamble .. code
|
|
289
|
+
|
|
290
|
+
return with_temporary_directory("diagram", function (tmpdir)
|
|
291
|
+
return with_working_directory(tmpdir, function ()
|
|
292
|
+
local outfile = 'diagram.' .. format
|
|
293
|
+
local execpath = self.execpath
|
|
294
|
+
if not execpath and quarto and quarto.version >= '1.4' then
|
|
295
|
+
-- fall back to the Typst exec shipped with Quarto.
|
|
296
|
+
execpath = List{'quarto', 'typst'}
|
|
297
|
+
end
|
|
298
|
+
pipe(
|
|
299
|
+
execpath or 'typst',
|
|
300
|
+
{"compile", "-f", format, "-", outfile},
|
|
301
|
+
typst_code
|
|
302
|
+
)
|
|
303
|
+
return read_file(outfile), mime_type
|
|
304
|
+
end)
|
|
305
|
+
end)
|
|
306
|
+
end,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
--- D2 engine for the D2 language
|
|
310
|
+
local d2 = {
|
|
311
|
+
line_comment_start = '#',
|
|
312
|
+
mime_types = mime_types_set{'png', 'svg'},
|
|
313
|
+
|
|
314
|
+
compile = function (self, code, user_opts)
|
|
315
|
+
return with_temporary_directory('diagram', function (tmpdir)
|
|
316
|
+
return with_working_directory(tmpdir, function ()
|
|
317
|
+
-- D2 format identifiers correspond to common file extensions.
|
|
318
|
+
local mime_type = self.mime_type or 'image/svg+xml'
|
|
319
|
+
local file_extension = extension_for_mimetype[mime_type]
|
|
320
|
+
local infile = 'diagram.d2'
|
|
321
|
+
local outfile = 'diagram.' .. file_extension
|
|
322
|
+
|
|
323
|
+
local d2_pad = os.getenv('CONSTRUCT_D2_PAD') or '8'
|
|
324
|
+
local d2_scale = os.getenv('CONSTRUCT_D2_SCALE') or '0.9'
|
|
325
|
+
args = {'--bundle', '--pad=' .. d2_pad, '--scale=' .. d2_scale}
|
|
326
|
+
|
|
327
|
+
local d2_theme = os.getenv('CONSTRUCT_D2_THEME')
|
|
328
|
+
if d2_theme and d2_theme ~= '' then
|
|
329
|
+
table.insert(args, '--theme=' .. d2_theme)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
local d2_sketch = os.getenv('CONSTRUCT_D2_SKETCH')
|
|
333
|
+
if d2_sketch == '1' or d2_sketch == 'true' then
|
|
334
|
+
table.insert(args, '--sketch')
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
d2_user_opts = {
|
|
338
|
+
'layout',
|
|
339
|
+
}
|
|
340
|
+
for _, d2_user_opt in pairs(d2_user_opts) do
|
|
341
|
+
if user_opts[d2_user_opt] then
|
|
342
|
+
table.insert(args, '--' .. d2_user_opt .. '=' .. user_opts[d2_user_opt])
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
table.insert(args, infile)
|
|
347
|
+
table.insert(args, outfile)
|
|
348
|
+
|
|
349
|
+
write_file(infile, code)
|
|
350
|
+
|
|
351
|
+
pipe(self.execpath or 'd2', args, '')
|
|
352
|
+
|
|
353
|
+
return read_file(outfile), mime_type
|
|
354
|
+
end)
|
|
355
|
+
end)
|
|
356
|
+
end,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
local default_engines = {
|
|
360
|
+
asymptote = asymptote,
|
|
361
|
+
dot = graphviz,
|
|
362
|
+
mermaid = mermaid,
|
|
363
|
+
plantuml = plantuml,
|
|
364
|
+
tikz = tikz,
|
|
365
|
+
cetz = cetz,
|
|
366
|
+
d2 = d2,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
--
|
|
370
|
+
-- Configuration
|
|
371
|
+
--
|
|
372
|
+
|
|
373
|
+
--- Options for the output format of the given name.
|
|
374
|
+
local function format_options (name)
|
|
375
|
+
local pdf2svg = name ~= 'latex' and name ~= 'context'
|
|
376
|
+
local is_office_format = name == 'docx' or name == 'odt'
|
|
377
|
+
-- Office formats seem to work better with PNG than with SVG.
|
|
378
|
+
local preferred_mime_types = is_office_format
|
|
379
|
+
and pandoc.List{'image/png', 'application/pdf'}
|
|
380
|
+
or pandoc.List{'application/pdf', 'image/png'}
|
|
381
|
+
-- Prefer SVG for non-PDF output formats, except for Office formats
|
|
382
|
+
if is_office_format then
|
|
383
|
+
preferred_mime_types:insert('image/svg+xml')
|
|
384
|
+
elseif pdf2svg then
|
|
385
|
+
preferred_mime_types:insert(1, 'image/svg+xml')
|
|
386
|
+
end
|
|
387
|
+
return {
|
|
388
|
+
name = name,
|
|
389
|
+
pdf2svg = pdf2svg,
|
|
390
|
+
preferred_mime_types = preferred_mime_types,
|
|
391
|
+
best_mime_type = function (self, supported_mime_types, requested)
|
|
392
|
+
return self.preferred_mime_types:find_if(function (preferred)
|
|
393
|
+
return supported_mime_types[preferred] and
|
|
394
|
+
(not requested or
|
|
395
|
+
(pandoc.utils.type(requested) == 'List' and
|
|
396
|
+
requested:includes(preferred)) or
|
|
397
|
+
(pandoc.utils.type(requested) == 'table' and
|
|
398
|
+
requested[preferred]) or
|
|
399
|
+
|
|
400
|
+
-- Assume string, Inlines, and Blocks values specify the only
|
|
401
|
+
-- acceptable MIME type.
|
|
402
|
+
stringify(requested) == preferred)
|
|
403
|
+
end)
|
|
404
|
+
end
|
|
405
|
+
}
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
--- Returns a configured diagram engine.
|
|
409
|
+
local function get_engine (name, engopts, format)
|
|
410
|
+
local engine = default_engines[name] or
|
|
411
|
+
select(2, pcall(require, stringify(engopts.package)))
|
|
412
|
+
|
|
413
|
+
-- Sanity check
|
|
414
|
+
if not engine then
|
|
415
|
+
warn(PANDOC_SCRIPT_FILE, ": No such engine '", name, "'.")
|
|
416
|
+
return nil
|
|
417
|
+
elseif engopts == false then
|
|
418
|
+
-- engine is disabled
|
|
419
|
+
return nil
|
|
420
|
+
elseif engopts == true then
|
|
421
|
+
-- use default options
|
|
422
|
+
return engine
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
local execpath = engopts.execpath or os.getenv(name:upper() .. '_BIN')
|
|
426
|
+
|
|
427
|
+
local mime_type = format:best_mime_type(
|
|
428
|
+
engine.mime_types,
|
|
429
|
+
engopts['mime-type'] or engopts['mime-types']
|
|
430
|
+
)
|
|
431
|
+
if not mime_type then
|
|
432
|
+
warn(PANDOC_SCRIPT_FILE, ": Cannot use ", name, " with ", format.name)
|
|
433
|
+
return nil
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
execpath = execpath,
|
|
438
|
+
compile = engine.compile,
|
|
439
|
+
line_comment_start = engine.line_comment_start,
|
|
440
|
+
mime_type = mime_type,
|
|
441
|
+
opt = engopts or {},
|
|
442
|
+
}
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
--- Returns the diagram engine configs.
|
|
446
|
+
local function configure (meta, format_name)
|
|
447
|
+
local conf = meta.diagram or {}
|
|
448
|
+
local format = format_options(format_name)
|
|
449
|
+
meta.diagram = nil
|
|
450
|
+
|
|
451
|
+
-- cache for image files
|
|
452
|
+
if conf.cache then
|
|
453
|
+
image_cache = conf['cache-dir']
|
|
454
|
+
and stringify(conf['cache-dir'])
|
|
455
|
+
or cachedir()
|
|
456
|
+
pandoc.system.make_directory(image_cache, true)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
-- engine configs
|
|
460
|
+
local engine = {}
|
|
461
|
+
for name, engopts in pairs(conf.engine or default_engines) do
|
|
462
|
+
engine[name] = get_engine(name, engopts, format)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
engine = engine,
|
|
467
|
+
format = format,
|
|
468
|
+
cache = image_cache and true,
|
|
469
|
+
image_cache = image_cache,
|
|
470
|
+
}
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
--
|
|
474
|
+
-- Format conversion
|
|
475
|
+
--
|
|
476
|
+
|
|
477
|
+
--- Converts a PDF to SVG.
|
|
478
|
+
local pdf2svg = function (imgdata)
|
|
479
|
+
-- Using `os.tmpname()` instead of a hash would be slightly cleaner, but the
|
|
480
|
+
-- function causes problems on Windows (and wasm). See, e.g.,
|
|
481
|
+
-- https://github.com/pandoc-ext/diagram/issues/49
|
|
482
|
+
local pdf_file = 'diagram-' .. pandoc.utils.sha1(imgdata) .. '.pdf'
|
|
483
|
+
write_file(pdf_file, imgdata)
|
|
484
|
+
local args = {
|
|
485
|
+
'--export-type=svg',
|
|
486
|
+
'--export-plain-svg',
|
|
487
|
+
'--export-filename=-',
|
|
488
|
+
pdf_file
|
|
489
|
+
}
|
|
490
|
+
return pandoc.pipe('inkscape', args, ''), os.remove(pdf_file)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
local function properties_from_code (code, comment_start)
|
|
494
|
+
local props = {}
|
|
495
|
+
local pattern = comment_start:gsub('%p', '%%%1') .. '| ' ..
|
|
496
|
+
'([-_%w]+): ([^\n]*)\n'
|
|
497
|
+
for key, value in code:gmatch(pattern) do
|
|
498
|
+
if key == 'fig-cap' then
|
|
499
|
+
props['caption'] = value
|
|
500
|
+
else
|
|
501
|
+
props[key] = value
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
return props
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
local function diagram_options (cb, comment_start)
|
|
508
|
+
local attribs = comment_start
|
|
509
|
+
and properties_from_code(cb.text, comment_start)
|
|
510
|
+
or {}
|
|
511
|
+
for key, value in pairs(cb.attributes) do
|
|
512
|
+
attribs[key] = value
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
local alt
|
|
516
|
+
local caption
|
|
517
|
+
local fig_attr = {id = cb.identifier}
|
|
518
|
+
local filename
|
|
519
|
+
local image_attr = {}
|
|
520
|
+
local user_opt = {}
|
|
521
|
+
|
|
522
|
+
for attr_name, value in pairs(attribs) do
|
|
523
|
+
if attr_name == 'alt' then
|
|
524
|
+
alt = value
|
|
525
|
+
elseif attr_name == 'caption' then
|
|
526
|
+
-- Read caption attribute as Markdown
|
|
527
|
+
caption = attribs.caption
|
|
528
|
+
and pandoc.read(attribs.caption).blocks
|
|
529
|
+
or nil
|
|
530
|
+
elseif attr_name == 'filename' then
|
|
531
|
+
filename = value
|
|
532
|
+
elseif attr_name == 'label' then
|
|
533
|
+
fig_attr.id = value
|
|
534
|
+
elseif attr_name == 'name' then
|
|
535
|
+
fig_attr.name = value
|
|
536
|
+
else
|
|
537
|
+
-- Check for prefixed attributes
|
|
538
|
+
local prefix, key = attr_name:match '^(%a+)%-(%a[-%w]*)$'
|
|
539
|
+
if prefix == 'fig' then
|
|
540
|
+
fig_attr[key] = value
|
|
541
|
+
elseif prefix == 'image' or prefix == 'img' then
|
|
542
|
+
image_attr[key] = value
|
|
543
|
+
elseif prefix == 'opt' then
|
|
544
|
+
user_opt[key] = value
|
|
545
|
+
else
|
|
546
|
+
-- Use as image attribute
|
|
547
|
+
image_attr[attr_name] = value
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
['alt'] = alt or
|
|
554
|
+
(caption and pandoc.utils.blocks_to_inlines(caption)) or
|
|
555
|
+
{},
|
|
556
|
+
['caption'] = caption,
|
|
557
|
+
['fig-attr'] = fig_attr,
|
|
558
|
+
['filename'] = filename,
|
|
559
|
+
['image-attr'] = image_attr,
|
|
560
|
+
['opt'] = user_opt,
|
|
561
|
+
}
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
local function get_cached_image (hash, mime_type)
|
|
565
|
+
if not image_cache then
|
|
566
|
+
return nil
|
|
567
|
+
end
|
|
568
|
+
local filename = hash .. '.' .. extension_for_mimetype[mime_type]
|
|
569
|
+
local imgpath = pandoc.path.join{image_cache, filename}
|
|
570
|
+
local success, imgdata = pcall(read_file, imgpath)
|
|
571
|
+
if success then
|
|
572
|
+
return imgdata, mime_type
|
|
573
|
+
end
|
|
574
|
+
return nil
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
local function cache_image (codeblock, imgdata, mimetype)
|
|
578
|
+
-- do nothing if caching is disabled or not possible.
|
|
579
|
+
if not image_cache then
|
|
580
|
+
return
|
|
581
|
+
end
|
|
582
|
+
local ext = extension_for_mimetype[mimetype]
|
|
583
|
+
local filename = pandoc.sha1(codeblock.text) .. '.' .. ext
|
|
584
|
+
local imgpath = pandoc.path.join{image_cache, filename}
|
|
585
|
+
write_file(imgpath, imgdata)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
-- Executes each document's code block to find matching code blocks:
|
|
589
|
+
local function code_to_figure (conf)
|
|
590
|
+
return function (block)
|
|
591
|
+
-- Check if a converter exists for this block. If not, return the block
|
|
592
|
+
-- unchanged.
|
|
593
|
+
local diagram_type = block.classes[1]
|
|
594
|
+
if not diagram_type then
|
|
595
|
+
return nil
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
local engine = conf.engine[diagram_type]
|
|
599
|
+
if not engine then
|
|
600
|
+
return nil
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
-- Unified properties.
|
|
604
|
+
local dgr_opt = diagram_options(block, engine.line_comment_start)
|
|
605
|
+
for optname, value in pairs(engine.opt or {}) do
|
|
606
|
+
dgr_opt.opt[optname] = dgr_opt.opt[optname] or value
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
local run_pdf2svg = engine.mime_type == 'application/pdf'
|
|
610
|
+
and conf.format.pdf2svg
|
|
611
|
+
|
|
612
|
+
-- Try to retrieve the image data from the cache.
|
|
613
|
+
local imgdata, imgtype
|
|
614
|
+
if conf.cache then
|
|
615
|
+
imgdata, imgtype = get_cached_image(
|
|
616
|
+
pandoc.sha1(block.text),
|
|
617
|
+
run_pdf2svg and 'image/svg+xml' or engine.mime_type
|
|
618
|
+
)
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
if not imgdata or not imgtype then
|
|
622
|
+
-- No cached image; call the converter
|
|
623
|
+
local success
|
|
624
|
+
success, imgdata, imgtype =
|
|
625
|
+
pcall(engine.compile, engine, block.text, dgr_opt.opt)
|
|
626
|
+
|
|
627
|
+
-- Bail if an error occurred; imgdata contains the error message
|
|
628
|
+
-- when that happens.
|
|
629
|
+
if not success then
|
|
630
|
+
warn(PANDOC_SCRIPT_FILE, ': ', tostring(imgdata))
|
|
631
|
+
return nil
|
|
632
|
+
elseif not imgdata then
|
|
633
|
+
warn(PANDOC_SCRIPT_FILE, ': Diagram engine returned no image data.')
|
|
634
|
+
return nil
|
|
635
|
+
elseif not imgtype then
|
|
636
|
+
warn(PANDOC_SCRIPT_FILE, ': Diagram engine did not return a MIME type.')
|
|
637
|
+
return nil
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
-- Convert SVG if necessary.
|
|
641
|
+
if imgtype == 'application/pdf' and conf.format.pdf2svg then
|
|
642
|
+
imgdata, imgtype = pdf2svg(imgdata), 'image/svg+xml'
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
-- If we got here, then the transformation went ok and `img` contains
|
|
646
|
+
-- the image data.
|
|
647
|
+
cache_image(block, imgdata, imgtype)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
-- Use the block's filename attribute or create a new name by hashing the
|
|
651
|
+
-- image content.
|
|
652
|
+
local basename, _extension = pandoc.path.split_extension(
|
|
653
|
+
dgr_opt.filename or pandoc.sha1(imgdata)
|
|
654
|
+
)
|
|
655
|
+
local fname = basename .. '.' .. extension_for_mimetype[imgtype]
|
|
656
|
+
|
|
657
|
+
-- Store the data in the media bag:
|
|
658
|
+
pandoc.mediabag.insert(fname, imgtype, imgdata)
|
|
659
|
+
|
|
660
|
+
-- Create the image object.
|
|
661
|
+
local image = pandoc.Image(dgr_opt.alt, fname, "", dgr_opt['image-attr'])
|
|
662
|
+
|
|
663
|
+
-- Create a figure if the diagram has a caption; otherwise return
|
|
664
|
+
-- just the image.
|
|
665
|
+
return dgr_opt.caption and
|
|
666
|
+
pandoc.Figure(
|
|
667
|
+
pandoc.Plain{image},
|
|
668
|
+
dgr_opt.caption,
|
|
669
|
+
dgr_opt['fig-attr']
|
|
670
|
+
) or
|
|
671
|
+
pandoc.Plain{image}
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
return setmetatable(
|
|
676
|
+
{{
|
|
677
|
+
Pandoc = function (doc)
|
|
678
|
+
local conf = configure(doc.meta, FORMAT)
|
|
679
|
+
return doc:walk {
|
|
680
|
+
CodeBlock = code_to_figure(conf),
|
|
681
|
+
}
|
|
682
|
+
end
|
|
683
|
+
}},
|
|
684
|
+
{
|
|
685
|
+
version = version,
|
|
686
|
+
}
|
|
687
|
+
)
|