summaryrefslogtreecommitdiff
path: root/packages/markdown-support/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/markdown-support/src')
-rw-r--r--packages/markdown-support/src/codeblock.ts43
-rw-r--r--packages/markdown-support/src/index.ts69
-rw-r--r--packages/markdown-support/src/rehype-collect-headers.ts30
-rw-r--r--packages/markdown-support/src/remark-scoped-styles.ts18
-rw-r--r--packages/markdown-support/src/types.ts6
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