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:
parent
1cd7184ca6
commit
0a3d3e51a6
10 changed files with 70 additions and 312 deletions
6
.changeset/lemon-needles-count.md
Normal file
6
.changeset/lemon-needles-count.md
Normal 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.
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
import '../styles/theme.css';
|
||||
import '../styles/code.css';
|
||||
import '../styles/index.css';
|
||||
---
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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'] }]]);
|
||||
|
|
|
@ -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:
|
||||
// <span class="line"
|
||||
|
||||
// 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 = [];
|
||||
|
|
|
@ -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[];
|
||||
|
|
Loading…
Reference in a new issue