From 0a3d3e51a66af80fa949ba0f5e2104439d2be634 Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Fri, 18 Mar 2022 17:29:51 -0400 Subject: [PATCH] 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 --- .changeset/lemon-needles-count.md | 6 + examples/blog/src/styles/blog.css | 21 +-- examples/docs/src/components/HeadCommon.astro | 1 - examples/docs/src/styles/code.css | 96 ---------- examples/docs/src/styles/index.css | 28 ++- examples/with-markdown/src/styles/global.css | 169 +----------------- packages/astro/test/astro-markdown.test.js | 36 +++- packages/markdown/remark/src/index.ts | 8 +- packages/markdown/remark/src/remark-shiki.ts | 15 +- packages/markdown/remark/src/types.ts | 2 +- 10 files changed, 70 insertions(+), 312 deletions(-) create mode 100644 .changeset/lemon-needles-count.md delete mode 100644 examples/docs/src/styles/code.css diff --git a/.changeset/lemon-needles-count.md b/.changeset/lemon-needles-count.md new file mode 100644 index 000000000000..c7770447dcd6 --- /dev/null +++ b/.changeset/lemon-needles-count.md @@ -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. diff --git a/examples/blog/src/styles/blog.css b/examples/blog/src/styles/blog.css index 12bf5390b73d..234e0162a669 100644 --- a/examples/blog/src/styles/blog.css +++ b/examples/blog/src/styles/blog.css @@ -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; } diff --git a/examples/docs/src/components/HeadCommon.astro b/examples/docs/src/components/HeadCommon.astro index 4906aaf7fd81..21504cf8953a 100644 --- a/examples/docs/src/components/HeadCommon.astro +++ b/examples/docs/src/components/HeadCommon.astro @@ -1,6 +1,5 @@ --- import '../styles/theme.css'; -import '../styles/code.css'; import '../styles/index.css'; --- diff --git a/examples/docs/src/styles/code.css b/examples/docs/src/styles/code.css deleted file mode 100644 index b4275adab256..000000000000 --- a/examples/docs/src/styles/code.css +++ /dev/null @@ -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); -} diff --git a/examples/docs/src/styles/index.css b/examples/docs/src/styles/index.css index ad0a5adf71b5..971ccf9e5a81 100644 --- a/examples/docs/src/styles/index.css +++ b/examples/docs/src/styles/index.css @@ -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); } diff --git a/examples/with-markdown/src/styles/global.css b/examples/with-markdown/src/styles/global.css index 577e061827ad..ac9323747719 100644 --- a/examples/with-markdown/src/styles/global.css +++ b/examples/with-markdown/src/styles/global.css @@ -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; -} - -.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; +pre.astro-code > code { + all: unset; } -.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; diff --git a/packages/astro/test/astro-markdown.test.js b/packages/astro/test/astro-markdown.test.js index 2bccf8d87277..44315a510f94 100644 --- a/packages/astro/test/astro-markdown.test.js +++ b/packages/astro/test/astro-markdown.test.js @@ -57,18 +57,36 @@ describe('Astro Markdown', () => { const html = await fixture.readFile('/scopedStyles-code/index.html'); const $ = cheerio.load(html); - // test 1:
 tag has scopedStyle class passed down
-		expect($('pre').is('[class]')).to.equal(true);
-		expect($('pre').attr('class').split(' ').length).to.equal(2);
+		// test 1: 
 tag has correct shiki class
+		expect($('pre').hasClass('astro-code')).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 2: 
 tag has correct language
-		expect($('pre').hasClass('language-js')).to.equal(true);
+	function isAstroScopedClass(cls) {
+		return /^astro-.*/.test(cls)
+	}
 
-		// test 3:  tag has correct language
-		expect($('code').hasClass('language-js')).to.equal(true);
+	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 4: There are child spans in code blocks
-		expect($('code span').length).to.be.greaterThan(0);
+		// 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 () => {
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts
index e8a315ef17b0..3ea436795b5b 100644
--- a/packages/markdown/remark/src/index.ts
+++ b/packages/markdown/remark/src/index.ts
@@ -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'] }]]);
diff --git a/packages/markdown/remark/src/remark-shiki.ts b/packages/markdown/remark/src/remark-shiki.ts
index 5bee7ef6e7bd..ebbe4032c11a 100644
--- a/packages/markdown/remark/src/remark-shiki.ts
+++ b/packages/markdown/remark/src/remark-shiki.ts
@@ -36,7 +36,7 @@ export interface ShikiConfig {
  */
 const highlighterCache = new Map();
 
-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 `/g, `