Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/core/handlers/base.ts
3583 views
1
/*
2
* base.ts
3
*
4
* Copyright (C) 2022 Posit Software, PBC
5
*/
6
7
import {
8
HandlerContextResults,
9
IncludeState,
10
LanguageCellHandlerContext,
11
LanguageCellHandlerOptions,
12
LanguageHandler,
13
PandocIncludeType,
14
} from "./types.ts";
15
import { breakQuartoMd, QuartoMdCell } from "../lib/break-quarto-md.ts";
16
import { mergeConfigs } from "../config.ts";
17
import { FormatDependency, kDependencies } from "../../config/types.ts";
18
import {
19
asMappedString,
20
join as mappedJoin,
21
mappedConcat,
22
mappedLines,
23
MappedString,
24
mappedTrim,
25
} from "../lib/mapped-text.ts";
26
import {
27
addLanguageComment,
28
optionCommentPatternFromLanguage,
29
} from "../lib/partition-cell-options.ts";
30
import { ConcreteSchema } from "../lib/yaml-schema/types.ts";
31
import {
32
pandocCode,
33
pandocDiv,
34
pandocList,
35
pandocRawStr,
36
} from "../pandoc/codegen.ts";
37
38
import {
39
kCapLoc,
40
kCellClasses,
41
kCellColumn,
42
kCellFigAlign,
43
kCellFigAlt,
44
kCellFigCap,
45
kCellFigColumn,
46
kCellFigEnv,
47
kCellFigLink,
48
kCellFigPos,
49
kCellFigScap,
50
kCellFigSubCap,
51
kCellLabel,
52
kCellLstCap,
53
kCellLstLabel,
54
kCellPanel,
55
kCellTblColumn,
56
kCodeFold,
57
kCodeLineNumbers,
58
kCodeOverflow,
59
kCodeSummary,
60
kEcho,
61
kFigAlign,
62
kFigCapLoc,
63
kLayout,
64
kLayoutNcol,
65
kLayoutNrow,
66
kOutput,
67
kTblCapLoc,
68
} from "../../config/constants.ts";
69
import { DirectiveCell } from "../lib/break-quarto-md-types.ts";
70
import {
71
basename,
72
dirname,
73
join,
74
relative,
75
resolve,
76
} from "../../deno_ral/path.ts";
77
import { figuresDir, inputFilesDir } from "../render.ts";
78
import { ensureDirSync } from "../../deno_ral/fs.ts";
79
import { mappedStringFromFile } from "../mapped-text.ts";
80
import { error } from "../../deno_ral/log.ts";
81
import { withCriClient } from "../cri/cri.ts";
82
import { normalizePath } from "../path.ts";
83
import {
84
InvalidShortcodeError,
85
isBlockShortcode,
86
} from "../lib/parse-shortcode.ts";
87
import { standaloneInclude } from "./include-standalone.ts";
88
import { LocalizedError } from "../lib/located-error.ts";
89
90
const handlers: Record<string, LanguageHandler> = {};
91
92
let globalFigureCounter: Record<string, number> = {};
93
94
export function resetFigureCounter() {
95
globalFigureCounter = {};
96
}
97
98
function makeHandlerContext(
99
options: LanguageCellHandlerOptions,
100
): {
101
context: LanguageCellHandlerContext;
102
results?: HandlerContextResults;
103
} {
104
if (options.state === undefined) {
105
// we mutate the parameter here so this works with the sharing of options
106
// in nested handler context calls (which can arise when handling directives)
107
options.state = {};
108
}
109
const results: HandlerContextResults = {
110
resourceFiles: [],
111
includes: {},
112
extras: {},
113
supporting: [],
114
};
115
const tempContext = options.temp;
116
const context: LanguageCellHandlerContext = {
117
options,
118
119
getState(): Record<string, unknown> {
120
if (options.state![options.name] === undefined) {
121
options.state![options.name] = {};
122
}
123
return options.state![options.name];
124
},
125
126
async extractHtml(opts: {
127
html: string;
128
selector: string;
129
resources?: [string, string][];
130
}): Promise<string[]> {
131
const {
132
html: content,
133
selector,
134
} = opts;
135
const nonEmptyHtmlResources: [string, string][] = opts.resources ||
136
[];
137
const dirName = context.options.temp.createDir();
138
// create temporary resources
139
for (const [name, content] of nonEmptyHtmlResources) {
140
Deno.writeTextFileSync(join(dirName, name), content);
141
}
142
const fileName = join(dirName, "index.html");
143
Deno.writeTextFileSync(fileName, content);
144
const url = `file://${fileName}`;
145
146
return await withCriClient(async (client) => {
147
await client.open(url);
148
return await client.contents(selector);
149
});
150
},
151
152
async createPngsFromHtml(opts: {
153
prefix: string;
154
html: string;
155
deviceScaleFactor: number;
156
selector: string;
157
resources?: [string, string][];
158
}): Promise<{
159
filenames: string[];
160
elements: string[];
161
}> {
162
const {
163
prefix,
164
html: content,
165
deviceScaleFactor,
166
selector,
167
} = opts;
168
const nonEmptyHtmlResources: [string, string][] = opts.resources ||
169
[];
170
const dirName = context.options.temp.createDir();
171
172
// create temporary resources
173
for (const [name, content] of nonEmptyHtmlResources) {
174
Deno.writeTextFileSync(join(dirName, name), content);
175
}
176
const fileName = join(dirName, "index.html");
177
Deno.writeTextFileSync(fileName, content);
178
const url = `file://${fileName}`;
179
180
const { elements, images } = await withCriClient(async (client) => {
181
await client.open(url);
182
const elements = await client.contents(selector);
183
const screenshots = await client.screenshots(
184
selector,
185
deviceScaleFactor,
186
);
187
return {
188
elements,
189
images: screenshots.map((x) => x.data),
190
};
191
});
192
193
// write figures to disk
194
const sourceNames: string[] = [];
195
196
for (let i = 0; i < images.length; ++i) {
197
const { sourceName, fullName } = context
198
.uniqueFigureName(prefix, ".png");
199
sourceNames.push(sourceName);
200
Deno.writeFileSync(fullName, images[i]);
201
}
202
203
return {
204
filenames: sourceNames,
205
elements,
206
};
207
},
208
209
cellContent(cell: QuartoMdCell): MappedString {
210
if (typeof cell?.options?.file === "string") {
211
// FIXME this file location won't be changed under include fixups...
212
try {
213
return mappedStringFromFile(
214
context.resolvePath(cell?.options?.file),
215
);
216
} catch (e) {
217
error(`Couldn't open file ${cell?.options?.file}`);
218
throw e;
219
}
220
} else {
221
return cell.source;
222
}
223
},
224
resolvePath(path: string): string {
225
const sourceDir = dirname(options.context.target.source);
226
const rootDir = options.context.project.isSingleFile
227
? sourceDir
228
: options.context.project.dir;
229
if (path.startsWith("/")) {
230
// it's a root-relative path
231
return resolve(rootDir, `.${path}`);
232
} else {
233
// it's a relative path
234
return resolve(sourceDir, path);
235
}
236
},
237
uniqueFigureName(prefix?: string, extension?: string) {
238
prefix = prefix ?? "figure-";
239
extension = extension ?? ".png";
240
241
if (!globalFigureCounter[prefix]) {
242
globalFigureCounter[prefix] = 1;
243
} else {
244
globalFigureCounter[prefix]++;
245
}
246
247
const pngName = `${prefix}${globalFigureCounter[prefix]}${extension}`;
248
const tempName = join(context.figuresDir(), pngName);
249
const baseDir = dirname(options.context.target.source);
250
const mdName = relative(baseDir, tempName);
251
252
this.addSupporting(relative(baseDir, context.figuresDir()));
253
254
return {
255
baseName: basename(mdName),
256
sourceName: mdName,
257
fullName: tempName,
258
};
259
},
260
figuresDir() {
261
const file = normalizePath(options.context.target.source);
262
const filesDir = join(dirname(file), inputFilesDir(file));
263
const result = join(
264
filesDir,
265
figuresDir(context.options.format.pandoc.to),
266
);
267
ensureDirSync(result);
268
return result;
269
},
270
addHtmlDependency(
271
dep: FormatDependency,
272
) {
273
if (results.extras.html === undefined) {
274
results.extras.html = { [kDependencies]: [dep] };
275
} else {
276
results.extras.html[kDependencies]!.push(dep);
277
}
278
},
279
addSupporting(dir: string) {
280
if (results.supporting.indexOf(dir) === -1) {
281
results.supporting.push(dir);
282
}
283
},
284
addResource(fileName: string) {
285
results.resourceFiles.push(fileName);
286
},
287
addInclude(content: string, where: PandocIncludeType) {
288
const fileName = tempContext.createFile();
289
Deno.writeTextFileSync(fileName, content);
290
if (results.includes[where] === undefined) {
291
results.includes[where] = [fileName];
292
} else {
293
results.includes[where]!.push(fileName);
294
}
295
},
296
};
297
298
return { context, results };
299
}
300
301
// return cell language handler only
302
export function languages(): string[] {
303
const cellLanguage = [];
304
for (const [k, v] of Object.entries(handlers)) {
305
if (v.type === "cell") {
306
cellLanguage.push(k);
307
}
308
}
309
return cellLanguage;
310
}
311
312
export async function languageSchema(
313
language: string,
314
): Promise<ConcreteSchema | undefined> {
315
if (handlers[language] === undefined) {
316
return undefined;
317
}
318
const call = handlers[language].schema;
319
if (call === undefined) {
320
return undefined;
321
} else {
322
return (await call());
323
}
324
}
325
326
export function install(handler: LanguageHandler) {
327
const language = handler.languageName;
328
handlers[language] = handler;
329
if (handler.comment !== undefined) {
330
addLanguageComment(language, handler.comment);
331
}
332
}
333
334
const processMarkdownIncludes = async (
335
newCells: MappedString[],
336
options: LanguageCellHandlerOptions,
337
filename?: string,
338
) => {
339
const includeHandler = makeHandlerContext(options);
340
341
if (!includeHandler.context.options.state) {
342
includeHandler.context.options.state = {};
343
}
344
if (!includeHandler.context.options.state.include) {
345
includeHandler.context.options.state.include = {
346
includes: [],
347
};
348
}
349
const includeState = includeHandler.context.options
350
.state
351
.include as IncludeState;
352
353
// search for include shortcodes in the cell content
354
for (let i = 0; i < newCells.length; ++i) {
355
if (
356
newCells[i].value.search(/\s*```\s*{\s*shortcodes\s*=\s*false\s*}/) !== -1
357
) {
358
continue;
359
}
360
const lines = mappedLines(newCells[i], true);
361
let foundShortcodes = false;
362
for (let j = 0; j < lines.length; ++j) {
363
try {
364
const shortcode = isBlockShortcode(lines[j].value);
365
if (shortcode && shortcode.name === "include") {
366
foundShortcodes = true;
367
const param = shortcode.params[0];
368
if (!param) {
369
throw new Error("Include directive needs filename as a parameter");
370
}
371
if (filename) {
372
includeState.includes.push({ source: filename, target: param });
373
}
374
lines[j] = await standaloneInclude(includeHandler.context, param);
375
}
376
} catch (e) {
377
if (e instanceof InvalidShortcodeError) {
378
const mapResult = newCells[i].map(newCells[i].value.indexOf("{"));
379
throw new LocalizedError(
380
"Invalid Shortcode",
381
e.message,
382
mapResult!.originalString,
383
mapResult!.index,
384
);
385
} else {
386
throw e;
387
}
388
}
389
}
390
if (foundShortcodes) {
391
newCells[i] = mappedConcat(lines);
392
}
393
}
394
};
395
396
export async function expandIncludes(
397
markdown: MappedString,
398
options: LanguageCellHandlerOptions,
399
filename: string,
400
): Promise<MappedString> {
401
const mdCells = (await breakQuartoMd(markdown, false)).cells;
402
if (mdCells.length === 0) {
403
return markdown;
404
}
405
const newCells: MappedString[] = [];
406
for (let i = 0; i < mdCells.length; ++i) {
407
const cell = mdCells[i];
408
newCells.push(cell.sourceVerbatim);
409
}
410
411
await processMarkdownIncludes(newCells, options, filename);
412
return mappedJoin(newCells, "");
413
}
414
415
export async function handleLanguageCells(
416
options: LanguageCellHandlerOptions,
417
): Promise<{
418
markdown: MappedString;
419
results?: HandlerContextResults;
420
}> {
421
const mdCells = (await breakQuartoMd(options.markdown, false))
422
.cells;
423
424
if (mdCells.length === 0) {
425
return {
426
markdown: options.markdown,
427
};
428
}
429
430
const newCells: MappedString[] = [];
431
const languageCellsPerLanguage: Record<
432
string,
433
{ index: number; source: QuartoMdCell }[]
434
> = {};
435
436
for (let i = 0; i < mdCells.length; ++i) {
437
const cell = mdCells[i];
438
newCells.push(cell.sourceVerbatim);
439
if (
440
cell.cell_type === "raw" ||
441
cell.cell_type === "markdown"
442
) {
443
continue;
444
}
445
const language = cell.cell_type.language;
446
if (language !== "_directive" && handlers[language] === undefined) {
447
continue;
448
}
449
if (
450
handlers[language]?.stage &&
451
handlers[language].stage !== "any" &&
452
options.stage !== handlers[language].stage
453
) {
454
continue;
455
}
456
if (languageCellsPerLanguage[language] === undefined) {
457
languageCellsPerLanguage[language] = [];
458
}
459
languageCellsPerLanguage[language].push({
460
index: i,
461
source: cell,
462
});
463
}
464
let results: HandlerContextResults | undefined = undefined;
465
466
for (const [language, cells] of Object.entries(languageCellsPerLanguage)) {
467
if (language === "_directive") {
468
// if this is a directive, the semantics are that each the _contents_ of the cell
469
// are first treated as if they were an entire markdown document that will be fully
470
// parsed/handled etc. The _resulting_ markdown is then sent for handling by the
471
// directive handler
472
for (const cell of cells) {
473
const directiveCellType = cell.source.cell_type as DirectiveCell;
474
const innerLanguage = directiveCellType.name;
475
const innerLanguageHandler = handlers[innerLanguage]!;
476
477
if (
478
innerLanguageHandler &&
479
(innerLanguageHandler.stage !== "any" &&
480
innerLanguageHandler.stage !== options.stage)
481
) { // we're in the wrong stage, so we don't actually do anything
482
continue;
483
}
484
if (
485
innerLanguageHandler === undefined ||
486
innerLanguageHandler.type === "cell"
487
) {
488
// if no handler is present (or a directive was included for something
489
// that responds to cells instead), we're a no-op
490
continue;
491
}
492
if (innerLanguageHandler.directive === undefined) {
493
throw new Error(
494
"Bad language handler: directive callback is undefined",
495
);
496
}
497
498
// call specific handler
499
const innerHandler = makeHandlerContext({
500
...options,
501
name: innerLanguage,
502
});
503
504
newCells[cell.index] = asMappedString(
505
await innerLanguageHandler.directive(
506
innerHandler.context,
507
directiveCellType,
508
),
509
);
510
511
results = mergeConfigs(results, innerHandler.results);
512
}
513
} else {
514
const handler = makeHandlerContext({
515
...options,
516
name: language,
517
});
518
const languageHandler = handlers[language];
519
if (
520
languageHandler !== undefined &&
521
languageHandler.type !== "directive"
522
) {
523
const transformedCells = await languageHandler.document(
524
handler.context,
525
cells.map((x) => x.source),
526
);
527
for (let i = 0; i < transformedCells.length; ++i) {
528
newCells[cells[i].index] = transformedCells[i];
529
}
530
if (results === undefined) {
531
results = handler.results;
532
} else {
533
results = mergeConfigs(results, handler.results);
534
}
535
}
536
}
537
}
538
539
// now handle the markdown content. This is necessary specifically for
540
// include shortcodes that can still be hiding inside of code blocks
541
await processMarkdownIncludes(newCells, options);
542
543
return {
544
markdown: mappedJoin(newCells, ""),
545
results,
546
};
547
}
548
549
export const baseHandler: LanguageHandler = {
550
type: "any",
551
stage: "any",
552
553
languageName:
554
"<<<< baseHandler: languageName should have been overridden >>>>",
555
556
defaultOptions: {
557
echo: true,
558
},
559
560
async document(
561
handlerContext: LanguageCellHandlerContext,
562
cells: QuartoMdCell[],
563
): Promise<MappedString[]> {
564
this.documentStart(handlerContext);
565
const mermaidExecute =
566
handlerContext.options.format.mergeAdditionalFormats!(
567
{
568
execute: this.defaultOptions,
569
},
570
).execute;
571
const result = await Promise.all(cells.map((cell) => {
572
return this.cell(
573
handlerContext,
574
cell,
575
mergeConfigs(
576
mermaidExecute as Record<string, unknown>,
577
cell.options ?? {},
578
),
579
);
580
}));
581
this.documentEnd(handlerContext);
582
return result;
583
},
584
585
// called once per document at the start of processing
586
documentStart(
587
_handlerContext: LanguageCellHandlerContext,
588
) {
589
},
590
591
// called once per document at the end of processing
592
documentEnd(
593
_handlerContext: LanguageCellHandlerContext,
594
) {
595
},
596
597
cell(
598
_handlerContext: LanguageCellHandlerContext,
599
cell: QuartoMdCell,
600
_options: Record<string, unknown>,
601
): Promise<MappedString> {
602
return Promise.resolve(cell.sourceVerbatim);
603
},
604
605
// FIXME attributes we're not handling yet:
606
// - code-summary
607
// - code-overflow
608
// - code-line-numbers
609
//
610
// FIXME how do we set up support for:
611
// - things that include subfigures, like tables?
612
//
613
// FIXME how should we interpret the difference between output and eval
614
// here?
615
616
build(
617
handlerContext: LanguageCellHandlerContext,
618
cell: QuartoMdCell,
619
content: MappedString,
620
options: Record<string, unknown>, // these include handler options
621
extraCellOptions?: Record<string, unknown>, // these will be passed directly to getDivAttributes
622
skipOptions?: Set<string>, // these will _not_ be serialized in the cell even if they're in the options
623
): MappedString {
624
// split content into front matter vs input
625
const contentLines = mappedLines(cell.sourceWithYaml!, true);
626
const frontMatterLines: MappedString[] = [];
627
const commentPattern = optionCommentPatternFromLanguage(this.languageName);
628
let inputIndex = 0;
629
for (const contentLine of contentLines) {
630
const commentMatch = contentLine.value.match(commentPattern);
631
if (commentMatch) {
632
if (contentLine.value.indexOf("echo: fenced") === -1) {
633
frontMatterLines.push(contentLine);
634
}
635
++inputIndex;
636
} else {
637
break;
638
}
639
}
640
const inputLines = contentLines.slice(inputIndex);
641
const { classes, attrs } = getDivAttributes({
642
...({
643
[kFigAlign]: handlerContext.options.format.render[kFigAlign],
644
}),
645
...(extraCellOptions || {}),
646
...cell.options,
647
}, skipOptions);
648
649
const hasAttribute = (attrKey: string) =>
650
attrs.some((attr) => attr === attrKey || attr.startsWith(`${attrKey}=`));
651
652
const hasLayoutAttributes = hasAttribute(kLayoutNrow) ||
653
hasAttribute(kLayoutNcol) || hasAttribute(kLayout);
654
const isPowerpointOutput = handlerContext.options.format.pandoc.to
655
?.startsWith("pptx");
656
657
const unrolledOutput = isPowerpointOutput && !hasLayoutAttributes;
658
659
const cellBlock = unrolledOutput
660
? pandocList({ skipFirstLineBreak: true })
661
: pandocDiv({
662
classes: ["cell", ...classes],
663
attrs,
664
});
665
666
const languageClass: string = this.languageClass === undefined
667
? this.languageName
668
: (typeof this.languageClass === "string"
669
? this.languageClass
670
: this.languageClass(handlerContext.options));
671
672
const cellInputClasses = [
673
languageClass,
674
"cell-code",
675
...((options["class-source"] as (string[] | undefined)) ?? []),
676
];
677
const cellInputAttrs: string[] = [
678
...((options["attr-source"] as (string[] | undefined)) ?? []),
679
];
680
const cellOutputClasses = [
681
"cell-output-display",
682
...((options["class-output"] as (string[] | undefined)) ?? []),
683
];
684
const cellOutputAttrs: string[] = [
685
...((options["attr-output"] as (string[] | undefined)) ?? []),
686
];
687
688
if (options[kCodeFold] !== undefined) {
689
cellInputAttrs.push(`code-fold="${options[kCodeFold]}"`);
690
}
691
692
switch (options.echo) {
693
case true: {
694
const cellInput = pandocCode({
695
classes: cellInputClasses,
696
attrs: cellInputAttrs,
697
});
698
cellInput.push(pandocRawStr(mappedTrim(mappedConcat(inputLines))));
699
cellBlock.push(cellInput);
700
break;
701
}
702
case "fenced": {
703
const cellInput = pandocCode({
704
classes: ["markdown", ...cellInputClasses.slice(1)], // replace the language class with markdown
705
attrs: cellInputAttrs,
706
});
707
const cellFence = pandocCode({
708
language: this.languageName,
709
skipFirstLineBreak: true,
710
});
711
const fencedInput = mappedConcat([
712
...frontMatterLines,
713
...inputLines,
714
]);
715
cellFence.push(pandocRawStr(mappedTrim(fencedInput)));
716
cellInput.push(cellFence);
717
cellBlock.push(cellInput);
718
break;
719
}
720
}
721
722
const divBlock = pandocDiv;
723
724
// PandocNodes ignore self-pushes (n.push(n))
725
// this makes it much easier to write the logic around "unrolled blocks"
726
const cellOutputDiv = unrolledOutput ? cellBlock : divBlock({
727
// id: cell.options?.label as (string | undefined),
728
attrs: cellOutputAttrs,
729
classes: cellOutputClasses,
730
});
731
732
cellBlock.push(cellOutputDiv);
733
734
const figureLikeOptions: Record<string, unknown> = {};
735
if (typeof cell.options?.label === "string") {
736
figureLikeOptions.id = cell.options?.label;
737
}
738
const figureLike = unrolledOutput ? cellBlock : divBlock(figureLikeOptions);
739
const cellOutput = unrolledOutput ? cellBlock : divBlock();
740
741
figureLike.push(cellOutput);
742
cellOutputDiv.push(figureLike);
743
744
if (options.eval === true) {
745
cellOutput.push(pandocRawStr(content));
746
}
747
if (cell.options?.[kCellFigCap]) {
748
// this is a hack to get around that if we have a figure caption but no label,
749
// nothing in our pipeline will recognize the caption and emit
750
// a figcaption element.
751
//
752
// necessary for https://github.com/quarto-dev/quarto-cli/issues/4376
753
let capOpen = "", capClose = "";
754
if (cell.options?.label === undefined) {
755
capOpen = "`<figcaption>`{=html} ";
756
capClose = "`</figcaption>`{=html} ";
757
}
758
759
figureLike.push(
760
pandocRawStr(
761
`\n\n${capOpen}${cell.options[kCellFigCap] as string}${capClose}`,
762
),
763
);
764
}
765
if (cell.options?.label === undefined) {
766
figureLike.unshift(pandocRawStr("`<figure class=''>`{=html}\n"));
767
figureLike.push(pandocRawStr("`</figure>`{=html}\n"));
768
}
769
770
return cellBlock.mappedString();
771
},
772
};
773
774
export function getDivAttributes(
775
options: Record<string, unknown>,
776
forceSkip?: Set<string>,
777
): { attrs: string[]; classes: string[] } {
778
const attrs: string[] = [];
779
if (forceSkip) {
780
options = { ...options };
781
for (const skip of forceSkip) {
782
delete options[skip];
783
}
784
}
785
786
const keysToNotSerialize = new Set([
787
kEcho,
788
kCellLabel,
789
kCellFigCap,
790
kCellFigSubCap,
791
kCellFigScap,
792
kCapLoc,
793
kFigCapLoc,
794
kTblCapLoc,
795
kCellFigColumn,
796
kCellTblColumn,
797
kCellFigLink,
798
kCellFigAlign,
799
kCellFigEnv,
800
kCellFigPos,
801
kCellFigAlt, // FIXME see if it's possible to do this right wrt accessibility
802
kOutput,
803
kCellLstCap,
804
kCellLstLabel,
805
kCodeFold,
806
kCodeLineNumbers,
807
kCodeSummary,
808
kCodeOverflow,
809
kCellClasses,
810
kCellPanel,
811
kCellColumn,
812
"include.hidden",
813
"source.hidden",
814
"plot.hidden",
815
"output.hidden",
816
"echo.hidden",
817
]);
818
819
for (const [key, value] of Object.entries(options || {})) {
820
if (!keysToNotSerialize.has(key)) {
821
const t = typeof value;
822
if (t === "undefined") {
823
continue;
824
}
825
if (t === "object") {
826
attrs.push(`${key}="${JSON.stringify(value)}"`);
827
} else if (t === "string") {
828
attrs.push(`${key}=${JSON.stringify(value)}`);
829
} else if (t === "number") {
830
attrs.push(`${key}="${value}"`);
831
} else if (t === "boolean") {
832
attrs.push(`${key}=${value}`);
833
} else {
834
throw new Error(
835
`Can't serialize yaml metadata value of type ${t}, key ${key}`,
836
);
837
}
838
}
839
}
840
if (options?.[kCellLstCap]) {
841
attrs.push(`lst-cap="${options?.[kCellLstCap]}"`);
842
}
843
const classStr = (options?.classes as (string | undefined)) || "";
844
845
const classes = classStr === "" ? [] : classStr.trim().split(" ");
846
if (typeof options?.[kFigAlign] === "string") {
847
attrs.push(`layout-align="${options?.[kFigAlign]}"`);
848
}
849
if (typeof options?.panel === "string") {
850
classes.push(`panel-${options?.panel}`);
851
}
852
if (typeof options?.column === "string") {
853
classes.push(`column-${options?.column}`);
854
}
855
if (typeof options?.[kCapLoc] === "string") {
856
classes.push(`caption-${options?.[kCapLoc]}`);
857
}
858
if (typeof options?.[kFigCapLoc] === "string") {
859
classes.push(`fig-cap-location-${options?.[kFigCapLoc]}`);
860
}
861
if (typeof options?.[kTblCapLoc] === "string") {
862
classes.push(`tbl-cap-location-${options?.[kTblCapLoc]}`);
863
}
864
if (typeof options?.[kCellFigColumn] === "string") {
865
classes.push(`fig-caption-${options?.[kCellFigColumn]}`);
866
}
867
if (typeof options?.[kCellTblColumn] === "string") {
868
classes.push(`fig-caption-${options?.[kCellTblColumn]}`);
869
}
870
return { attrs, classes };
871
}
872
873