@html-eslint/eslint-plugin
Advanced tools
Comparing version 0.26.0 to 0.27.0
@@ -10,3 +10,8 @@ module.exports = { | ||
"@html-eslint/attrs-newline": "error", | ||
"@html-eslint/element-newline": "error", | ||
"@html-eslint/element-newline": [ | ||
"error", | ||
{ | ||
inline: [`$inline`], | ||
}, | ||
], | ||
"@html-eslint/no-duplicate-id": "error", | ||
@@ -13,0 +18,0 @@ "@html-eslint/indent": "error", |
@@ -6,2 +6,13 @@ /** | ||
* @typedef { import("../types").BaseNode } BaseNode | ||
* @typedef { import("../types").CommentNode } CommentNode | ||
* @typedef { import("../types").DoctypeNode } DoctypeNode | ||
* @typedef { import("../types").ScriptTagNode } ScriptTagNode | ||
* @typedef { import("../types").StyleTagNode } StyleTagNode | ||
* @typedef { import("../types").TextNode } TextNode | ||
* @typedef { CommentNode | DoctypeNode | ScriptTagNode | StyleTagNode | TagNode | TextNode } NewlineNode | ||
* @typedef {{ | ||
* childFirst: NewlineNode | null; | ||
* childLast: NewlineNode | null; | ||
* shouldBeNewline: boolean; | ||
* }} NodeMeta | ||
*/ | ||
@@ -13,6 +24,48 @@ | ||
EXPECT_NEW_LINE_AFTER: "expectAfter", | ||
EXPECT_NEW_LINE_AFTER_OPEN: "expectAfterOpen", | ||
EXPECT_NEW_LINE_BEFORE: "expectBefore", | ||
EXPECT_NEW_LINE_BEFORE_CLOSE: "expectBeforeClose", | ||
}; | ||
/** | ||
* @type {Object.<string, Array<string>>} | ||
*/ | ||
const PRESETS = { | ||
// From https://developer.mozilla.org/en-US/docs/Web/HTML/Element#inline_text_semantics | ||
$inline: ` | ||
a | ||
abbr | ||
b | ||
bdi | ||
bdo | ||
br | ||
cite | ||
code | ||
data | ||
dfn | ||
em | ||
i | ||
kbd | ||
mark | ||
q | ||
rp | ||
rt | ||
ruby | ||
s | ||
samp | ||
small | ||
span | ||
strong | ||
sub | ||
sup | ||
time | ||
u | ||
var | ||
wbr | ||
` | ||
.trim() | ||
.split(`\n`), | ||
}; | ||
/** | ||
* @type {RuleModule} | ||
@@ -35,2 +88,9 @@ */ | ||
properties: { | ||
inline: { | ||
type: "array", | ||
items: { | ||
type: "string", | ||
}, | ||
}, | ||
skip: { | ||
@@ -47,5 +107,9 @@ type: "array", | ||
[MESSAGE_IDS.EXPECT_NEW_LINE_AFTER]: | ||
"There should be a linebreak after {{tag}}.", | ||
"There should be a linebreak after {{tag}} element.", | ||
[MESSAGE_IDS.EXPECT_NEW_LINE_AFTER_OPEN]: | ||
"There should be a linebreak after {{tag}} open.", | ||
[MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE]: | ||
"There should be a linebreak before {{tag}}.", | ||
"There should be a linebreak before {{tag}} element.", | ||
[MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE_CLOSE]: | ||
"There should be a linebreak before {{tag}} close.", | ||
}, | ||
@@ -55,102 +119,201 @@ }, | ||
create(context) { | ||
const option = context.options[0] || { skip: [] }; | ||
const skipTags = option.skip; | ||
let skipTagCount = 0; | ||
const option = context.options[0] || {}; | ||
const skipTags = option.skip || []; | ||
const inlineTags = optionsOrPresets(option.inline || []); | ||
/** | ||
* @param {import("../types").ChildType<TagNode | ProgramNode>[]} siblings | ||
* @param {Array<NewlineNode>} siblings | ||
* @returns {NodeMeta} meta | ||
*/ | ||
function checkSiblings(siblings) { | ||
siblings | ||
.filter((node) => node.type !== "Text") | ||
.forEach((current, index, arr) => { | ||
const after = arr[index + 1]; | ||
if (after) { | ||
if (isOnTheSameLine(current, after)) { | ||
/** | ||
* @type {NodeMeta} | ||
*/ | ||
const meta = { | ||
childFirst: null, | ||
childLast: null, | ||
shouldBeNewline: false, | ||
}; | ||
const nodesWithContent = []; | ||
for ( | ||
let length = siblings.length, index = 0; | ||
index < length; | ||
index += 1 | ||
) { | ||
const node = siblings[index]; | ||
if (isEmptyText(node) === false) { | ||
nodesWithContent.push(node); | ||
} | ||
} | ||
for ( | ||
let length = nodesWithContent.length, index = 0; | ||
index < length; | ||
index += 1 | ||
) { | ||
const node = nodesWithContent[index]; | ||
const nodeNext = nodesWithContent[index + 1]; | ||
if (meta.childFirst === null) { | ||
meta.childFirst = node; | ||
} | ||
meta.childLast = node; | ||
const nodeShouldBeNewline = shouldBeNewline(node); | ||
if (node.type === `Tag` && skipTags.includes(node.name) === false) { | ||
const nodeMeta = checkSiblings(node.children); | ||
const nodeChildShouldBeNewline = nodeMeta.shouldBeNewline; | ||
if (nodeShouldBeNewline || nodeChildShouldBeNewline) { | ||
meta.shouldBeNewline = true; | ||
} | ||
if ( | ||
nodeShouldBeNewline && | ||
nodeChildShouldBeNewline && | ||
nodeMeta.childFirst && | ||
nodeMeta.childLast | ||
) { | ||
if ( | ||
node.openEnd.loc.end.line === nodeMeta.childFirst.loc.start.line | ||
) { | ||
if (isNotNewlineStart(nodeMeta.childFirst)) { | ||
context.report({ | ||
node: node, | ||
messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER_OPEN, | ||
data: { tag: label(node) }, | ||
fix(fixer) { | ||
return fixer.insertTextAfter(node.openEnd, `\n`); | ||
}, | ||
}); | ||
} | ||
} | ||
if (nodeMeta.childLast.loc.end.line === node.close.loc.start.line) { | ||
if (isNotNewlineEnd(nodeMeta.childLast)) { | ||
context.report({ | ||
node: node, | ||
messageId: MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE_CLOSE, | ||
data: { tag: label(node, { isClose: true }) }, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(node.close, `\n`); | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
if (nodeNext && node.loc.end.line === nodeNext.loc.start.line) { | ||
if (nodeShouldBeNewline) { | ||
if (isNotNewlineStart(nodeNext)) { | ||
context.report({ | ||
node: current, | ||
node: nodeNext, | ||
messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER, | ||
// @ts-ignore | ||
data: { tag: `<${current.name}>` }, | ||
data: { tag: label(node) }, | ||
fix(fixer) { | ||
return fixer.insertTextAfter(current, "\n"); | ||
return fixer.insertTextAfter(node, `\n`); | ||
}, | ||
}); | ||
} | ||
} else if (shouldBeNewline(nodeNext)) { | ||
if (isNotNewlineEnd(node)) { | ||
context.report({ | ||
node: nodeNext, | ||
messageId: MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE, | ||
data: { tag: label(nodeNext) }, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(nodeNext, `\n`); | ||
}, | ||
}); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
return meta; | ||
} | ||
/** | ||
* @param {TagNode} node | ||
* @param {import("../types").ChildType<TagNode>[]} children | ||
* @param {NewlineNode} node | ||
*/ | ||
function checkChild(node, children) { | ||
const targetChildren = children.filter((n) => n.type !== "Text"); | ||
const first = targetChildren[0]; | ||
const last = targetChildren[targetChildren.length - 1]; | ||
if (first) { | ||
if (isOnTheSameLine(node.openEnd, first)) { | ||
context.report({ | ||
node: node.openEnd, | ||
messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER, | ||
data: { tag: `<${node.name}>` }, | ||
fix(fixer) { | ||
return fixer.insertTextAfter(node.openEnd, "\n"); | ||
}, | ||
}); | ||
} | ||
function isEmptyText(node) { | ||
return node.type === `Text` && node.value.trim().length === 0; | ||
} | ||
/** | ||
* @param {NewlineNode} node | ||
*/ | ||
function isNotNewlineEnd(node) { | ||
return node.type !== `Text` || /\n\s*$/.test(node.value) === false; | ||
} | ||
/** | ||
* @param {NewlineNode} node | ||
*/ | ||
function isNotNewlineStart(node) { | ||
return node.type !== `Text` || /^\n/.test(node.value) === false; | ||
} | ||
/** | ||
* @param {NewlineNode} node | ||
* @param {{ isClose?: boolean }} options | ||
*/ | ||
function label(node, options = {}) { | ||
const isClose = options.isClose || false; | ||
switch (node.type) { | ||
case `Tag`: | ||
if (isClose) { | ||
return `</${node.name}>`; | ||
} | ||
return `<${node.name}>`; | ||
default: | ||
return `<${node.type}>`; | ||
} | ||
} | ||
if (last) { | ||
if (node.close && isOnTheSameLine(node.close, last)) { | ||
context.report({ | ||
node: node.close, | ||
messageId: MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE, | ||
data: { tag: `</${node.name}>` }, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(node.close, "\n"); | ||
}, | ||
}); | ||
/** | ||
* @param {Array<string>} options | ||
*/ | ||
function optionsOrPresets(options) { | ||
const result = []; | ||
for (const option of options) { | ||
if (option in PRESETS) { | ||
const preset = PRESETS[option]; | ||
result.push(...preset); | ||
} else { | ||
result.push(option); | ||
} | ||
} | ||
return result; | ||
} | ||
/** | ||
* @param {NewlineNode} node | ||
*/ | ||
function shouldBeNewline(node) { | ||
switch (node.type) { | ||
case `Comment`: | ||
return /[\n\r]+/.test(node.value.value.trim()); | ||
case `Tag`: | ||
return inlineTags.includes(node.name.toLowerCase()) === false; | ||
case `Text`: | ||
return /[\n\r]+/.test(node.value.trim()); | ||
default: | ||
return true; | ||
} | ||
} | ||
return { | ||
Program(node) { | ||
// @ts-ignore | ||
checkSiblings(node.body); | ||
}, | ||
Tag(node) { | ||
if (skipTagCount > 0) { | ||
return; | ||
} | ||
if (skipTags.includes(node.name)) { | ||
skipTagCount++; | ||
return; | ||
} | ||
checkSiblings(node.children); | ||
checkChild(node, node.children); | ||
}, | ||
/** | ||
* @param {TagNode} node | ||
* @returns | ||
*/ | ||
"Tag:exit"(node) { | ||
if (skipTags.includes(node.name)) { | ||
skipTagCount--; | ||
return; | ||
} | ||
}, | ||
}; | ||
}, | ||
}; | ||
/** | ||
* @param {BaseNode} nodeBefore | ||
* @param {BaseNode} nodeAfter | ||
* @returns | ||
*/ | ||
function isOnTheSameLine(nodeBefore, nodeAfter) { | ||
if (nodeBefore && nodeAfter) { | ||
return nodeBefore.loc.end.line === nodeAfter.loc.start.line; | ||
} | ||
return false; | ||
} |
@@ -9,2 +9,3 @@ const requireLang = require("./require-lang"); | ||
const noExtraSpacingAttrs = require("./no-extra-spacing-attrs"); | ||
const noExtraSpacingText = require("./no-extra-spacing-text"); | ||
const attrsNewline = require("./attrs-newline"); | ||
@@ -50,2 +51,3 @@ const elementNewLine = require("./element-newline"); | ||
"no-extra-spacing-attrs": noExtraSpacingAttrs, | ||
"no-extra-spacing-text": noExtraSpacingText, | ||
"attrs-newline": attrsNewline, | ||
@@ -52,0 +54,0 @@ "element-newline": elementNewLine, |
@@ -24,2 +24,3 @@ /** | ||
EXTRA_BEFORE_CLOSE: "unexpectedBeforeClose", | ||
EXTRA_IN_ASSIGNMENT: "unexpectedInAssignment", | ||
MISSING_BEFORE: "missingBefore", | ||
@@ -51,2 +52,5 @@ MISSING_BEFORE_SELF_CLOSE: "missingBeforeSelfClose", | ||
properties: { | ||
disallowInAssignment: { | ||
type: "boolean", | ||
}, | ||
disallowMissing: { | ||
@@ -69,2 +73,4 @@ type: "boolean", | ||
[MESSAGE_IDS.EXTRA_BEFORE_CLOSE]: "Unexpected space before closing", | ||
[MESSAGE_IDS.EXTRA_IN_ASSIGNMENT]: | ||
"Unexpected space in attribute assignment", | ||
[MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE]: | ||
@@ -88,2 +94,4 @@ "Missing space before self closing", | ||
const disallowTabs = !!(context.options[0] || {}).disallowTabs; | ||
const disallowInAssignment = !!(context.options[0] || []) | ||
.disallowInAssignment; | ||
@@ -111,3 +119,6 @@ const sourceCode = context.getSourceCode().text; | ||
fix(fixer) { | ||
return fixer.removeRange([current.range[1] + 1, after.range[0]]); | ||
return fixer.replaceTextRange( | ||
[current.range[1], after.range[0]], | ||
` ` | ||
); | ||
}, | ||
@@ -130,3 +141,3 @@ }); | ||
return fixer.replaceTextRange( | ||
[current.range[1], current.range[1] + 1], | ||
[current.range[1], after.range[0]], | ||
` ` | ||
@@ -193,2 +204,21 @@ ); | ||
checkExtraSpaceBefore(node.openStart, node.attributes[0]); | ||
for (const attr of node.attributes) { | ||
if (attr.startWrapper && attr.value) { | ||
if ( | ||
disallowInAssignment && | ||
attr.startWrapper.loc.start.column - attr.key.loc.end.column > 1 | ||
) { | ||
const start = attr.key.range[1]; | ||
const end = attr.startWrapper.range[0]; | ||
context.report({ | ||
node: attr, | ||
messageId: MESSAGE_IDS.EXTRA_IN_ASSIGNMENT, | ||
fix(fixer) { | ||
return fixer.replaceTextRange([start, end], `=`); | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
@@ -195,0 +225,0 @@ |
@@ -270,1 +270,9 @@ import ESTree from "estree"; | ||
: never; | ||
export type ContentNode = | ||
| CommentNode | ||
| DoctypeNode | ||
| ScriptTagNode | ||
| StyleTagNode | ||
| TagNode | ||
| TextNode; |
{ | ||
"name": "@html-eslint/eslint-plugin", | ||
"version": "0.26.0", | ||
"version": "0.27.0", | ||
"description": "ESLint plugin for html", | ||
@@ -48,3 +48,3 @@ "author": "yeonjuan", | ||
"devDependencies": { | ||
"@html-eslint/parser": "^0.26.0", | ||
"@html-eslint/parser": "^0.27.0", | ||
"@types/eslint": "^8.56.2", | ||
@@ -55,3 +55,3 @@ "@types/estree": "^0.0.47", | ||
}, | ||
"gitHead": "34d55c3b5be5a29cc416063b4b4375cb89b3a519" | ||
"gitHead": "a7c09dfb3090bb779d6fe62fda814d4d7ca07d4a" | ||
} |
@@ -9,3 +9,5 @@ export const rules: { | ||
"@html-eslint/attrs-newline": string; | ||
"@html-eslint/element-newline": string; | ||
"@html-eslint/element-newline": (string | { | ||
inline: string[]; | ||
})[]; | ||
"@html-eslint/no-duplicate-id": string; | ||
@@ -12,0 +14,0 @@ "@html-eslint/indent": string; |
@@ -7,2 +7,13 @@ declare const _exports: RuleModule; | ||
export type BaseNode = import("../types").BaseNode; | ||
export type CommentNode = import("../types").CommentNode; | ||
export type DoctypeNode = import("../types").DoctypeNode; | ||
export type ScriptTagNode = import("../types").ScriptTagNode; | ||
export type StyleTagNode = import("../types").StyleTagNode; | ||
export type TextNode = import("../types").TextNode; | ||
export type NewlineNode = CommentNode | DoctypeNode | ScriptTagNode | StyleTagNode | TagNode | TextNode; | ||
export type NodeMeta = { | ||
childFirst: NewlineNode | null; | ||
childLast: NewlineNode | null; | ||
shouldBeNewline: boolean; | ||
}; | ||
//# sourceMappingURL=element-newline.d.ts.map |
@@ -10,2 +10,3 @@ declare const _exports: { | ||
"no-extra-spacing-attrs": import("../types").RuleModule; | ||
"no-extra-spacing-text": import("../types").RuleModule; | ||
"attrs-newline": import("../types").RuleModule; | ||
@@ -12,0 +13,0 @@ "element-newline": import("../types").RuleModule; |
@@ -16,3 +16,3 @@ export type TagNode = import("../../types").TagNode; | ||
*/ | ||
declare function findAttr(node: import("../../types").TagNode | import("../../types").ScriptTagNode | import("../../types").StyleTagNode, key: string): import("../../types").AttributeNode | undefined; | ||
declare function findAttr(node: import("../../types").ScriptTagNode | import("../../types").TagNode | import("../../types").StyleTagNode, key: string): import("../../types").AttributeNode | undefined; | ||
/** | ||
@@ -19,0 +19,0 @@ * Checks whether a node's all tokens are on the same line or not. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
154777
154
4744