Skip to content

Commit 114831c

Browse files
committed
feat: processor support for normalize comments in jsx [ci skip]
1 parent 1b5e4fb commit 114831c

19 files changed

+277
-398
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<img src="https://eslint.org/assets/img/logo.svg" height="50">
44
</a>
55
<a href="#readme">
6-
<img src="assets/heart.svg" height="50">
6+
<img src="https://rx-ts.github.io/assets/heart.svg" height="50">
77
</a>
88
<a href="https://github.com/mdx-js/mdx">
99
<img src="https://mdx-logo.now.sh" height="50">

assets/heart.svg

Lines changed: 0 additions & 11 deletions
This file was deleted.

jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
}

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@
2929
"eslint": ">=5.0.0"
3030
},
3131
"dependencies": {
32-
"@mdx-js/mdx": "^1.1.0",
33-
"remark-mdx": "^1.1.0",
32+
"remark-mdx": "^1.1.4",
3433
"remark-parse": "^7.0.0",
3534
"remark-stringify": "^7.0.1",
3635
"unified": "^8.3.2"
@@ -39,21 +38,23 @@
3938
"@commitlint/config-conventional": "^8.1.0",
4039
"@rxts/eslint-plugin-mdx": "file:config",
4140
"@types/eslint": "^4.16.6",
41+
"@types/jest": "^24.0.16",
4242
"@types/node": "^12.6.8",
43-
"@types/react": "^16.8.23",
43+
"@types/react": "^16.8.24",
4444
"@types/unist": "^2.0.3",
4545
"babel-eslint": "^10.0.2",
4646
"commitlint": "^8.1.0",
4747
"eslint": "^6.1.0",
4848
"eslint-config-1stg": "~5.4.1",
49-
"eslint-plugin-jest": "^22.14.0",
49+
"eslint-plugin-jest": "^22.14.1",
5050
"husky": "^3.0.2",
5151
"jest": "^24.8.0",
5252
"lint-staged": "^9.2.1",
5353
"prettier": "1.18.2",
5454
"prettier-config-1stg": "^0.1.0",
5555
"react": "^16.8.6",
5656
"standard-version": "^7.0.0",
57+
"ts-jest": "^24.0.2",
5758
"ts-node": "^8.3.0",
5859
"typescript": "^3.5.3"
5960
}

