eslint-plugin-react-pug
Version:
Add supporting of pugjs with react
203 lines (176 loc) • 7.42 kB
JavaScript
/**
* @fileoverview Manage empty lines in Pug
* @author Eugene Zhlobo
*/
const { isReactPugReference, buildLocation, docsUrl } = require('../util/eslint')
const getTemplate = require('../util/getTemplate')
const getTokens = require('../util/getTokens')
const normalizeToken = require('../util/normalizeToken')
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const MESSAGE = {
newline_start: 'Expected new line in the beginning',
newline_end: 'Expected new line in the end',
single_empty_lines: 'Use 1 empty line',
no_lines_indent: 'Expected no empty lines for nested items',
need_empty_siblings: 'Need empty line for more than two siblings',
need_empty_outdent: 'Need empty line when you are off from the scope',
}
const isLineEmpty = line => typeof line === 'string' && line.trim() === ''
module.exports = {
meta: {
docs: {
description: 'Manage empty lines in Pug',
category: 'Best Practices',
recommended: true,
url: docsUrl('empty-lines'),
},
schema: [],
},
create: function (context) {
return {
TaggedTemplateExpression: function (node) {
if (isReactPugReference(node)) {
// If multiline
if (node.loc.start.line !== node.loc.end.line) {
const template = getTemplate(node)
const lines = template.split('\n')
const tokens = getTokens(template)
const eosToken = normalizeToken(tokens.find(token => token.type === 'eos'))
// Don't treat attributes only as a multiline template
if (
normalizeToken(tokens[0]).type === 'tag'
&& normalizeToken(tokens[1]).type === 'start-attributes'
&& normalizeToken(tokens[tokens.length - 2]).type === 'end-attributes'
&& normalizeToken(tokens[tokens.length - 1]).type === 'eos'
) return
// Group is a bunch of newline tokens between 'indent'/'outdent' tokens
const newlineGroup = []
// No new line in the beginning of template
if (!isLineEmpty(lines[0])) {
const firstLine = lines[0]
context.report({
node,
loc: buildLocation(
[node.loc.start.line, node.quasi.loc.start.column],
[node.loc.start.line, node.quasi.loc.start.column + firstLine.length + 1],
),
message: MESSAGE.newline_start,
})
}
// No new line in the end of template
if (!isLineEmpty(lines[lines.length - 1])) {
const lastLine = lines[lines.length - 1]
context.report({
node,
loc: buildLocation(
[node.loc.end.line, lastLine.search(/[^\s]/)],
[node.loc.end.line, node.loc.end.column],
),
message: MESSAGE.newline_end,
})
}
// No more than one empty line
for (let index = 0; index < lines.length; index += 1) {
if (isLineEmpty(lines[index]) && isLineEmpty(lines[index + 1])) {
context.report({
node,
loc: buildLocation(
[node.loc.start.line + index, 0],
[node.loc.start.line + index + 1, 0],
),
message: MESSAGE.single_empty_lines,
})
}
}
const eosTokenStringLocation = JSON.stringify(eosToken.loc)
tokens.forEach((token, index) => {
const nextToken = normalizeToken(tokens[index + 1])
const prevToken = normalizeToken(tokens[index - 1])
const beforePrevToken = normalizeToken(tokens[index - 2])
const prevPrevToken = normalizeToken(tokens[index - 2])
if (
// When it goes out from nesting without empty line
(
token.type === 'outdent'
&& JSON.stringify(token.loc) !== eosTokenStringLocation
&& token.loc.start.line - prevToken.loc.end.line === 1
)
// When it goes out from text block
|| (
token.type === 'newline'
&& prevToken.type === 'end-pipeless-text'
&& !(prevPrevToken.type === 'text' && prevPrevToken.val === '')
)
) {
context.report({
node,
loc: buildLocation(
[node.loc.start.line + token.loc.start.line - 1, 0],
[node.loc.start.line + token.loc.start.line - 1, token.loc.end.column - 1],
),
message: MESSAGE.need_empty_outdent,
})
}
if (token.type === 'indent') {
// Prohibit empty lines for nested items
if (prevToken.type && token.loc.start.line - prevToken.loc.start.line > 1) {
const startLine = node.loc.start.line + prevToken.loc.end.line - 1
const endLine = node.loc.start.line + token.loc.end.line - 1
context.report({
node,
loc: buildLocation(
[startLine, prevToken.loc.end.column - 1],
[endLine, token.loc.end.column - 1],
),
message: MESSAGE.no_lines_indent,
})
}
}
if (
token.type === 'newline'
// Don't include text-only lines
&& nextToken.type !== 'text'
&& nextToken.type !== 'comment'
&& !(prevToken.type === 'text' && beforePrevToken.type === 'newline')
) {
newlineGroup.push({
...token,
prev: prevToken,
})
}
if (
token.type === 'indent' || token.type === 'outdent'
|| (token.type === 'newline' && prevToken.type === 'end-pipeless-text')
) {
if (newlineGroup.length >= 2) {
newlineGroup.forEach((item) => {
const endLine = item.prev.type === 'end-pipeless-text'
? item.prev.loc.end.line - 1
: item.prev.loc.end.line
// If there is no empty line between siblings in newline group with more
// than 2 siblings
if (item.loc.start.line - endLine === 1) {
const line = item.loc.start.line + node.loc.start.line - 1
context.report({
node,
loc: buildLocation(
[line, 0],
[line, item.loc.end.column - 1],
),
message: MESSAGE.need_empty_siblings,
})
}
})
}
// Reset the array
newlineGroup.length = 0
}
})
}
}
},
}
},
}