From 612173000d3d3d6c800d093074da038b7440ae53 Mon Sep 17 00:00:00 2001 From: shotanue Date: Sat, 16 Dec 2023 17:25:40 +0900 Subject: [PATCH] prototype --- .github/workflows/code-quality.yml | 19 +++ .github/workflows/release.yml | 48 ++++++++ .gitignore | 183 +++++++++++++++++++++++++++++ README.md | 56 +++++++++ biome.json | 20 ++++ bunfig.toml | 4 + package.json | 35 ++++++ renovate.json | 6 + resource/help.txt | 9 ++ src/index.ts | 91 ++++++++++++++ src/makeParser.ts | 48 ++++++++ tsconfig.json | 22 ++++ 12 files changed, 541 insertions(+) create mode 100644 .github/workflows/code-quality.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 biome.json create mode 100644 bunfig.toml create mode 100644 package.json create mode 100644 renovate.json create mode 100644 resource/help.txt create mode 100644 src/index.ts create mode 100644 src/makeParser.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..cc9063c --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,19 @@ +name: Code quality + +on: + push: + pull_request: + +jobs: + quality: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.0.18 + - run: bun install + - run: bun run ci + - run: bun run test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ff337b4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + build: + name: Release binary + strategy: + matrix: + include: + - os: ubuntu-22.04 + asset_name: md2hype-linux-amd64 + - os: macos-12 + asset_name: md2hype-darwin-amd64 + - os: macos-13-xlarge + asset_name: md2hype-darwin-arm64 + + runs-on: ${{ matrix.os }} + + steps: + - name: Dump runner information + run: | + echo "Runner Name: ${{ runner.name }}" + echo "Operating System: ${{ runner.os }}" + echo "CPU Architecture: ${{ runner.arch}}" + - name: Checkout + uses: actions/checkout@v4 + - name: Setup bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.0.18 + + - run: bun install + - run: bun ci + - run: bun test + - run: bun run build + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@2.7.0 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: md2hype + asset_name: ${{ matrix.asset_name }} + tag: ${{ github.ref }} + overwrite: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b12614 --- /dev/null +++ b/.gitignore @@ -0,0 +1,183 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + + +# ------ + +test.html +test.md + +md2hype diff --git a/README.md b/README.md index 2479095..0689a36 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,61 @@ # md2hype +A text converter, markdown to html, powered by [unified.js](https://unifiedjs.com) +## Built-in unifiedjs plugins + +for markdown parsing +- remark-breaks +- remark-frontmatter +- remark-gfm +- remark-parse + +## Installation + +WIP + +Install the excutable, built by [bun.sh](https://bun.sh) + +```bash +brew install shotanue/tap/md2hype +``` + +## How to use + + +```bash +md2hype --file foo.md +``` + +- foo.md + +```md +--- +tag: + - foo +--- +hello world +``` + +- output + +```json +{ + "html": "

hello world

", + "frontmatter": { + "tag": ["foo"] + } +} +``` + +### If you want output only html + +```bash +md2hype --file foo.md --html +``` + +```html +

hello world

