Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default defineConfig([
'Fix',
'Limitations',
'When Not To Use It',
'Further Reading',
'Prior Art',
],
},
Expand Down
3 changes: 2 additions & 1 deletion packages/eslint-plugin-mark/src/core/ast/index.js
Original file line number Diff line number Diff line change
@@ -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 };
29 changes: 29 additions & 0 deletions packages/eslint-plugin-mark/src/core/ast/is-blank-line.js
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// TODO
2 changes: 2 additions & 0 deletions packages/eslint-plugin-mark/src/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
126 changes: 126 additions & 0 deletions packages/eslint-plugin-mark/src/rules/no-consecutive-blank-line.js
Original file line number Diff line number Diff line change
@@ -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<RuleOptions, MessageIds>} */
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',
});
}
},
};
},
};
Original file line number Diff line number Diff line change
@@ -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: [],
});
141 changes: 141 additions & 0 deletions website/docs/rules/no-consecutive-blank-line.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<!-- markdownlint-disable-next-line no-inline-html first-line-h1 -->
<header v-html="$frontmatter.rule"></header>

## 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
<!-- eslint mark/consistent-emphasis-style: "error" -->

*foo*
_bar_
*baz*
**_foo_**
__*bar*__
_**foo**_
*__bar__*
___foo___
***bar***
```

```md eslint-check
<!-- eslint mark/consistent-emphasis-style: "error" -->

_foo_
*bar*
_baz_
__*foo*__
**_bar_**
*__foo__*
_**bar**_
***foo***
___bar___
```

#### With `{ style: '*' }` Option

```md eslint-check
<!-- eslint mark/consistent-emphasis-style: ["error", { style: '*' }] -->

_foo_
**_bar_**
_**baz**_
___qux___
```

#### With `{ style: '_' }` Option

```md eslint-check
<!-- eslint mark/consistent-emphasis-style: ["error", { style: '_' }] -->

*foo*
__*bar*__
*__baz__*
***qux***
```

### :white_check_mark: Correct {#correct}

Examples of **correct** code for this rule:

#### Default

```md eslint-check
<!-- eslint mark/consistent-emphasis-style: "error" -->

*foo*
__*bar*__
*__baz__*
***qux***
```

```md eslint-check
<!-- eslint mark/consistent-emphasis-style: "error" -->

_foo_
**_bar_**
_**baz**_
___qux___
```

#### With `{ style: '*' }` Option

```md eslint-check
<!-- eslint mark/consistent-emphasis-style: ["error", { style: '*' }] -->

*foo*
__*bar*__
*__baz__*
***qux***
```

#### With `{ style: '_' }` Option

```md eslint-check
<!-- eslint mark/consistent-emphasis-style: ["error", { style: '_' }] -->

_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)
Loading