Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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.60 kB",
"brotli": false,
"gzip": false
}
Expand Down
107 changes: 55 additions & 52 deletions build/cli.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,75 +62,78 @@ 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 = /^/;
let endRe = /^$/;
let linePrefix = "";
let closeOut = "";
let closeBlank = false;
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}}`) : /^/;
endRe = new RegExp(`^ {0,3}${fenceChar}{${g.fence.length},}[ \\t]*$`);
if (g.js) {
out.push("");
linePrefix = "";
closeOut = "";
closeBlank = true;
} else if (g.bash) {
out.push("await $`");
linePrefix = "";
closeOut = "`";
closeBlank = false;
} else {
out.push("");
linePrefix = "// ";
closeOut = "";
closeBlank = true;
}
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)) {
if (closeOut) out.push(closeOut);
else if (closeBlank) out.push("");
state = "root";
prevEmpty = true;
} else {
output.push("// " + line);
out.push(linePrefix + line.replace(stripRe, ""));
prevEmpty = false;
}
break;
}
}
return output.join("\n");
return out.join("\n");
}

// src/cli.ts
Expand Down
120 changes: 67 additions & 53 deletions src/md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,74 +16,88 @@
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 = /^/
let endRe = /^$/
let linePrefix = ''
let closeOut = ''
let closeBlank = false

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}}`) : /^/
endRe = new RegExp(`^ {0,3}${fenceChar}{${g.fence.length},}[ \\t]*$`)

if (g.js) {
out.push('')
linePrefix = ''
closeOut = ''
closeBlank = true
} else if (g.bash) {
out.push('await $`')
linePrefix = ''
closeOut = '`'
closeBlank = false
} else {
out.push('')
linePrefix = '// '
closeOut = ''
closeBlank = true
}

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)) {
if (closeOut) out.push(closeOut)
else if (closeBlank) out.push('')
state = 'root'
prevEmpty = true
} else {
output.push('// ' + line)
out.push(linePrefix + line.replace(stripRe, ''))
Comment thread Fixed
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