Skip to content

Commit

Permalink
update cli
Browse files Browse the repository at this point in the history
1. allow url as input
2. allow wildcard file path
3. new flag -o
4. change flag -s to -i
  • Loading branch information
ksw2000 committed Jan 25, 2024
1 parent 2a09312 commit 762c684
Show file tree
Hide file tree
Showing 4 changed files with 803 additions and 151 deletions.
158 changes: 150 additions & 8 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,163 @@
import commander from 'commander'
import fs from 'fs'
import { Converter } from './converter'
import path from 'path'
import * as https from 'https'
import * as http from 'http'
import { glob } from 'glob'
import { createHash } from 'node:crypto'

commander.program.version('1.0.0', '-v, --version', 'output the current version')
const hash = createHash('sha256');

commander.program.version('1.1.0', '-v, --version', 'output the current version')
commander.program
.requiredOption('-s, --src <files_or_dirs...>', 'specify the input markdown files or directories')
.addOption(new commander.Option('-d, --dest <path>', 'specify the output directory').default('./output', './output'))
.requiredOption('-i, --input <files_or_urls...>', 'the path/url of input markdown files')
.addOption(new commander.Option('-d, --dest <dir>', 'the path of output directory (filename is generated automatically)').default('', './output'))
.addOption(new commander.Option('-o, --output <files...>', 'the path of output file (ignored if the flag -d is set)').default('', '""'))
.addOption(new commander.Option('-l, --layout <html_file>', 'specify the layout file').default('', '""'))
.addOption(new commander.Option('-b, --hardBreak', 'use hard break instead of soft break'))
.addOption(new commander.Option('-k, --dark', 'use the dark mode layout (only activate it when the -l option is not set)'))
.parse(process.argv)

const options = commander.program.opts()

const dest: string = options.dest === '' ? './output' : options.dest
const layout: string | null = options.layout !== '' ? fs.readFileSync(options.layout, { encoding: 'utf-8' }) : null
const inputs: fs.PathLike[] = options.input
const dest: fs.PathLike = options.dest === '' ? './output' : options.dest
const outputs: fs.PathLike[] | null = options.dest === '' && options.output !== '' ? options.output : null
const layout: fs.PathLike | null = options.layout !== '' ? fs.readFileSync(options.layout, { encoding: 'utf-8' }) : null
const hardBreak: boolean = options.hardBreak
const darkMode: boolean = options.dark

function main() {
const converter = new Converter(layout, hardBreak, darkMode)
let errorCounter = 0
let outputIndex = 0
const outputFilenameSet = new Set<string>()

const isURL = (s: string): URL | null => {
try {
const url = new URL(s);
return url
} catch (err) {
return null
}
}

const printError = (fn: string | fs.PathLike, e: any) => {
console.error(`❌ #${errorCounter} ${fn}`)
console.error(`${e}`)
errorCounter++
}

const convertURL = (inputURL: URL, output: fs.PathLike, res: http.IncomingMessage) => {
let data = ""
res.on('data', (d) => {
data += d
})
res.on('end', () => {
const converted = converter.convert(data)
try {
fs.writeFileSync(output, converted)
console.log(`✅ ${inputURL} ➡️ ${output}`)
} catch (e) {
printError(inputURL, e)
return
}
})
}

const generateOutputFilename = (inputFilename: fs.PathLike): string => {
// if `output` is non-null, use `output` as output file name
let ret: string
if (outputs !== null) {
if (outputIndex < outputs.length) {
ret = outputs![outputIndex]!.toString()
outputIndex++
} else {
throw ('the number of --output is smaller than the number of --input');
}
} else {
ret = path.join(dest.toString(), path.basename(inputFilename.toString()).replace(/\.md$/, '') + '.html')
}
// if `o` repeat, use hash function to generate new filename
let hashIn: string = inputFilename.toString()
const retExtname = path.extname(ret) // including leading dot '.'
const tmpRet = ret.replace(new RegExp(retExtname + "$"), '')
while (outputFilenameSet.has(ret)) {
hashIn = hash.update(hashIn).digest('hex').toString().substring(0, 5)
ret = tmpRet + '.' + hashIn + retExtname
}
outputFilenameSet.add(ret)
return ret
}

// if `output` is null, generate the output file in the directory `dest`
// if `dest` existed, check `dest` is directory
// otherwise create the new directory `dest`
if (outputs === null) {
if (fs.existsSync(dest)) {
const stats = fs.statSync(dest)
if (!stats.isDirectory()) {
printError(dest, `${dest} is not directory`)
return
}
}

if (!fs.existsSync(dest)) {
fs.mkdirSync(dest)
}
}

const converter = new Converter(layout, hardBreak)
inputs.forEach((fn: fs.PathLike) => {
// 1. http/https mode
const url = isURL(fn.toString())
if (url != null) {
if (outputs !== null && outputIndex >= outputs?.length) {
return
}
if (url.protocol === 'https:') {
https.get(url, (res) => {
convertURL(url, generateOutputFilename(url), res)
}).on('error', (e) => {
printError(fn, e)
})
} else if (url.protocol === 'http:') {
http.get(url, (res) => {
convertURL(url, generateOutputFilename(url), res)
}).on('error', (e) => {
printError(fn, e)
})
} else {
printError(url, "protocol not supported")
}
} else {
// 2. File mode
glob(fn.toString()).then((fileList: string[]) => {
fileList.forEach((f) => {
try {
const stats = fs.statSync(f)
if (stats.isDirectory()) {
// printError(fn, "not support directory path as input since v1.1.0")
return
}
} catch (e) {
printError(fn, e)
return
}
const markdown = fs.readFileSync(f, { encoding: 'utf-8' })
const converted = converter.convert(markdown)
const o = generateOutputFilename(f)
try {
fs.writeFileSync(o, converted)
console.log(`✅ ${f} ➡️ ${o}`)
} catch (e) {
printError(f, e)
}
})
}).catch((e) => {
printError(fn, e)
})
}
})
}

