Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/config/metadata.ts
6449 views
1
/*
2
* config.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import * as ld from "../core/lodash.ts";
8
9
import { existsSync } from "../deno_ral/fs.ts";
10
import { join } from "../deno_ral/path.ts";
11
import { error } from "../deno_ral/log.ts";
12
13
import { readAndValidateYamlFromFile } from "../core/schema/validated-yaml.ts";
14
import { mergeArrayCustomizer } from "../core/config.ts";
15
import { Schema } from "../core/lib/yaml-schema/types.ts";
16
17
import {
18
kCodeLinks,
19
kExecuteDefaults,
20
kExecuteDefaultsKeys,
21
kExecuteEnabled,
22
kHeaderIncludes,
23
kIdentifierDefaults,
24
kIdentifierDefaultsKeys,
25
kIncludeAfter,
26
kIncludeBefore,
27
kIpynbFilter,
28
kIpynbFilters,
29
kKeepMd,
30
kKeepTex,
31
kKeepTyp,
32
kLanguageDefaults,
33
kLanguageDefaultsKeys,
34
kMetadataFile,
35
kMetadataFiles,
36
kMetadataFormat,
37
kOtherLinks,
38
kPandocDefaults,
39
kPandocDefaultsKeys,
40
kPandocMetadata,
41
kRenderDefaults,
42
kRenderDefaultsKeys,
43
kServer,
44
kTblColwidths,
45
kVariant,
46
} from "./constants.ts";
47
import { Format, Metadata } from "./types.ts";
48
import { kGfmCommonmarkVariant } from "../format/markdown/format-markdown-consts.ts";
49
import { kJupyterEngine, kKnitrEngine } from "../execute/types.ts";
50
51
export async function includedMetadata(
52
dir: string,
53
baseMetadata: Metadata,
54
schema: Schema,
55
): Promise<{ metadata: Metadata; files: string[] }> {
56
// Read any metadata files that are defined in the metadata itself
57
const yamlFiles: string[] = [];
58
const metadataFile = baseMetadata[kMetadataFile];
59
if (metadataFile) {
60
yamlFiles.push(join(dir, metadataFile as string));
61
}
62
63
const metadataFiles = baseMetadata[kMetadataFiles];
64
if (metadataFiles && Array.isArray(metadataFiles)) {
65
metadataFiles.forEach((file) => yamlFiles.push(join(dir, file)));
66
}
67
68
// Read the yaml
69
const filesMetadata = await Promise.all(yamlFiles.map(async (yamlFile) => {
70
if (existsSync(yamlFile)) {
71
try {
72
const yaml = await readAndValidateYamlFromFile(
73
yamlFile,
74
schema,
75
`Validation of metadata file ${yamlFile} failed.`,
76
);
77
return yaml;
78
} catch (e) {
79
error("\nError reading metadata file from " + yamlFile + "\n");
80
throw e;
81
}
82
} else {
83
return undefined;
84
}
85
})) as Array<Metadata>;
86
87
// merge the result
88
return {
89
metadata: mergeFormatMetadata({}, ...filesMetadata),
90
files: yamlFiles,
91
};
92
}
93
94
export function formatFromMetadata(
95
baseFormat: Format,
96
to: string,
97
debug?: boolean,
98
): Format {
99
// user format options (allow any b/c this is just untyped yaml)
100
const typedFormat: Format = {
101
identifier: {},
102
render: {},
103
execute: {},
104
pandoc: {},
105
language: {},
106
metadata: {},
107
};
108
// deno-lint-ignore no-explicit-any
109
let format = typedFormat as any;
110
111
// see if there is user config for this writer that we need to merge in
112
const configFormats = baseFormat.metadata[kMetadataFormat];
113
if (configFormats instanceof Object) {
114
// deno-lint-ignore no-explicit-any
115
const configFormat = (configFormats as any)[to];
116
if (configFormat === "default" || configFormat === true) {
117
format = metadataAsFormat({});
118
} else if (configFormat instanceof Object) {
119
format = metadataAsFormat(configFormat);
120
}
121
}
122
123
// merge user config into default config
124
const mergedFormat = mergeFormatMetadata(
125
baseFormat,
126
format,
127
);
128
129
// force keep_md and keep_tex if we are in debug mode
130
if (debug) {
131
mergedFormat.execute[kKeepMd] = true;
132
mergedFormat.render[kKeepTex] = true;
133
mergedFormat.render[kKeepTyp] = true;
134
}
135
136
return mergedFormat;
137
}
138
139
// determine all target formats
140
export function formatKeys(metadata: Metadata): string[] {
141
if (typeof metadata[kMetadataFormat] === "string") {
142
return [metadata[kMetadataFormat] as string];
143
} else if (metadata[kMetadataFormat] instanceof Object) {
144
return Object.keys(metadata[kMetadataFormat] as Metadata).filter((key) => {
145
const format = (metadata[kMetadataFormat] as Metadata)[key];
146
return format !== null && format !== false;
147
});
148
} else {
149
return [];
150
}
151
}
152
153
export function isQuartoMetadata(key: string) {
154
return kRenderDefaultsKeys.includes(key) ||
155
kExecuteDefaultsKeys.includes(key) ||
156
kPandocDefaultsKeys.includes(key) ||
157
kLanguageDefaultsKeys.includes(key) ||
158
[kKnitrEngine, kJupyterEngine].includes(key);
159
}
160
161
export function isIncludeMetadata(key: string) {
162
return [kHeaderIncludes, kIncludeBefore, kIncludeAfter].includes(key);
163
}
164
165
export function metadataAsFormat(metadata: Metadata): Format {
166
const typedFormat: Format = {
167
identifier: {},
168
render: {},
169
execute: {},
170
pandoc: {},
171
language: {},
172
metadata: {},
173
};
174
// deno-lint-ignore no-explicit-any
175
const format = typedFormat as { [key: string]: any };
176
Object.keys(metadata).forEach((key) => {
177
// allow stuff already sorted into a top level key through unmodified
178
if (
179
[
180
kIdentifierDefaults,
181
kRenderDefaults,
182
kExecuteDefaults,
183
kPandocDefaults,
184
kLanguageDefaults,
185
kPandocMetadata,
186
]
187
.includes(key)
188
) {
189
// special case for 'execute' as boolean
190
if (typeof (metadata[key]) == "boolean") {
191
if (key === kExecuteDefaults) {
192
format[key] = format[key] || {};
193
format[kExecuteDefaults][kExecuteEnabled] = metadata[key];
194
}
195
} else {
196
format[key] = { ...format[key], ...(metadata[key] as Metadata) };
197
}
198
} else {
199
// move the key into the appropriate top level key
200
if (kIdentifierDefaultsKeys.includes(key)) {
201
format.identifier[key] = metadata[key];
202
} else if (kRenderDefaultsKeys.includes(key)) {
203
format.render[key] = metadata[key];
204
} else if (kExecuteDefaultsKeys.includes(key)) {
205
format.execute[key] = metadata[key];
206
} else if (kPandocDefaultsKeys.includes(key)) {
207
format.pandoc[key] = metadata[key];
208
} else {
209
format.metadata[key] = metadata[key];
210
}
211
}
212
});
213
214
// normalize server type
215
if (typeof (format.metadata[kServer]) === "string") {
216
format.metadata[kServer] = {
217
type: format.metadata[kServer],
218
};
219
}
220
221
// coalese ipynb-filter to ipynb-filters
222
const filter = format.execute[kIpynbFilter];
223
if (typeof filter === "string") {
224
typedFormat.execute[kIpynbFilters] = typedFormat.execute[kIpynbFilters] ||
225
[];
226
typedFormat.execute[kIpynbFilters]?.push(filter);
227
delete (typedFormat.execute as Record<string, unknown>)[kIpynbFilter];
228
}
229
230
// expand gfm alias in variant
231
if (typeof (typedFormat.render.variant) === "string") {
232
typedFormat.render.variant = typedFormat.render.variant.replace(
233
/^gfm/,
234
kGfmCommonmarkVariant,
235
);
236
}
237
238
return typedFormat;
239
}
240
241
export function setFormatMetadata(
242
format: Format,
243
metadata: string,
244
key: string,
245
value: unknown,
246
) {
247
if (typeof format.metadata[metadata] !== "object") {
248
format.metadata[metadata] = {} as Record<string, unknown>;
249
}
250
// deno-lint-ignore no-explicit-any
251
(format.metadata[metadata] as any)[key] = value;
252
}
253
254
export function metadataGetDeep(metadata: Metadata, property: string) {
255
let values: unknown[] = [];
256
ld.each(metadata, (value: unknown, key: string) => {
257
if (key === property) {
258
values.push(value);
259
} else if (ld.isObject(value)) {
260
values = values.concat(metadataGetDeep(value as Metadata, property));
261
}
262
});
263
return values;
264
}
265
266
export function mergeFormatMetadata<T>(
267
config: T,
268
...configs: Array<T>
269
) {
270
// certain keys are unmergeable (e.g. because they are an array type
271
// that should not be combined with other types)
272
const kUnmergeableKeys = [kTblColwidths];
273
274
// These boolean keys will disable array values
275
const kBooleanDisableArrays = [kCodeLinks, kOtherLinks];
276
277
return mergeConfigsCustomized<T>(
278
(objValue: unknown, srcValue: unknown, key: string) => {
279
if (kUnmergeableKeys.includes(key)) {
280
return srcValue;
281
} else if (key === kVariant) {
282
return mergePandocVariant(objValue, srcValue);
283
} else if (kBooleanDisableArrays.includes(key)) {
284
return mergeDisablableArray(objValue, srcValue);
285
} else {
286
return undefined;
287
}
288
},
289
config,
290
...configs,
291
);
292
}
293
294
export function mergeProjectMetadata<T>(
295
config: T,
296
...configs: Array<T>
297
) {
298
// certain keys that expand into arrays should be overriden if they
299
// are just a string
300
const kExandableStringKeys = ["contents"];
301
302
return mergeConfigsCustomized<T>(
303
(objValue: unknown, srcValue: unknown, key: string) => {
304
if (
305
kExandableStringKeys.includes(key) && typeof objValue === "string"
306
) {
307
return srcValue;
308
} else {
309
return undefined;
310
}
311
},
312
config,
313
...configs,
314
);
315
}
316
317
export function mergeConfigsCustomized<T>(
318
customizer: (
319
objValue: unknown,
320
srcValue: unknown,
321
key: string,
322
) => unknown | undefined,
323
config: T,
324
...configs: Array<T>
325
) {
326
// copy all formats so we don't mutate them
327
config = ld.cloneDeep(config);
328
configs = ld.cloneDeep(configs);
329
330
return ld.mergeWith(
331
config,
332
...configs,
333
(objValue: unknown, srcValue: unknown, key: string) => {
334
const custom = customizer(objValue, srcValue, key);
335
if (custom !== undefined) {
336
return custom;
337
} else {
338
return mergeArrayCustomizer(objValue, srcValue);
339
}
340
},
341
);
342
}
343
344
export function mergeDisablableArray(objValue: unknown, srcValue: unknown) {
345
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
346
return mergeArrayCustomizer(objValue, srcValue);
347
} else {
348
if (srcValue === false) {
349
return [];
350
} else {
351
const srcArr = srcValue !== undefined
352
? Array.isArray(srcValue) ? srcValue : [srcValue]
353
: [];
354
const objArr = objValue !== undefined
355
? Array.isArray(objValue) ? objValue : [objValue]
356
: [];
357
return mergeArrayCustomizer(objArr, srcArr);
358
}
359
}
360
}
361
362
export function mergePandocVariant(objValue: unknown, srcValue: unknown) {
363
if (
364
typeof objValue === "string" && typeof srcValue === "string" &&
365
(objValue !== srcValue)
366
) {
367
// merge srcValue into objValue
368
const extensions: { [key: string]: boolean } = {};
369
[...parsePandocVariant(objValue), ...parsePandocVariant(srcValue)]
370
.forEach((extension) => {
371
extensions[extension.name] = extension.enabled;
372
});
373
return Object.keys(extensions).map((name) =>
374
`${extensions[name] ? "+" : "-"}${name}`
375
).join("");
376
} else {
377
return undefined;
378
}
379
}
380
381
function parsePandocVariant(variant: string) {
382
// remove any linebreaks
383
variant = variant.split("\n").join();
384
385
// parse into separate entries
386
const extensions: Array<{ name: string; enabled: boolean }> = [];
387
const re = /([+-])([a-z_]+)/g;
388
let match = re.exec(variant);
389
while (match) {
390
extensions.push({ name: match[2], enabled: match[1] === "+" });
391
match = re.exec(variant);
392
}
393
394
return extensions;
395
}
396
397