Skip to content

Commit 72feb8e

Browse files
committed
feat: add frontmatter and preview helpers to Markdown class
1 parent 167025f commit 72feb8e

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

src/markdown.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
*/
99

1010
import { VFile } from 'vfile'
11+
import { type Root } from 'hast'
1112
import { type Edge } from 'edge.js'
1213
import { readFile } from 'node:fs/promises'
14+
import { parseFrontMatter } from 'remark-mdc'
1315

1416
import { type Cache } from './cache.ts'
1517
import { MarkdownParser } from './parser.ts'
@@ -216,4 +218,65 @@ export class Markdown {
216218

217219
return result
218220
}
221+
222+
/**
223+
* Extract YAML frontmatter from a markdown file or content string
224+
* without running the full parsing pipeline.
225+
*
226+
* @param options - Options specifying either file path or content string
227+
* @returns Promise resolving to the parsed frontmatter object
228+
*
229+
* @example
230+
* ```typescript
231+
* const data = await markdown.frontmatter({ file: './content.md' })
232+
* console.log(data.title) // frontmatter field
233+
* ```
234+
*/
235+
async frontmatter(options: { file: string } | { content: string }) {
236+
let contents: string
237+
if ('file' in options) {
238+
contents = await readFile(options.file, 'utf-8')
239+
} else {
240+
contents = options.content
241+
}
242+
const { data } = parseFrontMatter(contents)
243+
return data
244+
}
245+
246+
/**
247+
* Parse and render only the content before the first h2 heading.
248+
* Returns the same shape as `render()` but with truncated content
249+
* and no table of contents.
250+
*
251+
* @param options - Options specifying either file path or content string to render
252+
* @returns Promise resolving to rendered preview HTML with frontmatter and messages
253+
*
254+
* @example
255+
* ```typescript
256+
* const result = await markdown.preview({ file: './content.md' })
257+
* console.log(result.content) // HTML before first h2
258+
* ```
259+
*/
260+
async preview(options: MarkdownOptions) {
261+
const { vFile, frontmatter } = await this.parse({ ...options, toc: false })
262+
const root = vFile.result as Root
263+
264+
const firstH2Index = root.children.findIndex(
265+
(node) => node.type === 'element' && node.tagName === 'h2'
266+
)
267+
if (firstH2Index !== -1) {
268+
root.children = root.children.slice(0, firstH2Index)
269+
}
270+
271+
const content = await this.#edgeRenderer.render('markdown_root', {
272+
node: root,
273+
$renderingContext: createRenderingContext(
274+
this.#mergeRendererOptions(options),
275+
vFile,
276+
frontmatter
277+
),
278+
})
279+
280+
return { content, frontmatter, messages: vFile.messages }
281+
}
219282
}

tests/markdown.spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,85 @@ Here is a paragraph with a [link](./foo)\`
289289

290290
assert.snapshot(result.content).match()
291291
})
292+
293+
test('extract frontmatter from a file', async ({ assert }) => {
294+
const edge = new Edge()
295+
edge.mount(join(import.meta.dirname, 'fixtures/views'))
296+
edge.use(edgeMarkdown, {})
297+
298+
const renderer = edge.share({})
299+
const data = await renderer
300+
.getState()
301+
.$markdown.frontmatter({ file: join(import.meta.dirname, 'fixtures/front_matter.mdc') })
302+
303+
assert.deepEqual(data, {
304+
title: 'Hello world',
305+
items: ['AdonisJS', 'Lucid', 'VineJS'],
306+
})
307+
})
308+
309+
test('extract frontmatter from raw content', async ({ assert }) => {
310+
const edge = new Edge()
311+
edge.mount(join(import.meta.dirname, 'fixtures/views'))
312+
edge.use(edgeMarkdown, {})
313+
314+
const renderer = edge.share({})
315+
const data = await renderer.getState().$markdown.frontmatter({
316+
content: dedent`
317+
---
318+
title: Test doc
319+
draft: true
320+
---
321+
322+
# Hello
323+
`,
324+
})
325+
326+
assert.deepEqual(data, { title: 'Test doc', draft: true })
327+
})
328+
329+
test('render preview with content before first h2', async ({ assert }) => {
330+
const edge = new Edge()
331+
edge.mount(join(import.meta.dirname, 'fixtures/views'))
332+
edge.use(edgeMarkdown, {})
333+
334+
const renderer = edge.share({})
335+
const result = await renderer.getState().$markdown.preview({
336+
content: dedent`
337+
# Main title
338+
339+
Intro paragraph
340+
341+
## First section
342+
343+
Section content
344+
`,
345+
})
346+
347+
assert.include(result.content, '<h1')
348+
assert.include(result.content, 'Intro paragraph')
349+
assert.notInclude(result.content, 'First section')
350+
assert.notInclude(result.content, 'Section content')
351+
})
352+
353+
test('preview returns full content when there is no h2', async ({ assert }) => {
354+
const edge = new Edge()
355+
edge.mount(join(import.meta.dirname, 'fixtures/views'))
356+
edge.use(edgeMarkdown, {})
357+
358+
const renderer = edge.share({})
359+
const result = await renderer.getState().$markdown.preview({
360+
content: dedent`
361+
# Main title
362+
363+
Some content here
364+
365+
More content
366+
`,
367+
})
368+
369+
assert.include(result.content, 'Main title')
370+
assert.include(result.content, 'Some content here')
371+
assert.include(result.content, 'More content')
372+
})
292373
})

0 commit comments

Comments
 (0)