Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"build/globals.js",
"build/deno.js"
],
"limit": "849.55 kB",
"limit": "849.90 kB",
"brotli": false,
"gzip": false
},
Expand Down Expand Up @@ -66,7 +66,7 @@
"README.md",
"LICENSE"
],
"limit": "911.30 kB",
"limit": "911.65 kB",
"brotli": false,
"gzip": false
}
Expand Down
104 changes: 52 additions & 52 deletions build/cli.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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("^(?<fence>(`{3,20}|~{3,20}))(?:(?<js>(js|javascript|ts|typescript))|(?<bash>(sh|shell|bash))|.*)$");
const fenceRe = new RegExp("^(?<indent> {0,3})(?<fence>(`{3,20}|~{3,20}))(?:(?<js>js|javascript|ts|typescript)|(?<bash>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
Expand Down
117 changes: 64 additions & 53 deletions src/md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
/^(?<fence>(`{3,20}|~{3,20}))(?:(?<js>(js|javascript|ts|typescript))|(?<bash>(sh|shell|bash))|.*)$/
const fenceRe =
/^(?<indent> {0,3})(?<fence>(`{3,20}|~{3,20}))(?:(?<js>js|javascript|ts|typescript)|(?<bash>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')
}
69 changes: 58 additions & 11 deletions test/md.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
})
})
})
Loading