diff --git a/eslint.config.mjs b/eslint.config.mjs index c9dc4d02..5c5074a3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -40,6 +40,7 @@ export default defineConfig([ 'Fix', 'Limitations', 'When Not To Use It', + 'Further Reading', 'Prior Art', ], }, diff --git a/packages/eslint-plugin-mark/src/core/ast/index.js b/packages/eslint-plugin-mark/src/core/ast/index.js index 99423f5b..3b2504fb 100644 --- a/packages/eslint-plugin-mark/src/core/ast/index.js +++ b/packages/eslint-plugin-mark/src/core/ast/index.js @@ -1,4 +1,5 @@ import getElementsByTagName from './html.js'; +import isBlankLine from './is-blank-line.js'; import SkipRanges from './skip-ranges.js'; -export { getElementsByTagName, SkipRanges }; +export { getElementsByTagName, isBlankLine, SkipRanges }; diff --git a/packages/eslint-plugin-mark/src/core/ast/is-blank-line.js b/packages/eslint-plugin-mark/src/core/ast/is-blank-line.js new file mode 100644 index 00000000..4b8ebfbc --- /dev/null +++ b/packages/eslint-plugin-mark/src/core/ast/is-blank-line.js @@ -0,0 +1,29 @@ +/** + * @fileoverview Check if a line is blank. + * @see https://spec.commonmark.org/0.31.2/#blank-line + */ + +// -------------------------------------------------------------------------------- +// Helper +// -------------------------------------------------------------------------------- + +const whitespaceChars = new Set([' ', '\t']); + +// -------------------------------------------------------------------------------- +// Export +// -------------------------------------------------------------------------------- + +/** + * Check if a line is blank. + * @param {string} str Line string. + * @returns {boolean} `true` if the line is blank. `false` otherwise. + */ +export default function isBlankLine(str) { + for (let i = 0; i < str.length; i++) { + if (!whitespaceChars.has(str[i])) { + return false; + } + } + + return true; +} diff --git a/packages/eslint-plugin-mark/src/core/ast/is-blank-line.test.js b/packages/eslint-plugin-mark/src/core/ast/is-blank-line.test.js new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/packages/eslint-plugin-mark/src/core/ast/is-blank-line.test.js @@ -0,0 +1 @@ +// TODO diff --git a/packages/eslint-plugin-mark/src/rules/index.js b/packages/eslint-plugin-mark/src/rules/index.js index 210e098a..6d3db228 100644 --- a/packages/eslint-plugin-mark/src/rules/index.js +++ b/packages/eslint-plugin-mark/src/rules/index.js @@ -12,6 +12,7 @@ import consistentThematicBreakStyle from './consistent-thematic-break-style.js'; import enCapitalization from './en-capitalization.js'; import headingId from './heading-id.js'; import noBoldParagraph from './no-bold-paragraph.js'; +import noConsecutiveBlankLine from './no-consecutive-blank-line.js'; import noControlCharacter from './no-control-character.js'; import noCurlyQuote from './no-curly-quote.js'; import noDoubleSpace from './no-double-space.js'; @@ -35,6 +36,7 @@ export default { 'en-capitalization': enCapitalization, 'heading-id': headingId, 'no-bold-paragraph': noBoldParagraph, + 'no-consecutive-blank-line': noConsecutiveBlankLine, 'no-control-character': noControlCharacter, 'no-curly-quote': noCurlyQuote, 'no-double-space': noDoubleSpace, diff --git a/packages/eslint-plugin-mark/src/rules/no-consecutive-blank-line.js b/packages/eslint-plugin-mark/src/rules/no-consecutive-blank-line.js new file mode 100644 index 00000000..d8af9818 --- /dev/null +++ b/packages/eslint-plugin-mark/src/rules/no-consecutive-blank-line.js @@ -0,0 +1,126 @@ +/** + * @fileoverview Rule to disallow consecutive blank lines. + * @author 루밀LuMir(lumirlumir) + */ + +// -------------------------------------------------------------------------------- +// Import +// -------------------------------------------------------------------------------- + +import { isBlankLine } from '../core/ast/index.js'; +import { URL_RULE_DOCS } from '../core/constants.js'; + +// -------------------------------------------------------------------------------- +// Typedef +// -------------------------------------------------------------------------------- + +/** + * @import { RuleModule } from '../core/types.js'; + * @typedef {[{ max: number, skipCode: boolean }]} RuleOptions + * @typedef {'noConsecutiveBlankLine'} MessageIds + */ + +// -------------------------------------------------------------------------------- +// Rule Definition +// -------------------------------------------------------------------------------- + +/** @type {RuleModule} */ +export default { + meta: { + type: 'layout', + + docs: { + description: 'Disallow consecutive blank lines', + url: URL_RULE_DOCS('no-consecutive-blank-line'), + recommended: false, + stylistic: true, + }, + + fixable: 'whitespace', + + schema: [ + { + type: 'object', + properties: { + max: { + type: 'integer', + minimum: 0, // TODO: Think about proper minimum value + }, + skipCode: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + + defaultOptions: [ + { + max: 1, + skipCode: true, + }, + ], + + messages: { + noConsecutiveBlankLine: 'Consecutive blank lines are not allowed.', + }, + + language: 'markdown', + + dialects: ['commonmark', 'gfm'], + }, + + create(context) { + const { + sourceCode: { lines }, + } = context; + const [{ max /* skipCode */ }] = context.options; + + return { + 'root:exit'() { + /** @type {number | null} */ + let startIdx = null; + + for (let currentIdx = 0; currentIdx < lines.length; currentIdx++) { + if (isBlankLine(lines[currentIdx])) { + if (startIdx === null) { + startIdx = currentIdx; + } + } else if (startIdx !== null) { + if (currentIdx - startIdx > max) { + context.report({ + loc: { + start: { line: startIdx + max + 1, column: 1 }, + end: { + line: currentIdx + 1, + column: 1, + }, + }, + messageId: 'noConsecutiveBlankLine', + }); + } + + startIdx = null; + } + } + + /* + * Handle the case where the file ends with blank lines. + * Now, `currentIdx` is equal to `lines.length`. + */ + if (startIdx !== null && lines.length - startIdx > max) { + context.report({ + loc: { + start: { line: startIdx + max + 1, column: 1 }, + end: { + line: lines.length + 1, + column: 1, + }, + }, + messageId: 'noConsecutiveBlankLine', + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-mark/src/rules/no-consecutive-blank-line.test.js b/packages/eslint-plugin-mark/src/rules/no-consecutive-blank-line.test.js new file mode 100644 index 00000000..fd0f4b1b --- /dev/null +++ b/packages/eslint-plugin-mark/src/rules/no-consecutive-blank-line.test.js @@ -0,0 +1,30 @@ +/** + * @fileoverview Test for `no-consecutive-blank-line.js`. + * @author 루밀LuMir(lumirlumir) + */ + +// -------------------------------------------------------------------------------- +// Import +// -------------------------------------------------------------------------------- + +import { getFileName, ruleTester } from '../core/tests/index.js'; +import rule from './no-consecutive-blank-line.js'; + +// -------------------------------------------------------------------------------- +// Test +// -------------------------------------------------------------------------------- + +ruleTester(getFileName(import.meta.url), rule, { + valid: [ + { + name: 'Empty', + code: '', + }, + { + name: 'Empty string', + code: ' ', + }, + ], + + invalid: [], +}); diff --git a/website/docs/rules/no-consecutive-blank-line.md b/website/docs/rules/no-consecutive-blank-line.md new file mode 100644 index 00000000..e89be664 --- /dev/null +++ b/website/docs/rules/no-consecutive-blank-line.md @@ -0,0 +1,141 @@ + +
+ +## Rule Details + +This rule enforces a single, consistent style for emphasis (italic text) in Markdown files. Consistent formatting makes it easier to understand a document, and mixing different emphasis styles can reduce readability. + +An emphasis is defined as text wrapped in either `*` (asterisks) or `_` (underscores). While Markdown allows any of these styles, this rule ensures that only one is used throughout the document. + +## Examples + +### :x: Incorrect {#incorrect} + +Examples of **incorrect** code for this rule: + +#### Default + +```md eslint-check + + +*foo* +_bar_ +*baz* +**_foo_** +__*bar*__ +_**foo**_ +*__bar__* +___foo___ +***bar*** +``` + +```md eslint-check + + +_foo_ +*bar* +_baz_ +__*foo*__ +**_bar_** +*__foo__* +_**bar**_ +***foo*** +___bar___ +``` + +#### With `{ style: '*' }` Option + +```md eslint-check + + +_foo_ +**_bar_** +_**baz**_ +___qux___ +``` + +#### With `{ style: '_' }` Option + +```md eslint-check + + +*foo* +__*bar*__ +*__baz__* +***qux*** +``` + +### :white_check_mark: Correct {#correct} + +Examples of **correct** code for this rule: + +#### Default + +```md eslint-check + + +*foo* +__*bar*__ +*__baz__* +***qux*** +``` + +```md eslint-check + + +_foo_ +**_bar_** +_**baz**_ +___qux___ +``` + +#### With `{ style: '*' }` Option + +```md eslint-check + + +*foo* +__*bar*__ +*__baz__* +***qux*** +``` + +#### With `{ style: '_' }` Option + +```md eslint-check + + +_foo_ +**_bar_** +_**baz**_ +___qux___ +``` + +## Options + +```js +'mark/consistent-emphasis-style': ['error', { + style: 'consistent', +}] +``` + +### `style` + +> Type: `'consistent' | '*' | '_'` / Default: `'consistent'` + +When `style` is set to `'consistent'`, the rule enforces that all emphasis in the document use the same style as the first one encountered. + +You can also specify a particular style by setting style to `'*'` or `'_'`, which will enforce that all emphasis use the specified style. + +## Fix + +This rule fixes the emphasis by replacing them with the configured style. + +## Further Reading + +- [CommonMark Spec: Blank Line](https://spec.commonmark.org/0.31.2/#blank-line) + +## Prior Art + +- [`MD012` - Multiple consecutive blank lines](https://github.com/DavidAnson/markdownlint/blob/main/doc/md012.md#md012---multiple-consecutive-blank-lines) +- [`remark-lint-no-consecutive-blank-lines`](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-no-consecutive-blank-lines#remark-lint-no-consecutive-blank-lines)