+``` diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..6c2a924 --- /dev/null +++ b/biome.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.4.1/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 120, + "ignore": [] + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..b112737 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,4 @@ +[install.lockfile] +# This project pins versions, due to using renovete. +save = false + diff --git a/package.json b/package.json new file mode 100644 index 0000000..eb854a1 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "md2hype", + "module": "index.ts", + "type": "module", + "dependencies": { + "arg": "5.0.2", + "chalk-template": "1.1.0", + "rehype-stringify": "10.0.0", + "remark-breaks": "4.0.0", + "remark-frontmatter": "5.0.0", + "remark-gfm": "4.0.0", + "remark-parse": "11.0.0", + "remark-rehype": "11.0.0", + "smol-toml": "1.1.3", + "unified": "11.0.4", + "unist-util-flat-filter": "2.0.0", + "yaml": "2.3.4", + "zod": "3.22.4" + }, + "devDependencies": { + "@biomejs/biome": "1.4.1", + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "scripts": { + "run": "bun run ./src/index.ts", + "build": "bun build ./src/index.ts --compile --outfile md2hype", + "test": "bun test", + "check": "biome check .", + "check:fix": "biome check --apply-unsafe .", + "ci": "biome ci ." + } +} \ No newline at end of file diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..c900426 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:base"], + "labels": ["renovate"], + "automerge": true +} diff --git a/resource/help.txt b/resource/help.txt new file mode 100644 index 0000000..e548252 --- /dev/null +++ b/resource/help.txt @@ -0,0 +1,9 @@ +{bold # md2hype} + +{bold ## USAGE} + + {dim $} {bold md2hype} [--help, -h] --file {underline path} + +{bold ## OPTIONS} + --help, -h Shows this help message + --file {underline path} Markdown file path diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7b1d0d1 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,91 @@ +import arg from "arg"; +import { template } from "chalk-template"; +import helpText from "../resource/help.txt"; +import { makeParser } from "./makeParser"; + +const getStdin = async (isTTY: boolean, stream: () => ReadableStream): Promise => { + if (!isTTY) { + let text = ""; + for await (const chunk of stream()) { + const chunkText = Buffer.from(chunk).toString(); + text = `${text}${chunkText}`; + } + return text; + } + + return ""; +}; + +const parseArguments = async ( + argv: string[], + stdin: string, +): Promise< + { kind: "help" } | { kind: "stdin"; text: string; html: boolean } | { kind: "file"; path: string; html: boolean } +> => { + const args = arg( + { + "--help": Boolean, + "-h": "--help", + "--file": String, + "--html": Boolean, + }, + { argv }, + ); + + if (args["--help"]) { + return { + kind: "help", + }; + } + + if (stdin !== "") { + return { + kind: "stdin", + text: stdin, + html: !!args["--html"], + }; + } + + if (args["--file"]) { + return { + kind: "file", + path: args["--file"], + html: !!args["--html"], + }; + } + + return { + kind: "help", + }; +}; + +const result = await parseArguments( + process.argv.slice(2), + await getStdin(process.stdin.isTTY, () => Bun.stdin.stream()), +); + +if (result.kind === "help") { + await Bun.write(Bun.stdout, template(helpText)); + process.exit(2); +} + +const mdText = result.kind === "stdin" ? result.text : await Bun.file(result.path).text(); + +const main = async (args: { mdText: string; html: boolean }): Promise => { + const { rehype, pickFrontmatter } = makeParser(); + + const html = rehype(mdText); + + if (args.html) { + return html; + } + + const output = { + html, + frontmatter: pickFrontmatter(args.mdText), + }; + + return JSON.stringify(output); +}; + +await Bun.write(Bun.stdout, await main({ mdText, html: result.html })); diff --git a/src/makeParser.ts b/src/makeParser.ts new file mode 100644 index 0000000..c6933f6 --- /dev/null +++ b/src/makeParser.ts @@ -0,0 +1,48 @@ +import { Root } from "mdast"; +import rehypeStringify from "rehype-stringify"; +import remarkBreaks from "remark-breaks"; +import remarkFrontmatter from "remark-frontmatter"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import * as TOML from "smol-toml"; +import { unified } from "unified"; +import flatFilter from "unist-util-flat-filter"; +import YAML from "yaml"; + +export const makeParser = () => { + const mdParser = unified().use(remarkParse).use(remarkBreaks).use(remarkFrontmatter, ["yaml", "toml"]).use(remarkGfm); + + return { + rehype: (text: string): string => String(mdParser.use(remarkRehype).use(rehypeStringify).processSync(text)), + pickFrontmatter: (text: string): Record => { + const mdast = mdParser.parse(text); + + const flattenTree = flatFilter(mdast, (node) => ["code", "heading", "yaml", "toml"].includes(node.type)); + + const pickPreferences = (tree: Root) => { + const parser = { + yaml: (value: string): Record => { + return YAML.parse(value); + }, + toml: (value: string): Record => { + return TOML.parse(value); + }, + }; + + const node = tree.children[0]; + const nodeType = node.type as "yaml" | "node" | string; + + if (nodeType === "yaml" || nodeType === "toml") { + return { ...parser[nodeType]("value" in node ? node.value : "") }; + } + + return {}; + }; + + const frontmatter = flattenTree === null ? {} : pickPreferences(flattenTree); + + return frontmatter; + }, + }; +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7556e1d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" // add Bun global + ] + } +}