Markdown compilation (#1593)

* Markdown compilation

* remove debugger
This commit is contained in:
Matthew Phillips 2021-10-19 10:41:55 -04:00 committed by Drew Powers
parent dd147c390a
commit f881a03961
15 changed files with 85 additions and 163 deletions

View file

@ -1,5 +1,6 @@
--- ---
import { renderMarkdown } from '@astrojs/markdown-remark'; import { renderMarkdown } from '@astrojs/markdown-remark';
import stripIndent from 'strip-indent';
export interface Props { export interface Props {
content?: string; content?: string;
@ -10,23 +11,22 @@ interface InternalProps extends Props {
$scope: string; $scope: string;
} }
const { content, $scope } = Astro.props as InternalProps; let { content, $scope } = Astro.props as InternalProps;
let html = null; let html = null;
// This flow is only triggered if a user passes `<Markdown content={content} />` // If no content prop provided, use the slot.
if (content) { if(!content) {
const { content: htmlContent } = await renderMarkdown(content, { const renderSlot = (Astro as any).privateRenderSlotDoNotUse;
content = stripIndent(await renderSlot('default'));
}
const { code: htmlContent } = await renderMarkdown(content, {
mode: 'md', mode: 'md',
$: { $: {
scopedClassName: $scope scopedClassName: $scope
} }
}); });
html = htmlContent;
}
/* html = htmlContent;
If we have rendered `html` for `content`, render that
Otherwise, just render the slotted content
*/
--- ---
{html ? html : <slot />} {html ? html : <slot />}

View file

@ -1,17 +1,4 @@
// export { default as Code } from './Code.astro'; // export { default as Code } from './Code.astro';
// export { default as Debug } from './Debug.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 { default as Prism } from './Prism.astro';
export const Code = () => {
throw new Error(`Cannot render <Code />. "astro/components" are still WIP!`)
}
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

@ -79,6 +79,7 @@
"morphdom": "^2.6.1", "morphdom": "^2.6.1",
"node-fetch": "^2.6.5", "node-fetch": "^2.6.5",
"path-to-regexp": "^6.2.0", "path-to-regexp": "^6.2.0",
"remark-slug": "^7.0.0",
"sass": "^1.43.2", "sass": "^1.43.2",
"semver": "^7.3.5", "semver": "^7.3.5",
"send": "^0.17.1", "send": "^0.17.1",
@ -88,6 +89,7 @@
"sourcemap-codec": "^1.4.8", "sourcemap-codec": "^1.4.8",
"string-width": "^5.0.0", "string-width": "^5.0.0",
"strip-ansi": "^7.0.1", "strip-ansi": "^7.0.1",
"strip-indent": "^4.0.0",
"supports-esm": "^1.0.0", "supports-esm": "^1.0.0",
"tiny-glob": "^0.2.8", "tiny-glob": "^0.2.8",
"vite": "^2.6.7", "vite": "^2.6.7",

View file

@ -9,7 +9,7 @@ import * as eslexer from 'es-module-lexer';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { renderPage } from '../../runtime/server/index.js'; import { renderPage, renderSlot } from '../../runtime/server/index.js';
import { parseNpmName, canonicalURL as getCanonicalURL, codeFrame } from '../util.js'; import { parseNpmName, canonicalURL as getCanonicalURL, codeFrame } from '../util.js';
import { generatePaginateFunction } from './paginate.js'; import { generatePaginateFunction } from './paginate.js';
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js'; import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
@ -144,6 +144,9 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna
url, url,
}, },
slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])), slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])),
privateRenderSlotDoNotUse(slotName: string) {
return renderSlot(result, slots ? slots[slotName] : null)
}
} as unknown as AstroGlobal; } as unknown as AstroGlobal;
}, },
_metadata: { renderers }, _metadata: { renderers },

View file

@ -43,7 +43,7 @@ async function _render(child: any): Promise<any> {
return child; return child;
} else if (!child && child !== 0) { } else if (!child && child !== 0) {
// do nothing, safe to ignore falsey values. // do nothing, safe to ignore falsey values.
} else if (child instanceof AstroComponent) { } else if (child instanceof AstroComponent || child.toString() === '[object AstroComponent]') {
return await renderAstroComponent(child); return await renderAstroComponent(child);
} else { } else {
return child; return child;
@ -59,6 +59,10 @@ export class AstroComponent {
this.expressions = expressions; this.expressions = expressions;
} }
get [Symbol.toStringTag]() {
return 'AstroComponent';
}
*[Symbol.iterator]() { *[Symbol.iterator]() {
const { htmlParts, expressions } = this; const { htmlParts, expressions } = this;

View file

@ -1,9 +1,8 @@
/**
* UNCOMMENT: add markdown support
import { expect } from 'chai'; import { expect } from 'chai';
import cheerio from 'cheerio'; import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js'; import { loadFixture } from './test-utils.js';
describe('Astro Markdown', () => {
let fixture; let fixture;
before(async () => { before(async () => {
@ -17,7 +16,6 @@ before(async () => {
await fixture.build(); await fixture.build();
}); });
describe('Astro Markdown', () => {
it('Can load markdown pages with Astro', async () => { it('Can load markdown pages with Astro', async () => {
const html = await fixture.readFile('/post/index.html'); const html = await fixture.readFile('/post/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
@ -37,7 +35,7 @@ describe('Astro Markdown', () => {
}); });
it('Empty code blocks do not fail', async () => { it('Empty code blocks do not fail', async () => {
const html = await fixture.fetch('/empty-code/index.html'); const html = await fixture.readFile('/empty-code/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
// test 1: There is not a `<code>` in the codeblock // test 1: There is not a `<code>` in the codeblock
@ -47,15 +45,17 @@ describe('Astro Markdown', () => {
expect($('pre')[1].children).to.have.lengthOf(0); expect($('pre')[1].children).to.have.lengthOf(0);
}); });
it('Runs code blocks through syntax highlighter', async () => { // This doesn't work because the markdown plugin doesn't have Prism support yet.
it.skip('Runs code blocks through syntax highlighter', async () => {
const html = await fixture.readFile('/code/index.html'); const html = await fixture.readFile('/code/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
// test 1: There are child spans in code blocks // test 1: There are child spans in code blocks
expect($('code span').length).toBeGreaterThan(0); expect($('code span').length).greaterThan(0);
}); });
it('Scoped styles should not break syntax highlight', async () => { // Blocked by lack of syntax highlighting
it.skip('Scoped styles should not break syntax highlight', async () => {
const html = await fixture.readFile('/scopedStyles-code/index.html'); const html = await fixture.readFile('/scopedStyles-code/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
@ -80,28 +80,19 @@ describe('Astro Markdown', () => {
expect($('#deep').children()).to.have.lengthOf(3); expect($('#deep').children()).to.have.lengthOf(3);
// tests 24: Only rendered title in each section // tests 24: Only rendered title in each section
assert.equal($('.a').children()).to.have.lengthOf(1); expect($('.a').children()).to.have.lengthOf(1);
assert.equal($('.b').children()).to.have.lengthOf(1); expect($('.b').children()).to.have.lengthOf(1);
assert.equal($('.c').children()).to.have.lengthOf(1); expect($('.c').children()).to.have.lengthOf(1);
// test 57: Rendered title in correct section // test 57: Rendered title in correct section
assert.equal($('.a > h2').text()).to.equal('A'); expect($('.a > h2').text()).to.equal('A');
assert.equal($('.b > h2').text()).to.equal('B'); expect($('.b > h2').text()).to.equal('B');
assert.equal($('.c > h2').text()).to.equal('C'); expect($('.c > h2').text()).to.equal('C');
}); });
it('Renders recursively', async () => { it.skip('Renders dynamic content though the content attribute', async () => {
const html = await fixture.readFile('/recursive/index.html');
const $ = cheerio.load(html);
// tests 12: Rendered title correctly
expect($('.a > h1').text()).to.equal('A');
expect($('.b > h1').text()).to.equal('B');
expect($('.c > h1').text()).to.equal('C');
});
it('Renders dynamic content though the content attribute', async () => {
const html = await fixture.readFile('/external/index.html'); const html = await fixture.readFile('/external/index.html');
console.log(html)
const $ = cheerio.load(html); const $ = cheerio.load(html);
// test 1: Rendered markdown content // test 1: Rendered markdown content
@ -140,8 +131,8 @@ describe('Astro Markdown', () => {
}); });
it('Can render markdown with --- for horizontal rule', async () => { it('Can render markdown with --- for horizontal rule', async () => {
const result = await fixture.readFile('/dash/index.html'); const html = await fixture.readFile('/dash/index.html');
expect(result.status).to.equal(200); expect(!!html).to.equal(true);
}); });
it('Can render markdown content prop (#1259)', async () => { it('Can render markdown content prop (#1259)', async () => {
@ -152,7 +143,3 @@ describe('Astro Markdown', () => {
expect($('h1').text()).to.equal('Foo'); expect($('h1').text()).to.equal('Foo');
}); });
}); });
*/
it.skip('is skipped', () => {});

View file

@ -8,13 +8,11 @@ const title = 'My Blog Post';
const description = 'This is a post about some stuff.'; const description = 'This is a post about some stuff.';
--- ---
<Markdown>
<Layout> <Layout>
<Markdown>
## Interesting Topic ## Interesting Topic
<Hello name={`world`} /> <Hello name={`world`} />
<Counter client:load /> <Counter client:load />
</Layout>
</Markdown> </Markdown>
</Layout>

View file

@ -1,11 +1,11 @@
--- ---
import { Markdown } from 'astro/components'; import { Markdown } from 'astro/components';
import Hello from '../components/Hello.jsx';
const outer = `# Outer`; const outer = `# Outer`;
const inner = `## Inner`; const inner = `## Inner`;
--- ---
<div>
<Markdown content={outer} /> <Markdown content={outer} />
<Markdown> <Markdown>
@ -13,3 +13,4 @@ const inner = `## Inner`;
<Markdown content={inner} /> <Markdown content={inner} />
</Markdown> </Markdown>
</div>

View file

@ -1,15 +0,0 @@
---
import { Markdown } from 'astro/components';
---
<Markdown>
<div class="a">
# A
<div class="b">
# B
<div class="c">
# C
</div>
</div>
</div>
</Markdown>

View file

@ -3,12 +3,12 @@ import { Markdown } from 'astro/components';
import Layout from '../layouts/content.astro'; import Layout from '../layouts/content.astro';
--- ---
<Markdown>
<Layout> <Layout>
<Markdown>
## Interesting Topic ## Interesting Topic
```js ```js
const thing = () => {}; const thing = () => {};
``` ```
</Layout>
</Markdown> </Markdown>
</Layout>

View file

@ -1,44 +0,0 @@
import { visit } from 'unist-util-visit';
import type { Element, Root as HastRoot, Properties } from 'hast';
import type { Root as MdastRoot } from 'mdast';
/** */
export function remarkCodeBlock() {
return function (tree: MdastRoot) {
visit(tree, 'code', (node) => {
const { data, meta } = node;
let lang = node.lang || 'html'; // default to html to match GFM behavior.
let currentClassName = (data?.hProperties as Properties)?.class ?? '';
node.data = node.data || {};
node.data.hProperties = node.data.hProperties || {};
node.data.hProperties = { ...(node.data.hProperties as Properties), class: `language-${lang} ${currentClassName}`.trim(), lang, meta };
});
};
}
/** */
export function rehypeCodeBlock() {
return function (tree: HastRoot) {
const escapeCode = (code: Element): void => {
code.children = code.children.map((child) => {
if (child.type === 'text') {
return { ...child, value: `{\`${child.value.replace(/\$\{/g, '\\$\\{').replace(/`/g, '\\`')}\`}` };
}
return child;
});
};
visit(tree, 'element', (node) => {
if (node.tagName === 'code') {
escapeCode(node);
return;
}
if (node.tagName !== 'pre') return;
if (!node.children[0]) return;
const code = node.children[0];
if (code.type !== 'element' || code.tagName !== 'code') return;
node.properties = { ...code.properties };
});
};
}

View file

@ -6,8 +6,7 @@ import { remarkExpressions, loadRemarkExpressions } from './remark-expressions.j
import rehypeExpressions from './rehype-expressions.js'; import rehypeExpressions from './rehype-expressions.js';
import { remarkJsx, loadRemarkJsx } from './remark-jsx.js'; import { remarkJsx, loadRemarkJsx } from './remark-jsx.js';
import rehypeJsx from './rehype-jsx.js'; import rehypeJsx from './rehype-jsx.js';
import { remarkCodeBlock, rehypeCodeBlock } from './codeblock.js'; //import { remarkCodeBlock } from './codeblock.js';
import remarkSlug from './remark-slug.js';
import { loadPlugins } from './load-plugins.js'; import { loadPlugins } from './load-plugins.js';
import { unified } from 'unified'; import { unified } from 'unified';
@ -59,7 +58,7 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
// parser.use(scopedStyles(scopedClassName)); // parser.use(scopedStyles(scopedClassName));
// } // }
parser.use(remarkCodeBlock); //parser.use(remarkCodeBlock);
parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression', 'mdxJsxTextElement', 'mdxJsxFlowElement']}); parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression', 'mdxJsxTextElement', 'mdxJsxFlowElement']});
loadedRehypePlugins.forEach(([plugin, opts]) => { loadedRehypePlugins.forEach(([plugin, opts]) => {
@ -72,11 +71,12 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
try { try {
const vfile = await parser const vfile = await parser
.use(rehypeCollectHeaders) .use(rehypeCollectHeaders)
.use(rehypeCodeBlock)
.use(rehypeStringify, { allowParseErrors: true, preferUnquoted: true, allowDangerousHtml: true }) .use(rehypeStringify, { allowParseErrors: true, preferUnquoted: true, allowDangerousHtml: true })
.process(content); .process(content);
result = vfile.toString(); result = vfile.toString();
} catch (err) { } catch (err) {
debugger;
console.error(err);
throw err; throw err;
} }

View file

@ -22,11 +22,10 @@ export default function createCollectHeaders() {
text += child.value; text += child.value;
}); });
let slug = node?.data?.id || slugger.slug(text); let slug = node?.properties?.id || slugger.slug(text);
node.data = node.data || {}; node.properties = node.properties || {};
node.data.properties = node.data.properties || {}; node.properties.id = slug;
node.data.properties = { ...(node.data.properties as Properties), slug };
headers.push({ depth, slug, text }); headers.push({ depth, slug, text });
}); });
}; };

View file

@ -7,7 +7,7 @@ export function remarkJsx(this: any, options: any) {
let settings = options || {}; let settings = options || {};
let data = this.data(); let data = this.data();
add('micromarkExtensions', mdxJsx({})); // TODO this seems to break adding slugs, no idea why add('micromarkExtensions', mdxJsx({}));
add('fromMarkdownExtensions', mdxJsxFromMarkdown); add('fromMarkdownExtensions', mdxJsxFromMarkdown);
add('toMarkdownExtensions', mdxJsxToMarkdown); add('toMarkdownExtensions', mdxJsxToMarkdown);