UNPKG

@bgotink/prettier-plugin-kdl

Version:

434 lines (378 loc) 9.93 kB
import { parse, Value, Identifier, Document, Node, Entry } from "@bgotink/kdl"; import { util, doc } from "prettier"; /** @typedef {import('prettier').Doc} Doc */ const { builders } = doc; const { getStringWidth } = util; const plainIdentifierRe = /^(?![+-][0-9])[\x21\x23-\x27\x2A\x2B\x2D\x2E\x3A\x3F-\x5A\x5E-\x7A\x7C\x7E-\uFFFF][\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x3A\x3F-\x5A\x5E-\x7A\x7C\x7E-\uFFFF]*$/; const reservedIdentifiers = new Set([ "inf", "-inf", "nan", "true", "false", "null", ]); /** @param {string} line */ function trimStart(line) { return line.replace( /^[ \t\uFEFF\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]*/, "", ); } /** @param {string} line */ function trimEnd(line) { return line.replace( /[ \t\uFEFF\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]*$/, "", ); } /** @param {string} line */ function trim(line) { return trimEnd(trimStart(line)); } /** * @param {Value} value * @returns {Doc & string} */ function printValue(value) { const rawValue = value.value; switch (typeof rawValue) { case "object": // null case "boolean": return `#${rawValue}`; case "string": { let numberOfHashes = 0; while (rawValue.includes(`"${"#".repeat(numberOfHashes)}`)) { numberOfHashes++; } if (numberOfHashes === 0) { return JSON.stringify(rawValue); } return `${"#".repeat(numberOfHashes)}"${rawValue}"${"#".repeat( numberOfHashes, )}`; } case "number": if (Number.isNaN(rawValue)) { return "#nan"; } else if (!Number.isFinite(rawValue)) { return rawValue < 0 ? "#-inf" : "#inf"; } else { return rawValue.toString(); } } } /** * @param {Pick<Identifier, 'name'>} identifier * @returns {Doc & string} */ function printIdentifier(identifier) { if ( identifier.name.length > 0 && !reservedIdentifiers.has(identifier.name) && plainIdentifierRe.test(identifier.name) ) { return identifier.name; } let numberOfHashes = 0; while (identifier.name.includes(`"${"#".repeat(numberOfHashes)}`)) { numberOfHashes++; } if (numberOfHashes === 0) { return `"${identifier.name}"`; } return `${"#".repeat(numberOfHashes)}"${identifier.name}"${"#".repeat( numberOfHashes, )}`; } /** * @param {Entry} entry * @returns {Doc & string} */ function printEntry(entry) { /** @type {string[]} */ const parts = []; if (entry.name != null) { parts.push(printIdentifier(entry.name), entry.equals ?? "="); } if (entry.tag != null) { parts.push( "(", trim(entry.tag.leading ?? ""), printIdentifier(entry.tag), trim(entry.tag.trailing ?? ""), ")", trim(entry.betweenTagAndValue ?? ""), ); } parts.push(printValue(entry.value)); return parts.join(""); } /** * @param {string} text */ function printLineSpace(text, { previousWasNewline = false } = {}) { /** @type {Doc[]} */ const parts = []; let hasAddedEmptyLine = false; let hasAddedNonEmptyContent = previousWasNewline; let lastWasNewline = previousWasNewline; for (const whitespace of parse(text, { as: "whitespace in document", })) { switch (whitespace.type) { case "newline": if (lastWasNewline && hasAddedNonEmptyContent && !hasAddedEmptyLine) { parts.push(""); hasAddedEmptyLine = true; } lastWasNewline = true; break; case "space": break; case "singleline": parts.push(trim(whitespace.text.slice(0, -1))); hasAddedEmptyLine = false; hasAddedNonEmptyContent = true; lastWasNewline = true; // single-line comment implies newline break; case "multiline": { const lines = whitespace.text.split( /\x0D\x0A|[\x0A\x0C\x0D\x85\u2028\u2029]/, ); /** @type {Doc} */ let comment; if ( lines.length === 1 || !lines.every((line, i) => i === 0 || trimStart(line).startsWith("*")) ) { comment = builders.join(builders.hardline, lines); } else { comment = builders.join(builders.hardline, [ trimStart(lines[0]), ...lines.slice(1).map((line) => ` ${trimStart(line)}`), ]); } parts.push(comment); hasAddedEmptyLine = false; hasAddedNonEmptyContent = true; lastWasNewline = false; break; } case "slashdash": parts.push(["/-", printNode(whitespace.value)]); hasAddedEmptyLine = false; hasAddedNonEmptyContent = true; lastWasNewline = false; break; } } return parts; } /** * @param {Node} node * @returns {Doc} */ function printNode(node, isFirstNode = false) { let name = printIdentifier(node.name); if (node.tag) { name = `(${trim(node.tag.leading ?? "")}${printIdentifier(node.tag)}${trim(node.tag.trailing ?? "")})${trim(node.betweenTagAndName ?? "")}${name}`; } const nameAlign = getStringWidth(name) + 1; /** @type {Doc[][]} */ const header = []; /** @type {Doc[]} */ let lastHeaderItem = []; const continuation = builders.ifBreak(" \\"); /** @param {string} text */ function addCommentToHeader(text) { for (const whitespace of parse(text, { as: "whitespace in node", })) { switch (whitespace.type) { case "space": break; case "line-escape": { for (const nestedWhitespace of parse(whitespace.text.slice(1), { as: "whitespace in document", })) { switch (nestedWhitespace.type) { case "multiline": const lines = whitespace.text.split( /\x0D\x0A|[\x0A\x0C\x0D\x85\u2028\u2029]/, ); /** @type {Doc} */ let comment; if ( lines.length === 1 || !lines.every( (line, i) => i === 0 || trimStart(line).startsWith("*"), ) ) { comment = builders.join( builders.hardlineWithoutBreakParent, lines, ); } else { comment = builders.join( builders.hardlineWithoutBreakParent, [ trim(lines[0]), ...lines.slice(1).map((line) => ` ${trim(line)}`), ], ); } lastHeaderItem.push(continuation); header.push((lastHeaderItem = [comment])); case "singleline": case "singleline": lastHeaderItem.push(continuation); lastHeaderItem = []; header.push([ builders.breakParent, `\\ ${trim(nestedWhitespace.text.slice(0, -1))}`, ]); break; default: // do nothing } } } break; case "slashdash": lastHeaderItem.push(continuation); header.push( (lastHeaderItem = [ "/-", whitespace.value instanceof Entry ? printEntry(whitespace.value) : ["{", printDocument(whitespace.value), "}"], ]), ); break; case "multiline": { const lines = whitespace.text.split( /\x0D\x0A|[\x0A\x0C\x0D\x85\u2028\u2029]/, ); /** @type {Doc} */ let comment; if ( lines.length === 1 || !lines.every( (line, i) => i === 0 || trimStart(line).startsWith("*"), ) ) { comment = builders.join(builders.hardlineWithoutBreakParent, lines); } else { comment = builders.join(builders.hardlineWithoutBreakParent, [ trim(lines[0]), ...lines.slice(1).map((line) => ` ${trim(line)}`), ]); } lastHeaderItem.push(continuation); header.push((lastHeaderItem = [comment])); } } } } const entries = Array.from(node.entries); const argEntries = node.getArgumentEntries(); if ( argEntries.length === 1 && entries[0] === argEntries[0] && (!argEntries[0].leading || !trim(argEntries[0].leading)) ) { entries.shift(); name = `${name} ${printEntry(argEntries[0])}`; } for (const entry of entries) { if (entry.leading) { addCommentToHeader(entry.leading); } lastHeaderItem.push(continuation); header.push((lastHeaderItem = [printEntry(entry)])); } if (node.beforeChildren) { addCommentToHeader(node.beforeChildren); } /** @type {Doc[]} */ const parts = []; if (node.leading) { // Nodes always end on a newline after we printed them, because we // (currently) don't ever print multiple nodes with just `;` in between parts.push( ...printLineSpace(node.leading, { previousWasNewline: !isFirstNode }), ); } if (!node.hasChildren() && !node.children?.trailing) { if (node.children != null) { lastHeaderItem.push(continuation); header.push(["{}"]); } const joinedHeader = builders.join(builders.line, header); parts.push( builders.group([ name, continuation, builders.ifBreak( builders.align(nameAlign, [builders.line, joinedHeader]), [builders.line, joinedHeader], ), ]), ); } else { lastHeaderItem.push(continuation); header.push(["{"]); const joinedHeader = builders.join(builders.line, header); parts.push([ builders.group([ name, continuation, builders.ifBreak( builders.align(nameAlign, [builders.line, joinedHeader]), [builders.line, joinedHeader], ), ]), builders.indent([ builders.hardline, printDocument(/** @type {Document} */ (node.children)), ]), builders.hardline, "}", ]); } if (node.trailing) { let trailing = node.trailing; if (trailing.endsWith(";")) { trailing = trailing.slice(0, -1); } parts.push(...printLineSpace(trailing)); } return builders.join(builders.hardline, parts); } /** * @param {Document} document * @returns {Doc} */ function printDocument(document) { const nodes = document.nodes.map((node, i) => printNode(node, i === 0)); /** @type {Doc[]} */ let trailing = []; if (document.trailing) { trailing.push(...printLineSpace(document.trailing)); } return builders.join(builders.hardline, [ builders.join(builders.hardline, nodes), ...trailing, ]); } /** * @type {import('prettier').Printer<Document>} */ export const printer = { print: (path) => { return [printDocument(path.getValue()), builders.hardline]; }, };