diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..77d0ebb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,45 @@ +name: Bug report +description: Report a reproducible issue or unexpected behavior +title: "[Bug]: " +labels: + - bug + +body: + - type: textarea + id: description + attributes: + label: Description + description: What happened? + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Provide a minimal reproduction if possible + placeholder: | + ```typescript + import { mattr } from "@stinobe/mattr" + ``` + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + + - type: input + id: version + attributes: + label: Package version + placeholder: 0.1.0 + + - type: input + id: runtime + attributes: + label: Runtime + placeholder: Node.js 22 / Bun 1.x diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..3617a35 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,18 @@ +name: Documentation improvement +description: Suggest improvements or additions to the documentation +title: "[Docs]: " +labels: + - documentation + +body: + - type: textarea + id: issue + attributes: + label: What is unclear or missing? + validations: + required: true + + - type: textarea + id: suggestion + attributes: + label: Suggested improvement diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..3228948 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,26 @@ +name: Feature request +description: Suggest an idea or improvement +title: "[Feature]: " +labels: + - enhancement + +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What problem does this solve? + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed solution + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered diff --git a/README.md b/README.md index b5b78b4..ddc3e82 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,48 @@ # Mattr -A tiny, type-safe Markdown frontmatter parser for modern TypeScript projects. Built for workflows where good typing, predictable APIs, and small utilities go a long way. This package __focuses on the essentials:__ clean ergonomics, minimal overhead, and a developer experience that integrates naturally into content-focused applications and tooling. +A tiny, type-safe Markdown frontmatter parser for modern TypeScript projects. Built for workflows where good typing, predictable APIs, and small utilities go a long way. This package **focuses on the essentials:** clean ergonomics, minimal overhead, and a developer experience that integrates naturally into content-focused applications and tooling. + +![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/stinobe/mattr/validation.yml?label=tests) +![NPM Version](https://img.shields.io/npm/v/%40stinobe%2Fmattr) ## Usage + ### Without type safety ```typescript import { mattr } from "@stinobe/mattr"; try { - const parsed = mattr(fileContents); -} catch(error) { - // ... + const parsed = mattr(fileContents); +} catch (error) { + // ... } ``` -___ + +--- + ### With TypeScript ```typescript import { mattr } from "@stinobe/mattr"; type Schema = { - title: string; - description?: string; -} + title: string; + description?: string; +}; try { - const parsed = mattr(fileContents); -} catch(error) { - // ... + const parsed = mattr(fileContents); +} catch (error) { + // ... } ``` + > [!IMPORTANT] -> This only provides type safety during compiling, __not__ during runtime -___ +> This only provides type safety during compiling, **not** during runtime + +--- + ### With Zod ```typescript @@ -41,24 +50,39 @@ import { mattr } from "@stinobe/mattr"; import { z } from "zod"; const Schema = z.object({ - title: z.string(), - description: z.optional(z.string()) -}) + title: z.string(), + description: z.optional(z.string()), +}); try { - // You can specify the type as well - // but that's not really necessary - // since it will be inferred from the schema option - const parsed = mattr(fileContents, { schema: Schema }); -} catch(error) { - // ... + // You can specify the type as well + // but that's not really necessary + // since it will be inferred from the schema option + const parsed = mattr(fileContents, { schema: Schema }); +} catch (error) { + // ... } -``` +``` + Visit the website for more information about [Zod](https://zod.dev/) +> [!NOTE] +> Zod is not required to use this library + +### What is returned + +No _mattr_ which option you're choosing, the output of the `mattr` function is always the same. +| Property | Description | +|---|---| +| `data` | The frontmatter as JSON ouput with given type _(default: `Record`)_ | +| `content` | The markdown content itself without the frontmatter | +| `excerpt` | The excerpt according to the settings as `string`, or `null` if no excerpt was found | +| `raw` | The file contents as they were passed to the mattr function | + ## Options ### `excerpt` _(optional)_ + Type: `boolean | ExcerptFunction` Default: `true` @@ -69,6 +93,7 @@ Enables and configures excerpt generation. - `ExcerptFunction` - custom extraction logic [More info](#create-a-custom-excerpt-function) #### Default strategy + When enabled (`true`), the excerpt is resovled using following rules 1. If `excerptSeparator` is defined, takes content until seperator, otherwise passes entire content to next step @@ -76,74 +101,96 @@ When enabled (`true`), the excerpt is resovled using following rules 3. If none of the above is defined, take first paragraph ### `excerptSeparator` _(optional)_ + Type: `string` Default: `undefined` When defined with a non-empty string, takes content from input file after frontmatter untill separator ### `excerptLength` _(optional)_ + Type: `number` Default: `undefined` Maximum character length of the excerpt. When less than or equal to `0` this will throw an `InvalidExcerptError`. ### `schema` _(optional)_ + Type: `ZodType` Default: `undefined` Takes a Zod-schema to validate frontmatter output against ## Customization + ### Create a custom excerpt function -I've added and exported a type in case you want to create a custom excerpt function. This contains some data passed on from options by the `mattr` function. -| `ExcerptFn` param | Description | -|---|---| -|`ctx.raw` | file contents | -|`ctx.content` | file content without frontmatter | -|`options.length` | `excerptLength` passed to `mattr` | -|`options.separator` | `excerptSeparator` passed to `mattr` | +I've added and exported a type in case you want to create a custom excerpt function. This contains some data passed on from options by the `mattr` function. + +| `ExcerptFn` param | Description | +| ------------------- | ------------------------------------ | +| `ctx.raw` | file contents | +| `ctx.content` | file content without frontmatter | +| `options.length` | `excerptLength` passed to `mattr` | +| `options.separator` | `excerptSeparator` passed to `mattr` | ```typescript import type { MattrExcerptFn } from "@stinobe/mattr"; const myCustomExcerptFunction: MattrExcerptFn = (ctx, excerptOptions) => { - // Do some magic - return "a string"; -} + // Do some magic + return "a string"; +}; ``` + ### Use in a wrapper function + You can also create a wrapper function where one of your parameters is an options object. For example if you would have a function traversing over files ```typescript -import type { MattrOptions, MattrFile, MattrAllowedTypes } from "@stinobe/mattr"; - -const myGlobFunction = async (globPath: string, options: MattrOptions): MattrFile[] => { - const posts: MattrFile[] = []; - for await (const post of glob(globPath)) { - try { - const parsed = mattr(post, options); - posts.push(parsed); - } catch { - // Handle errors thrown - } +import type { + MattrOptions, + MattrFile, + MattrAllowedTypes, +} from "@stinobe/mattr"; + +const myGlobFunction = async ( + globPath: string, + options: MattrOptions, +): MattrFile[] => { + const posts: MattrFile[] = []; + for await (const post of glob(globPath)) { + try { + const parsed = mattr(post, options); + posts.push(parsed); + } catch { + // Handle errors thrown } - return posts; -} + } + return posts; +}; ``` + ## Erorr handling + In some situations an erorr will be thrown so don't forget to do the handling correct ### MatterParseError + Thrown when: + - A Yaml block in a markdownfile is not closed - The `parse` function from the `yaml` package has thrown an error. _The error thrown by the `parse` function is passed as cause._ ### MattrSchemaError + Thrown when: + - unsuccesful parse of the Zod schema, issues form `safeParse`are passed to the error. ### MattrExcerptError + Thrown when: + - `excerptLength` is smaller than or equal to `0` diff --git a/package.json b/package.json index e8edf0d..7e4ad67 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "markdown", "mdx", "md", + "parser", + "typescript", + "zod", "parser" ], "publishConfig": { @@ -49,6 +52,9 @@ "license": "MIT", "packageManager": "pnpm", "type": "module", + "engines": { + "node": ">=18" + }, "dependencies": { "yaml": "^2.8.4" }, diff --git a/tsup.config.ts b/tsup.config.ts index 25f4642..0c98487 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -14,5 +14,5 @@ export default defineConfig({ treeshake: true, minify: true, target: "es2022", - external: ["zod"], + external: ["yaml", "zod"], });