import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import { EsJsxElement, EsJsxIdentifier, esNodeIs, resolveEstreePropertyPath } from './ast/estree.js'; import { eswalk } from './ast/eswalk.js'; import type { MdNodeType, MdRoot } from './ast/mdast.js'; import { mdNodeIs, mdNodeIsJsxElement } from './ast/mdast.js'; import { unwalk } from './ast/unwalk.js'; import { type Config, isHtmlTag, isFrontmatterFieldIncluded, isHtmlElementIncluded, isHtmlElementAttributeIncluded, isJsonOrYamlPropertyIncluded, isJsxComponentIncluded, isJsxComponentAttributeIncluded, isMarkdownNodeIncluded, isHtmlElementChildrenIncluded, isJsxComponentChildrenIncluded } from './config.js'; import { isArray, isEmptyArray, isEmptyString, isObject, isString } from './utils.js'; export function replaceMdastStrings({ mdast, config, strings }: { mdast: MdRoot; strings: string[]; config: Config; }): MdRoot { strings = strings.reverse(); unwalk( mdast, (node, __, _) => { if (mdNodeIs(node, 'text')) { node.value = strings.pop()!; return; } if (mdNodeIsJsxElement(node) && node.name) { if (isHtmlTag(node.name)) { for (const attribute of node.attributes) { if (!mdNodeIs(attribute, 'mdxJsxAttribute')) continue; if ( !isHtmlElementAttributeIncluded({ tag: node.name, attribute: attribute.name, config }) ) continue; if (isString(attribute.value)) { attribute.value = strings.pop(); } else if (attribute.value?.data?.estree) { const estree = attribute.value.data.estree; eswalk(estree, { SimpleLiteral(esnode, _) { if (isString(esnode.value)) esnode.value = strings.pop()!; } }); } } } else { for (const attribute of node.attributes) { if (!mdNodeIs(attribute, 'mdxJsxAttribute')) continue; const componentName: string = node.name; const isAttributeIncluded = isJsxComponentAttributeIncluded({ name: componentName, attribute: attribute.name, config }); if (isString(attribute.value)) { if (!isAttributeIncluded) continue; attribute.value = strings.pop(); } else if (attribute.value?.data?.estree) { if ( !config.jsxComponents.include[componentName] || !config.jsxComponents.include[componentName].attributes.some( (attrName) => attrName === attribute.name || attrName.startsWith(`${attribute.name}.`) ) ) continue; const estree = attribute.value.data.estree; eswalk(estree, { SimpleLiteral(esnode, _) { if (isString(esnode.value)) esnode.value = strings.pop()!; }, JSXElement(esnode, _) { const name = (esnode.openingElement.name as EsJsxIdentifier).name; if (isHtmlTag(name)) { if ( !isHtmlElementIncluded({ tag: name, config }) || !isHtmlElementChildrenIncluded({ tag: name, config }) ) return false; } else if ( !isJsxComponentIncluded({ name: name, config }) || !isJsxComponentChildrenIncluded({ name: name, config }) ) return false; }, JSXAttribute(esnode, parents) { const name = typeof esnode.name.name === 'string' ? esnode.name.name : esnode.name.name.name; const parentName = ( (parents[parents.length - 1] as EsJsxElement).openingElement .name as EsJsxIdentifier ).name; if (isHtmlTag(parentName)) { if ( !isHtmlElementAttributeIncluded({ tag: parentName, attribute: name, config }) ) return false; } else if ( !config.jsxComponents.include[name] || !config.jsxComponents.include[name].attributes.some( (attrName) => attrName === attribute.name || attrName.startsWith(`${attribute.name}.`) ) ) { return false; } }, JSXText(esnode, _) { esnode.value = strings.pop()!; }, Property(esnode, parents) { if (!esNodeIs(esnode, 'Identifier')) return false; const propertyPath = resolveEstreePropertyPath(esnode, parents, attribute.name); if ( !propertyPath || !isJsxComponentAttributeIncluded({ name: componentName, attribute: propertyPath, config }) ) return false; } }); } } } } if (mdNodeIs(node, 'yaml')) { if (isEmptyArray(config.frontmatterFields.include)) return; if (isEmptyString(node.value)) return; const object: Record = parseYaml(node.value); for (const field in object) { if (!isFrontmatterFieldIncluded({ field, config })) continue; const value = object[field]; if (isString(value)) { object[field] = strings.pop(); continue; } if (isArray(value)) { for (const [index, item] of value.entries()) { if (!isString(item)) continue; value[index] = strings.pop(); } } } return; } }, (node, parent) => { if (!isMarkdownNodeIncluded({ type: node.type as MdNodeType, config })) return false; if (parent && mdNodeIsJsxElement(parent) && parent.name) { if (isHtmlTag(parent.name)) { if (!isHtmlElementChildrenIncluded({ tag: parent.name, config })) return false; } else { if (!isJsxComponentChildrenIncluded({ name: parent.name, config })) return false; } return true; } if (mdNodeIsJsxElement(node) && node.name) { if (isHtmlTag(node.name)) { if (!isHtmlElementIncluded({ tag: node.name, config })) return false; } else { if (!isJsxComponentIncluded({ name: node.name, config })) return false; } return true; } return true; } ); return mdast; } export function replaceJsonOrYamlStrings({ source, type = 'json', strings, config }: { source: string; type?: 'json' | 'yaml'; strings: string[]; config: Config; }): string { if (isEmptyArray(config.jsonOrYamlProperties.include)) return source; strings = strings.reverse(); const parsed = type === 'json' ? JSON.parse(source) : parseYaml(source); process({ value: parsed }); function process(args: { value: unknown; parent?: never; property?: never; index?: never }): void; function process(args: { value: unknown; parent: unknown[]; property?: string | number | symbol; index: number; }): void; function process(args: { value: unknown; parent: Record; property: string | number | symbol; index?: never; }): void; function process({ value, parent, property, index }: { value: unknown; parent?: unknown[] | Record; property?: string | number | symbol; index?: number; }) { if (isArray(value)) { for (const [index, item] of value.entries()) { process({ value: item, parent: value, property, index }); } return; } if (isObject(value)) { for (const property in value) { const item = (value as Record)[property]; process({ value: item, parent: value, property }); } return; } if (typeof value === 'string') { if (property && isJsonOrYamlPropertyIncluded({ property, config })) { if (isArray(parent) && index) { parent[index] = strings.pop(); return; } if (isObject(parent)) { parent[property] = strings.pop(); return; } } return; } } if (type === 'json') return JSON.stringify(parsed); return stringifyYaml(parsed); }