diff options
Diffstat (limited to 'packages/markdown-support/src')
-rw-r--r-- | packages/markdown-support/src/codeblock.ts | 43 | ||||
-rw-r--r-- | packages/markdown-support/src/index.ts | 69 | ||||
-rw-r--r-- | packages/markdown-support/src/rehype-collect-headers.ts | 30 | ||||
-rw-r--r-- | packages/markdown-support/src/remark-scoped-styles.ts | 18 | ||||
-rw-r--r-- | packages/markdown-support/src/types.ts | 6 |
5 files changed, 166 insertions, 0 deletions
diff --git a/packages/markdown-support/src/codeblock.ts b/packages/markdown-support/src/codeblock.ts new file mode 100644 index 000000000..2f48c6631 --- /dev/null +++ b/packages/markdown-support/src/codeblock.ts @@ -0,0 +1,43 @@ +import { visit } from 'unist-util-visit'; + +/** */ +export function remarkCodeBlock() { + const visitor = (node: any) => { + const { data, meta } = node; + let lang = node.lang || 'html'; // default to html matches GFM behavior. + + let currentClassName = data?.hProperties?.class ?? ''; + node.data = node.data || {}; + node.data.hProperties = node.data.hProperties || {}; + node.data.hProperties = { ...node.data.hProperties, class: `language-${lang} ${currentClassName}`.trim(), lang, meta }; + + return node; + }; + return () => (tree: any) => visit(tree, 'code', visitor); +} + +/** */ +export function rehypeCodeBlock() { + const escapeCode = (code: any) => { + code.children = code.children.map((child: any) => { + if (child.type === 'text') { + return { ...child, value: child.value.replace(/\{/g, '{') }; + } + return child; + }); + }; + const visitor = (node: any) => { + if (node.tagName === 'code') { + escapeCode(node); + return; + } + + if (node.tagName !== 'pre') return; + const code = node.children[0]; + if (code.tagName !== 'code') return; + node.properties = { ...code.properties }; + + return node; + }; + return () => (tree: any) => visit(tree, 'element', visitor); +} diff --git a/packages/markdown-support/src/index.ts b/packages/markdown-support/src/index.ts new file mode 100644 index 000000000..4db3089ba --- /dev/null +++ b/packages/markdown-support/src/index.ts @@ -0,0 +1,69 @@ +import type { AstroMarkdownOptions } from './types'; + +import createCollectHeaders from './rehype-collect-headers.js'; +import scopedStyles from './remark-scoped-styles.js'; +import { remarkCodeBlock, rehypeCodeBlock } from './codeblock.js'; +import raw from 'rehype-raw'; + +import unified from 'unified'; +import markdown from 'remark-parse'; +import markdownToHtml from 'remark-rehype'; +// import smartypants from '@silvenon/remark-smartypants'; +import rehypeStringify from 'rehype-stringify'; + +export interface MarkdownRenderingOptions extends Partial<AstroMarkdownOptions> { + $?: { + scopedClassName: string | null; + }; + mode: 'md' | 'astro-md'; +} + +/** Internal utility for rendering a full markdown file and extracting Frontmatter data */ +export async function renderMarkdownWithFrontmatter(contents: string, opts?: MarkdownRenderingOptions | null) { + // Dynamic import to ensure that "gray-matter" isn't built by Snowpack + const { default: matter } = await import('gray-matter'); + const { data: frontmatter, content } = matter(contents); + const value = await renderMarkdown(content, { ...opts, mode: 'md' }); + return { ...value, frontmatter }; +} + +/** Shared utility for rendering markdown */ +export async function renderMarkdown(content: string, opts?: MarkdownRenderingOptions | null) { + const { $: { scopedClassName = null } = {}, mode = 'astro-md', footnotes: useFootnotes = true, gfm: useGfm = true } = opts ?? {}; + const { headers, rehypeCollectHeaders } = createCollectHeaders(); + + let parser = unified().use(markdown).use(remarkCodeBlock()); + + if (scopedClassName) { + parser = parser.use(scopedStyles(scopedClassName)); + } + + if (useGfm) { + const { default: gfm } = await import('remark-gfm'); + parser = parser.use(gfm); + } + + if (useFootnotes) { + const { default: footnotes } = await import('remark-footnotes'); + parser = parser.use(footnotes); + } + + let result: string; + try { + const vfile = await parser + .use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw'] }) + .use(raw) + .use(rehypeCollectHeaders) + .use(rehypeCodeBlock()) + .use(rehypeStringify) + .process(content); + result = vfile.contents.toString(); + } catch (err) { + throw err; + } + + return { + astro: { headers, source: content }, + content: result.toString(), + }; +}
\ No newline at end of file diff --git a/packages/markdown-support/src/rehype-collect-headers.ts b/packages/markdown-support/src/rehype-collect-headers.ts new file mode 100644 index 000000000..edfcd29bc --- /dev/null +++ b/packages/markdown-support/src/rehype-collect-headers.ts @@ -0,0 +1,30 @@ +import { visit } from 'unist-util-visit'; +import slugger from 'github-slugger'; + +/** */ +export default function createCollectHeaders() { + const headers: any[] = []; + + const visitor = (node: any) => { + if (node.type !== 'element') return; + const { tagName, children } = node; + if (tagName[0] !== 'h') return; + let [_, depth] = tagName.match(/h([0-6])/) ?? []; + if (!depth) return; + depth = Number.parseInt(depth); + + let text = ''; + visit(node, 'text', (child) => { + text += child.value; + }); + + let slug = slugger.slug(text); + node.properties = node.properties || {}; + node.properties.id = slug; + headers.push({ depth, slug, text }); + + return node; + }; + + return { headers, rehypeCollectHeaders: () => (tree: any) => visit(tree, visitor) }; +} diff --git a/packages/markdown-support/src/remark-scoped-styles.ts b/packages/markdown-support/src/remark-scoped-styles.ts new file mode 100644 index 000000000..9ca70c029 --- /dev/null +++ b/packages/markdown-support/src/remark-scoped-styles.ts @@ -0,0 +1,18 @@ +import { visit } from 'unist-util-visit'; +const noVisit = new Set(['root', 'html', 'text']); + +/** */ +export default function scopedStyles(className: string) { + const visitor = (node: any) => { + if (noVisit.has(node.type)) return; + + const { data } = node; + let currentClassName = data?.hProperties?.class ?? ''; + node.data = node.data || {}; + node.data.hProperties = node.data.hProperties || {}; + node.data.hProperties.class = `${className} ${currentClassName}`.trim(); + + return node; + }; + return () => (tree: any) => visit(tree, visitor); +} diff --git a/packages/markdown-support/src/types.ts b/packages/markdown-support/src/types.ts new file mode 100644 index 000000000..e86535567 --- /dev/null +++ b/packages/markdown-support/src/types.ts @@ -0,0 +1,6 @@ +export interface AstroMarkdownOptions { + /** Enable or disable footnotes syntax extension */ + footnotes: boolean; + /** Enable or disable GitHub-flavored Markdown syntax extension */ + gfm: boolean; +}
\ No newline at end of file |