markdown-it 0.6.1

Rust port of popular markdown-it.js library.
Documentation
// use `deno run --allow-read --allow-write ./fixtures/testgen.js ./commonmark.rs` to run this script

if (Deno.args.length !== 1) {
    console.error(`
Usage: deno run --allow-read --allow-write ./fixtures/testgen.js ./commonmark.rs
`)
    Deno.exit(1)
}

import testgen from 'npm:markdown-it-testgen@^0.1.6'

function rust_escape(s) {
    if (s.match(/( $|\t)/m)) {
        return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/ $/mg, '\\x20').replace(/\t/g, '\\t') + '"'
    }

    if (s.match(/#"|"#/)) return 'r##"' + s + '"##'
    return 'r#"' + s + '"#'
}

let identmap = new Set()
function ident(str) {
    str = str.toLowerCase()
            .replace(/[^a-z0-9]+/g, '_')
            .replace(/^_+|_+$/g, '')

    if (!str) str = 'unnamed'

    let result = str
    let idx = 0
    while (identmap.has(result)) {
        result = str + '_' + (++idx)
    }

    identmap.add(result)
    return result
}

function generate(fixture) {
    return `
#[test]
fn ${ident(fixture.header)}() {
    let input = ${rust_escape(fixture.first.text.replace(/\n$/, ''))};
    let output = ${rust_escape(fixture.second.text.replace(/\n$/, ''))};
    run(input, output);
}
`.trim()
}

let input_raw = await Deno.readFile(Deno.args[0])
let input = new TextDecoder().decode(input_raw)
let lines = []
let state = 'passthrough'

for (let line of input.split('\n')) {
    switch (state) {
        case 'passthrough':
            lines.push(line)
            if (line.match(/^\/{4,}$/)) {
                state = 'maybeheader'
            }
            break
        case 'skipping':
            if (line.match(/^\/{4,}$/)) {
                lines.push(line)
                state = 'maybeheader'
            }
            break
        case 'maybeheader':
            lines.push(line)
            let match = line.match(/^\/{2,}\s+TESTGEN:\s*(.+)\s*$/)
            if (match) {
                let has_data = false
                lines.push('#[rustfmt::skip]')
                lines.push(`mod ${ident(match[1])} {`)
                lines.push('use super::run;')
                lines.push('// this part of the file is auto-generated')
                lines.push('// don\'t edit it, otherwise your changes might be lost')
                testgen.load(match[1], data => {
                    data.fixtures.forEach((data, idx) => {
                        has_data = true
                        if (idx++ > 0) lines.push('')
                        let generated = generate(data).replace(/\r?\n$/, '')
                        lines = lines.concat(generated.split('\n'))
                    })
                })
                lines.push('// end of auto-generated module')
                lines.push('}')
                if (!has_data) throw new Error(`no data found for ${match[1]}`)
                state = 'skipping'
            }
            break
        default:
            throw Error('unknown state')
    }
}

await Deno.rename(Deno.args[0], Deno.args[0] + '.old')
await Deno.writeFile(Deno.args[0], new TextEncoder().encode(lines.join('\n') + '\n'))