Skip to content

Commit 8049d5e

Browse files
authored
feat: add new rules space-around-number (#27)
* feat: add new rules `space-around-number` * refactor(text): extract shared boundary spacing utils - move tokenizer helpers into src/utils/text - extract shared CJK boundary spacing logic for word and number rules - update rule and test imports to use the new text utils entry
1 parent f987a5f commit 8049d5e

23 files changed

Lines changed: 516 additions & 256 deletions

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
".": "./dist/index.mjs",
1919
"./package.json": "./package.json"
2020
},
21-
"main": "./dist/index.mjs",
22-
"module": "./dist/index.mjs",
2321
"types": "./dist/index.d.mts",
2422
"files": [
2523
"dist"

src/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ export const plugin: ESLint.Plugin = {
1010
},
1111
}
1212

13-
const allRuleEntries: Array<[string, Linter.RuleEntry]> = Object.keys(rules)
14-
.map(ruleName => [`md-style/${ruleName}`, 'error'])
15-
1613
const recommendedRules: Partial<Linter.RulesRecord> = {
1714
'md-style/valid-heading-anchor': 'error',
1815
'md-style/space-around-inline-element': 'error',
1916
}
20-
const allRules: Partial<Linter.RulesRecord> = Object.fromEntries(allRuleEntries)
17+
const allRules: Partial<Linter.RulesRecord>
18+
= Object.fromEntries(Object.keys(rules)
19+
.map(ruleName => [`md-style/${ruleName}`, 'error']))
2120

