import EleventyBaseError from "./Errors/EleventyBaseError.js"; import EleventyExtensionMap from "./EleventyExtensionMap.js"; import TemplateEngineManager from "./Engines/TemplateEngineManager.js"; import CustomEngine from "./Engines/Custom.js"; // import debugUtil from "debug"; // const debug = debugUtil("Eleventy:TemplateRender"); class TemplateRenderConfigError extends EleventyBaseError {} class TemplateRenderUnknownEngineError extends EleventyBaseError {} // works with full path names or short engine name class TemplateRender { constructor(tmplPath, config) { if (!tmplPath) { throw new Error(`TemplateRender requires a tmplPath argument, instead of ${tmplPath}`); } if (!config) { throw new TemplateRenderConfigError("Missing `config` argument."); } if (config.constructor.name === "TemplateConfig") { this.eleventyConfig = config; this.config = config.getConfig(); } else { throw new Error("Third argument to TemplateRender must be a TemplateConfig instance."); } this.engineNameOrPath = tmplPath; this.parseMarkdownWith = this.config.markdownTemplateEngine; this.parseHtmlWith = this.config.htmlTemplateEngine; } get dirs() { return this.eleventyConfig.directories; } get inputDir() { return this.dirs.input; } get includesDir() { return this.dirs.includes; } /* Backwards compat */ getIncludesDir() { return this.includesDir; } get config() { return this._config; } set config(config) { this._config = config; } set extensionMap(extensionMap) { this._extensionMap = extensionMap; } get extensionMap() { if (!this._extensionMap) { this._extensionMap = new EleventyExtensionMap(this.eleventyConfig); this._extensionMap.setFormats([]); } return this._extensionMap; } async getEngineByName(name) { let engine = await this.extensionMap.engineManager.getEngine(name, this.extensionMap); engine.eleventyConfig = this.eleventyConfig; return engine; } // Runs once per template async init(engineNameOrPath) { let name = engineNameOrPath || this.engineNameOrPath; this.extensionMap.config = this.eleventyConfig; let extensionEntry = this.extensionMap.getExtensionEntry(name); let engineName = extensionEntry?.aliasKey || extensionEntry?.key; if (TemplateEngineManager.isSimpleAlias(extensionEntry)) { engineName = extensionEntry?.key; } this._engineName = engineName; if (!extensionEntry || !this._engineName) { throw new TemplateRenderUnknownEngineError( `Unknown engine for ${name} (supported extensions: ${this.extensionMap.getReadableFileExtensions()})`, ); } this._engine = await this.getEngineByName(this._engineName); if (this.useMarkdown === undefined) { this.setUseMarkdown(this._engineName === "md"); } } get engineName() { if (!this._engineName) { throw new Error("TemplateRender needs a call to the init() method."); } return this._engineName; } get engine() { if (!this._engine) { throw new Error("TemplateRender needs a call to the init() method."); } return this._engine; } static parseEngineOverrides(engineName) { if (typeof (engineName || "") !== "string") { throw new Error("Expected String passed to parseEngineOverrides. Received: " + engineName); } let overlappingEngineWarningCount = 0; let engines = []; let uniqueLookup = {}; let usingMarkdown = false; (engineName || "") .split(",") .map((name) => { return name.toLowerCase().trim(); }) .forEach((name) => { // html is assumed (treated as plaintext by the system) if (!name || name === "html") { return; } if (name === "md") { usingMarkdown = true; return; } if (!uniqueLookup[name]) { engines.push(name); uniqueLookup[name] = true; // we already short circuit md and html types above overlappingEngineWarningCount++; } }); if (overlappingEngineWarningCount > 1) { throw new Error( `Don’t mix multiple templating engines in your front matter overrides (exceptions for HTML and Markdown). You used: ${engineName}`, ); } // markdown should always be first if (usingMarkdown) { engines.unshift("md"); } return engines; } // used for error logging and console output. getReadableEnginesList() { return this.getReadableEnginesListDifferingFromFileExtension() || this.engineName; } getReadableEnginesListDifferingFromFileExtension() { let keyFromFilename = this.extensionMap.getKey(this.engineNameOrPath); if (this.engine instanceof CustomEngine) { if ( this.engine.entry && this.engine.entry.name && keyFromFilename !== this.engine.entry.name ) { return this.engine.entry.name; } else { // We don’t have a name for it so we return nothing so we don’t misreport (per #2386) return; } } if (this.engineName === "md" && this.useMarkdown && this.parseMarkdownWith) { return this.parseMarkdownWith; } if (this.engineName === "html" && this.parseHtmlWith) { return this.parseHtmlWith; } // templateEngineOverride in play and template language differs from file extension if (keyFromFilename !== this.engineName) { return this.engineName; } } // TODO templateEngineOverride getPreprocessorEngine() { if (this.engineName === "md" && this.parseMarkdownWith) { return this.parseMarkdownWith; } if (this.engineName === "html" && this.parseHtmlWith) { return this.parseHtmlWith; } return this.extensionMap.getKey(this.engineNameOrPath); } // We pass in templateEngineOverride here because it isn’t yet applied to templateRender getEnginesList(engineOverride) { if (engineOverride) { let engines = TemplateRender.parseEngineOverrides(engineOverride).reverse(); return engines.join(","); } if (this.engineName === "md" && this.useMarkdown && this.parseMarkdownWith) { return `${this.parseMarkdownWith},md`; } if (this.engineName === "html" && this.parseHtmlWith) { return this.parseHtmlWith; } // templateEngineOverride in play return this.extensionMap.getKey(this.engineNameOrPath); } async setEngineOverride(engineName, bypassMarkdown) { let engines = TemplateRender.parseEngineOverrides(engineName); // when overriding, Template Engines with HTML will instead use the Template Engine as primary and output HTML // So any HTML engine usage here will never use a preprocessor templating engine. this.setHtmlEngine(false); if (!engines.length) { await this.init("html"); return; } await this.init(engines[0]); let usingMarkdown = engines[0] === "md" && !bypassMarkdown; this.setUseMarkdown(usingMarkdown); if (usingMarkdown) { // false means only parse markdown and not with a preprocessor template engine this.setMarkdownEngine(engines.length > 1 ? engines[1] : false); } } getEngineName() { return this.engineName; } isEngine(engine) { return this.engineName === engine; } setUseMarkdown(useMarkdown) { this.useMarkdown = !!useMarkdown; } // this is only called for templateEngineOverride setMarkdownEngine(markdownEngine) { this.parseMarkdownWith = markdownEngine; } // this is only called for templateEngineOverride setHtmlEngine(htmlEngineName) { this.parseHtmlWith = htmlEngineName; } async _testRender(str, data) { return this.engine._testRender(str, data); } async getCompiledTemplate(str) { // TODO refactor better, move into TemplateEngine logic if (this.engineName === "md") { return this.engine.compile( str, this.engineNameOrPath, this.parseMarkdownWith, !this.useMarkdown, ); } else if (this.engineName === "html") { return this.engine.compile(str, this.engineNameOrPath, this.parseHtmlWith); } else { return this.engine.compile(str, this.engineNameOrPath); } } } export default TemplateRender;