Skip to content

Commit 3ba8a2a

Browse files
committed
Merge branch 'perf'
2 parents 7cfe8c7 + cad6380 commit 3ba8a2a

File tree

13 files changed

+161
-106
lines changed

13 files changed

+161
-106
lines changed

.github/workflows/bench.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: bench
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
bench:
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- uses: pnpm/action-setup@v4
14+
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 22
18+
cache: pnpm
19+
20+
- run: pnpm install --frozen-lockfile
21+
- run: pnpm run build
22+
23+
- name: Run benchmarks
24+
run: pnpm -F bench run bench --run
25+
env:
26+
CI: true

packages/bench/compare.bench.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import { createRunners } from './runners'
44
const fixtures = import.meta.glob<string>('./samples/*', { query: '?raw', import: 'default', eager: true })
55
const contents = Object.values(fixtures)
66

7-
describe('all samples', () => {
8-
const runners = createRunners()
9-
for (const [name, runner] of Object.entries(runners)) {
10-
bench(name, () => {
11-
for (const content of contents) {
12-
runner(content)
13-
}
14-
})
15-
}
16-
})
7+
const runners = createRunners()
8+
9+
for (const [category, runnerGroup] of Object.entries(runners)) {
10+
describe(`category: ${category}`, () => {
11+
for (const [name, runner] of Object.entries(runnerGroup)) {
12+
bench(name, () => {
13+
for (const content of contents) {
14+
runner(content)
15+
}
16+
}, { time: 1000, warmupTime: 200 })
17+
}
18+
})
19+
}

packages/bench/runners.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@ export function createCommonmark() {
5555
return (input: string): string => renderer.render(parser.parse(input))
5656
}
5757