2221
interface PluginConfigMap {
2322
recommended: Linter.Config

src/rules/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import spaceAroundInlineElement from './space-around-inline-element/index'
2+
import spaceAroundNumber from './space-around-number'
23
import spaceAroundWord from './space-around-word'
34
import validHeadingAnchor from './valid-heading-anchor/index'
45

56
export const rules = {
67
'space-around-inline-element': spaceAroundInlineElement,
7-
'valid-heading-anchor': validHeadingAnchor,
8+
'space-around-number': spaceAroundNumber,
89
'space-around-word': spaceAroundWord,
10+
'valid-heading-anchor': validHeadingAnchor,
911
}

src/rules/space-around-inline-element/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { InvalidTestCase, ValidTestCase } from 'eslint-vitest-rule-tester'
22
import markdown from '@eslint/markdown'
33
import { run } from 'eslint-vitest-rule-tester'
4-
import { INLINE_SPACE_MESSAGE_IDS as MESSAGE_IDS } from '@/utils/inline-element'
4+
import { MESSAGE_IDS } from '@/rules/space-around-inline-element'
55
import rule, { RULE_NAME } from './index'
66

77
const valid: ValidTestCase[] = [

src/rules/space-around-inline-element/index.ts

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ import type { RuleContext, ValueOf } from '@/types'
33
import type { InlineElement } from '@/types/inline-element'
44
import { createRule } from '@/utils'
55
import { getNodeContext, getNodePosition, isNestedInlineElement } from '@/utils/ast'
6-
import { INLINE_SPACE_MESSAGE_IDS as MESSAGE_IDS, validateSpace } from '@/utils/inline-element'
6+
import { validateSpace } from '@/utils/inline-element'
77
import { getSpaceContext } from '@/utils/space'
88

99
export const RULE_NAME = 'space-around-inline-element'
10-
10+
export const MESSAGE_IDS = {
11+
missingSpaceBefore: 'missingSpaceBefore',
12+
missingSpaceAfter: 'missingSpaceAfter',
13+
multipleSpacesBefore: 'multipleSpacesBefore',
14+
multipleSpacesAfter: 'multipleSpacesAfter',
15+
multipleSpacesAfterPunctuation: 'multipleSpacesAfterPunctuation',
16+
unexpectedSpaceBefore: 'unexpectedSpaceBefore',
17+
unexpectedSpaceAfter: 'unexpectedSpaceAfter',
18+
} as const
1119
type MessageIds = ValueOf<typeof MESSAGE_IDS>
1220
type Options = []
1321

@@ -18,6 +26,47 @@ const BEFORE_INLINE_ELEMENT_MESSAGE_IDS = new Set<MessageIds>([
1826
MESSAGE_IDS.unexpectedSpaceBefore,
1927
])
2028

29+
export default createRule<Options, MessageIds>({
30+
name: RULE_NAME,
31+
meta: {
32+
type: 'layout',
33+
docs: {
34+
description: 'Enforce spacing around Markdown inline elements.',
35+
},
36+
messages: {
37+
missingSpaceBefore: 'A space is required before the inline element.',
38+
missingSpaceAfter: 'A space is required after the inline element.',
39+
multipleSpacesBefore: 'Use exactly one space before the inline element.',
40+
multipleSpacesAfter: 'Use exactly one space after the inline element.',
41+
multipleSpacesAfterPunctuation: 'Use one space after punctuation.',
42+
unexpectedSpaceBefore: 'Do not add a space between punctuation and the inline element.',
43+
unexpectedSpaceAfter: 'Do not add a space between the inline element and punctuation.',
44+
},
45+
fixable: 'whitespace',
46+
schema: [],
47+
},
48+
defaultOptions: [],
49+
create(context) {
50+
return {
51+
link(node: Link) {
52+
checkInlineElement(context, node)
53+
},
54+
image(node: Image) {
55+
checkInlineElement(context, node)
56+
},
57+
inlineCode(node: InlineCode) {
58+
checkInlineElement(context, node)
59+
},
60+
emphasis(node: Emphasis) {
61+
checkInlineElement(context, node)
62+
},
63+
strong(node: Strong) {
64+
checkInlineElement(context, node)
65+
},
66+
}
67+
},
68+
})
69+
2170
/**
2271
* Checks one selected inline element and reports the fix range around it.
2372
*/
@@ -63,44 +112,3 @@ function checkInlineElement(context: RuleContext<MessageIds, Options>, node: Inl
63112
})
64113
}
65114
}
66-
67-
export default createRule<Options, MessageIds>({
68-
name: RULE_NAME,
69-
meta: {
70-
type: 'layout',
71-
docs: {
72-
description: 'Enforce spacing around Markdown inline elements.',
73-
},
74-
messages: {
75-
missingSpaceBefore: 'A space is required before the inline element.',
76-
missingSpaceAfter: 'A space is required after the inline element.',
77-
multipleSpacesBefore: 'Use exactly one space before the inline element.',
78-
multipleSpacesAfter: 'Use exactly one space after the inline element.',
79-
multipleSpacesAfterPunctuation: 'Use one space after punctuation.',
80-
unexpectedSpaceBefore: 'Do not add a space between punctuation and the inline element.',
81-
unexpectedSpaceAfter: 'Do not add a space between the inline element and punctuation.',
82-
},
83-
fixable: 'whitespace',
84-
schema: [],
85-
},
86-
defaultOptions: [],
87-
create(context) {
88-
return {
89-
link(node: Link) {
90-
checkInlineElement(context, node)
91-
},
92-
image(node: Image) {
93-
checkInlineElement(context, node)
94-
},
95-
inlineCode(node: InlineCode) {
96-
checkInlineElement(context, node)
97-
},
98-
emphasis(node: Emphasis) {
99-
checkInlineElement(context, node)
100-
},
101-
strong(node: Strong) {
102-
checkInlineElement(context, node)
103-
},
104-
}
105-
},
106-
})
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { InvalidTestCase, ValidTestCase } from 'eslint-vitest-rule-tester'
2+
import markdown from '@eslint/markdown'
3+
import { run } from 'eslint-vitest-rule-tester'
4+
import { SPACE_MESSAGE_IDS as MESSAGE_IDS } from '@/utils/space'
5+
import rule, { RULE_NAME } from './index'
6+
7+
const valid: ValidTestCase[] = [
8+
{
9+
description: 'number between chinese text with spaces',
10+
code: '支持 123 个规则',
11+
},
12+
{
13+
description: 'decimal and percent number between chinese text with spaces',
14+
code: '提升 4.0% 效率',
15+
},
16+
{
17+
description: 'number adjacent to punctuation is allowed',
18+
code: '版本:2.0,现已发布。',
19+
},
20+
]
21+
22+
const invalid: InvalidTestCase[] = [
23+
{
24+
description: 'reports a missing space before a number after CJK text',
25+
code: '支持123 个规则',
26+
output: '支持 123 个规则',
27+
errors: [{ messageId: MESSAGE_IDS.missingSpaceBefore }],
28+
},
29+
{
30+
description: 'reports a missing space after a number before CJK text',
31+
code: '支持 123个规则',
32+
output: '支持 123 个规则',
33+
errors: [{ messageId: MESSAGE_IDS.missingSpaceAfter }],
34+
},
35+
{
36+
description: 'reports missing spaces on both sides of an embedded number',
37+
code: '支持123个规则',
38+
output: '支持 123 个规则',
39+
errors: [{ messageId: MESSAGE_IDS.missingSpacesAround }],
40+
},
41+
{
42+
description: 'reports missing spaces around decimal percent numbers',
43+
code: '提升4.0%效率',
44+
output: '提升 4.0% 效率',
45+
errors: [{ messageId: MESSAGE_IDS.missingSpacesAround }],
46+
},
47+
{
48+
description: 'reports an unexpected space before a number after CJK text',
49+
code: '支持 123 个规则',
50+
output: '支持 123 个规则',
51+
errors: [{ messageId: MESSAGE_IDS.unexpectedSpaceBefore }],
52+
},
53+
{
54+
description: 'reports an unexpected space after a number before CJK text',
55+
code: '支持 123 个规则',
56+
output: '支持 123 个规则',
57+
errors: [{ messageId: MESSAGE_IDS.unexpectedSpaceAfter }],
58+
},
59+
{
60+
description: 'reports unexpected spaces on both sides of a number',
61+
code: '支持 123 个规则',
62+
output: '支持 123 个规则',
63+
errors: [{ messageId: MESSAGE_IDS.unexpectedSpaceAround }],
64+
},
65+
]
66+
67+
run({
68+
name: RULE_NAME,
69+
rule,
70+
valid,
71+
invalid,
72+
configs: {
73+
plugins: { markdown },
74+
language: 'markdown/gfm',
75+
},
76+
})
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Text } from 'mdast'
2+
import type { ValueOf } from '@/types'
3+
import type { SPACE_MESSAGE_IDS as MESSAGE_IDS } from '@/utils/space'
4+
import { createRule } from '@/utils'
5+
import { fixBoundarySpace, getBoundarySpaceMessageId, isNumberType } from '@/utils/text'
6+
7+
export const RULE_NAME = 'space-around-number'
8+
9+
type MessageIds = ValueOf<typeof MESSAGE_IDS>
10+
type Options = []
11+
12+
export default createRule<Options, MessageIds>({
13+
name: RULE_NAME,
14+
meta: {
15+
type: 'layout',
16+
docs: {
17+
description: 'Enforce a single space between CJK characters and numbers.',
18+
},
19+
messages: {
20+
missingSpaceBefore: 'Add a space before the number.',
21+
missingSpaceAfter: 'Add a space after the number.',
22+
missingSpacesAround: 'Add spaces before and after the number.',
23+
unexpectedSpaceBefore: 'Remove the unexpected space before the number.',
24+
unexpectedSpaceAfter: 'Remove the unexpected space after the number.',
25+
unexpectedSpaceAround: 'Remove the unexpected spaces around the number.',
26+
},
27+
fixable: 'whitespace',
28+
schema: [],
29+
},
30+
defaultOptions: [],
31+
create(context) {
32+
return {
33+
text(node: Text) {
34+
const { fixed, missingBefore, missingAfter, unexpectedBefore, unexpectedAfter } = fixBoundarySpace(node, isNumberType)
35+
36+
if (fixed === node.value)
37+
return
38+
39+
context.report({
40+
node,
41+
messageId: getBoundarySpaceMessageId({ missingBefore, missingAfter, unexpectedBefore, unexpectedAfter }),
42+
fix(fixer) {
43+
return fixer.replaceText(node, fixed)
44+
},
45+
})
46+
},
47+
}
48+
},
49+
})

src/rules/space-around-word/index.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { InvalidTestCase, ValidTestCase } from 'eslint-vitest-rule-tester'
22
import markdown from '@eslint/markdown'
33
import { run } from 'eslint-vitest-rule-tester'
4-
import rule, { MESSAGE_IDS, RULE_NAME } from './index'
4+
import { SPACE_MESSAGE_IDS as MESSAGE_IDS } from '@/utils/space'
5+
import rule, { RULE_NAME } from './index'
56

67
const valid: ValidTestCase[] = [
78
{

0 commit comments

Comments
 (0)