Initial commit
This commit is contained in:
commit
c75752b1b7
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
@ -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
|
24
LICENSE
Normal file
24
LICENSE
Normal file
|
@ -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 <https://unlicense.org>
|
51
README.md
Normal file
51
README.md
Normal file
|
@ -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
|
||||
```
|
34
biome.json
Normal file
34
biome.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
9
build.ts
Normal file
9
build.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
await Bun.build({
|
||||
entrypoints: ["./src/index.ts"],
|
||||
outdir: "./dist",
|
||||
target: "bun",
|
||||
external: ["yaml"],
|
||||
minify: true,
|
||||
});
|
||||
|
||||
export { };
|
51
bun.lock
Normal file
51
bun.lock
Normal file
|
@ -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=="],
|
||||
}
|
||||
}
|
27
package.json
Normal file
27
package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
61
src/index.ts
Normal file
61
src/index.ts
Normal file
|
@ -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<string, unknown> {
|
||||
const markdown = content.trim().replace(FRONTMATTER_REGEX, "").trim();
|
||||
let frontmatter: Record<string, unknown> = {};
|
||||
|
||||
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<string, unknown>,
|
||||
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;
|
67
src/test/emdy.test.ts
Normal file
67
src/test/emdy.test.ts
Normal file
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
|
@ -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"]
|
||||
}
|
Loading…
Reference in a new issue