Skip to content

Commit 3dd9503

Browse files
authored
fix: handle fence indents in md processor (#1419)
1 parent f61cbb8 commit 3dd9503

4 files changed

Lines changed: 176 additions & 118 deletions

File tree

.size-limit.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"build/globals.js",
3434
"build/deno.js"
3535
],
36-
"limit": "849.55 kB",
36+
"limit": "849.90 kB",
3737
"brotli": false,
3838
"gzip": false
3939
},
@@ -66,7 +66,7 @@
6666
"README.md",
6767
"LICENSE"
6868
],
69-
"limit": "911.30 kB",
69+
"limit": "911.65 kB",
7070
"brotli": false,
7171
"gzip": false
7272
}

build/cli.cjs

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -62,75 +62,75 @@ var import_util2 = require("./util.cjs");
6262
var import_util = require("./util.cjs");
6363
function transformMarkdown(buf) {
6464
var _a2;
65-
const output = [];
65+
const out = [];
6666
const tabRe = /^( +|\t)/;
67-
const codeBlockRe = new RegExp("^(?<fence>(`{3,20}|~{3,20}))(?:(?<js>(js|javascript|ts|typescript))|(?<bash>(sh|shell|bash))|.*)$");
67+
const fenceRe = new RegExp("^(?<indent> {0,3})(?<fence>(`{3,20}|~{3,20}))(?:(?<js>js|javascript|ts|typescript)|(?<bash>sh|shell|bash)|.*)$");
6868
let state = "root";
69-
let codeBlockEnd = "";
70-
let prevLineIsEmpty = true;
69+
let prevEmpty = true;
70+
let fenceChar = "";
71+
let stripRe = null;
72+
let endRe = /^$/;
73+
let linePrefix = "";
74+
let closeOut = "";
75+
const isEnd = (s) => fenceChar !== "" && endRe.test(s);
7176
for (const line of (0, import_util.bufToString)(buf).split(/\r?\n/)) {
7277
switch (state) {
73-
case "root":
74-
if (tabRe.test(line) && prevLineIsEmpty) {
75-
output.push(line);
76-
state = "tab";
77-
continue;
78+
case "root": {
79+
const g = (_a2 = line.match(fenceRe)) == null ? void 0 : _a2.groups;
80+
if (g == null ? void 0 : g.fence) {
81+
fenceChar = g.fence[0];
82+
stripRe = g.indent ? new RegExp(`^ {0,${g.indent.length}}`) : null;
83+
endRe = new RegExp(`^ {0,3}${fenceChar}{${g.fence.length},}[ \\t]*$`);
84+
if (g.js) {
85+
out.push("");
86+
linePrefix = "";
87+
closeOut = "";
88+
} else if (g.bash) {
89+
out.push("await $`");
90+
linePrefix = "";
91+
closeOut = "`";
92+
} else {
93+
out.push("");
94+
linePrefix = "// ";
95+
closeOut = "";
96+
}
97+
state = "fence";
98+
prevEmpty = false;
99+
break;
78100
}
79-
const { fence, js, bash } = ((_a2 = line.match(codeBlockRe)) == null ? void 0 : _a2.groups) || {};
80-
if (!fence) {
81-
prevLineIsEmpty = line === "";
82-
output.push("// " + line);
101+
if (prevEmpty && tabRe.test(line)) {
102+
out.push(line);
103+
state = "tab";
83104
continue;
84105
}
85-
codeBlockEnd = fence;
86-
if (js) {
87-
state = "js";
88-
output.push("");
89-
} else if (bash) {
90-
state = "bash";
91-
output.push("await $`");
92-
} else {
93-
state = "other";
94-
output.push("");
95-
}
96-
break;
106+
prevEmpty = line === "";
107+
out.push("// " + line);
108+
continue;
109+
}
97110
case "tab":
98-
if (line === "") {
99-
output.push("");
100-
} else if (tabRe.test(line)) {
101-
output.push(line);
102-
} else {
103-
output.push("// " + line);
111+
if (line === "") out.push("");
112+
else if (tabRe.test(line)) out.push(line);
113+
else {
114+
out.push("// " + line);
104115
state = "root";
105116
}
117+
prevEmpty = line === "";
106118
break;
107-
case "js":
108-
if (line === codeBlockEnd) {
109-
output.push("");
110-
state = "root";
111-
} else {
112-
output.push(line);
113-
}
114-
break;
115-
case "bash":
116-
if (line === codeBlockEnd) {
117-
output.push("`");
118-
state = "root";
119-
} else {
120-
output.push(line);
121-
}
122-
break;
123-
case "other":
124-
if (line === codeBlockEnd) {
125-
output.push("");
119+
case "fence":
120+
if (isEnd(line)) {
121+
out.push(closeOut);
126122
state = "root";
123+
prevEmpty = true;
124+
fenceChar = "";
127125
} else {
128-
output.push("// " + line);
126+
const s = stripRe ? line.replace(stripRe, "") : line;
127+
out.push(linePrefix + s);
128+
prevEmpty = false;
129129
}
130130
break;
131131
}
132132
}
133-
return output.join("\n");
133+
return out.join("\n");
134134
}
135135

136136
// src/cli.ts

src/md.ts

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,74 +16,85 @@ import { type Buffer } from 'node:buffer'
1616
import { bufToString } from './util.ts'
1717

1818
export function transformMarkdown(buf: Buffer | string): string {
19-
const output = []
19+
const out: string[] = []
2020
const tabRe = /^( +|\t)/
21-
const codeBlockRe =
22-
/^(?<fence>(`{3,20}|~{3,20}))(?:(?<js>(js|javascript|ts|typescript))|(?<bash>(sh|shell|bash))|.*)$/
21+
const fenceRe =
22+
/^(?<indent> {0,3})(?<fence>(`{3,20}|~{3,20}))(?:(?<js>js|javascript|ts|typescript)|(?<bash>sh|shell|bash)|.*)$/
23+
2324
let state = 'root'
24-
let codeBlockEnd = ''
25-
let prevLineIsEmpty = true
25+
let prevEmpty = true
26+
27+
let fenceChar = ''
28+
let stripRe: RegExp | null = null
29+
let endRe = /^$/
30+
let linePrefix = ''
31+
let closeOut = ''
32+
33+
const isEnd = (s: string) => fenceChar !== '' && endRe.test(s)
34+
2635
for (const line of bufToString(buf).split(/\r?\n/)) {
2736
switch (state) {
28-
case 'root':
29-
if (tabRe.test(line) && prevLineIsEmpty) {
30-
output.push(line)
31-
state = 'tab'
32-
continue
37+
case 'root': {
38+
const g = line.match(fenceRe)?.groups
39+
if (g?.fence) {
40+
fenceChar = g.fence[0]
41+
stripRe = g.indent ? new RegExp(`^ {0,${g.indent.length}}`) : null
42+
endRe = new RegExp(`^ {0,3}${fenceChar}{${g.fence.length},}[ \\t]*$`)
43+
44+
if (g.js) {
45+
out.push('')
46+
linePrefix = ''
47+
closeOut = ''
48+
} else if (g.bash) {
49+
out.push('await $`')
50+
linePrefix = ''
51+
closeOut = '`'
52+
} else {
53+
out.push('')
54+
linePrefix = '// '
55+
closeOut = ''
56+
}
57+
58+
state = 'fence'
59+
prevEmpty = false
60+
break
3361
}
34-
const { fence, js, bash } = line.match(codeBlockRe)?.groups || {}
35-
if (!fence) {
36-
prevLineIsEmpty = line === ''
37-
output.push('// ' + line)
62+
63+
if (prevEmpty && tabRe.test(line)) {
64+
out.push(line)
65+
state = 'tab'
3866
continue
3967
}
40-
codeBlockEnd = fence
41-
if (js) {
42-
state = 'js'
43-
output.push('')
44-
} else if (bash) {
45-
state = 'bash'
46-
output.push('await $`')
47-
} else {
48-
state = 'other'
49-
output.push('')
50-
}
51-
break
68+
69+
prevEmpty = line === ''
70+
out.push('// ' + line)
71+
continue
72+
}
73+
5274
case 'tab':
53-
if (line === '') {
54-
output.push('')
55-
} else if (tabRe.test(line)) {
56-
output.push(line)
57-
} else {
58-
output.push('// ' + line)
75+
if (line === '') out.push('')
76+
else if (tabRe.test(line)) out.push(line)
77+
else {
78+
out.push('// ' + line)
5979
state = 'root'
6080
}
81+
prevEmpty = line === ''
6182
break
62-
case 'js':
63-
if (line === codeBlockEnd) {
64-
output.push('')
65-
state = 'root'
66-
} else {
67-
output.push(line)
68-
}
69-
break
70-
case 'bash':
71-
if (line === codeBlockEnd) {
72-
output.push('`')
73-
state = 'root'
74-
} else {
75-
output.push(line)
76-
}
77-
break
78-
case 'other':
79-
if (line === codeBlockEnd) {
80-
output.push('')
83+
84+
case 'fence':
85+
if (isEnd(line)) {
86+
out.push(closeOut)
8187
state = 'root'
88+
prevEmpty = true
89+
fenceChar = ''
8290
} else {
83-
output.push('// ' + line)
91+
const s = stripRe ? line.replace(stripRe, '') : line
92+
out.push(linePrefix + s)
93+
prevEmpty = false
8494
}
8595
break
8696
}
8797
}
88-
return output.join('\n')
98+
99+
return out.join('\n')
89100
}

test/md.test.ts

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,34 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { test, describe } from 'node:test'
15+
import { describe, test } from 'node:test'
1616
import assert from 'node:assert'
1717
import { transformMarkdown } from '../src/md.ts'
1818

19-
describe('md', () => {
20-
test('transformMarkdown()', () => {
21-
assert.equal(transformMarkdown('\n'), '// \n// ')
22-
assert.equal(transformMarkdown(' \n '), ' \n ')
23-
assert.equal(
24-
transformMarkdown(`
19+
describe('transformMarkdown()', () => {
20+
describe('root handling', () => {
21+
test('comments out plain lines (including empty line)', () => {
22+
assert.equal(transformMarkdown('\n'), '// \n// ')
23+
})
24+
25+
test('preserves tab-indented blocks after a blank line (legacy behavior)', () => {
26+
assert.equal(transformMarkdown(' \n '), ' \n ')
27+
})
28+
29+
test('does not treat a mid-paragraph fence as a fenced block (legacy behavior)', () => {
30+
assert.equal(
31+
transformMarkdown(`
2532
\t~~~js
2633
console.log('js')`),
27-
`// \n\t~~~js\n// console.log('js')`
28-
)
29-
// prettier-ignore
30-
assert.equal(transformMarkdown(`
34+
`// \n\t~~~js\n// console.log('js')`
35+
)
36+
})
37+
})
38+
39+
describe('fenced code blocks', () => {
40+
test('converts js/ts to raw code, bash to await $`...` and comments unknown fences', () => {
41+
// prettier-ignore
42+
assert.equal(transformMarkdown(`
3143
# Title
3244
3345
~~~js
@@ -68,5 +80,40 @@ echo foo
6880
\`
6981
//
7082
// `)
83+
})
84+
85+
test('accepts fences indented up to 3 spaces (CommonMark) and converts them', () => {
86+
const input = `# h1
87+
88+
paragraph
89+
90+
## h2
91+
92+
### h3
93+
94+
\`\`\`bash
95+
echo "1"
96+
\`\`\`
97+
98+
### h3
99+
100+
- item 1
101+
102+
\`\`\`bash
103+
echo "2"
104+
\`\`\`
105+
106+
### h3
107+
108+
\`\`\`bash
109+
echo "4"
110+
\`\`\`
111+
`
112+
const result = transformMarkdown(input)
113+
114+
assert.ok(!/```|~~~/.test(result), 'no raw markdown fences should remain')
115+
assert.equal((result.match(/await \$`/g) ?? []).length, 3)
116+
assert.equal((result.match(/^`$/gm) ?? []).length, 3)
117+
})
71118
})
72119
})

0 commit comments

Comments
 (0)