@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.
Files changed (63) hide show
  1. package/bin/construct +2 -1
  2. package/bin/construct-postinstall.mjs +27 -2
  3. package/config/tag-vocabulary.json +264 -0
  4. package/lib/config/schema.mjs +1 -1
  5. package/lib/doctor/diagnosis.mjs +20 -0
  6. package/lib/doctor/index.mjs +5 -0
  7. package/lib/embed/worker.mjs +5 -0
  8. package/lib/env-config.mjs +10 -2
  9. package/lib/export-validate.mjs +34 -2
  10. package/lib/host/readiness.mjs +109 -0
  11. package/lib/host-disposition.mjs +10 -1
  12. package/lib/install/stage-project.mjs +41 -4
  13. package/lib/intake/prepare.mjs +2 -0
  14. package/lib/mcp/destructive-approval.mjs +57 -0
  15. package/lib/mcp/server.mjs +208 -34
  16. package/lib/mcp/tool-rate-limit.mjs +47 -0
  17. package/lib/mcp/tool-safety.mjs +94 -0
  18. package/lib/mcp/tool-surface-parity.mjs +60 -0
  19. package/lib/mcp/tools/orchestration-run.mjs +45 -7
  20. package/lib/mcp/tools/project.mjs +25 -8
  21. package/lib/mcp/tools/storage.mjs +9 -1
  22. package/lib/mcp/tools/web-search-governance.mjs +96 -0
  23. package/lib/mcp/tools/web-search.mjs +5 -76
  24. package/lib/mcp-platform-config.mjs +27 -18
  25. package/lib/oracle/daemon-entry.mjs +6 -0
  26. package/lib/orchestration/runtime.mjs +81 -42
  27. package/lib/orchestration/web-capability.mjs +59 -0
  28. package/lib/orchestration/worker.mjs +263 -19
  29. package/lib/output-quality.mjs +61 -2
  30. package/lib/path-policy.mjs +56 -0
  31. package/lib/providers/secret-audit-wiring.mjs +35 -18
  32. package/lib/providers/secret-resolver.mjs +28 -13
  33. package/lib/registry/catalog.mjs +6 -0
  34. package/lib/registry/loader.mjs +7 -1
  35. package/lib/registry/validate.mjs +3 -3
  36. package/lib/sandbox.mjs +1 -1
  37. package/lib/service-manager.mjs +59 -9
  38. package/lib/storage/admin.mjs +7 -2
  39. package/package.json +6 -1
  40. package/registry/agent-manifest.json +117 -0
  41. package/registry/capabilities.json +1880 -0
  42. package/schemas/brand-voice.schema.json +24 -0
  43. package/schemas/capability-registry.schema.json +72 -0
  44. package/schemas/certification-run.schema.json +130 -0
  45. package/schemas/demo-recording.schema.json +46 -0
  46. package/schemas/eval-dataset.schema.json +79 -0
  47. package/schemas/execution-capability-profile.schema.json +46 -0
  48. package/schemas/execution-policy.schema.json +114 -0
  49. package/schemas/improvement-proposal.schema.json +65 -0
  50. package/schemas/mcp-tool-output.schema.json +61 -0
  51. package/schemas/platform-capabilities.schema.json +83 -0
  52. package/schemas/project-config.schema.json +227 -0
  53. package/schemas/project-demo.schema.json +60 -0
  54. package/schemas/provider-behavior-matrix.schema.json +91 -0
  55. package/schemas/scope.schema.json +197 -0
  56. package/schemas/specialist-trace.schema.json +107 -0
  57. package/schemas/team.schema.json +99 -0
  58. package/schemas/unified-registry.schema.json +548 -0
  59. package/scripts/sync-specialists.mjs +52 -25
  60. package/specialists/org/specialists/cx-researcher.json +1 -0
  61. package/specialists/prompts/cx-researcher.md +9 -8
  62. package/vendor/pandoc-ext/README.md +3 -0
  63. 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
+ )