Feat: change to shiki default md renderer (#2824)

* feat: change Shiki to default

* refactor: update blog styles for shiki

* feat: update examples/docs styles for Shiki

* refactor: remove Prism-ish examples/docs styles

* refactor: simplify rules with `all: unset`

* refactor: remove Prism styles

* refactor: examples/with-md remove Prism-specific line-highlight

* chore: add changeset

* chore: update changeset versions

* refactor: update syntax highlight test for scoped styles

* fix: apply scoped style class to pre and span lines

* feat: test that scoped styles cascade to shiki code

* refactor: pass scopedClassName explicitly
This commit is contained in:
Ben Holmes 2022-03-18 17:29:51 -04:00 committed by GitHub
parent 1cd7184ca6
commit 0a3d3e51a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 70 additions and 312 deletions

View file

@ -0,0 +1,6 @@
---
'@astrojs/markdown-remark': minor
'astro': minor
---
Change shiki to our default markdown syntax highlighter. This includes updates to all relevant starter projects that used Prism-specific styles.

View file

@ -163,14 +163,14 @@ a {
gap: 0.5rem;
}
a > code:not([class*='language']) {
a > code {
position: relative;
color: var(--theme-accent);
background: transparent;
text-underline-offset: var(--padding-block);
}
a > code:not([class*='language'])::before {
a > code::before {
content: '';
position: absolute;
top: 0;
@ -200,7 +200,7 @@ strong {
/* Supporting Content */
code:not([class*='language']) {
code {
--border-radius: 3px;
--padding-block: 0.2rem;
--padding-inline: 0.33rem;
@ -215,28 +215,17 @@ code:not([class*='language']) {
word-break: break-word;
}
pre > code:not([class*='language']) {
background-color: transparent;
padding: 0;
margin: 0;
border-radius: 0;
color: inherit;
pre.astro-code > code {
all: unset;
}
pre {
position: relative;
background-color: var(--theme-code-bg);
color: var(--theme-code-text);
--padding-block: 1rem;
--padding-inline: 2rem;
padding: var(--padding-block) var(--padding-inline);
padding-right: calc(var(--padding-inline) * 2);
margin-left: calc(50vw - var(--padding-inline));
transform: translateX(-50);
line-height: 1.414;
width: calc(100vw + (var(--padding-inline) * 2));
max-width: calc(100% + (var(--padding-inline) * 2));
overflow-y: hidden;
overflow-x: auto;
}

View file

@ -1,6 +1,5 @@
---
import '../styles/theme.css';
import '../styles/code.css';
import '../styles/index.css';
---

View file

@ -1,96 +0,0 @@
.language-css > code,
.language-sass > code,
.language-scss > code {
color: #fd9170;
}
[class*='language-'] .namespace {
opacity: 0.7;
}
.token.plain-text,
[class*='language-bash'] span.token,
[class*='language-shell'] span.token {
color: hsla(var(--color-gray-90), 1);
}
[class*='language-bash'] span.token,
[class*='language-shell'] span.token {
font-style: bold;
}
.token.prolog,
.token.comment,
[class*='language-bash'] span.token.comment,
[class*='language-shell'] span.token.comment {
color: hsla(var(--color-gray-70), 1);
}
.token.selector,
.token.tag,
.token.unit,
.token.url,
.token.variable,
.token.entity,
.token.deleted {
color: #fa5e5b;
}
.token.boolean,
.token.constant,
.token.doctype,
.token.number,
.token.regex,
.token.builtin,
.token.class,
.token.hexcode,
.token.class-name,
.token.attr-name {
color: hsla(var(--color-yellow), 1);
}
.token.atrule,
.token.attribute,
.token.attr-value .token.punctuation,
.token.attr-value,
.token.pseudo-class,
.token.pseudo-element,
.token.string {
color: hsla(var(--color-green), 1);
}
.token.symbol,
.token.function,
.token.id,
.token.important {
color: hsla(var(--color-blue), 1);
}
.token.important,
.token.id {
font-weight: bold;
}
.token.cdata,
.token.char,
.token.property {
color: #23b1af;
}
.token.inserted {
color: hsla(var(--color-green), 1);
}
.token.keyword {
color: #ff657c;
font-style: italic;
}
.token.operator {
color: hsla(var(--color-gray-70), 1);
}
.token.attr-value .token.attr-equals,
.token.punctuation {
color: hsla(var(--color-gray-80), 1);
}

View file

@ -151,14 +151,14 @@ article > section iframe {
aspect-ratio: 16 / 9;
}
a > code:not([class*='language']) {
a > code {
position: relative;
color: var(--theme-text-accent);
background: transparent;
text-underline-offset: var(--padding-block);
}
a > code:not([class*='language'])::before {
a > code::before {
content: '';
position: absolute;
top: 0;
@ -187,30 +187,24 @@ strong {
}
/* Supporting Content */
code {
font-family: var(--font-mono);
font-size: 0.85em;
}
code:not([class*='language']) {
code {
--border-radius: 3px;
--padding-block: 0.2rem;
--padding-inline: 0.4rem;
color: var(--theme-code-inline-text);
--padding-inline: 0.33rem;
font-family: var(--font-mono);
font-size: 0.85em;
color: inherit;
background-color: var(--theme-code-inline-bg);
padding: var(--padding-block) var(--padding-inline);
margin: calc(var(--padding-block) * -1) -0.125em;
border-radius: var(--border-radius);
box-shadow: 0 2px 1px 0 rgba(0, 0, 0, 0.08);
word-break: break-word;
}
pre > code:not([class*='language']) {
background-color: transparent;
padding: 0;
margin: 0;
border-radius: 0;
color: inherit;
pre.astro-code > code {
all: unset;
}
pre > code {
@ -261,7 +255,7 @@ pre {
color: var(--theme-code-text);
}
blockquote code:not([class*='language']) {
blockquote code {
background-color: var(--theme-bg);
}

View file

@ -40,7 +40,7 @@ pre {
border-radius: 4px;
}
:not(pre) > code {
code {
padding: 0.1em 0.3em;
color: #db4c69;
background: #f9f2f4;
@ -48,151 +48,10 @@ pre {
white-space: pre-wrap;
}
/*********************************************************
* Tokens
*/
.namespace {
opacity: 0.7;
pre.astro-code > code {
all: unset;
}
.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: #2d3748;
}
.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
@ -206,28 +65,6 @@ pre > code {
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;
}
body {
max-width: 900px;
margin: auto;

View file

@ -57,18 +57,36 @@ describe('Astro Markdown', () => {
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 1: <pre> tag has correct shiki class
expect($('pre').hasClass('astro-code')).to.equal(true);
// test 2: <pre> tag has correct language
expect($('pre').hasClass('language-js')).to.equal(true);
// test 2: inline styles are still applied
expect($('pre').is('[style]')).to.equal(true);
// test 3: There are styled child spans in code blocks
expect($('pre code span').length).to.be.greaterThan(0);
expect($('pre code span').is('[style]')).to.equal(true);
});
// test 3: <code> tag has correct language
expect($('code').hasClass('language-js')).to.equal(true);
function isAstroScopedClass(cls) {
return /^astro-.*/.test(cls)
}
// test 4: There are child spans in code blocks
expect($('code span').length).to.be.greaterThan(0);
it('Scoped styles should be applied to syntax highlighted lines', async () => {
const html = await fixture.readFile('/scopedStyles-code/index.html');
const $ = cheerio.load(html);
// test 1: the "pre" tag receives scoped style
const preClassList = $('pre').attr('class').split(/\s+/);
expect(preClassList.length).to.equal(2);
const preAstroClass = preClassList.find(isAstroScopedClass);
expect(Boolean(preAstroClass)).to.equal(true);
// test 2: each "span" line receives scoped style
const spanClassList = $('pre code span').attr('class').split(/\s+/);
expect(spanClassList.length).to.equal(2);
const spanAstroClass = spanClassList.find(isAstroScopedClass);
expect(Boolean(spanAstroClass)).to.equal(true);
});
it('Renders correctly when deeply nested on a page', async () => {

View file

@ -38,7 +38,7 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
let { remarkPlugins = [], rehypePlugins = [] } = opts ?? {};
const scopedClassName = opts?.$?.scopedClassName;
const mode = opts?.mode ?? 'mdx';
const syntaxHighlight = opts?.syntaxHighlight ?? 'prism';
const syntaxHighlight = opts?.syntaxHighlight ?? 'shiki';
const shikiConfig = opts?.shikiConfig ?? {};
const isMDX = mode === 'mdx';
const { headers, rehypeCollectHeaders } = createCollectHeaders();
@ -67,10 +67,10 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
parser.use([scopedStyles(scopedClassName)]);
}
if (syntaxHighlight === 'prism') {
if (syntaxHighlight === 'shiki') {
parser.use([await remarkShiki(shikiConfig, scopedClassName)]);
} else if (syntaxHighlight === 'prism') {
parser.use([remarkPrism(scopedClassName)]);
} else if (syntaxHighlight === 'shiki') {
parser.use([await remarkShiki(shikiConfig)]);
}
parser.use([[markdownToHtml as any, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression', 'mdxJsxTextElement', 'mdxJsxFlowElement'] }]]);

View file

@ -36,7 +36,7 @@ export interface ShikiConfig {
*/
const highlighterCache = new Map<string, shiki.Highlighter>();
const remarkShiki = async ({ langs = [], theme = 'github-dark', wrap = false }: ShikiConfig) => {
const remarkShiki = async ({ langs = [], theme = 'github-dark', wrap = false }: ShikiConfig, scopedClassName?: string | null) => {
const cacheID: string = typeof theme === 'string' ? theme : theme.name;
let highlighter = highlighterCache.get(cacheID);
if (!highlighter) {
@ -50,8 +50,14 @@ const remarkShiki = async ({ langs = [], theme = 'github-dark', wrap = false }:
visit(tree, 'code', (node) => {
let html = highlighter!.codeToHtml(node.value, { lang: node.lang ?? 'plaintext' });
// Q: Couldn't these regexes match on a user's inputted code blocks?
// A: Nope! All rendered HTML is properly escaped.
// Ex. If a user typed `<span class="line"` into a code block,
// It would become this before hitting our regexes:
// &lt;span class=&quot;line&quot;
// Replace "shiki" class naming with "astro" and add "is:raw".
html = html.replace('<pre class="shiki"', '<pre is:raw class="astro-code"');
html = html.replace('<pre class="shiki"', `<pre is:raw class="astro-code${scopedClassName ? ' ' + scopedClassName : ''}"`);
// Replace "shiki" css variable naming with "astro".
html = html.replace(/style="(background-)?color: var\(--shiki-/g, 'style="$1color: var(--astro-code-');
// Handle code wrapping
@ -62,6 +68,11 @@ const remarkShiki = async ({ langs = [], theme = 'github-dark', wrap = false }:
html = html.replace(/style="(.*?)"/, 'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"');
}
// Apply scopedClassName to all nested lines
if (scopedClassName) {
html = html.replace(/\<span class="line"\>/g, `<span class="line ${scopedClassName}"`);
}
node.type = 'html';
node.value = html;
node.children = [];

View file

@ -5,7 +5,7 @@ export type Plugin = string | [string, any] | unified.Plugin | [unified.Plugin,
export interface AstroMarkdownOptions {
mode?: 'md' | 'mdx';
syntaxHighlight?: 'prism' | 'shiki' | false;
syntaxHighlight?: 'shiki' | 'prism' | false;
shikiConfig?: ShikiConfig;
remarkPlugins?: Plugin[];
rehypePlugins?: Plugin[];