WIP: markdown support

This commit is contained in:
Nate Moore 2021-10-07 16:34:57 -05:00
parent f0839c62df
commit ce1b160bf9
15 changed files with 143 additions and 128 deletions

View file

@ -2,10 +2,10 @@
// Component Imports
import { Markdown } from 'astro/components';
import Layout from '../layouts/main.astro';
import ReactCounter from '../components/ReactCounter.jsx';
import PreactCounter from '../components/PreactCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
// import ReactCounter from '../components/ReactCounter.jsx';
// import PreactCounter from '../components/PreactCounter.tsx';
// import VueCounter from '../components/VueCounter.vue';
// import SvelteCounter from '../components/SvelteCounter.svelte';
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
@ -32,10 +32,10 @@ const items = ['A', 'B', 'C'];
## Embed framework components
<ReactCounter client:visible />
<PreactCounter client:visible />
<VueCounter client:visible />
<SvelteCounter client:visible />
<!-- <ReactCounter client:visible /> -->
<!-- <PreactCounter client:visible /> -->
<!-- <VueCounter client:visible /> -->
<!-- <SvelteCounter client:visible /> -->
## Use Expressions
@ -43,11 +43,11 @@ const items = ['A', 'B', 'C'];
## Oh yeah...
<ReactCounter client:visible>
<!-- <ReactCounter client:visible>
🤯 It's also _recursive_!
### Markdown can be embedded in any child component
</ReactCounter>
</ReactCounter> -->
## Code

View file

