From c75752b1b7fe94cc0a47699567dec06f175a66c4 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Wed, 2 Apr 2025 10:33:16 -0400 Subject: [PATCH] Initial commit --- .gitignore | 34 ++++++++++++++++++++++ LICENSE | 24 ++++++++++++++++ README.md | 51 ++++++++++++++++++++++++++++++++ biome.json | 34 ++++++++++++++++++++++ build.ts | 9 ++++++ bun.lock | 51 ++++++++++++++++++++++++++++++++ package.json | 27 +++++++++++++++++ src/index.ts | 61 +++++++++++++++++++++++++++++++++++++++ src/test/emdy.test.ts | 67 +++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 29 +++++++++++++++++++ 10 files changed, 387 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 biome.json create mode 100644 build.ts create mode 100644 bun.lock create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 src/test/emdy.test.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4323c7 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# EMDY + +Parse and serialize Markdown files with optional frontmatter + +## Install + +Configure your environment to be aware of @endeavorance packages: + +```toml +# Bunfig.toml +[install.scopes] +endeavorance = "https://git.astral.camp/api/packages/endeavorance/npm/" +``` + +Then install the package: + +```sh +bun add @endeavorance/emdy +``` +## Usage + +EMDY exports a top level `EMDY` constant which has two functions: + +- `EMDY.parse(md: string, markdownKey = "content")` +- `EMDY.stringify(data: Object, markdownKey = "content")` + +```typescript +import EMDY from "@endeavorance/emdy"; + +const myMDFile = ` +--- +tags: ["fancy", "stuff"] +--- + +This is my markdown document! +`.trim(); + +const parsed = EMDY.parse(myMDFile); + +console.log(parsed); +/* +{ + content: "This is my markdown document!", + tags: ["fancy", "stuff"] +} +*/ + +const stringified = EMDY.stringify(parsed); +console.log(stringified); +// Prints the original document +``` diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..5c0f93f --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": ["dist", "*.d.ts"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noArrayIndexKey": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..ad9f7c5 --- /dev/null +++ b/build.ts @@ -0,0 +1,9 @@ +await Bun.build({ + entrypoints: ["./src/index.ts"], + outdir: "./dist", + target: "bun", + external: ["yaml"], + minify: true, +}); + +export { }; diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a0f70ec --- /dev/null +++ b/bun.lock @@ -0,0 +1,51 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "emdy", + "dependencies": { + "yaml": "^2.7.1", + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="], + + "@types/node": ["@types/node@22.13.17", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], + + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..33fcbfe --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "@endeavorance/emdy", + "version": "1.0.0", + "module": "dist/index.js", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "type": "module", + "files": ["dist"], + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "yaml": "^2.7.1" + }, + "scripts": { + "build": "bun run ./build.ts && tsc", + "lint": "bunx --bun biome check --fix" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d2b7df7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,61 @@ +import YAML from "yaml"; +const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm; + +/** + * Parse a markdown document with optional YAML frontmatter into an object + * with the markdown content and any frontmatter as keys. + * @param content The raw markdown content + * @param markdownKey The key to use for the markdown content + * @returns The parsed markdown content and frontmatter + */ +function parse( + content: string, + markdownKey = "content", +): Record { + const markdown = content.trim().replace(FRONTMATTER_REGEX, "").trim(); + let frontmatter: Record = {}; + + if (FRONTMATTER_REGEX.test(content)) { + const frontmatterString = content.match(FRONTMATTER_REGEX)?.[0] ?? ""; + const cleanFrontmatter = frontmatterString.replaceAll("---", "").trim(); + frontmatter = YAML.parse(cleanFrontmatter); + } + + return { + [markdownKey]: markdown, + ...frontmatter, + }; +} + +/** + * Serialize an object with markdown content and optional frontmatter into a + * Markdown document. Uses the `content` key for the markdown content by default. + * @param data The object to serialize + * @param markdownKey The key to use for the markdown content + * @returns The parsed markdown content and frontmatter + */ +function stringify( + data: Record, + markdownKey = "content", +): string { + const markdown = String(data[markdownKey] ?? ""); + const frontmatter = { ...data }; + delete frontmatter[markdownKey]; + + const remainingKeys = Object.keys(frontmatter).length; + + if (remainingKeys === 0) { + return markdown; + } + + const stringifiedFrontmatter = YAML.stringify(frontmatter).trim(); + + return `---\n${stringifiedFrontmatter}\n---\n${markdown}`; +} + +const EMDY = { + parse, + stringify, +} as const; + +export default EMDY; diff --git a/src/test/emdy.test.ts b/src/test/emdy.test.ts new file mode 100644 index 0000000..4291bf8 --- /dev/null +++ b/src/test/emdy.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test } from "bun:test"; +import EMDY from "../index"; + +describe(EMDY.parse, () => { + test("parses markdown content without frontmatter", () => { + const content = "# this is some markdown"; + const parsed = EMDY.parse(content); + expect(parsed).toEqual({ content }); + expect(Object.keys(parsed)).toEqual(["content"]); + }); + + test("parses markdown content with custom key", () => { + const content = "# this is some markdown"; + const parsed = EMDY.parse(content, "mark"); + expect(parsed).toEqual({ mark: content }); + expect(Object.keys(parsed)).toEqual(["mark"]); + }); + + test("parses markdown content with frontmatter", () => { + const content = `--- +some: value +also: true +--- +And here is the content + `.trim(); + + const parsed = EMDY.parse(content); + expect(parsed).toEqual({ + some: "value", + also: true, + content: "And here is the content", + }); + }); +}); + +describe(EMDY.stringify, () => { + test("stringifies markdown content without frontmatter", () => { + const obj = { + content: "This is some markdown", + }; + + const stringified = EMDY.stringify(obj); + expect(stringified).toEqual("This is some markdown"); + }); + + test("uses custom key for markdown content", () => { + const obj = { + mark: "This is some markdown", + }; + + const stringified = EMDY.stringify(obj, "mark"); + expect(stringified).toEqual("This is some markdown"); + }); + + test("stringifies markdown content with frontmatter", () => { + const obj = { + content: "This is some markdown", + some: "value", + also: true, + }; + + const stringified = EMDY.stringify(obj); + expect(stringified).toEqual( + "---\nsome: value\nalso: true\n---\nThis is some markdown", + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..95b2999 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "declaration": true, + "emitDeclarationOnly": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": true, + // Output + "outDir": "./dist" + }, + "include": ["./src/index.ts"] +}