Fix codeblocks in markdown components (#264)

* Fix codeblocks in markdown components

* Debugging

* More debugging

* remove extra debugging stuff
This commit is contained in:
Matthew Phillips 2021-05-27 13:54:38 -04:00 committed by GitHub
parent 3dc141b868
commit dd7cc798e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 340 additions and 12 deletions

View file

@ -2,5 +2,6 @@ export default {
extensions: { extensions: {
'.jsx': 'react', '.jsx': 'react',
'.tsx': 'preact', '.tsx': 'preact',
} },
public: './public'
}; };

View file

@ -0,0 +1,6 @@
@use "./prism.scss";
body {
max-width: 900px;
margin: auto;
}

View file

@ -0,0 +1,228 @@
pre,
code {
color: #d4d4d4;
font-size: 14px;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
line-height: 1.5;
direction: ltr;
white-space: pre;
text-align: left;
text-shadow: none;
word-break: normal;
word-spacing: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre::selection,
code::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
pre,
code {
text-shadow: none;
}
}
pre {
margin: 0.5rem 0 16px;
padding: 0.8rem 1rem 0.9rem;
overflow: auto;
background: #282a36;
border-radius: 4px;
}
:not(pre) > code {
padding: 0.1em 0.3em;
color: #db4c69;
background: #f9f2f4;
border-radius: 0.3em;
white-space: pre-wrap;
}
/*********************************************************
* Tokens
*/
.namespace {
opacity: 0.7;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #6a9955;
}
.token.punctuation {
color: #d4d4d4;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #b5cea8;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #ce9178;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #d4d4d4;
background: rgb(45, 55, 72);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #c586c0;
}
.token.function {
color: #dcdcaa;
}
.token.regex,
.token.important,
.token.variable {
color: #d16969;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.constant {
color: #9cdcfe;
}
.token.class-name {
color: #4ec9b0;
}
.token.parameter {
color: #9cdcfe;
}
.token.interpolation {
color: #9cdcfe;
}
.token.punctuation.interpolation-punctuation {
color: #569cd6;
}
.token.boolean {
color: #569cd6;
}
.token.property {
color: #9cdcfe;
}
.token.selector {
color: #d7ba7d;
}
.token.tag {
color: #569cd6;
}
.token.attr-name {
color: #9cdcfe;
}
.token.attr-value {
color: #ce9178;
}
.token.entity {
color: #4ec9b0;
cursor: unset;
}
.token.namespace {
color: #4ec9b0;
}
/*********************************************************
* Language Specific
*/
pre[class*='language-javascript'],
code[class*='language-javascript'] {
color: #4ec9b0;
}
pre[class*='language-css'],
code[class*='language-css'] {
color: #ce9178;
}
pre[class*='language-html'],
code[class*='language-html'] {
color: #d4d4d4;
}
.language-html .token.punctuation {
color: #808080;
}
/*********************************************************
* Line highlighting
*/
pre[data-line] {
position: relative;
}
pre > code {
position: relative;
z-index: 1;
}
.line-highlight {
position: absolute;
right: 0;
left: 0;
z-index: 0;
margin-top: 1em;
padding: inherit 0;
line-height: inherit;
white-space: pre;
background: #f7ebc6;
box-shadow: inset 5px 0 0 #f7d87c;
pointer-events: none;
}
pre[class*='language-bash'] .token.function {
color: #d4d4d4;
}
.token.comment {
color: #fff7;
}

View file

@ -4,9 +4,10 @@ export let content;
<html> <html>
<head> <head>
<meta charset="utf-8">
<title>{content.title}</title> <title>{content.title}</title>
<link rel="stylesheet" href="/styles/global.css">
</head> </head>
<body> <body>
<slot/> <slot/>
</body> </body>

View file

