Skip to content

Commit

Permalink
feat: support linting html in template literals in no-extra-spacing-t…
Browse files Browse the repository at this point in the history
…ext (#239)

* feat: support linting html in template literals in no-extra-spacing-text

* refactor

* refactor

* no-multiple-empty-lines

* no-trailing-spaces

* typo
  • Loading branch information
yeonjuan authored Dec 1, 2024
1 parent ed2a3b3 commit 80a2bec
Show file tree
Hide file tree
Showing 21 changed files with 474 additions and 181 deletions.
20 changes: 5 additions & 15 deletions packages/eslint-plugin/lib/rules/element-newline.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/**
* @typedef { import("../types").RuleModule } RuleModule
* @typedef { import("../types").ProgramNode } ProgramNode
* @typedef { import("../types").TagNode } TagNode
* @typedef { import("../types").BaseNode } BaseNode
* @typedef { import("../types").CommentNode } CommentNode
Expand Down Expand Up @@ -302,20 +301,11 @@ module.exports = {
return true;
}

return createVisitors(
context,
{
Program(node) {
// @ts-ignore
checkSiblings(node.body);
},
},
{
return createVisitors(context, {
Document(node) {
// @ts-ignore
Document(node) {
checkSiblings(node.children);
},
}
);
checkSiblings(node.children);
},
});
},
};
2 changes: 1 addition & 1 deletion packages/eslint-plugin/lib/rules/no-duplicate-id.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ module.exports = {

return {
Tag: createTagVisitor(htmlIdAttrsMap),
"Program:exit"() {
"Document:exit"() {
report(htmlIdAttrsMap);
},
TaggedTemplateExpression(node) {
Expand Down
93 changes: 66 additions & 27 deletions packages/eslint-plugin/lib/rules/no-extra-spacing-text.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/**
* @typedef { import("../types").RuleModule } RuleModule
* @typedef { import("../types").ProgramNode } ProgramNode
* @typedef { import("es-html-parser").CommentContentNode } CommentContentNode
* @typedef { import("es-html-parser").TagNode } TagNode
* @typedef { import("es-html-parser").CommentNode } CommentNode
* @typedef { import("../types").ContentNode } ContentNode
* @typedef { import("../types").TextNode } TextNode
* @typedef { import("es-html-parser").TextNode } TextNode
* @typedef { import("../types").LineNode } LineNode
* @typedef { import("../types").Range } Range
*/

const { RULE_CATEGORY } = require("../constants");
const { isTag, isText, isComment } = require("./utils/node");
const { isTag, isOverlapWithTemplates } = require("./utils/node");
const { getSourceCode } = require("./utils/source-code");
const { createVisitors } = require("./utils/visitors");

const MESSAGE_IDS = {
UNEXPECTED: "unexpected",
Expand Down Expand Up @@ -49,39 +54,36 @@ module.exports = {

create(context) {
const options = context.options[0] || {};
/**
* @type {string[]}
*/
const skipTags = options.skip || [];
const sourceCode = context.getSourceCode();
const sourceCode = getSourceCode(context);
/**
* @type {TagNode[]}
*/
const tagStack = [];

/**
* @param {Array<ContentNode>} siblings
* @param {CommentNode | TextNode} node
* @returns {boolean}
*/
function checkSiblings(siblings) {
for (
let length = siblings.length, index = 0;
index < length;
index += 1
function hasSkipTagOnParent(node) {
// @ts-ignore
const parent = node.parent;
if (
parent &&
// @ts-ignore
isTag(parent) &&
skipTags.some((skipTag) => skipTag === parent.name)
) {
const node = siblings[index];

if (isTag(node) && skipTags.includes(node.name) === false) {
checkSiblings(node.children);
} else if (isText(node)) {
stripConsecutiveSpaces(node);
} else if (isComment(node)) {
stripConsecutiveSpaces(node.value);
}
return true;
}
return false;
}

return {
Program(node) {
// @ts-ignore
checkSiblings(node.body);
},
};

/**
* @param {TextNode | CommentContentNode} node
* @param {CommentContentNode | TextNode} node
*/
function stripConsecutiveSpaces(node) {
const text = node.value;
Expand All @@ -98,6 +100,15 @@ module.exports = {
const indexStart = node.range[0] + matcher.lastIndex - space.length;
const indexEnd = indexStart + space.length;

const hasOverlap = isOverlapWithTemplates(node.templates, [
indexStart,
indexEnd,
]);

if (hasOverlap) {
return;
}

context.report({
node: node,
loc: {
Expand All @@ -114,5 +125,33 @@ module.exports = {
});
}
}

return createVisitors(context, {
Comment(node) {
if (hasSkipTagOnParent(node)) {
return;
}
stripConsecutiveSpaces(node.value);
},
Text(node) {
if (hasSkipTagOnParent(node)) {
return;
}
stripConsecutiveSpaces(node);
},
Tag(node) {
tagStack.push(node);
if (
skipTags.some((skipTag) =>
tagStack.some((tag) => tag.name === skipTag)
)
) {
return;
}
},
"Tag:exit"() {
tagStack.pop();
},
});
},
};
136 changes: 94 additions & 42 deletions packages/eslint-plugin/lib/rules/no-multiple-empty-lines.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
/**
* @typedef { import("../types").RuleModule } RuleModule
* @typedef { import("../types").ProgramNode } ProgramNode
* @typedef { import("es-html-parser").DocumentNode } DocumentNode
* @typedef { import("es-html-parser").AnyToken } AnyToken
* @typedef { import("es-html-parser").CommentContentNode } CommentContentNode
* @typedef { import("es-html-parser").TextNode } TextNode
*/

const { parse } = require("@html-eslint/template-parser");
const { RULE_CATEGORY } = require("../constants");
const {
shouldCheckTaggedTemplateExpression,
shouldCheckTemplateLiteral,
} = require("./utils/settings");
const { getSourceCode } = require("./utils/source-code");
const {
codeToLines,
isRangesOverlap,
getTemplateTokens,
} = require("./utils/node");

const MESSAGE_IDS = {
UNEXPECTED: "unexpected",
Expand Down Expand Up @@ -42,52 +56,90 @@ module.exports = {
},

create(context) {
const sourceCode = context.getSourceCode();
const lines = sourceCode.lines;
const max = context.options.length ? context.options[0].max : 2;
return {
/**
* @param {ProgramNode} node
*/
"Program:exit"(node) {
/** @type {number[]} */
const nonEmptyLineNumbers = [];
const sourceCode = getSourceCode(context);

lines.forEach((line, index) => {
if (line.trim().length > 0) {
nonEmptyLineNumbers.push(index + 1);
}
});
/**
* @param {string[]} lines
* @param {number} lineOffset
* @param {((CommentContentNode | TextNode)['templates'][number])[]} tokens
*/
function check(lines, lineOffset, tokens) {
/** @type {number[]} */
const nonEmptyLineNumbers = [];

nonEmptyLineNumbers.forEach((current, index, arr) => {
const before = arr[index - 1];
if (typeof before === "number") {
if (current - before - 1 > max) {
context.report({
node,
loc: {
start: { line: before, column: 0 },
end: { line: current, column: 0 },
},
messageId: MESSAGE_IDS.UNEXPECTED,
data: {
max,
},
fix(fixer) {
const start = sourceCode.getIndexFromLoc({
line: before + 1,
column: 0,
});
const end = sourceCode.getIndexFromLoc({
line: current - max,
column: 0,
});
return fixer.removeRange([start, end]);
},
});
lines.forEach((line, index) => {
if (line.trim().length > 0) {
nonEmptyLineNumbers.push(index + lineOffset);
}
});

nonEmptyLineNumbers.forEach((current, index, arr) => {
const before = arr[index - 1];
if (typeof before === "number") {
if (current - before - 1 > max) {
const start = sourceCode.getIndexFromLoc({
line: before + 1,
column: 0,
});
const end = sourceCode.getIndexFromLoc({
line: current - max,
column: 0,
});
if (
tokens.some((token) => isRangesOverlap(token.range, [start, end]))
) {
return;
}

context.report({
loc: {
start: { line: before, column: 0 },
end: { line: current, column: 0 },
},
messageId: MESSAGE_IDS.UNEXPECTED,
data: {
max,
},
fix(fixer) {
return fixer.removeRange([start, end]);
},
});
}
});
}
});
}
return {
"Document:exit"() {
check(sourceCode.lines, 1, []);
},
TaggedTemplateExpression(node) {
if (shouldCheckTaggedTemplateExpression(node, context)) {
const { html, tokens } = parse(
node.quasi,
getSourceCode(context),
{}
);
const lines = codeToLines(html);
check(
lines,
// @ts-ignore
node.quasi.loc.start.line,
getTemplateTokens(tokens)
);
}
},
TemplateLiteral(node) {
if (shouldCheckTemplateLiteral(node, context)) {
const { html, tokens } = parse(node, getSourceCode(context), {});
const lines = codeToLines(html);
check(
lines,
// @ts-ignore
node.quasi.loc.start.line,
getTemplateTokens(tokens)
);
}
},
};
},
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin/lib/rules/no-multiple-h1.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module.exports = {
h1s.push(node);
}
},
"Program:exit"() {
"Document:exit"() {
if (h1s.length > 1) {
h1s.forEach((h1) => {
context.report({
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin/lib/rules/no-skip-heading-levels.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ module.exports = {
level: parseInt(node.name.replace("h", ""), 10),
});
},
"Program:exit"() {
"Document:exit"() {
if (headings.length <= 1) {
return;
}
Expand Down
Loading

0 comments on commit 80a2bec

Please sign in to comment.