Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for working with markdown front matter #642

Merged
merged 8 commits into from
Aug 23, 2024
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
31 changes: 31 additions & 0 deletions docs/genaisrc/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions docs/src/content/docs/reference/scripts/md.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: Markdown
sidebar:
order: 9.2
keywords: markdown, mdx, frontmatter
---

The `MD` class contains several helpers to work with [Markdown](https://www.markdownguide.org/cheat-sheet/) and [frontmatter text](https://jekyllrb.com/docs/front-matter/).

The parser also support markdown variants like [MDX](https://mdxjs.com/).

Check warning on line 10 in docs/src/content/docs/reference/scripts/md.md

View workflow job for this annotation

GitHub Actions / build

The parser also "support" should be "supports".

## `frontmatter`

Extracts and parses the frontmatter text from a markdown file. Returns `undefined` if no frontmatter is not found or the parsing failed. Default format is `yaml`.

Check failure on line 14 in docs/src/content/docs/reference/scripts/md.md

View workflow job for this annotation

GitHub Actions / build

There seems to be a typo here. "no frontmatter is not found" should be "no frontmatter is found".

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be a typo here. "no frontmatter is not found" should be "no frontmatter is found".

generated by pr-docs-review-commit typo


```javascript
const frontmatter = MD.frontmatter(text, "yaml")
```

## `content`

Extracts the markdown source without the frontmatter.

```javascript
const content = MD.content(text)
```

## `updateFrontmatter`

Merges frontmatter values into the existing in a markdown file. Use `null` value to delete fields.

```javascript
const updated = MD.updateFrontmatter(text, { title: "New Title" })
```
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/scripts/parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
...
```

You can use the `parsers.frontmatter` to parse out the metadata into an object
You can use the `parsers.frontmatter` or [MD](./md.md) to parse out the metadata into an object

Check failure on line 132 in docs/src/content/docs/reference/scripts/parsers.md

View workflow job for this annotation

GitHub Actions / build

The link to MD in "[MD](./md.md)" is incorrect. It should be "[MD](md.md)" without the "./" since it's in the same directory.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The relative link to MD documentation should be verified to ensure it correctly points to the md.md file within the same directory.

generated by pr-docs-review-commit relative_link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link to MD in "MD" is incorrect. It should be "MD" without the "./" since it's in the same directory.

generated by pr-docs-review-commit incorrect_link


```js
const meta = parsers.frontmatter(file)
Expand Down
9 changes: 8 additions & 1 deletion genaisrc/blog-generator.genai.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,15 @@ defOutputProcessor((output) => {
if (/\`\`\`markdown\n/.test(md)) {
md = md.replace(/\`\`\`markdown\n/g, "").replace(/\`\`\`\n?$/g, "")
}
const fm = parsers.frontmatter(md)
const fm = MD.frontmatter(md)
if (!fm) throw new Error("No frontmatter found")

md = MD.updateFrontmatter(md, {
draft: true,
date: formattedDate,
authors: "genaiscript",
})

const fn =
`docs/src/content/docs/blog/drafts/${fm.title.replace(/[^a-z0-9]+/gi, "-")}.md`.toLocaleLowerCase()
const sn =
Expand Down
31 changes: 31 additions & 0 deletions genaisrc/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/core/src/annotations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("annotations", () => {
`

const diags = parseAnnotations(output)
console.log(diags)
// console.log(diags)
assert.strictEqual(diags.length, 3)
assert.strictEqual(diags[0].severity, "error")
assert.strictEqual(diags[0].filename, "packages/core/src/github.ts")
Expand Down
63 changes: 62 additions & 1 deletion packages/core/src/frontmatter.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, test } from "node:test"
import assert from "node:assert/strict"
import { frontmatterTryParse } from "./frontmatter"
import {
frontmatterTryParse,
splitMarkdown,
updateFrontmatter,
} from "./frontmatter"
import { YAMLTryParse } from "./yaml"

describe("replace frontmatter", () => {
test("only", () => {
Expand All @@ -21,3 +26,59 @@ foo bar
assert.deepEqual(res, { foo: "bar" })
})
})

describe("splitMarkdown", () => {
test("split markdown with yaml frontmatter", () => {
const markdown = `---
title: Test
---
This is a test.`
const { frontmatter, content } = splitMarkdown(markdown)
assert.deepEqual(YAMLTryParse(frontmatter), { title: "Test" })
assert.equal(content, "This is a test.")
})

test("split markdown with json frontmatter", () => {
const markdown = `---
{
"title": "Test"
}
---
This is a test.`
const { frontmatter, content } = splitMarkdown(markdown)
assert.deepEqual(JSON.parse(frontmatter), { title: "Test" })
assert.equal(content, "This is a test.")
})
})

describe("updateFrontmatter", () => {
test("update yaml frontmatter", () => {
const markdown = `---
title: Old Title
foo: bar
---
This is a test.`
const newFrontmatter: any = { title: "New Title", foo: null }
const updatedMarkdown = updateFrontmatter(markdown, newFrontmatter)
const { frontmatter, content } = splitMarkdown(updatedMarkdown)
assert.deepEqual(YAMLTryParse(frontmatter), { title: "New Title" })
assert.equal(content, "This is a test.")
})

test("update json frontmatter", () => {
const markdown = `---
{
"title": "Old Title",
"foo": "bar"
}
---
This is a test.`
const newFrontmatter: any = { title: "New Title", foo: null }
const updatedMarkdown = updateFrontmatter(markdown, newFrontmatter, {
format: "json",
})
const { frontmatter, content } = splitMarkdown(updatedMarkdown)
assert.deepEqual(JSON.parse(frontmatter), { title: "New Title" })
assert.equal(content, "This is a test.")
})
})
78 changes: 63 additions & 15 deletions packages/core/src/frontmatter.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,84 @@
import { objectFromMap } from "pdfjs-dist/types/src/shared/util"
import { JSON5TryParse } from "./json5"
import { TOMLTryParse } from "./toml"
import { YAMLTryParse } from "./yaml"
import { YAMLTryParse, YAMLStringify } from "./yaml"

export function frontmatterTryParse(
text: string,
options?: { format: "yaml" | "json" | "toml" }
): { end: number; value: any } {
if (!text) return undefined

options?: { format: "yaml" | "json" | "toml" | "text" }
): { text: string; value: any; endLine?: number } | undefined {
const { format = "yaml" } = options || {}
const { frontmatter, endLine } = splitMarkdown(text)
if (!frontmatter) return undefined

let res: any
switch (format) {
case "text":
res = frontmatter
break
case "json":
res = JSON5TryParse(frontmatter)
break
case "toml":
res = TOMLTryParse(frontmatter)
break
default:
res = YAMLTryParse(frontmatter)
break
}
return { text: frontmatter, value: res, endLine }
}

export function splitMarkdown(text: string): {
frontmatter?: string
endLine?: number
content: string
} {
if (!text) return { content: text }
const lines = text.split(/\r?\n/g)
const delimiter = "---"
if (lines[0] !== delimiter) return undefined
if (lines[0] !== delimiter) return { content: text }
let end = 1
while (end < lines.length) {
if (lines[end] === delimiter) break
end++
}
if (end >= lines.length) return undefined
const fm = lines.slice(1, end).join("\n")
let res: any
if (end >= lines.length) return { content: text }
const frontmatter = lines.slice(1, end).join("\n")
const content = lines.slice(end + 1).join("\n")
return { frontmatter, content, endLine: end }
}
pelikhan marked this conversation as resolved.
Show resolved Hide resolved

export function updateFrontmatter(
text: string,
newFrontmatter: any,
options?: { format: "yaml" | "json" }
): string {
const { content = "" } = splitMarkdown(text)
if (newFrontmatter === null) return content

const frontmatter = frontmatterTryParse(text, options)?.value ?? {}

// merge object
for (const [key, value] of Object.entries(newFrontmatter ?? {})) {
if (value === null) {
delete frontmatter[key]
} else {
frontmatter[key] = value
}

Check failure on line 68 in packages/core/src/frontmatter.ts

View workflow job for this annotation

GitHub Actions / build

The code deletes a key from the `frontmatter` object if the value is null. This could lead to unexpected behavior if other parts of the code rely on this key existing. Consider setting the key to `null` or `undefined` instead of deleting it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code deletes a key from the frontmatter object if the value is null. This could lead to unexpected behavior if other parts of the code rely on this key existing. Consider setting the key to null or undefined instead of deleting it.

generated by pr-review-commit delete_key

}

const { format = "yaml" } = options || {}
let fm: string
switch (format) {
case "json":
res = JSON5TryParse(fm)
fm = JSON.stringify(frontmatter, null, 2)
break
case "toml":
res = TOMLTryParse(fm)
case "yaml":
fm = YAMLStringify(frontmatter)
break
default:
res = YAMLTryParse(fm)
break
throw new Error(`Unsupported format: ${format}`)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default case in the switch statement throws an error for unsupported formats. However, the function does not handle this error, which could lead to unhandled exceptions. Consider providing a default format or handling this error in a way that does not interrupt the execution of the program. 😊

generated by pr-review-commit unsupported_format

}

Check failure on line 82 in packages/core/src/frontmatter.ts

View workflow job for this annotation

GitHub Actions / build

The default case in the switch statement throws an error for unsupported formats. However, the function does not handle this error, which could lead to unhandled exceptions. Consider adding error handling or a default format.
return res !== undefined ? { end: end + 1, value: res } : undefined
return `---\n${fm}\n---\n${content}`
pelikhan marked this conversation as resolved.
Show resolved Hide resolved
}
31 changes: 31 additions & 0 deletions packages/core/src/genaisrc/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading