@mdfriday/foundry
Version:
The core engine of MDFriday. Convert Markdown and shortcodes into fully themed static sites – Hugo-style, powered by TypeScript.
332 lines • 13.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
exports.newParser = newParser;
const text_template_1 = require("@mdfriday/text-template");
const text_template_2 = require("@mdfriday/text-template");
const type_1 = require("../type");
const info_1 = require("../vo/info");
const log_1 = require("../../../../pkg/log");
const cache_1 = require("../../../../pkg/cahce/cache");
// Create domain-specific logger for parser operations
const log = (0, log_1.getDomainLogger)('template', { component: 'parser' });
/**
* Parser entity for parsing templates
* TypeScript version of Go's Parser struct
*/
class Parser {
constructor(funcMap = new Map()) {
this.readyInit = false;
this.funcMap = funcMap;
this.prototypeText = (0, text_template_1.New)('');
this.parseOverlapCache = new cache_1.ConcurrentCache();
// Apply function map to prototype
if (this.funcMap.size > 0) {
this.prototypeText.Funcs(this.funcMap);
}
}
/**
* Mark parser as ready and create clone
*/
async markReady() {
if (!this.readyInit) {
// Create clone for thread safety
this.prototypeTextClone = this.prototypeText;
this.readyInit = true;
}
}
/**
* Parse template info into template state
*/
async parse(info) {
try {
const tmpl = (0, text_template_1.New)(info.name);
if (this.funcMap && this.funcMap.size > 0) {
tmpl.Funcs(this.funcMap);
}
const [parsedTmpl, parseErr] = tmpl.Parse(info.template);
if (parseErr) {
throw new type_1.TemplateError(`Parse failed: ${parseErr.message}`, 'PARSE_FAILED');
}
return {
template: parsedTmpl,
info,
type: (0, info_1.resolveTemplateType)(info.name),
};
}
catch (error) {
if (error instanceof type_1.TemplateError) {
throw error;
}
const message = error instanceof Error ? error.message : String(error);
throw new type_1.TemplateError(`Parse failed: ${message}`, 'PARSE_FAILED');
}
}
/**
* Parse template with lock (for thread safety)
*/
async parseWithLock(name, tpl) {
try {
if (!this.prototypeTextClone) {
await this.markReady();
}
const tmpl = (0, text_template_1.New)(name);
if (this.funcMap && this.funcMap.size > 0) {
tmpl.Funcs(this.funcMap);
}
const [parsedTmpl, parseErr] = tmpl.Parse(tpl);
if (parseErr) {
throw new type_1.TemplateError(`Parse with lock failed: ${parseErr.message}`, 'PARSE_LOCK_FAILED');
}
return parsedTmpl;
}
catch (error) {
if (error instanceof type_1.TemplateError) {
throw error;
}
const message = error instanceof Error ? error.message : String(error);
throw new type_1.TemplateError(`Parse with lock failed: ${message}`, 'PARSE_LOCK_FAILED');
}
}
/**
* Parse template with overlay and base template
*/
async parseOverlap(overlay, base, lookup) {
// 创建缓存key:使用overlay和base的name组合
const cacheKey = `${overlay.name}::${base.name || 'empty'}`;
return this.parseOverlapCache.getOrCreate(cacheKey, async () => {
try {
const tmpl = await this.applyBaseTemplate(overlay, base, lookup);
const ts = {
template: tmpl,
info: overlay,
type: (0, info_1.resolveTemplateType)(overlay.name),
};
if (!base.isZero || base.name) {
ts.baseInfo = base;
}
return [ts, true, null];
}
catch (error) {
const templateError = error instanceof type_1.TemplateError
? error
: new type_1.TemplateError(`Parse overlap failed: ${error.message}`, 'PARSE_OVERLAP_FAILED');
log.error(`Failed to parse template overlap for key: ${cacheKey}: ${templateError.message}`);
return [null, false, templateError];
}
});
}
/**
* Apply base template to overlay
*/
async applyBaseTemplate(overlay, base, lookup) {
try {
const tmpl = (0, text_template_1.New)(overlay.name);
if (this.funcMap && this.funcMap.size > 0) {
tmpl.Funcs(this.funcMap);
}
// Parse base template first if it exists
if (!base.isZero || base.name) {
const [baseTmpl, baseErr] = tmpl.Parse(base.template);
if (baseErr) {
throw new type_1.TemplateError(`Base template parse failed: ${baseErr.message}`, 'BASE_PARSE_FAILED');
}
}
// Clone and parse overlay template (matching Go's template.Must(tmpl.Clone()).Parse pattern)
const [clonedTmpl, cloneErr] = tmpl.Clone();
if (cloneErr) {
throw new type_1.TemplateError(`Template clone failed: ${cloneErr.message}`, 'CLONE_FAILED');
}
const [overlayTmpl, overlayErr] = clonedTmpl.Parse(overlay.template);
if (overlayErr) {
throw new type_1.TemplateError(`Overlay template parse failed: ${overlayErr.message}`, 'OVERLAY_PARSE_FAILED');
}
// Get dependencies using syntax tree traversal (与Go版本一致的遍历方法)
const dependencies = await this.getDependencies(overlayTmpl, new Map(), lookup);
// Add dependencies to template using AddParseTree (matching Go implementation)
for (const [name, state] of dependencies) {
if (name === overlay.name) {
continue;
}
try {
// Use AddParseTree to associate the dependency template - check Tree is not null
if (state.template.Tree) {
const [, addErr] = overlayTmpl.AddParseTree(name, state.template.Tree);
if (addErr) {
log.error(`AddParseTree failed: ${addErr.message} with name ${name}`);
throw new type_1.TemplateError(`AddParseTree failed: ${addErr.message}`, 'ADD_PARSE_TREE_FAILED');
}
}
}
catch (error) {
log.error(`Error adding dependency ${name} to template ${overlay.name}:`, error);
throw new type_1.TemplateError(`Failed to add dependency ${name}: ${error.message}`, 'DEPENDENCY_ADD_FAILED');
}
}
return overlayTmpl;
}
catch (error) {
if (error instanceof type_1.TemplateError) {
throw error;
}
const message = error instanceof Error ? error.message : String(error);
throw new type_1.TemplateError(`Apply base template failed: ${message}`, 'APPLY_BASE_FAILED');
}
}
/**
* Get template dependencies using syntax tree traversal (与Go版本一致的遍历方法)
*/
async getDependencies(tmpl, discovered, lookup) {
const resultMap = new Map();
if (discovered.has(tmpl.Name())) {
return resultMap;
}
discovered.set(tmpl.Name(), tmpl);
try {
const depNames = new Set();
// 遍历语法树的所有节点(与Go版本一致的方法)
const visit = (node) => {
if (!node)
return;
const nodeType = node.Type();
if (nodeType === text_template_2.NodeType.NodeTemplate) {
// 找到template引用
const templateNode = node;
const name = templateNode.Name;
if (name) {
depNames.add(name);
}
}
else if (nodeType === text_template_2.NodeType.NodeList) {
// 递归处理ListNode中的所有节点
const listNode = node;
if (listNode.Nodes) {
for (const childNode of listNode.Nodes) {
visit(childNode);
}
}
}
// 处理其他类型的节点,可能包含嵌套的Node
// 例如:ActionNode、IfNode、RangeNode、WithNode等都可能包含Pipe或List
// 这里需要根据具体的Node类型来递归访问其子节点
if (nodeType === text_template_2.NodeType.NodeAction) {
const actionNode = node;
if (actionNode.Pipe) {
visit(actionNode.Pipe);
}
}
else if (nodeType === text_template_2.NodeType.NodePipe) {
const pipeNode = node;
if (pipeNode.Cmds) {
for (const cmd of pipeNode.Cmds) {
visit(cmd);
}
}
}
else if (nodeType === text_template_2.NodeType.NodeCommand) {
const commandNode = node;
if (commandNode.Args) {
for (const arg of commandNode.Args) {
visit(arg);
}
}
}
else if (nodeType === text_template_2.NodeType.NodeIf ||
nodeType === text_template_2.NodeType.NodeRange ||
nodeType === text_template_2.NodeType.NodeWith) {
// BranchNode包含Pipe、List和ElseList
const branchNode = node;
if (branchNode.Pipe) {
visit(branchNode.Pipe);
}
if (branchNode.List) {
visit(branchNode.List);
}
if (branchNode.ElseList) {
visit(branchNode.ElseList);
}
}
};
// 从Root开始遍历
if (tmpl.Tree && tmpl.Tree.Root) {
visit(tmpl.Tree.Root);
}
// 递归解析依赖
for (const depName of depNames) {
const state = lookup(depName);
if (state) {
resultMap.set(depName, state);
// 递归获取依赖的依赖(但要避免循环依赖)
if (!discovered.has(state.template.Name())) {
const subDeps = await this.getDependencies(state.template, discovered, lookup);
for (const [subName, subState] of subDeps) {
if (!resultMap.has(subName)) {
resultMap.set(subName, subState);
}
}
}
}
}
}
catch (error) {
log.error(`Error getting dependencies for template ${tmpl.Name()}:`, error);
}
return resultMap;
}
/**
* Set function map
*/
setFuncMap(funcMap) {
this.funcMap = new Map([...this.funcMap, ...funcMap]);
// Apply to prototype
if (this.funcMap.size > 0) {
this.prototypeText.Funcs(this.funcMap);
}
}
/**
* Get function map
*/
getFuncMap() {
return new Map(this.funcMap);
}
/**
* Parse multiple templates
*/
async parseMultiple(templates) {
const results = await Promise.allSettled(templates.map(info => this.parse(info)));
const states = [];
const errors = [];
for (const result of results) {
if (result.status === 'fulfilled') {
states.push(result.value);
}
else {
errors.push(result.reason);
}
}
if (errors.length > 0) {
throw new type_1.TemplateError(`Failed to parse ${errors.length} templates: ${errors.map(e => e.message).join(', ')}`, 'MULTIPLE_PARSE_FAILED');
}
return states;
}
/**
* Clear the parse overlap cache
*/
clearCache() {
this.parseOverlapCache.clear();
}
/**
* Get cache statistics
*/
getCacheStats() {
return this.parseOverlapCache.getStats();
}
}
exports.Parser = Parser;
/**
* Create a new Parser instance
*/
function newParser(funcMap = new Map()) {
return new Parser(funcMap);
}
//# sourceMappingURL=parser.js.map