Scoped styles with markdown (#1599)

This commit is contained in:
Matthew Phillips 2021-10-19 16:49:31 -04:00 committed by Drew Powers
parent b695c8aa15
commit d1f42353e8
7 changed files with 56 additions and 39 deletions

View file

@ -11,7 +11,7 @@ interface InternalProps extends Props {
$scope: string;
}
let { content, $scope } = Astro.props as InternalProps;
let { content, class: className } = Astro.props as InternalProps;
let html = null;
// If no content prop provided, use the slot.
@ -23,7 +23,7 @@ if(!content) {
const { code: htmlContent } = await renderMarkdown(content, {
mode: 'md',
$: {
scopedClassName: $scope
scopedClassName: className
}
});

View file

@ -45,7 +45,6 @@ describe('Astro Markdown', () => {
expect($('pre')[1].children).to.have.lengthOf(0);
});
// This doesn't work because the markdown plugin doesn't have Prism support yet.
it('Runs code blocks through syntax highlighter', async () => {
const html = await fixture.readFile('/code/index.html');
const $ = cheerio.load(html);
@ -54,13 +53,13 @@ describe('Astro Markdown', () => {
expect($('code span').length).greaterThan(0);
});
// Blocked by lack of syntax highlighting
it.skip('Scoped styles should not break syntax highlight', async () => {
it('Scoped styles should not break syntax highlight', async () => {
const html = await fixture.readFile('/scopedStyles-code/index.html');
const $ = cheerio.load(html);
// test 1: <pre> tag has scopedStyle class passed down
expect($('pre').is('[class]')).to.equal(true);
expect($('pre').attr('class').split(' ').length).to.equal(2)
// test 2: <pre> tag has correct language
expect($('pre').hasClass('language-js')).to.equal(true);
@ -69,7 +68,7 @@ describe('Astro Markdown', () => {
expect($('code').hasClass('language-js')).to.equal(true);
// test 4: There are child spans in code blocks
expect($('code span').length).toBeGreaterThan(0);
expect($('code span').length).to.be.greaterThan(0);
});
it('Renders correctly when deeply nested on a page', async () => {
@ -90,9 +89,8 @@ describe('Astro Markdown', () => {
expect($('.c > h2').text()).to.equal('C');
});
it.skip('Renders dynamic content though the content attribute', async () => {
it('Renders dynamic content though the content attribute', async () => {
const html = await fixture.readFile('/external/index.html');
console.log(html)
const $ = cheerio.load(html);
// test 1: Rendered markdown content

View file

@ -5,7 +5,10 @@ const outer = `# Outer`;
const inner = `## Inner`;
---
<div>
<style>
#root { color: green; }
</style>
<div id="root">
<Markdown content={outer} />
<Markdown>

View file

@ -0,0 +1,21 @@
---
import { Markdown } from 'astro/components';
import Layout from '../layouts/content.astro';
---
<style>
#root {
color: green;
}
</style>
<Layout>
<div id="root">
<Markdown>
## Interesting Topic
```js
const thing = () => {};
```
</Markdown>
</div>
</Layout>

View file

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

View file

@ -39,6 +39,7 @@ 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 scopedClassName = opts?.$?.scopedClassName;
const { headers, rehypeCollectHeaders } = createCollectHeaders();
await Promise.all([loadRemarkExpressions(), loadRemarkJsx()]); // Vite bug: dynamically import() these because of CJS interop (this will cache)
@ -47,7 +48,7 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
.use(markdown)
.use([remarkJsx])
.use([remarkExpressions])
.use([remarkPrism])
.use([remarkPrism(scopedClassName)])
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
@ -56,9 +57,9 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
parser.use([[plugin, opts]]);
});
// if (scopedClassName) {
// parser.use(scopedStyles(scopedClassName));
// }
if (scopedClassName) {
parser.use([scopedStyles(scopedClassName)]);
}
//parser.use(remarkCodeBlock);
parser.use([[markdownToHtml as any, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression', 'mdxJsxTextElement', 'mdxJsxFlowElement']}]]);

View file

@ -44,22 +44,30 @@ function runHighlighter(lang: string, code: string) {
return { classLanguage, html };
}
/** */
function transformer(tree: any) {
const visitor = (node: any) => {
let {lang, value} = node;
node.type = 'html';
type MaybeString = string | null | undefined;
let { html, classLanguage } = runHighlighter(lang, value);
node.value = `<pre class="${classLanguage}"><code class="${classLanguage}">${html}</code></pre>`;
return node;
};
return visit(tree, 'code', visitor)
/** */
function transformer(className: MaybeString) {
return function(tree: any) {
const visitor = (node: any) => {
let {lang, value} = node;
node.type = 'html';
let { html, classLanguage } = runHighlighter(lang, value);
let classes = [classLanguage];
if(className) {
classes.push(className);
}
node.value = `<pre class="${classes.join(' ')}"><code class="${classLanguage}">${html}</code></pre>`;
return node;
};
return visit(tree, 'code', visitor)
}
}
function plugin() {
return transformer;
function plugin(className: MaybeString) {
return transformer.bind(null, className);
}
export default plugin;