diff --git a/.size-limit.json b/.size-limit.json index 84f6dc4f53..56d3c9ee20 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -33,7 +33,7 @@ "build/globals.js", "build/deno.js" ], - "limit": "849.55 kB", + "limit": "849.90 kB", "brotli": false, "gzip": false }, @@ -66,7 +66,7 @@ "README.md", "LICENSE" ], - "limit": "911.30 kB", + "limit": "911.65 kB", "brotli": false, "gzip": false } diff --git a/build/cli.cjs b/build/cli.cjs index 5b547df14e..ae7a8f4edd 100755 --- a/build/cli.cjs +++ b/build/cli.cjs @@ -62,75 +62,75 @@ var import_util2 = require("./util.cjs"); var import_util = require("./util.cjs"); function transformMarkdown(buf) { var _a2; - const output = []; + const out = []; const tabRe = /^( +|\t)/; - const codeBlockRe = new RegExp("^(?(`{3,20}|~{3,20}))(?:(?(js|javascript|ts|typescript))|(?(sh|shell|bash))|.*)$"); + const fenceRe = new RegExp("^(? {0,3})(?(`{3,20}|~{3,20}))(?:(?js|javascript|ts|typescript)|(?sh|shell|bash)|.*)$"); let state = "root"; - let codeBlockEnd = ""; - let prevLineIsEmpty = true; + let prevEmpty = true; + let fenceChar = ""; + let stripRe = null; + let endRe = /^$/; + let linePrefix = ""; + let closeOut = ""; + const isEnd = (s) => fenceChar !== "" && endRe.test(s); for (const line of (0, import_util.bufToString)(buf).split(/\r?\n/)) { switch (state) { - case "root": - if (tabRe.test(line) && prevLineIsEmpty) { - output.push(line); - state = "tab"; - continue; + case "root": { + const g = (_a2 = line.match(fenceRe)) == null ? void 0 : _a2.groups; + if (g == null ? void 0 : g.fence) { + fenceChar = g.fence[0]; + stripRe = g.indent ? new RegExp(`^ {0,${g.indent.length}}`) : null; + endRe = new RegExp(`^ {0,3}${fenceChar}{${g.fence.length},}[ \\t]*$`); + if (g.js) { + out.push(""); + linePrefix = ""; + closeOut = ""; + } else if (g.bash) { + out.push("await $`"); + linePrefix = ""; + closeOut = "`"; + } else { + out.push(""); + linePrefix = "// "; + closeOut = ""; + } + state = "fence"; + prevEmpty = false; + break; } - const { fence, js, bash } = ((_a2 = line.match(codeBlockRe)) == null ? void 0 : _a2.groups) || {}; - if (!fence) { - prevLineIsEmpty = line === ""; - output.push("// " + line); + if (prevEmpty && tabRe.test(line)) { + out.push(line); + state = "tab"; continue; } - codeBlockEnd = fence; - if (js) { - state = "js"; - output.push(""); - } else if (bash) { - state = "bash"; - output.push("await $`"); - } else { - state = "other"; - output.push(""); - } - break; + prevEmpty = line === ""; + out.push("// " + line); + continue; + } case "tab": - if (line === "") { - output.push(""); - } else if (tabRe.test(line)) { - output.push(line); - } else { - output.push("// " + line); + if (line === "") out.push(""); + else if (tabRe.test(line)) out.push(line); + else { + out.push("// " + line); state = "root"; } + prevEmpty = line === ""; break; - case "js": - if (line === codeBlockEnd) { - output.push(""); - state = "root"; - } else { - output.push(line); - } - break; - case "bash": - if (line === codeBlockEnd) { - output.push("`"); - state = "root"; - } else { - output.push(line); - } - break; - case "other": - if (line === codeBlockEnd) { - output.push(""); + case "fence": + if (isEnd(line)) { + out.push(closeOut); state = "root"; + prevEmpty = true; + fenceChar = ""; } else { - output.push("// " + line); + const s = stripRe ? line.replace(stripRe, "") : line; + out.push(linePrefix + s); + prevEmpty = false; } break; } } - return output.join("\n"); + return out.join("\n"); } // src/cli.ts diff --git a/src/md.ts b/src/md.ts index 3e88b95d75..a96d6149d8 100644 --- a/src/md.ts +++ b/src/md.ts @@ -16,74 +16,85 @@ import { type Buffer } from 'node:buffer' import { bufToString } from './util.ts' export function transformMarkdown(buf: Buffer | string): string { - const output = [] + const out: string[] = [] const tabRe = /^( +|\t)/ - const codeBlockRe = - /^(?(`{3,20}|~{3,20}))(?:(?(js|javascript|ts|typescript))|(?(sh|shell|bash))|.*)$/ + const fenceRe = + /^(? {0,3})(?(`{3,20}|~{3,20}))(?:(?js|javascript|ts|typescript)|(?sh|shell|bash)|.*)$/ + let state = 'root' - let codeBlockEnd = '' - let prevLineIsEmpty = true + let prevEmpty = true + + let fenceChar = '' + let stripRe: RegExp | null = null + let endRe = /^$/ + let linePrefix = '' + let closeOut = '' + + const isEnd = (s: string) => fenceChar !== '' && endRe.test(s) + for (const line of bufToString(buf).split(/\r?\n/)) { switch (state) { - case 'root': - if (tabRe.test(line) && prevLineIsEmpty) { - output.push(line) - state = 'tab' - continue + case 'root': { + const g = line.match(fenceRe)?.groups + if (g?.fence) { + fenceChar = g.fence[0] + stripRe = g.indent ? new RegExp(`^ {0,${g.indent.length}}`) : null + endRe = new RegExp(`^ {0,3}${fenceChar}{${g.fence.length},}[ \\t]*$`) + + if (g.js) { + out.push('') + linePrefix = '' + closeOut = '' + } else if (g.bash) { + out.push('await $`') + linePrefix = '' + closeOut = '`' + } else { + out.push('') + linePrefix = '// ' + closeOut = '' + } + + state = 'fence' + prevEmpty = false + break } - const { fence, js, bash } = line.match(codeBlockRe)?.groups || {} - if (!fence) { - prevLineIsEmpty = line === '' - output.push('// ' + line) + + if (prevEmpty && tabRe.test(line)) { + out.push(line) + state = 'tab' continue } - codeBlockEnd = fence - if (js) { - state = 'js' - output.push('') - } else if (bash) { - state = 'bash' - output.push('await $`') - } else { - state = 'other' - output.push('') - } - break + + prevEmpty = line === '' + out.push('// ' + line) + continue + } + case 'tab': - if (line === '') { - output.push('') - } else if (tabRe.test(line)) { - output.push(line) - } else { - output.push('// ' + line) + if (line === '') out.push('') + else if (tabRe.test(line)) out.push(line) + else { + out.push('// ' + line) state = 'root' } + prevEmpty = line === '' break - case 'js': - if (line === codeBlockEnd) { - output.push('') - state = 'root' - } else { - output.push(line) - } - break - case 'bash': - if (line === codeBlockEnd) { - output.push('`') - state = 'root' - } else { - output.push(line) - } - break - case 'other': - if (line === codeBlockEnd) { - output.push('') + + case 'fence': + if (isEnd(line)) { + out.push(closeOut) state = 'root' + prevEmpty = true + fenceChar = '' } else { - output.push('// ' + line) + const s = stripRe ? line.replace(stripRe, '') : line + out.push(linePrefix + s) + prevEmpty = false } break } } - return output.join('\n') + + return out.join('\n') } diff --git a/test/md.test.ts b/test/md.test.ts index 412940d6c4..6603d59407 100644 --- a/test/md.test.ts +++ b/test/md.test.ts @@ -12,22 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { test, describe } from 'node:test' +import { describe, test } from 'node:test' import assert from 'node:assert' import { transformMarkdown } from '../src/md.ts' -describe('md', () => { - test('transformMarkdown()', () => { - assert.equal(transformMarkdown('\n'), '// \n// ') - assert.equal(transformMarkdown(' \n '), ' \n ') - assert.equal( - transformMarkdown(` +describe('transformMarkdown()', () => { + describe('root handling', () => { + test('comments out plain lines (including empty line)', () => { + assert.equal(transformMarkdown('\n'), '// \n// ') + }) + + test('preserves tab-indented blocks after a blank line (legacy behavior)', () => { + assert.equal(transformMarkdown(' \n '), ' \n ') + }) + + test('does not treat a mid-paragraph fence as a fenced block (legacy behavior)', () => { + assert.equal( + transformMarkdown(` \t~~~js console.log('js')`), - `// \n\t~~~js\n// console.log('js')` - ) - // prettier-ignore - assert.equal(transformMarkdown(` + `// \n\t~~~js\n// console.log('js')` + ) + }) + }) + + describe('fenced code blocks', () => { + test('converts js/ts to raw code, bash to await $`...` and comments unknown fences', () => { + // prettier-ignore + assert.equal(transformMarkdown(` # Title ~~~js @@ -68,5 +80,40 @@ echo foo \` // // `) + }) + + test('accepts fences indented up to 3 spaces (CommonMark) and converts them', () => { + const input = `# h1 + +paragraph + +## h2 + +### h3 + +\`\`\`bash +echo "1" +\`\`\` + +### h3 + +- item 1 + + \`\`\`bash + echo "2" + \`\`\` + +### h3 + +\`\`\`bash +echo "4" +\`\`\` +` + const result = transformMarkdown(input) + + assert.ok(!/```|~~~/.test(result), 'no raw markdown fences should remain') + assert.equal((result.match(/await \$`/g) ?? []).length, 3) + assert.equal((result.match(/^`$/gm) ?? []).length, 3) + }) }) })