@ -1,32 +1,39 @@
---
import { renderMarkdown } from '@astrojs/markdown-remark';
import { dedent } from './utils.js';
export interface Props {
content?: string;
// export interface Props {
// content?: string;
// }
// // Internal props that should not be part of the external interface.
// interface InternalProps extends Props {
// $scope: string;
// }
// const { content, $scope } = Astro.props as InternalProps;
// let html = null;
// // This flow is only triggered if a user passes `<Markdown content={content} />`
// if (content) {
// const { content: htmlContent } = await renderMarkdown(content, {
// mode: 'md',
// $: {
// scopedClassName: $scope
// }
// });
// html = htmlContent;
// }
const { content = await $$slots.default() } = Astro.props;
let markdown = typeof content === 'string' ? content : await content.render();
markdown = dedent(markdown);
let html: string;
try {
({ code: html } = await renderMarkdown(markdown));
} catch (e) {
console.log(e)
}
// Internal props that should not be part of the external interface.
interface InternalProps extends Props {
$scope: string;
}
const { content, $scope } = Astro.props as InternalProps;
let html = null;
// This flow is only triggered if a user passes `<Markdown content={content} />`
if (content) {
const { content: htmlContent } = await renderMarkdown(content, {
mode: 'md',
$: {
scopedClassName: $scope
}
});
html = htmlContent;
}
/*
If we have rendered `html` for `content`, render that
Otherwise, just render the slotted content
*/
---
{html ? html : <slot />}
{html}

View file

@ -1,6 +1,6 @@
// export { default as Code } from './Code.astro';
// export { default as Debug } from './Debug.astro';
// export { default as Markdown } from './Markdown.astro';
export { default as Markdown } from './Markdown.astro';
// export { default as Prism } from './Prism.astro';
export const Code = () => {
@ -9,9 +9,6 @@ export const Code = () => {
export const Debug = () => {
throw new Error(`Cannot render <Debug />. "astro/components" are still WIP!`)
}
export const Markdown = () => {
throw new Error(`Cannot render <Markdown />. "astro/components" are still WIP!`)
}
export const Prism = () => {
throw new Error(`Cannot render <Prism />. "astro/components" are still WIP!`)
}

View file

@ -0,0 +1,48 @@
export function dedent(
strings,
...values
) {
// $FlowFixMe: Flow doesn't undestand .raw
const raw = typeof strings === "string" ? [strings] : strings.raw;
// first, perform interpolation
let result = "";
for (let i = 0; i < raw.length; i++) {
result += raw[i]
// join lines when there is a suppressed newline
.replace(/\\\n[ \t]*/g, "")
// handle escaped backticks
.replace(/\\`/g, "`");
if (i < values.length) {
result += values[i];
}
}
// now strip indentation
const lines = result.split("\n");
let mindent = null;
lines.forEach(l => {
let m = l.match(/^(\s+)\S+/);
if (m) {
let indent = m[1].length;
if (!mindent) {
// this is the first indented line
mindent = indent;
} else {
mindent = Math.min(mindent, indent);
}
}
});
if (mindent !== null) {
const m = mindent; // appease Flow
result = lines.map(l => l[0] === " " ? l.slice(m) : l).join("\n");
}
return result
// dedent eats leading and trailing whitespace too
.trim()
// handle escaped newlines at the end to ensure they don't get stripped too
.replace(/\\n/g, "\n");
}

View file

@ -54,7 +54,7 @@
"connect": "^3.7.0",
"del": "^6.0.0",
"es-module-lexer": "^0.7.1",
"esbuild": "^0.12.28",
"esbuild": "0.12.28",
"estree-util-value-to-estree": "^1.2.0",
"fast-xml-parser": "^3.19.0",
"html-entities": "^2.3.2",
@ -69,14 +69,14 @@
"shiki": "^0.9.10",
"shorthash": "^0.0.2",
"slash": "^4.0.0",
"srcset-parse": "^1.1.0",
"source-map": "^0.7.3",
"sourcemap-codec": "^1.4.8",
"srcset-parse": "^1.1.0",
"string-width": "^5.0.0",
"strip-ansi": "^7.0.1",
"supports-esm": "^1.0.0",
"tiny-glob": "^0.2.8",
"vite": "^2.5.10",
"vite": "2.6.2",
"yargs-parser": "^20.2.9",
"zod": "^3.8.1"
},

View file

@ -54,6 +54,10 @@ export class AstroComponent {
this.expressions = expressions;
}
public render() {
return renderAstroComponent(this);
}
*[Symbol.iterator]() {
const { htmlParts, expressions } = this;

View file

@ -34,7 +34,8 @@ export async function loadViteConfig(
});
const userDevDeps = Object.keys(packageJSON?.devDependencies || {});
const { external, noExternal } = await viteSSRDeps([...userDeps, ...userDevDeps]);
// console.log(external.has('tiny-glob'), noExternal.has('tiny-glob'));
// console.log(external);
// load Astro renderers
await Promise.all(
@ -99,13 +100,20 @@ export async function loadViteConfig(
/** Note: SSR API is in beta (https://vitejs.dev/guide/ssr.html) */
ssr: {
external: [...external],
noExternal: [...noExternal],
noExternal: [],
},
},
viteConfig
);
}
const MANUAL_NO_EXTERNAL = new Set([
'astro',
'@astrojs/markdown-remark',
'micromark-factory-mdx-expression',
'remark',
])
/** Try and automatically figure out Vite external & noExternal */
async function viteSSRDeps(deps: string[]): Promise<{ external: Set<string>; noExternal: Set<string> }> {
const skip = new Set<string>();
@ -149,26 +157,15 @@ async function viteSSRDeps(deps: string[]): Promise<{ external: Set<string>; noE
return;
}
// sort this package
let isExternal = true; // external by default
// ESM gets noExternal
if (packageJSON.type === 'module') isExternal = false;
// TODO: manual bugfixes for Vite
if (pkg.name === '@sveltejs/vite-plugin-svelte') isExternal = true;
if (pkg.name === 'micromark-util-events-to-acorn') isExternal = true;
if (pkg.name === 'unified') isExternal = true;
// TODO: add more checks here if needed
// add to list
if (isExternal === true) {
external.add(spec);
} else {
if (packageJSON.type === 'module' || MANUAL_NO_EXTERNAL.has(spec) || spec.startsWith('node:')) {
noExternal.add(spec);
} else {
external.add(spec);
}
// recursively load dependencies for package (but not devDeps)
await Promise.all(Object.keys(packageJSON.dependencies || {}).map(sortPkg));
await Promise.all(Object.keys(packageJSON.dependencies || {}).map(spec => sortPkg(spec)));
} catch (err) {
// cant load package: skip
skip.add(spec);

View file

@ -22,10 +22,10 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
// 2. Transform from `.md` to valid `.astro`
let render = config.markdownOptions.render;
let renderOpts = {};
let renderOpts = { mode: "mdx" };
if (Array.isArray(render)) {
render = render[0];
renderOpts = render[1];
renderOpts = render[1] || renderOpts;
}
if (typeof render === 'string') {
({ default: render } = await import(render));

View file

@ -23,6 +23,7 @@ export function rehypeCodeBlock() {
const escapeCode = (code: Element): void => {
code.children = code.children.map((child) => {
if (child.type === 'text') {
// return { ...child };
return { ...child, value: `{\`${child.value.replace(/\$\{/g, '\\$\\{').replace(/`/g, '\\`')}\`}` };
}
return child;

View file

@ -2,19 +2,21 @@ import type { AstroMarkdownOptions, MarkdownRenderingOptions } from './types';
import createCollectHeaders from './rehype-collect-headers.js';
import scopedStyles from './remark-scoped-styles.js';
import { remarkExpressions, loadRemarkExpressions } from './remark-expressions.js';
import { remarkExpressions } from './remark-expressions.js';
import rehypeExpressions from './rehype-expressions.js';
import { remarkJsx, loadRemarkJsx } from './remark-jsx.js';
import { remarkJsx } from './remark-jsx.js';
import rehypeJsx from './rehype-jsx.js';
import { remarkCodeBlock, rehypeCodeBlock } from './codeblock.js';
import remarkSlug from './remark-slug.js';
import { loadPlugins } from './load-plugins.js';
import { unified } from 'unified';
import markdown from 'remark-parse';
import markdownToHtml from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import matter from 'gray-matter';
import { unified } from 'unified';
import rehypeStringify from 'rehype-stringify';
import remarkGFM from 'remark-gfm';
import remarkFootnotes from 'remark-footnotes';
export { AstroMarkdownOptions, MarkdownRenderingOptions };
@ -25,9 +27,9 @@ export async function renderMarkdownWithFrontmatter(contents: string, opts?: Mar
return { ...value, frontmatter };
}
export const DEFAULT_REMARK_PLUGINS = [
'remark-gfm',
'remark-footnotes',
export const DEFAULT_REMARK_PLUGINS: any[] = [
remarkGFM,
remarkFootnotes,
// TODO: reenable smartypants!
// '@silvenon/remark-smartypants'
]
@ -38,11 +40,9 @@ export const DEFAULT_REHYPE_PLUGINS = [
/** Shared utility for rendering markdown */
export async function renderMarkdown(content: string, opts?: MarkdownRenderingOptions | null) {
const { remarkPlugins = DEFAULT_REMARK_PLUGINS, rehypePlugins = DEFAULT_REHYPE_PLUGINS } = opts ?? {};
const { remarkPlugins = DEFAULT_REMARK_PLUGINS, rehypePlugins = DEFAULT_REHYPE_PLUGINS, mode = 'md' } = opts ?? {};
const { headers, rehypeCollectHeaders } = createCollectHeaders();
await Promise.all([loadRemarkExpressions(), loadRemarkJsx()]); // Vite bug: dynamically import() these because of CJS interop (this will cache)
let parser = unified()
.use(markdown)
.use([remarkJsx])
@ -72,11 +72,12 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
try {
const vfile = await parser
.use(rehypeCollectHeaders)
.use(rehypeCodeBlock)
.use([rehypeCodeBlock, { mode }])
.use(rehypeStringify, { allowParseErrors: true, preferUnquoted: true, allowDangerousHtml: true })
.process(content);
result = vfile.toString();
} catch (err) {
console.log(err);
throw err;
}

View file

@ -1,7 +1,6 @@
// Vite bug: dynamically import() modules needed for CJS. Cache in memory to keep side effects
let mdxExpression: any;
let mdxExpressionFromMarkdown: any;
let mdxExpressionToMarkdown: any;
import { mdxExpression } from 'micromark-extension-mdx-expression';
import { mdxExpressionFromMarkdown, mdxExpressionToMarkdown } from 'mdast-util-mdx-expression';
export function remarkExpressions(this: any, options: any) {
let settings = options || {};
@ -17,15 +16,3 @@ export function remarkExpressions(this: any, options: any) {
else data[field] = [value];
}
}
export async function loadRemarkExpressions() {
if (!mdxExpression) {
const micromarkMdxExpression = await import('micromark-extension-mdx-expression');
mdxExpression = micromarkMdxExpression.mdxExpression;
}
if (!mdxExpressionFromMarkdown || !mdxExpressionToMarkdown) {
const mdastUtilMdxExpression = await import('mdast-util-mdx-expression');
mdxExpressionFromMarkdown = mdastUtilMdxExpression.mdxExpressionFromMarkdown;
mdxExpressionToMarkdown = mdastUtilMdxExpression.mdxExpressionToMarkdown;
}
}

View file

@ -1,7 +1,6 @@
// Vite bug: dynamically import() modules needed for CJS. Cache in memory to keep side effects
let mdxJsx: any;
let mdxJsxFromMarkdown: any;
let mdxJsxToMarkdown: any;
import { mdxJsx } from 'micromark-extension-mdx-jsx';
import { mdxJsxFromMarkdown, mdxJsxToMarkdown } from 'mdast-util-mdx-jsx';
export function remarkJsx(this: any, options: any) {
let settings = options || {};
@ -17,15 +16,3 @@ export function remarkJsx(this: any, options: any) {
else data[field] = [value];
}
}
export async function loadRemarkJsx() {
if (!mdxJsx) {
const micromarkMdxJsx = await import('micromark-extension-mdx-jsx');
mdxJsx = micromarkMdxJsx.mdxJsx;
}
if (!mdxJsxFromMarkdown || !mdxJsxToMarkdown) {
const mdastUtilMdxJsx = await import('mdast-util-mdx-jsx');
mdxJsxFromMarkdown = mdastUtilMdxJsx.mdxJsxFromMarkdown;
mdxJsxToMarkdown = mdastUtilMdxJsx.mdxJsxToMarkdown;
}
}

View file

@ -3,8 +3,8 @@
* @typedef {import('hast').Properties} Properties
*/
import {toString} from 'mdast-util-to-string'
import {visit} from 'unist-util-visit'
import { toString } from 'mdast-util-to-string'
import { visit } from 'unist-util-visit'
import BananaSlug from 'github-slugger'
const slugs = new BananaSlug()

View file

@ -4,6 +4,7 @@ export type UnifiedPluginImport = Promise<{ default: unified.Plugin }>;
export type Plugin = string | [string, any] | UnifiedPluginImport | [UnifiedPluginImport, any];
export interface AstroMarkdownOptions {
mode: 'md'|'mdx';
remarkPlugins?: Plugin[];
rehypePlugins?: Plugin[];
}

View file

@ -3975,16 +3975,16 @@ es6-promisify@^5.0.0:
dependencies:
es6-promise "^4.0.3"
esbuild@0.12.28, esbuild@^0.12.17, esbuild@^0.12.28:
version "0.12.28"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.28.tgz#84da0d2a0d0dee181281545271e0d65cf6fab1ef"
integrity sha512-pZ0FrWZXlvQOATlp14lRSk1N9GkeJ3vLIwOcUoo3ICQn9WNR4rWoNi81pbn6sC1iYUy7QPqNzI3+AEzokwyVcA==
esbuild@^0.11.16:
version "0.11.23"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.11.23.tgz#c42534f632e165120671d64db67883634333b4b8"
integrity sha512-iaiZZ9vUF5wJV8ob1tl+5aJTrwDczlvGP0JoMmnpC2B0ppiMCu8n8gmy5ZTGl5bcG081XBVn+U+jP+mPFm5T5Q==
esbuild@^0.12.17, esbuild@^0.12.28:
version "0.12.28"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.28.tgz#84da0d2a0d0dee181281545271e0d65cf6fab1ef"
integrity sha512-pZ0FrWZXlvQOATlp14lRSk1N9GkeJ3vLIwOcUoo3ICQn9WNR4rWoNi81pbn6sC1iYUy7QPqNzI3+AEzokwyVcA==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@ -9421,7 +9421,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@~0.7.2:
source-map@^0.7.3, source-map@~0.7.2:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
@ -9516,21 +9516,6 @@ srcset-parse@^1.1.0:
resolved "https://registry.yarnpkg.com/srcset-parse/-/srcset-parse-1.1.0.tgz#73f787f38b73ede2c5af775e0a3465579488122b"
integrity sha512-JWp4cG2eybkvKA1QUHGoNK6JDEYcOnSuhzNGjZuYUPqXreDl/VkkvP2sZW7Rmh+icuCttrR9ccb2WPIazyM/Cw==
sqlite3@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.2.tgz#00924adcc001c17686e0a6643b6cbbc2d3965083"
integrity sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==
dependencies:
node-addon-api "^3.0.0"
node-pre-gyp "^0.11.0"
optionalDependencies:
node-gyp "3.x"
sqlite@^4.0.23:
version "4.0.23"
resolved "https://registry.yarnpkg.com/sqlite/-/sqlite-4.0.23.tgz#ada09028b38e91883db08ac465d841e814d1bb00"
integrity sha512-dSdmSkrdIhUL7xP/fiEMfFuAo4dxb0afag3rK8T4Y9lYxE3g3fXT0J8H9qSFvmcKxnM0zEA8yvLbpdWQ8mom3g==
sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@ -10659,7 +10644,7 @@ vfile@^5.0.0:
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
vite@^2.5.10:
vite@2.5.10:
version "2.5.10"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.5.10.tgz#c598e3b5a7e1956ffc52eb3b3420d177fc2ed2a5"
integrity sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==