58-
export function createRunners(): Record<string, (input: string) => string> {
58+
export function createRunners(): Record<string, Record<string, (input: string) => string>> {
5959
return {
60-
'markdown-exit': createMarkdownExit(),
61-
'markdown-exit-commonmark': createMarkdownExitCommonMark(),
62-
'markdown-it': createMarkdownIt(),
63-
'markdown-it-commonmark': createMarkdownItCommonMark(),
64-
'marked': createMarked(),
65-
'commonmark': createCommonmark(),
60+
markdown: {
61+
'markdown-exit': createMarkdownExit(),
62+
'markdown-it': createMarkdownIt(),
63+
'marked': createMarked(),
64+
},
65+
commonmark: {
66+
'commonmark': createCommonmark(),
67+
'markdown-exit-commonmark': createMarkdownExitCommonMark(),
68+
'markdown-it-commonmark': createMarkdownItCommonMark(),
69+
},
6670
}
6771
}

packages/markdown-exit/src/parser/block/rules/hr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default function hr(state: StateBlock, startLine: number, endLine: number
4040

4141
const token = state.push('hr', 'hr', 0)
4242
token.map = [startLine, state.line]
43-
token.markup = Array.from({ length: cnt + 1 }).join(String.fromCharCode(marker))
43+
token.markup = String.fromCharCode(marker).repeat(cnt)
4444

4545
return true
4646
}

packages/markdown-exit/src/parser/block/state_block.ts

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -107,39 +107,33 @@ export default class StateBlock<T extends Parser = Parser> {
107107
// Create caches
108108
// Generate markers.
109109
const s = this.src
110+
const len = s.length
110111

111-
for (let start = 0, pos = 0, indent = 0, offset = 0, len = s.length, indent_found = false; pos < len; pos++) {
112-
const ch = s.charCodeAt(pos)
112+
for (let start = 0; start < len;) {
113+
const lineEnd = s.indexOf('\n', start)
114+
const end = lineEnd === -1 ? len : lineEnd
115+
let indent = 0
116+
let offset = 0
117+
let pos = start
113118

114-
if (!indent_found) {
119+
while (pos < end) {
120+
const ch = s.charCodeAt(pos)
115121
if (isSpace(ch)) {
116122
indent++
117-
118-
if (ch === 0x09) {
119-
offset += 4 - offset % 4
120-
} else {
121-
offset++
122-
}
123+
offset += (ch === 0x09) ? (4 - offset % 4) : 1
124+
pos++
123125
continue
124-
} else {
125-
indent_found = true
126126
}
127+
break
127128
}
128129

129-
if (ch === 0x0A || pos === len - 1) {
130-
if (ch !== 0x0A)
131-
pos++
132-
this.bMarks.push(start)
133-
this.eMarks.push(pos)
134-
this.tShift.push(indent)
135-
this.sCount.push(offset)
136-
this.bsCount.push(0)
137-
138-
indent_found = false
139-
indent = 0
140-
offset = 0
141-
start = pos + 1
142-
}
130+
this.bMarks.push(start)
131+
this.eMarks.push(end)
132+
this.tShift.push(indent)
133+
this.sCount.push(offset)
134+
this.bsCount.push(0)
135+
136+
start = end + 1
143137
}
144138

145139
// Push fake entry to simplify cache bounds checks
@@ -186,8 +180,9 @@ export default class StateBlock<T extends Parser = Parser> {
186180
* Skip spaces from given position.
187181
*/
188182
skipSpaces(pos: number): number {
189-
for (let max = this.src.length; pos < max; pos++) {
190-
const ch = this.src.charCodeAt(pos)
183+
const src = this.src
184+
for (let max = src.length; pos < max; pos++) {
185+
const ch = src.charCodeAt(pos)
191186
if (!isSpace(ch))
192187
break
193188
}
@@ -201,8 +196,10 @@ export default class StateBlock<T extends Parser = Parser> {
201196
if (pos <= min)
202197
return pos
203198

199+
const src = this.src
204200
while (pos > min) {
205-
if (!isSpace(this.src.charCodeAt(--pos)))
201+
const code = src.charCodeAt(--pos)
202+
if (!isSpace(code))
206203
return pos + 1
207204
}
208205
return pos
@@ -212,8 +209,9 @@ export default class StateBlock<T extends Parser = Parser> {
212209
* Skip char codes from given position
213210
*/
214211
skipChars(pos: number, code: number): number {
215-
for (let max = this.src.length; pos < max; pos++) {
216-
if (this.src.charCodeAt(pos) !== code)
212+
const src = this.src
213+
for (let max = src.length; pos < max; pos++) {
214+
if (src.charCodeAt(pos) !== code)
217215
break
218216
}
219217
return pos
@@ -226,8 +224,9 @@ export default class StateBlock<T extends Parser = Parser> {
226224
if (pos <= min)
227225
return pos
228226

227+
const src = this.src
229228
while (pos > min) {
230-
if (code !== this.src.charCodeAt(--pos))
229+
if (code !== src.charCodeAt(--pos))
231230
return pos + 1
232231
}
233232
return pos
@@ -241,8 +240,10 @@ export default class StateBlock<T extends Parser = Parser> {
241240
return ''
242241
}
243242

244-
const queue = Array.from({ length: end - begin })
243+
/* perf */// eslint-disable-next-line unicorn/no-new-array
244+
const queue = new Array<string>(end - begin)
245245

246+
const src = this.src
246247
for (let i = 0, line = begin; line < end; line++, i++) {
247248
let lineIndent = 0
248249
const lineStart = this.bMarks[line]
@@ -257,7 +258,7 @@ export default class StateBlock<T extends Parser = Parser> {
257258
}
258259

259260
while (first < last && lineIndent < indent) {
260-
const ch = this.src.charCodeAt(first)
261+
const ch = src.charCodeAt(first)
261262

262263
if (isSpace(ch)) {
263264
if (ch === 0x09) {
@@ -278,7 +279,7 @@ export default class StateBlock<T extends Parser = Parser> {
278279
if (lineIndent > indent) {
279280
// partially expanding tabs in code blocks, e.g '\t\tfoobar'
280281
// with indent=2 becomes ' \tfoobar'
281-
queue[i] = Array.from({ length: lineIndent - indent + 1 }).join(' ') + this.src.slice(first, last)
282+
queue[i] = ' '.repeat(lineIndent - indent) + this.src.slice(first, last)
282283
} else {
283284
queue[i] = this.src.slice(first, last)
284285
}

packages/markdown-exit/src/parser/core/rules/linkify.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
//
55

66
import type StateCore from '../state_core'
7-
import { arrayReplaceAt } from '../../../common/utils'
87

98
function isLinkOpen(str: string) {
109
return /^<a[>\s]/i.test(str)
@@ -14,18 +13,19 @@ function isLinkClose(str: string) {
1413
}
1514

1615
export default function linkify(state: StateCore) {
17-
const blockTokens = state.tokens
18-
1916
if (!state.md.options.linkify)
2017
return
2118

19+
const blockTokens = state.tokens
20+
const linkify = state.md.linkify
21+
2222
for (let j = 0, l = blockTokens.length; j < l; j++) {
2323
if (blockTokens[j].type !== 'inline' ||
24-
!state.md.linkify.pretest(blockTokens[j].content)) {
24+
!linkify.pretest(blockTokens[j].content)) {
2525
continue
2626
}
2727

28-
let tokens = blockTokens[j].children!
28+
const tokens = blockTokens[j].children!
2929

3030
let htmlLinkLevel = 0
3131

@@ -55,9 +55,14 @@ export default function linkify(state: StateCore) {
5555
if (htmlLinkLevel > 0)
5656
continue
5757

58-
if (currentToken.type === 'text' && state.md.linkify.test(currentToken.content)) {
58+
if (currentToken.type === 'text') {
5959
const text = currentToken.content
60-
let links = state.md.linkify.match(text) ?? []
60+
if (!linkify.pretest(text))
61+
continue
62+
63+
const links = linkify.match(text)
64+
if (!links?.length)
65+
continue
6166

6267
// Now split string to nodes
6368
const nodes = []
@@ -67,34 +72,33 @@ export default function linkify(state: StateCore) {
6772
// forbid escape sequence at the start of the string,
6873
// this avoids http\://example.com/ from being linkified as
6974
// http:<a href="//example.com/">//example.com/</a>
70-
if (links.length > 0 &&
71-
links[0].index === 0 &&
75+
const startFrom = (links[0].index === 0 &&
7276
i > 0 &&
73-
tokens[i - 1].type === 'text_special') {
74-
links = links.slice(1)
75-
}
77+
tokens[i - 1].type === 'text_special')
78+
? 1
79+
: 0
7680

77-
for (let ln = 0; ln < links.length; ln++) {
78-
const url = links[ln].url
79-
const fullUrl = state.md.normalizeLink(url)
81+
for (let ln = startFrom; ln < links.length; ln++) {
82+
const link = links[ln]
83+
const fullUrl = state.md.normalizeLink(link.url)
8084
if (!state.md.validateLink(fullUrl))
8185
continue
8286

83-
let urlText = links[ln].text
87+
let urlText = link.text
8488

8589
// Linkifier might send raw hostnames like "example.com", where url
8690
// starts with domain name. So we prepend http:// in those cases,
8791
// and remove it afterwards.
8892
//
89-
if (!links[ln].schema) {
93+
if (!link.schema) {
9094
urlText = state.md.normalizeLinkText(`http://${urlText}`).replace(/^http:\/\//, '')
91-
} else if (links[ln].schema === 'mailto:' && !/^mailto:/i.test(urlText)) {
95+
} else if (link.schema === 'mailto:' && !/^mailto:/i.test(urlText)) {
9296
urlText = state.md.normalizeLinkText(`mailto:${urlText}`).replace(/^mailto:/, '')
9397
} else {
9498
urlText = state.md.normalizeLinkText(urlText)
9599
}
96100

97-
const pos = links[ln].index
101+
const pos = link.index
98102

99103
if (pos > lastPos) {
100104
const token = new state.Token('text', '', 0)
@@ -121,7 +125,7 @@ export default function linkify(state: StateCore) {
121125
token_c.info = 'auto'
122126
nodes.push(token_c)
123127

124-
lastPos = links[ln].lastIndex
128+
lastPos = link.lastIndex
125129
}
126130
if (lastPos < text.length) {
127131
const token = new state.Token('text', '', 0)
@@ -131,7 +135,7 @@ export default function linkify(state: StateCore) {
131135
}
132136

133137
// replace current node
134-
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes)
138+
tokens.splice(i, 1, ...nodes)
135139
}
136140
}
137141
}

packages/markdown-exit/src/parser/core/rules/normalize.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@ const NEWLINES_RE = /\r\n?|\n/g
77
const NULL_RE = /\0/g
88

99
export default function normalize(state: StateCore) {
10-
let str
10+
let str = state.src
11+
const hasCR = str.includes('\r')
12+
const hasNull = str.includes('\0')
13+
14+
if (!hasCR && !hasNull)
15+
return
1116

1217
// Normalize newlines
13-
str = state.src.replace(NEWLINES_RE, '\n')
18+
if (hasCR) {
19+
str = str.replace(NEWLINES_RE, '\n')
20+
}
1421

1522
// Replace NULL characters
16-
str = str.replace(NULL_RE, '\uFFFD')
23+
if (hasNull) {
24+
str = str.replace(NULL_RE, '\uFFFD')
25+
}
1726

1827
state.src = str
1928
}

packages/markdown-exit/src/parser/inline/parser_inline.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ export default class ParserInline<T extends Parser = Parser> {
113113
const maxNesting = state.md.options.maxNesting
114114
const cache = state.cache
115115

116-
if (typeof cache[pos] !== 'undefined') {
117-
state.pos = cache[pos]
116+
const cachedPos = cache[pos]
117+
if (cachedPos !== undefined) {
118+
state.pos = cachedPos
118119
return
119120
}
120121

0 commit comments

Comments
 (0)