@ -21,6 +21,8 @@ const items = ['A', 'B', 'C'];
The best part? It comes with all the Astro features you expect. The best part? It comes with all the Astro features you expect.
[Other example](./other)
## Embed framework components ## Embed framework components
<ReactCounter:visible /> <ReactCounter:visible />
@ -40,5 +42,15 @@ const items = ['A', 'B', 'C'];
### Markdown can be embedded in any child component ### Markdown can be embedded in any child component
</ReactCounter:visible> </ReactCounter:visible>
## Code
Should work!
```js
import Something from './another';
const thing = new Something();
```
</Markdown> </Markdown>
</Layout> </Layout>

View file

@ -0,0 +1,18 @@
---
title: Some Markdown Page
layout: ../layouts/main.astro
---
# Code
```js
var foo = 'bar';
function doSomething() {
return foo;
}
```
# Paragraph
text here.

View file

@ -43,4 +43,4 @@ if (grammar) {
let className = lang ? `language-${lang}` : ''; let className = lang ? `language-${lang}` : '';
--- ---
<pre class={className}><code class={className} />{html}</pre> <pre class={className}><code class={className}>{html}</code></pre>

View file

@ -1,7 +1,7 @@
import type { Ast, Script, Style, TemplateNode } from 'astro-parser'; import type { Ast, Script, Style, TemplateNode } from 'astro-parser';
import type { CompileOptions } from '../../@types/compiler'; import type { CompileOptions } from '../../@types/compiler';
import type { AstroConfig, AstroMarkdownOptions, TransformResult, ComponentInfo, Components } from '../../@types/astro'; import type { AstroConfig, AstroMarkdownOptions, TransformResult, ComponentInfo, Components } from '../../@types/astro';
import type { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier } from '@babel/types'; import type { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier, ImportDefaultSpecifier } from '@babel/types';
import 'source-map-support/register.js'; import 'source-map-support/register.js';
import eslexer from 'es-module-lexer'; import eslexer from 'es-module-lexer';
@ -18,6 +18,8 @@ import { fetchContent } from './content.js';
import { isFetchContent } from './utils.js'; import { isFetchContent } from './utils.js';
import { yellow } from 'kleur/colors'; import { yellow } from 'kleur/colors';
import { isComponentTag, renderMarkdown } from '../utils'; import { isComponentTag, renderMarkdown } from '../utils';
import { transform } from '../transform/index.js';
import { PRISM_IMPORT } from '../transform/prism.js';
const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default; const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default;
@ -186,6 +188,7 @@ interface CompileResult {
interface CodegenState { interface CodegenState {
filename: string; filename: string;
fileID: string;
components: Components; components: Components;
css: string[]; css: string[];
markers: { markers: {
@ -418,7 +421,7 @@ function dedent(str: string) {
/** Compile page markup */ /** Compile page markup */
async function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions): Promise<string> { async function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {
const { components, css, importExportStatements, filename } = state; const { components, css, importExportStatements, filename, fileID } = state;
const { astroConfig } = compileOptions; const { astroConfig } = compileOptions;
let paren = -1; let paren = -1;
@ -438,7 +441,18 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
mode: 'astro-md', mode: 'astro-md',
$: { scopedClassName: scopedClassName.slice(1, -1) }, $: { scopedClassName: scopedClassName.slice(1, -1) },
}); });
// 1. Parse
const ast = parse(rendered); const ast = parse(rendered);
// 2. Transform the AST
await transform(ast, {
compileOptions,
filename,
fileID
});
// 3. Codegen
const result = await compileHtml(ast.html, { ...state, markers: { ...state.markers, insideMarkdown: false } }, compileOptions); const result = await compileHtml(ast.html, { ...state, markers: { ...state.markers, insideMarkdown: false } }, compileOptions);
buffers.out += ',' + result; buffers.out += ',' + result;
@ -476,7 +490,26 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
break; break;
case 'Slot': case 'Slot':
case 'Head': case 'Head':
case 'InlineComponent': case 'InlineComponent': {
switch(node.name) {
case 'Prism': {
if(!importExportStatements.has(PRISM_IMPORT)) {
importExportStatements.add(PRISM_IMPORT);
}
if(!components.has('Prism')) {
components.set('Prism', {
importSpecifier: {
type: 'ImportDefaultSpecifier',
local: { type: 'Identifier', name: 'Prism' } as Identifier,
} as ImportDefaultSpecifier,
url: 'astro/components/Prism.astro'
});
}
break;
}
}
// Do not break.
}
case 'Title': case 'Title':
case 'Element': { case 'Element': {
const name: string = node.name; const name: string = node.name;
@ -590,9 +623,15 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
case 'Head': case 'Head':
case 'Body': case 'Body':
case 'Title': case 'Title':
case 'Element': case 'Element': {
if (paren !== -1) {
buffers.out += ')';
paren--;
}
return;
}
case 'InlineComponent': { case 'InlineComponent': {
if (node.type === 'InlineComponent' && curr === 'markdown' && buffers.markdown !== '') { if (curr === 'markdown' && buffers.markdown !== '') {
await pushMarkdownToBuffer(); await pushMarkdownToBuffer();
} }
if (paren !== -1) { if (paren !== -1) {
@ -626,11 +665,12 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
* @param {Ast} AST The parsed AST to crawl * @param {Ast} AST The parsed AST to crawl
* @param {object} CodeGenOptions * @param {object} CodeGenOptions
*/ */
export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOptions): Promise<TransformResult> { export async function codegen(ast: Ast, { compileOptions, filename, fileID }: CodeGenOptions): Promise<TransformResult> {
await eslexer.init; await eslexer.init;
const state: CodegenState = { const state: CodegenState = {
filename, filename,
fileID,
components: new Map(), components: new Map(),
css: [], css: [],
markers: { markers: {

View file

@ -2,7 +2,7 @@ import type { Transformer } from '../../@types/transformer';
import type { Script, TemplateNode } from 'astro-parser'; import type { Script, TemplateNode } from 'astro-parser';
import { getAttrValue } from '../../ast.js'; import { getAttrValue } from '../../ast.js';
const PRISM_IMPORT = `import Prism from 'astro/components/Prism.astro';\n`; export const PRISM_IMPORT = `import Prism from 'astro/components/Prism.astro';`;
const prismImportExp = /import Prism from ['"]astro\/components\/Prism.astro['"]/; const prismImportExp = /import Prism from ['"]astro\/components\/Prism.astro['"]/;
/** escaping code samples that contain template string replacement parts, ${foo} or example. */ /** escaping code samples that contain template string replacement parts, ${foo} or example. */
function escape(code: string) { function escape(code: string) {
@ -100,8 +100,8 @@ export default function (module: Script): Transformer {
}, },
async finalize() { async finalize() {
// Add the Prism import if needed. // Add the Prism import if needed.
if (usesPrism && !prismImportExp.test(module.content)) { if (usesPrism && module && !prismImportExp.test(module.content)) {
module.content = PRISM_IMPORT + module.content; module.content = PRISM_IMPORT + '\n' + module.content;
} }
}, },
}; };

View file

@ -26,6 +26,15 @@ Markdown('Can load more complex jsxy stuff', async ({ runtime }) => {
assert.equal($el.text(), 'Hello world'); assert.equal($el.text(), 'Hello world');
}); });
Markdown('Runs code blocks through syntax highlighter', async ({ runtime }) => {
const result = await runtime.load('/code');
if (result.error) throw new Error(result.error);
const $ = doc(result.contents);
const $el = $('code span');
assert.ok($el.length > 0, 'There are child spans in code blocks');
});
Markdown('Bundles client-side JS for prod', async (context) => { Markdown('Bundles client-side JS for prod', async (context) => {
await context.build(); await context.build();

View file

@ -0,0 +1,13 @@
---
import Markdown from 'astro/components/Markdown.astro';
export const title = 'My Blog Post';
export const description = 'This is a post about some stuff.';
---
<Markdown>
## Interesting Topic
```js
const thing = () => {};
```
</Markdown>