src/helper.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { Position } from 'unist'
1+
import {
2+
isOpenTag,
3+
isCloseTag,
4+
isComment,
5+
isSelfClosingTag,
6+
isOpenCloseTag,
7+
} from './regexp'
8+
9+
import { Node, Position } from 'unist'
210
import { AST } from 'eslint'
311
// SourceLocation` is not exported from estree, but it is actually working
412
// eslint-disable-next-line import/named
@@ -73,3 +81,60 @@ export function restoreNodeLocation<T extends BaseNode>(
7381
},
7482
}
7583
}
84+
85+
// fix #7
86+
export const combineJsxNodes = (nodes: Node[]) => {
87+
let offset = 0
88+
const jsxNodes: Node[] = []
89+
const { length } = nodes
90+
return nodes.reduce<Node[]>((acc, node, index) => {
91+
if (node.type === 'jsx') {
92+
const rawText = node.value as string
93+
if (isOpenTag(rawText as string)) {
94+
offset++
95+
jsxNodes.push(node)
96+
} else {
97+
if (isCloseTag(rawText)) {
98+
offset--
99+
} else if (
100+
!isComment(rawText) &&
101+
!isSelfClosingTag(rawText) &&
102+
!isOpenCloseTag(rawText)
103+
) {
104+
const { start } = node.position
105+
throw Object.assign(
106+
new SyntaxError(
107+
`'Unknown node type: ${JSON.stringify(
108+
node.type,
109+
)}, text: ${JSON.stringify(rawText)}`,
110+
),
111+
{
112+
lineNumber: start.line,
113+
column: start.column,
114+
index: start.offset,
115+
},
116+
)
117+
}
118+
119+
jsxNodes.push(node)
120+
121+
if (!offset || index === length - 1) {
122+
acc.push({
123+
type: 'jsx',
124+
value: jsxNodes.reduce((acc, { value }) => (acc += value), ''),
125+
position: {
126+
start: jsxNodes[0].position.start,
127+
end: jsxNodes[jsxNodes.length - 1].position.end,
128+
},
129+
})
130+
jsxNodes.length = 0
131+
}
132+
}
133+
} else if (offset) {
134+
jsxNodes.push(node)
135+
} else {
136+
acc.push(node)
137+
}
138+
return acc
139+
}, [])
140+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import path from 'path'
22

33
export * from './helper'
4+
export * from './normalizer'
45
export * from './parser'
6+
export * from './processors'
57
export * from './regexp'
68
export * from './rules'
79
export * from './traverse'

src/normalizer.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { isComment, COMMENT_CONTENT_REGEX } from './regexp'
2+
import { mdxProcessor } from './parser'
3+
4+
import { Node, Parent } from 'unist'
5+
6+
export const normalizeJsxNode = (node: Node) => {
7+
const rawText = node.value as string
8+
if (!isComment(rawText)) {
9+
const matched = rawText.match(COMMENT_CONTENT_REGEX)
10+
if (matched) {
11+
node.value = rawText.replace(
12+
COMMENT_CONTENT_REGEX,
13+
(_matched, $0) => `{/*${$0}*/}`,
14+
)
15+
}
16+
}
17+
return node
18+
}
19+
20+
export const normalizeMdx = (source: string) => {
21+
const lines = source.split('\n').length
22+
const { children } = mdxProcessor.parse(source) as Parent
23+
let lastLine: number
24+
return children.reduce((result, node, index) => {
25+
const {
26+
position: { start, end },
27+
} = node
28+
const startLine = start.line
29+
const endLine = end.line
30+
if (lastLine != null && lastLine !== startLine) {
31+
result += '\n'.repeat(startLine - lastLine)
32+
}
33+
if (node.type === 'jsx') {
34+
result += normalizeJsxNode(node).value
35+
} else {
36+
result += source.slice(start.offset, end.offset)
37+
}
38+
39+
if (index === children.length - 1 && endLine < lines) {
40+
result += '\n'.repeat(lines - endLine)
41+
}
42+
43+
lastLine = endLine
44+
return result
45+
}, '')
46+
}

src/parser.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ export const ES_NODE_TYPES = ['export', 'import', 'jsx'] as const
1919

2020
export type EsNodeType = (typeof ES_NODE_TYPES)[number]
2121

22-
export const parseMdx = unified()
22+
export const mdxProcessor = unified()
2323
.use<any>(remarkParse)
2424
.use<any>(remarkStringify)
2525
.use(remarkMdx)
26-
.freeze().parse
26+
.freeze()
2727

2828
export const parseForESLint = (
2929
code: string,
@@ -53,7 +53,7 @@ export const parseForESLint = (
5353
}
5454
}
5555

56-
const root = parseMdx(code) as Parent
56+
const root = mdxProcessor.parse(code) as Parent
5757

5858
const ast: AST.Program = {
5959
...normalizePosition(root.position),
@@ -65,36 +65,36 @@ export const parseForESLint = (
6565
}
6666

6767
traverse(root, {
68-
enter({ position, type }) {
69-
if (!ES_NODE_TYPES.includes(type as EsNodeType)) {
68+
enter(node) {
69+
if (!ES_NODE_TYPES.includes(node.type as EsNodeType)) {
7070
return
7171
}
7272

73-
const rawText = code.slice(position.start.offset, position.end.offset)
73+
const rawText = node.value as string
7474

7575
// fix #4
7676
if (isComment(rawText)) {
7777
return
7878
}
7979

80-
const node = normalizePosition(position)
81-
const startLine = node.loc.start.line - 1 //! line is 1-indexed, change to 0-indexed to simplify usage
80+
const { loc, start } = normalizePosition(node.position)
81+
const startLine = loc.start.line - 1 //! line is 1-indexed, change to 0-indexed to simplify usage
8282

8383
let program: AST.Program
8484

8585
try {
8686
program = parser(rawText, options)
8787
} catch (e) {
8888
if (e instanceof SyntaxError) {
89-
e.index += node.start
90-
e.column += node.loc.start.column
89+
e.index += start
90+
e.column += loc.start.column
9191
e.lineNumber += startLine
9292
}
9393

9494
throw e
9595
}
9696

97-
const offset = node.start - program.range[0]
97+
const offset = start - program.range[0]
9898

9999
AST_PROPS.forEach(prop =>
100100
ast[prop].push(

src/processors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { normalizeMdx } from './normalizer'
2+
3+
export const processors = {
4+
'.mdx': {
5+
preprocess(code: string) {
6+
return [normalizeMdx(code)]
7+
},
8+
supportsAutofix: true,
9+
},
10+
}

src/regexp.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,29 @@ const attributeValue =
1818
')'
1919
const attribute =
2020
'(?:\\s+' + attributeName + '(?:\\s*=\\s*' + attributeValue + ')?)'
21+
2122
const openTag = '<[A-Za-z]*[A-Za-z0-9\\.\\-]*' + attribute + '*\\s*>'
2223
const closeTag = '<\\s*\\/[A-Za-z]*[A-Za-z0-9\\.\\-]*\\s*>'
23-
const voidTag = '<[A-Za-z]*[A-Za-z0-9\\.\\-]*' + attribute + '*\\s*\\/?>'
24+
const selfClosingTag = '<[A-Za-z]*[A-Za-z0-9\\.\\-]*' + attribute + '*\\s*\\/?>'
2425
const comment = '<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->'
26+
const commentOpen = '<!---?'
27+
const commentClose = '-->'
2528

2629
export const OPEN_TAG_REGEX = new RegExp(`^(?:${openTag})$`)
2730
export const CLOSE_TAG_REGEX = new RegExp(`^(?:${closeTag})$`)
2831
export const OPEN_CLOSE_TAG_REGEX = new RegExp(
2932
`^(?:${openTag + '[\\s\\S]*' + closeTag})$`,
3033
)
31-
export const VOID_TAG_REGEX = new RegExp(`^(?:${voidTag})$`)
34+
export const SELF_CLOSING_TAG_REGEX = new RegExp(`^(?:${selfClosingTag})$`)
3235
export const COMMENT_REGEX = new RegExp(`^(?:${comment})$`)
36+
export const COMMENT_CONTENT_REGEX = new RegExp(
37+
`${commentOpen}([\\s\\S]*?)${commentClose}`,
38+
'g',
39+
)
3340

3441
export const isOpenTag = (text: string) => OPEN_TAG_REGEX.test(text)
3542
export const isCloseTag = (text: string) => CLOSE_TAG_REGEX.test(text)
3643
export const isOpenCloseTag = (text: string) => OPEN_CLOSE_TAG_REGEX.test(text)
37-
export const isVoidTag = (text: string) => VOID_TAG_REGEX.test(text)
44+
export const isSelfClosingTag = (text: string) =>
45+
SELF_CLOSING_TAG_REGEX.test(text)
3846
export const isComment = (text: string) => COMMENT_REGEX.test(text)

0 commit comments

Comments
 (0)