commit c75752b1b7fe94cc0a47699567dec06f175a66c4 Author: Endeavorance Date: Wed Apr 2 10:33:16 2025 -0400 Initial commit 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"] +}