prettier-plugin-nginx
Version:
NGINX configuration plugin for Prettier
550 lines (549 loc) • 23.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.printers = exports.parsers = exports.languages = exports.defaultOptions = exports.options = void 0;
const doc_1 = require("prettier/doc");
const newline = doc_1.builders.hardline;
exports.options = {
alignDirectives: {
type: "boolean",
category: "nginx",
default: true,
description: "Align directive parameters within a block to the same column.",
},
alignUniversally: {
type: "boolean",
category: "nginx",
default: false,
description: "Align all directive parameters within a file to the same column.",
},
wrapParameters: {
type: "boolean",
category: "nginx",
default: true,
description: "Wrap parameters to new lines to fit print width.",
},
continuationIndent: {
type: "int",
category: "nginx",
default: 2,
description: "Additional indentation for wrapped lines.",
},
};
exports.defaultOptions = {
tabWidth: 4,
};
exports.languages = [
{
name: "nginx",
parsers: ["nginx"],
extensions: [".nginx", ".nginxconf"],
linguistLanguageId: 248,
},
];
exports.parsers = {
nginx: {
astFormat: "nginx",
parse: (text, parsers, options) => {
const parseRecursive = (t, rootIndex) => {
let nodes = [];
let tokenBuffer = [];
let insideComment = false;
let insideString = false;
let skipTo = undefined;
const breakToken = () => {
insideComment = false;
insideString = false;
if (tokenBuffer.length <= 0) {
return;
}
const start = tokenBuffer[0][0] + rootIndex;
const end = tokenBuffer[tokenBuffer.length - 1][0] + rootIndex;
const token = tokenBuffer
.map(([_, c]) => {
return c;
})
.join("");
tokenBuffer = [];
if (token === ";") {
nodes.push({
type: "semicolon",
content: undefined,
start: start,
end: end,
});
return;
}
if (token === "\n") {
if (nodes.length > 0 &&
nodes[nodes.length - 1].type === "linebreak") {
return;
}
nodes.push({
type: "linebreak",
content: undefined,
start: start,
end: end,
});
return;
}
if (token[0] === "#") {
let commentType = "comment";
if (nodes.length > 0 &&
nodes[nodes.length - 1].type != "linebreak") {
commentType = "inlinecomment";
}
// if the rootIndex is 0, then this is the main block -
// if there is no break at the start of a block, and the comment
// is the first item, then it is an inline comment
if (rootIndex != 0 && nodes.length === 0) {
commentType = "inlinecomment";
}
nodes.push({
type: commentType,
content: token,
start: start,
end: end,
});
return;
}
if (token === "\t" || token === " ") {
return;
}
const semanticTokens = nodes.filter((n) => {
return (n.type != "linebreak" &&
n.type != "inlinecomment" &&
n.type != "comment");
});
if (semanticTokens.length === 0 ||
semanticTokens[semanticTokens.length - 1].type === "semicolon" ||
semanticTokens[semanticTokens.length - 1].type === "block") {
nodes.push({
type: "name",
content: token,
start: start,
end: end,
});
return;
}
nodes.push({
type: "parameter",
content: token,
start: start,
end: end,
});
};
const findParenEnd = (startIndex) => {
let blockEnd = t.length;
let parenCount = 0;
for (let j = startIndex; j < t.length; j++) {
switch (t[j]) {
case "{":
parenCount += 1;
break;
case "}":
parenCount -= 1;
break;
}
if (parenCount === 0) {
blockEnd = j + 1;
break;
}
}
return blockEnd;
};
for (let i = 0; i < t.length; i++) {
if (skipTo && i < skipTo) {
continue;
}
const c = t[i];
if (c === "\r") {
continue;
}
if (c === "\n") {
breakToken();
tokenBuffer.push([i, c]);
breakToken();
continue;
}
if (insideComment) {
tokenBuffer.push([i, c]);
continue;
}
if (c === "$" && i + 1 < t.length && t[i + 1] === "{") {
let envVarEnd = findParenEnd(i + 1);
for (let q = i; q < envVarEnd; q++) {
tokenBuffer.push([q, t[q]]);
}
skipTo = envVarEnd;
continue;
}
if (c === "#") {
breakToken();
insideComment = true;
tokenBuffer.push([i, c]);
continue;
}
if (insideString) {
if (insideString === c) {
insideString = false;
}
tokenBuffer.push([i, c]);
continue;
}
if (c === "'" || c === '"') {
insideString = c;
tokenBuffer.push([i, c]);
continue;
}
if (c === " " || c === "\t") {
breakToken();
tokenBuffer.push([i, c]);
breakToken();
continue;
}
if (c === ";") {
breakToken();
tokenBuffer.push([i, c]);
breakToken();
continue;
}
if (c === "{") {
breakToken();
let blockEnd = findParenEnd(i);
nodes.push({
type: "block",
content: parseRecursive(t.slice(i + 1, blockEnd + 1), rootIndex + i),
start: i,
end: blockEnd,
});
skipTo = blockEnd;
continue;
}
if (c === "}") {
continue;
}
tokenBuffer.push([i, c]);
}
// re-parse nodes to gather directives, remove linebreaks
nodes = nodes.filter((n) => n.type != "linebreak");
skipTo = 0;
let gatheredNodes = [];
for (let i = 0; i < nodes.length; i++) {
if (i < skipTo) {
continue;
}
const subnode = nodes[i];
if (subnode.type === "name") {
let directiveType = "directive";
skipTo = i + 1;
for (let j = i; j < nodes.length; j++) {
if (nodes[j].type === "semicolon" || nodes[j].type === "block") {
skipTo = j + 1;
if (nodes[j].type === "block") {
directiveType = "blockdirective";
}
if (j + 1 < nodes.length &&
nodes[j + 1].type === "inlinecomment") {
skipTo = j + 2;
}
break;
}
}
gatheredNodes.push({
type: directiveType,
content: nodes.slice(i, skipTo),
start: subnode.start,
end: nodes[skipTo - 1].end,
});
}
else {
gatheredNodes.push(subnode);
}
}
return gatheredNodes;
};
return {
type: "main",
start: 0,
end: text.length,
content: parseRecursive(text, 0),
};
},
locStart: (node) => {
return node.start;
},
locEnd: (node) => {
return node.end;
},
hasPragma: (text) => {
// TODO: Check all comments before directives, and include @format
let firstLine = null;
text.split(/\r?\n/).some((line) => {
line = line.replace(" ", "");
if (line.length > 0)
firstLine = line;
if (line[0])
return firstLine === null;
});
return firstLine === "#@prettier";
},
},
};
exports.printers = {
nginx: {
print(path, options, print) {
const root = path.getNode();
if (!root || root.type != "main") {
throw Error("Invalid root node");
}
// pre-parse the AST to add linebreaks between blocks and directives
const preparseRecursive = (node) => {
if (node.type != "main" &&
node.type != "block" &&
node.type != "directive" &&
node.type != "blockdirective") {
return;
}
let insertions = [];
for (let i = 0; i < node.content.length; i++) {
const subnode = node.content[i];
if (subnode.type === "main" ||
subnode.type === "block" ||
subnode.type === "directive" ||
subnode.type === "blockdirective") {
preparseRecursive(subnode);
}
if (subnode.type !== "blockdirective") {
continue;
}
// if there is a previous directive, insert a break
for (let j = i - 1; j >= 0; j--) {
if (node.content[j].type != "comment" &&
node.content[j].type != "inlinecomment") {
let pos;
if (j > 0) {
pos = node.content[j - 1].end;
}
else {
pos = 0;
}
insertions.push([
j + 1,
{
type: "hardbreak",
content: undefined,
start: pos,
end: pos,
},
]);
break;
}
}
// if there is any following element, insert a break
if (i != node.content.length - 1) {
let hasExtraElement = false;
for (let j = i + 1; j < node.content.length && !hasExtraElement; j++) {
switch (node.content[j].type) {
case "comment":
case "inlinecomment":
break;
default:
hasExtraElement = true;
}
}
if (hasExtraElement) {
insertions.push([
i + 1,
{
type: "hardbreak",
content: undefined,
start: subnode.end,
end: subnode.end,
},
]);
}
}
}
for (let i = 0; i < insertions.length; i++) {
const [index, breakNode] = insertions[i];
node.content.splice(i + index, 0, breakNode);
}
};
preparseRecursive(root);
const getIndents = (indents) => {
if (options.useTabs) {
return "\t".repeat(indents);
}
return " ".repeat(options.tabWidth).repeat(indents);
};
const getLineLength = (lineDocs) => {
let width = 0;
for (let i = lineDocs.length - 1; i >= 0; i--) {
for (let j = lineDocs[i].length - 1; j >= 0; j--) {
switch (lineDocs[i][j]) {
case "\t":
width += options.tabWidth;
break;
case "\n":
case "\r":
return width;
default:
width += 1;
}
}
}
return width;
};
// find length of longest directive, for aligning parameters to columns
const getNameColEnd = (node, all, longest = 0, indents = 0) => {
indents = Math.max(indents, 0);
if (node.type === "name") {
const paramColStart = getLineLength([getIndents(indents)]) + node.content.length;
longest = Math.max(longest, paramColStart);
}
if (node.type === "main" ||
node.type === "directive" ||
node.type === "block" ||
node.type === "blockdirective") {
node.content.forEach((n) => {
if ((!all && n.type === "block") || n.type === "main") {
return;
}
// block directives don't get aligned
if (node.type === "blockdirective" && n.type === "name") {
return;
}
longest = Math.max(getNameColEnd(n, all, longest, node.type === "block" ? indents + 1 : indents), longest);
});
}
return longest;
};
let universalColEnd = 0;
if (options.alignUniversally && options.alignDirectives) {
universalColEnd = getNameColEnd(root, true);
}
const generateDirectiveDocs = (node, indentsCount, blockColEnd) => {
var _a;
let directiveDocs = [];
if (node.content.length <= 0) {
return [];
}
let maxColName = 0;
if (options.alignDirectives) {
maxColName = blockColEnd;
if (options.alignUniversally) {
maxColName = universalColEnd;
}
}
const indents = getIndents(indentsCount);
directiveDocs.push(indents);
for (let i = 0; i < node.content.length; i++) {
const subnode = node.content[i];
if (subnode.type === "name") {
if (i != 0) {
throw Error("Invalid index of name node");
}
directiveDocs.push(subnode.content);
// add column alignment spaces
let alignmentSpaces = maxColName - getLineLength(directiveDocs) + 1;
if (node.type != "blockdirective" && alignmentSpaces > 0) {
directiveDocs.push(" ".repeat(alignmentSpaces));
}
}
else if (subnode.type === "semicolon") {
directiveDocs.push(";");
}
else if (subnode.type === "inlinecomment") {
directiveDocs.push(subnode.content);
directiveDocs.push("\n");
}
else if (subnode.type === "parameter") {
if (options.wrapParameters &&
getLineLength(directiveDocs) + subnode.content.length + 1 >
options.printWidth &&
i >= 1 &&
node.content[i - 1].type != "name") {
directiveDocs.push("\n");
directiveDocs.push(indents);
directiveDocs.push(" ".repeat(options.continuationIndent ? options.continuationIndent : 2));
}
directiveDocs.push(subnode.content);
}
else if (subnode.type === "block") {
directiveDocs.push("{");
directiveDocs = directiveDocs.concat(generateBlockDocs(subnode, indentsCount + 1, blockColEnd));
directiveDocs.push("\n");
directiveDocs.push(indents);
directiveDocs.push("}");
if (node.content.length > i + 1 &&
node.content[i + 1].type != "inlinecomment") {
directiveDocs.push("\n");
}
}
if (i < node.content.length - 1 &&
node.content[i + 1].type != "semicolon" &&
(directiveDocs.length > 0
? [" ", "\t", undefined].indexOf((_a = directiveDocs.at(-1)) === null || _a === void 0 ? void 0 : _a.at(-1)) ===
-1
: true)) {
directiveDocs.push(" ");
}
}
return directiveDocs.flat();
};
const generateBlockDocs = (node, indentsCount, blockColEnd) => {
let blockDocs = [];
if (options.alignDirectives) {
blockColEnd = getNameColEnd(node, false, 0, indentsCount - 1);
}
if (!(node.content.length > 0 && node.content[0].type == "inlinecomment")) {
blockDocs.push("\n");
}
node.content.forEach((subnode) => {
switch (subnode.type) {
case "inlinecomment":
blockDocs.push(" ");
blockDocs.push(subnode.content);
blockDocs.push("\n");
break;
case "comment":
blockDocs.push("\n");
blockDocs.push(getIndents(indentsCount) + subnode.content);
blockDocs.push("\n");
break;
case "blockdirective":
blockDocs = blockDocs.concat(generateDirectiveDocs(subnode, indentsCount, 0));
break;
case "directive":
blockDocs = blockDocs.concat(generateDirectiveDocs(subnode, indentsCount, blockColEnd));
blockDocs.push("\n");
break;
case "hardbreak":
blockDocs.push("\r");
break;
}
});
return blockDocs.flat();
};
let docs = [];
const addToDocs = (item) => {
if (item === "\n") {
if (docs.length > 0 && docs[docs.length - 1] != newline) {
docs.push(newline);
}
}
else if (item === "\r") {
while ((docs.length > 0 ? docs[docs.length - 1] : " ") != newline ||
(docs.length > 1 ? docs[docs.length - 2] : " ") != newline) {
docs.push(newline);
}
}
else {
docs.push(item);
}
};
generateBlockDocs(root, 0, 0).forEach((d) => addToDocs(d));
return docs;
},
},
};
;