'use strict'; var doc = require('prettier/doc'); var babel = require('prettier/plugins/babel'); const Placeholder = { jinja: { startToken: "`~", endToken: "~`", }, json: { startToken: "__~", endToken: "~__", }, }; const NOT_FOUND = -1; const placeholderGenerator = (text, style) => { let id = 0; return () => { id++; const placeholder = Placeholder[style].startToken + id + Placeholder[style].endToken; if (text.includes(placeholder)) { throw new Error("text have this Placeholder"); } return placeholder; }; }; const replaceAt = (str, replacement, start, length) => { return str.slice(0, start) + replacement + str.slice(start + length); }; const STYLE$2 = "jinja"; const regex = /(?{{(?[-+]?)\s*(?'([^']|\\')*'|"([^"]|\\")*"|[\S\s]*?)\s*(?[-+]?)}}|{%(?[-+]?)\s*(?(?\w+)('([^']|\\')*'|"([^"]|\\")*"|[\S\s])*?)\s*(?[-+]?)%}|(?{#[\S\s]*?#}))/; const parse = (text) => { const statementStack = []; const root = { id: "0", type: "root", content: text, preNewLines: 0, originalText: text, index: 0, length: 0, nodes: {}, }; const generatePlaceholder = placeholderGenerator(text, STYLE$2); let match; let i = 0; while ((match = root.content.slice(i).match(regex)) !== null) { if (!match.groups || match.index === undefined) { continue; } const matchLength = match[0].length; const matchText = match.groups.node; const expression = match.groups.expression; const statement = match.groups.statement; const ignoreBlock = match.groups.ignoreBlock; const comment = match.groups.comment; if (!matchText && !expression && !statement && !ignoreBlock && !comment) { continue; } const placeholder = generatePlaceholder(); const emptyLinesBetween = root.content .slice(i, i + match.index) .match(/^\s+$/) || [""]; const preNewLines = emptyLinesBetween.length ? emptyLinesBetween[0].split("\n").length - 1 : 0; const node = { id: placeholder, preNewLines, originalText: matchText, index: match.index + i, length: matchText.length, nodes: root.nodes, }; if (comment != undefined || ignoreBlock != undefined) { root.content = replaceAt(root.content, placeholder, match.index + i, matchLength); root.nodes[node.id] = { ...node, type: comment ? "comment" : "ignore", content: comment || ignoreBlock, }; i += match.index + placeholder.length; } if (expression != undefined) { const delimiter = { start: match.groups.startDelimiterEx, end: match.groups.endDelimiterEx, }; root.content = replaceAt(root.content, placeholder, match.index + i, matchLength); root.nodes[node.id] = { ...node, type: "expression", content: expression, delimiter, }; i += match.index + placeholder.length; } if (statement != undefined) { const keyword = match.groups.keyword; const delimiter = { start: match.groups.startDelimiter, end: match.groups.endDelimiter, }; if (keyword.startsWith("end")) { let start; while (!start) { start = statementStack.pop(); if (!start) { throw new Error(`No opening statement found for closing statement "{% ${statement} %}".`); } if (keyword.replace("end", "") !== start.keyword) { root.content = replaceAt(root.content, start.id, start.index, start.length); i += start.id.length - start.length; start = undefined; } } const end = { ...node, index: match.index + i, type: "statement", content: statement, keyword, delimiter, }; root.nodes[end.id] = end; const originalText = root.content.slice(start.index, end.index + end.length); const block = { id: generatePlaceholder(), type: "block", start: start, end: end, content: originalText.slice(start.length, originalText.length - end.length), preNewLines: start.preNewLines, containsNewLines: originalText.search("\n") !== NOT_FOUND, originalText, index: start.index, length: end.index + end.length - start.index, nodes: root.nodes, }; root.nodes[block.id] = block; root.content = replaceAt(root.content, block.id, start.index, originalText.length); i += match.index + block.id.length + end.length - originalText.length; } else { root.nodes[node.id] = { ...node, type: "statement", content: statement, keyword, delimiter, }; statementStack.push(root.nodes[placeholder]); i += match.index + matchLength; } } } for (const stmt of statementStack) { root.content = root.content.replace(stmt.originalText, stmt.id); } return root; }; /** * Returns the indexs of the first and the last character of any placeholder * occuring in a string. */ const findPlaceholders = (text, style) => { const placeholders = []; const { startToken, endToken } = Placeholder[style]; const regex = new RegExp(`${startToken}\\d+${endToken}`, "d"); let match; let i = 0; while ((match = text.slice(i).match(regex)) !== null) { if (!match.indices) { continue; } const [[start, end]] = match.indices; const position = [i + start, i + end]; placeholders.push(position); i = position[1]; } return placeholders; }; /** * Deeply replace placeholders in input * @param input * @param placeholders */ const replacePlaceholders = (input, placeholders) => { let result = input; let placeholderPositions = findPlaceholders(result, "json"); // Loop until there are no more placeholders to replace while (placeholderPositions.length > 0) { for (const [start, end] of placeholderPositions) { const placeholder = result.slice(start, end); const replacement = placeholders[placeholder]; if (replacement) { // Replace the placeholder in the result string result = replaceAt(result, replacement, start, placeholder.length); } } // Find new placeholders in the updated result string placeholderPositions = findPlaceholders(result, "json"); } return result; }; const STYLE$1 = "json"; /** * The groups of the largest JSON closed objects. * @param input */ const transformJsonToGroups = (input) => { const jsonRegex = /({[^{}]*})/; let match; let content = input; const placeholderGen = placeholderGenerator(content, STYLE$1); const placeholders = {}; // replace valid jsons groups by placeholders while ((match = content.match(jsonRegex)) !== null) { const startMatch = match.index; if (startMatch === undefined) { continue; } const matchText = match[0]; const placeholder = placeholderGen(); placeholders[placeholder] = matchText; content = replaceAt(content, placeholder, startMatch, matchText.length); } const idsPlaceholders = findPlaceholders(content, STYLE$1); if (!idsPlaceholders.length) { return [content]; } const groups = []; let startIndex = 0; const addGroup = (data) => { const newData = data.trim(); if (newData) { groups.push(newData); } }; idsPlaceholders.forEach(([start, end]) => { const notJsonContent = content.slice(startIndex, start); addGroup(notJsonContent); startIndex = end; const placeholder = content.slice(start, end); const placeholderValue = placeholders[placeholder]; const readyJson = replacePlaceholders(placeholderValue, placeholders); addGroup(readyJson); }); const lastIndex = content.length; const notJsonContent = content.slice(startIndex, lastIndex); addGroup(notJsonContent); return groups; }; const STYLE = "jinja"; const getVisitorKeys = (ast) => { if ("type" in ast) { return ast.type === "root" ? ["nodes"] : []; } return Object.values(ast) .filter((node) => { return node.type === "block"; }) .map((e) => e.id); }; const print = (path) => { const node = path.getNode(); if (!node) { return []; } switch (node.type) { case "expression": return printExpression(node); case "statement": return printStatement(node); case "comment": return printCommentBlock(node); case "ignore": return printIgnoreBlock(node); } return []; }; const printExpression = (node) => { const multiline = node.content.includes("\n"); const expression = doc.builders.group(doc.builders.join(" ", [ ["{{", node.delimiter.start], multiline ? doc.builders.indent(getMultilineGroup(node.content)) : node.content, multiline ? [doc.builders.hardline, node.delimiter.end, "}}"] : [node.delimiter.end, "}}"], ]), { shouldBreak: node.preNewLines > 0, }); return node.preNewLines > 1 ? doc.builders.group([doc.builders.trim, doc.builders.hardline, expression]) : expression; }; const printStatement = (node) => { const multiline = node.content.includes("\n"); const statement = doc.builders.group(doc.builders.join(" ", [ ["{%", node.delimiter.start], multiline ? doc.builders.indent(getMultilineGroup(node.content)) : node.content, multiline ? [doc.builders.hardline, node.delimiter.end, "%}"] : [node.delimiter.end, "%}"], ]), { shouldBreak: node.preNewLines > 0 }); if (["else", "elif"].includes(node.keyword) && surroundingBlock(node)?.containsNewLines) { return [doc.builders.dedent(doc.builders.hardline), statement, doc.builders.hardline]; } return statement; }; const printCommentBlock = (node) => { const comment = doc.builders.group(node.content, { shouldBreak: node.preNewLines > 0, }); return node.preNewLines > 1 ? doc.builders.group([doc.builders.trim, doc.builders.hardline, comment]) : comment; }; const printIgnoreBlock = (node) => { return node.content; }; const embed = () => async (textToDoc, print, path, options) => { const node = path.getNode(); if (!node || !["root", "block"].includes(node.type)) { return undefined; } const mapped = await Promise.all(splitAtElse(node).map(async (content) => { const contentGroups = transformJsonToGroups(content); const document = []; for (const group of contentGroups) { try { document.push(await textToDoc(group, { ...options, parser: "json", })); } catch (e) { document.push(group); } } let ignoreDoc = false; return doc.utils.mapDoc(document, (currentDoc) => { if (typeof currentDoc !== "string") { return currentDoc; } const idxs = findPlaceholders(currentDoc, STYLE).filter(([start, end]) => currentDoc.slice(start, end) in node.nodes); if (!idxs.length) { ignoreDoc = false; return currentDoc; } const res = []; let lastEnd = 0; for (const [start, end] of idxs) { if (lastEnd < start) { res.push(currentDoc.slice(lastEnd, start)); } const p = currentDoc.slice(start, end); if (ignoreDoc) { res.push(node.nodes[p].originalText); } else { res.push(path.call(print, "nodes", p)); } lastEnd = end; } if (lastEnd > 0 && currentDoc.length > lastEnd) { res.push(currentDoc.slice(lastEnd)); } ignoreDoc = false; return res; }); })); if (node.type === "block") { const block = buildBlock(path, print, node, mapped); return node.preNewLines > 1 ? doc.builders.group([doc.builders.trim, doc.builders.hardline, block]) : block; } return [...mapped, doc.builders.hardline]; }; const getMultilineGroup = (content) => { // Dedent the content by the minimum indentation of any non-blank lines. const lines = content.split("\n"); const minIndent = Math.min(...lines .slice(1) // can't be the first line .filter((line) => line.trim()) .map((line) => line.search(/\S/))); return doc.builders.group(lines.map((line, i) => [ doc.builders.hardline, i === 0 ? line.trim() // don't dedent the first line : line.trim() ? line.slice(minIndent).trimEnd() : "", ])); }; const splitAtElse = (node) => { const content = node.content; const elseNodes = Object.values(node.nodes).filter((statement) => statement.type === "statement" && ["else", "elif"].includes(statement.keyword) && content.search(statement.id) !== NOT_FOUND); if (!elseNodes.length) { return [content]; } const re = new RegExp(`(${elseNodes.map((e) => e.id).join(")|(")})`); return content.split(re).filter(Boolean); }; const surroundingBlock = (node) => { return Object.values(node.nodes).find((n) => n.type === "block" && n.content.search(node.id) !== NOT_FOUND); }; const buildBlock = (path, print, block, mapped) => { // if the content is empty or whitespace only. if (block.content.match(/^\s*$/)) { return doc.builders.fill([ path.call(print, "nodes", block.start.id), doc.builders.softline, path.call(print, "nodes", block.end.id), ]); } if (block.containsNewLines) { return doc.builders.group([ path.call(print, "nodes", block.start.id), doc.builders.indent([doc.builders.softline, mapped]), doc.builders.hardline, path.call(print, "nodes", block.end.id), ]); } return doc.builders.group([ path.call(print, "nodes", block.start.id), mapped, path.call(print, "nodes", block.end.id), ]); }; // only common js imports.. without typing :: why? i dont know // eslint-disable-next-line @typescript-eslint/no-var-requires const estree = require("prettier/plugins/estree"); const PLUGIN_KEY = "jinja-json-template"; const languages = [ { name: "JinjaJsonTemplate", parsers: [PLUGIN_KEY, "json"], extensions: [".json.jinja", ".json.jinja2", ".json.j2"], vscodeLanguageIds: ["jinja"], }, ]; const parsers = { json: babel.parsers.json, [PLUGIN_KEY]: { astFormat: PLUGIN_KEY, parse, locStart: (node) => node.index, locEnd: (node) => node.index + node.length, }, }; const printers = { estree: estree.printers.estree, [PLUGIN_KEY]: { print, embed, getVisitorKeys, }, }; const options = {}; const plugin = { languages, parsers, printers, options, }; exports.PLUGIN_KEY = PLUGIN_KEY; exports.languages = languages; exports.options = options; exports.parsers = parsers; exports.plugin = plugin; exports.printers = printers; //# sourceMappingURL=index.cjs.map