converter.convertFiles(options.src, dest)
main()
57 changes: 5 additions & 52 deletions lib/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ const htmlEncode = require('htmlencode').htmlEncode;
export class Converter {
private md: MarkdownIt
private metadata: Metadata
private layout: string
private layout: fs.PathLike

/**
* @param layout set null if you want to use default layout,
* @param hardBreak set true if want to use hardBread
*/
constructor(layout: string | null, hardBreak = false) {
constructor(layout: fs.PathLike | null, hardBreak = false, darkMode = false) {
this.metadata = new Metadata()
if (layout === null) {
layout = this.defaultLayout()
layout = this.defaultLayout(darkMode)
}
this.layout = layout
// https://hackmd.io/c/codimd-documentation/%2F%40codimd%2Fmarkdown-syntax
Expand Down Expand Up @@ -132,54 +132,7 @@ export class Converter {
/**
* @returns default HTML layout
*/
public defaultLayout(): string {
return fs.readFileSync(path.join(__dirname, '../layout.html'), { encoding: 'utf-8' })
}

/**
* ```
*
* .
* ├── foo
* │ ├── a.md
* │ └── b.md
* ├── c.md
* └── out
* ├── foo
* │ ├── a.html
* │ └── b.html
* └── c.html
* ```
* @param filePathsOrDir a list of the path of files or directories e.g. ["./foo", "c.md"]
* @param destDir the path of destination directory e.g. ["./build"]
*/
public convertFiles(filePathsOrDir: fs.PathLike[], destDir: fs.PathLike) {
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir)
}

const files: fs.PathLike[] = []
filePathsOrDir.forEach((fn: fs.PathLike) => {
if (!fs.existsSync(fn)) {
console.error(`${fn} is not found`)
return
}
const stats = fs.statSync(fn)
if (stats.isDirectory()) {
const f = fs.readdirSync(fn)
f.forEach((e: fs.PathLike) => {
files.push(path.join(fn.toString(), e.toString()))
})
} else if (stats.isFile()) {
files.push(fn)
}
})

files.forEach((fn: fs.PathLike) => {
const markdown = fs.readFileSync(fn, { encoding: 'utf-8' })
const res = this.convert(markdown)
const basename = path.basename(fn.toString())
fs.writeFileSync(path.join(destDir.toString(), basename.replace(/\.md$/, '.html')), res)
});
public defaultLayout(dark = false): string {
return fs.readFileSync(path.join(__dirname, !dark ? '../layout.html' : '../layout.dark.html'), { encoding: 'utf-8' })
}
}
Loading

0 comments on commit 762c684

Please sign in to comment.