Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions .github/ISSUE_TEMPLATE/documentation.yml
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -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
139 changes: 93 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,88 @@
# 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<Schema>(fileContents);
} catch(error) {
// ...
const parsed = mattr<Schema>(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
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<string, unknown>`)_ |
| `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`

Expand All @@ -69,81 +93,104 @@ 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
2. If `excerptLength` is defined, limits the number of charactors to the length of the excerpt
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 <T extends MattrAllowedTypes>(globPath: string, options: MattrOptions<T>): MattrFile<T>[] => {
const posts: MattrFile<T>[] = [];
for await (const post of glob(globPath)) {
try {
const parsed = mattr<T>(post, options);
posts.push(parsed);
} catch {
// Handle errors thrown
}
import type {
MattrOptions,
MattrFile,
MattrAllowedTypes,
} from "@stinobe/mattr";

const myGlobFunction = async <T extends MattrAllowedTypes>(
globPath: string,
options: MattrOptions<T>,
): MattrFile<T>[] => {
const posts: MattrFile<T>[] = [];
for await (const post of glob(globPath)) {
try {
const parsed = mattr<T>(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`
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"markdown",
"mdx",
"md",
"parser",
"typescript",
"zod",
"parser"
],
"publishConfig": {
Expand All @@ -49,6 +52,9 @@
"license": "MIT",
"packageManager": "pnpm",
"type": "module",
"engines": {
"node": ">=18"
},
"dependencies": {
"yaml": "^2.8.4"
},
Expand Down
2 changes: 1 addition & 1 deletion tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ export default defineConfig({
treeshake: true,
minify: true,
target: "es2022",
external: ["zod"],
external: ["yaml", "zod"],
});