prettier-plugin-gherkin
Version:
This prettier plugin format your gherkin (`.feature` files) documents.
319 lines (318 loc) • 9.93 kB
JavaScript
export function isWithLocation(node) {
return typeof node === 'object' && node !== null && 'location' in node;
}
function reEscapeTableCell(str) {
const out = str
// espace all pipes that have been unescaped
.replaceAll('|', '\\|')
// espace all backslashes that have been unescaped
.replace(/\\/g, '\\\\');
if (OPTIONS.escapeBackslashes) {
return out;
}
// replace all escaped backslashes, except those that are followed by a newline or a pipe
return out.replace(/(\\\\)(?![n\n\\])/g, '\\');
}
/**
* Set the max size of each column in the table
*/
function generateColumnSizes(tableRows) {
return tableRows.reduce((acc, row) => {
row.cells.forEach((cell, index) => {
if (!acc[index]) {
acc[index] = 0;
}
acc[index] = Math.max(acc[index], reEscapeTableCell(cell.value).length);
});
return acc;
}, []);
}
export class TypedGherkinNode {
constructor(originalNode) { }
}
export function isHasChildren(node) {
return typeof node === 'object' && node !== null && 'children' in node;
}
export function isHasChild(node) {
return typeof node === 'object' && node !== null && 'child' in node;
}
export class TypedGherkinNodeWithLocation extends TypedGherkinNode {
location;
constructor(originalNode) {
super(originalNode);
this.location = originalNode.location;
}
get lastLine() {
return this.location.line;
}
}
/**
* An options object that will be set for a document
* It is stored as a singleton that will be reset on each document to avoid passing it around for now.
* A better option may to have access to the options on every classes.
*/
let OPTIONS;
export class TypedGherkinDocument extends TypedGherkinNode {
uri;
feature;
comments;
constructor(originalNode, options) {
super(originalNode);
OPTIONS = options;
this.uri = originalNode.uri;
this.feature = originalNode.feature
? new TypedFeature(originalNode.feature)
: undefined;
this.comments = originalNode.comments.map((c) => new TypedComment(c));
}
get child() {
return this.feature;
}
}
export class TypedFeature extends TypedGherkinNodeWithLocation {
tags;
language;
keyword;
name;
description;
children;
constructor(originalNode) {
super(originalNode);
this.tags = originalNode.tags.map((t) => new TypedTag(t));
this.language = originalNode.language;
this.keyword = originalNode.keyword;
this.name = originalNode.name;
this.description = originalNode.description;
this.children = originalNode.children.map((c) => new TypedFeatureChild(c));
}
}
export class TypedTag extends TypedGherkinNodeWithLocation {
name;
id;
constructor(originalNode) {
super(originalNode);
this.name = originalNode.name;
this.id = originalNode.id;
}
}
export class TypedComment extends TypedGherkinNodeWithLocation {
text;
constructor(originalNode) {
super(originalNode);
this.text = originalNode.text;
}
get value() {
return this.text.trim();
}
}
export class TypedFeatureChild extends TypedGherkinNode {
rule;
background;
scenario;
constructor(originalNode) {
super(originalNode);
this.rule = originalNode.rule
? new TypedRule(originalNode.rule)
: undefined;
this.background = originalNode.background
? new TypedBackground(originalNode.background)
: undefined;
this.scenario = originalNode.scenario
? new TypedScenario(originalNode.scenario)
: undefined;
}
get child() {
return this.rule || this.background || this.scenario;
}
}
export class TypedRule extends TypedGherkinNodeWithLocation {
tags;
keyword;
name;
description;
children;
id;
constructor(originalNode) {
super(originalNode);
this.tags = originalNode.tags.map((t) => new TypedTag(t));
this.keyword = originalNode.keyword;
this.name = originalNode.name;
this.description = originalNode.description;
this.children = originalNode.children.map((c) => new TypedRuleChild(c));
this.id = originalNode.id;
}
}
export class TypedRuleChild extends TypedGherkinNode {
background;
scenario;
constructor(originalNode) {
super(originalNode);
this.background = originalNode.background
? new TypedBackground(originalNode.background)
: undefined;
this.scenario = originalNode.scenario
? new TypedScenario(originalNode.scenario)
: undefined;
}
get child() {
return this.scenario;
}
}
export class TypedBackground extends TypedGherkinNodeWithLocation {
keyword;
name;
description;
steps;
id;
constructor(originalNode) {
super(originalNode);
this.keyword = originalNode.keyword;
this.name = originalNode.name;
this.description = originalNode.description;
this.steps = originalNode.steps.map((s) => new TypedStep(s));
this.id = originalNode.id;
}
get children() {
return this.steps;
}
}
export class TypedStep extends TypedGherkinNodeWithLocation {
keyword;
keywordType;
text;
docString;
dataTable;
id;
constructor(originalNode) {
super(originalNode);
this.keyword = originalNode.keyword;
this.keywordType = originalNode.keywordType;
this.text = originalNode.text;
this.docString = originalNode.docString
? new TypedDocString(originalNode.docString)
: undefined;
this.dataTable = originalNode.dataTable
? new TypedDataTable(originalNode.dataTable)
: undefined;
this.id = originalNode.id;
}
get children() {
return [this.docString, this.dataTable].filter((c) => typeof c !== 'undefined');
}
get lastLine() {
const dataTableLength = this.dataTable?.nbRows ?? 0;
const docStringLength = this.docString?.nbRows ?? 0;
return this.location.line + dataTableLength + docStringLength;
}
}
export class TypedScenario extends TypedGherkinNodeWithLocation {
tags;
keyword;
name;
description;
steps;
examples;
id;
constructor(originalNode) {
super(originalNode);
this.location = originalNode.location;
this.tags = originalNode.tags.map((t) => new TypedTag(t));
this.keyword = originalNode.keyword;
this.name = originalNode.name;
this.description = originalNode.description;
this.steps = originalNode.steps.map((s) => new TypedStep(s));
this.examples = originalNode.examples.map((e) => new TypedExamples(e));
this.id = originalNode.id;
}
get children() {
return [...this.steps, ...this.examples, ...this.tags];
}
}
export class TypedExamples extends TypedGherkinNodeWithLocation {
tags;
keyword;
name;
description;
tableHeader;
tableBody;
id;
constructor(originalNode) {
super(originalNode);
const rows = [
originalNode.tableHeader,
...(originalNode.tableBody ?? []),
].filter((row) => typeof row !== 'undefined');
const columnSizes = generateColumnSizes(rows);
this.tags = originalNode.tags.map((t) => new TypedTag(t));
this.keyword = originalNode.keyword;
this.name = originalNode.name;
this.description = originalNode.description;
this.tableHeader = originalNode.tableHeader
? new TypedTableRow(originalNode.tableHeader, columnSizes)
: undefined;
this.tableBody = originalNode.tableBody.map((r) => new TypedTableRow(r, columnSizes));
this.id = originalNode.id;
}
get children() {
return [
...(this.tableHeader ? [this.tableHeader] : []),
...this.tableBody,
...this.tags,
];
}
}
export class TypedTableRow extends TypedGherkinNodeWithLocation {
cells;
id;
columnSizes;
constructor(originalNode, columnSizes) {
super(originalNode);
this.cells = originalNode.cells.map((c, index) => new TypedTableCell(c, columnSizes[index]));
this.id = originalNode.id;
this.columnSizes = columnSizes;
}
get children() {
return this.cells;
}
}
export class TypedTableCell extends TypedGherkinNodeWithLocation {
value;
displaySize;
constructor(originalNode, displaySize) {
super(originalNode);
this.value = reEscapeTableCell(originalNode.value);
this.displaySize = Math.max(displaySize ?? 0, this.value.length);
}
}
export class TypedDocString extends TypedGherkinNodeWithLocation {
mediaType;
content;
delimiter;
constructor(originalNode) {
super(originalNode);
this.mediaType = originalNode.mediaType;
this.content = originalNode.content;
this.delimiter = originalNode.delimiter;
}
get nbRows() {
return this.content.split('\n').length + 2;
}
}
export class TypedDataTable extends TypedGherkinNodeWithLocation {
rows;
constructor(originalNode) {
super(originalNode);
const columnSizes = generateColumnSizes(originalNode.rows);
this.rows = originalNode.rows.map((r) => new TypedTableRow(r, columnSizes));
}
get nbRows() {
return (this.rows.reduce((acc, row) => {
// @ts-expect-error comments are added by prettier directly
const commentLines = row.comments?.length ?? 0;
return acc + commentLines + 1;
}, 0) ?? 0);
}
get children() {
return this.rows;
}
}