import * as ld from "../core/lodash.ts";
import { existsSync } from "../deno_ral/fs.ts";
import { join } from "../deno_ral/path.ts";
import { error } from "../deno_ral/log.ts";
import { readAndValidateYamlFromFile } from "../core/schema/validated-yaml.ts";
import { mergeArrayCustomizer } from "../core/config.ts";
import { Schema } from "../core/lib/yaml-schema/types.ts";
import {
kCodeLinks,
kExecuteDefaults,
kExecuteDefaultsKeys,
kExecuteEnabled,
kHeaderIncludes,
kIdentifierDefaults,
kIdentifierDefaultsKeys,
kIncludeAfter,
kIncludeBefore,
kIpynbFilter,
kIpynbFilters,
kKeepMd,
kKeepTex,
kKeepTyp,
kLanguageDefaults,
kLanguageDefaultsKeys,
kMetadataFile,
kMetadataFiles,
kMetadataFormat,
kOtherLinks,
kPandocDefaults,
kPandocDefaultsKeys,
kPandocMetadata,
kRenderDefaults,
kRenderDefaultsKeys,
kServer,
kTblColwidths,
kVariant,
} from "./constants.ts";
import { Format, Metadata } from "./types.ts";
import { kGfmCommonmarkVariant } from "../format/markdown/format-markdown-consts.ts";
import { kJupyterEngine, kKnitrEngine } from "../execute/types.ts";
export async function includedMetadata(
dir: string,
baseMetadata: Metadata,
schema: Schema,
): Promise<{ metadata: Metadata; files: string[] }> {
const yamlFiles: string[] = [];
const metadataFile = baseMetadata[kMetadataFile];
if (metadataFile) {
yamlFiles.push(join(dir, metadataFile as string));
}
const metadataFiles = baseMetadata[kMetadataFiles];
if (metadataFiles && Array.isArray(metadataFiles)) {
metadataFiles.forEach((file) => yamlFiles.push(join(dir, file)));
}
const filesMetadata = await Promise.all(yamlFiles.map(async (yamlFile) => {
if (existsSync(yamlFile)) {
try {
const yaml = await readAndValidateYamlFromFile(
yamlFile,
schema,
`Validation of metadata file ${yamlFile} failed.`,
);
return yaml;
} catch (e) {
error("\nError reading metadata file from " + yamlFile + "\n");
throw e;
}
} else {
return undefined;
}
})) as Array<Metadata>;
return {
metadata: mergeFormatMetadata({}, ...filesMetadata),
files: yamlFiles,
};
}
export function formatFromMetadata(
baseFormat: Format,
to: string,
debug?: boolean,
): Format {
const typedFormat: Format = {
identifier: {},
render: {},
execute: {},
pandoc: {},
language: {},
metadata: {},
};
let format = typedFormat as any;
const configFormats = baseFormat.metadata[kMetadataFormat];
if (configFormats instanceof Object) {
const configFormat = (configFormats as any)[to];
if (configFormat === "default" || configFormat === true) {
format = metadataAsFormat({});
} else if (configFormat instanceof Object) {
format = metadataAsFormat(configFormat);
}
}
const mergedFormat = mergeFormatMetadata(
baseFormat,
format,
);
if (debug) {
mergedFormat.execute[kKeepMd] = true;
mergedFormat.render[kKeepTex] = true;
mergedFormat.render[kKeepTyp] = true;
}
return mergedFormat;
}
export function formatKeys(metadata: Metadata): string[] {
if (typeof metadata[kMetadataFormat] === "string") {
return [metadata[kMetadataFormat] as string];
} else if (metadata[kMetadataFormat] instanceof Object) {
return Object.keys(metadata[kMetadataFormat] as Metadata).filter((key) => {
const format = (metadata[kMetadataFormat] as Metadata)[key];
return format !== null && format !== false;
});
} else {
return [];
}
}
export function isQuartoMetadata(key: string) {
return kRenderDefaultsKeys.includes(key) ||
kExecuteDefaultsKeys.includes(key) ||
kPandocDefaultsKeys.includes(key) ||
kLanguageDefaultsKeys.includes(key) ||
[kKnitrEngine, kJupyterEngine].includes(key);
}
export function isIncludeMetadata(key: string) {
return [kHeaderIncludes, kIncludeBefore, kIncludeAfter].includes(key);
}
export function metadataAsFormat(metadata: Metadata): Format {
const typedFormat: Format = {
identifier: {},
render: {},
execute: {},
pandoc: {},
language: {},
metadata: {},
};
const format = typedFormat as { [key: string]: any };
Object.keys(metadata).forEach((key) => {
if (
[
kIdentifierDefaults,
kRenderDefaults,
kExecuteDefaults,
kPandocDefaults,
kLanguageDefaults,
kPandocMetadata,
]
.includes(key)
) {
if (typeof (metadata[key]) == "boolean") {
if (key === kExecuteDefaults) {
format[key] = format[key] || {};
format[kExecuteDefaults][kExecuteEnabled] = metadata[key];
}
} else {
format[key] = { ...format[key], ...(metadata[key] as Metadata) };
}
} else {
if (kIdentifierDefaultsKeys.includes(key)) {
format.identifier[key] = metadata[key];
} else if (kRenderDefaultsKeys.includes(key)) {
format.render[key] = metadata[key];
} else if (kExecuteDefaultsKeys.includes(key)) {
format.execute[key] = metadata[key];
} else if (kPandocDefaultsKeys.includes(key)) {
format.pandoc[key] = metadata[key];
} else {
format.metadata[key] = metadata[key];
}
}
});
if (typeof (format.metadata[kServer]) === "string") {
format.metadata[kServer] = {
type: format.metadata[kServer],
};
}
const filter = format.execute[kIpynbFilter];
if (typeof filter === "string") {
typedFormat.execute[kIpynbFilters] = typedFormat.execute[kIpynbFilters] ||
[];
typedFormat.execute[kIpynbFilters]?.push(filter);
delete (typedFormat.execute as Record<string, unknown>)[kIpynbFilter];
}
if (typeof (typedFormat.render.variant) === "string") {
typedFormat.render.variant = typedFormat.render.variant.replace(
/^gfm/,
kGfmCommonmarkVariant,
);
}
return typedFormat;
}
export function setFormatMetadata(
format: Format,
metadata: string,
key: string,
value: unknown,
) {
if (typeof format.metadata[metadata] !== "object") {
format.metadata[metadata] = {} as Record<string, unknown>;
}
(format.metadata[metadata] as any)[key] = value;
}
export function metadataGetDeep(metadata: Metadata, property: string) {
let values: unknown[] = [];
ld.each(metadata, (value: unknown, key: string) => {
if (key === property) {
values.push(value);
} else if (ld.isObject(value)) {
values = values.concat(metadataGetDeep(value as Metadata, property));
}
});
return values;
}
export function mergeFormatMetadata<T>(
config: T,
...configs: Array<T>
) {
const kUnmergeableKeys = [kTblColwidths];
const kBooleanDisableArrays = [kCodeLinks, kOtherLinks];
return mergeConfigsCustomized<T>(
(objValue: unknown, srcValue: unknown, key: string) => {
if (kUnmergeableKeys.includes(key)) {
return srcValue;
} else if (key === kVariant) {
return mergePandocVariant(objValue, srcValue);
} else if (kBooleanDisableArrays.includes(key)) {
return mergeDisablableArray(objValue, srcValue);
} else {
return undefined;
}
},
config,
...configs,
);
}
export function mergeProjectMetadata<T>(
config: T,
...configs: Array<T>
) {
const kExandableStringKeys = ["contents"];
return mergeConfigsCustomized<T>(
(objValue: unknown, srcValue: unknown, key: string) => {
if (
kExandableStringKeys.includes(key) && typeof objValue === "string"
) {
return srcValue;
} else {
return undefined;
}
},
config,
...configs,
);
}
export function mergeConfigsCustomized<T>(
customizer: (
objValue: unknown,
srcValue: unknown,
key: string,
) => unknown | undefined,
config: T,
...configs: Array<T>
) {
config = ld.cloneDeep(config);
configs = ld.cloneDeep(configs);
return ld.mergeWith(
config,
...configs,
(objValue: unknown, srcValue: unknown, key: string) => {
const custom = customizer(objValue, srcValue, key);
if (custom !== undefined) {
return custom;
} else {
return mergeArrayCustomizer(objValue, srcValue);
}
},
);
}
export function mergeDisablableArray(objValue: unknown, srcValue: unknown) {
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
return mergeArrayCustomizer(objValue, srcValue);
} else {
if (srcValue === false) {
return [];
} else {
const srcArr = srcValue !== undefined
? Array.isArray(srcValue) ? srcValue : [srcValue]
: [];
const objArr = objValue !== undefined
? Array.isArray(objValue) ? objValue : [objValue]
: [];
return mergeArrayCustomizer(objArr, srcArr);
}
}
}
export function mergePandocVariant(objValue: unknown, srcValue: unknown) {
if (
typeof objValue === "string" && typeof srcValue === "string" &&
(objValue !== srcValue)
) {
const extensions: { [key: string]: boolean } = {};
[...parsePandocVariant(objValue), ...parsePandocVariant(srcValue)]
.forEach((extension) => {
extensions[extension.name] = extension.enabled;
});
return Object.keys(extensions).map((name) =>
`${extensions[name] ? "+" : "-"}${name}`
).join("");
} else {
return undefined;
}
}
function parsePandocVariant(variant: string) {
variant = variant.split("\n").join();
const extensions: Array<{ name: string; enabled: boolean }> = [];
const re = /([+-])([a-z_]+)/g;
let match = re.exec(variant);
while (match) {
extensions.push({ name: match[2], enabled: match[1] === "+" });
match = re.exec(variant);
}
return extensions;
}