diff --git a/.changeset/lemon-needles-count.md b/.changeset/lemon-needles-count.md new file mode 100644 index 000000000..c7770447d --- /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 12bf5390b..234e0162a 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 4906aaf7f..21504cf89 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 b4275adab..000000000 --- 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 ad0a5adf7..971ccf9e5 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 577e06182..ac9323747 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; +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; diff --git a/packages/astro/test/astro-markdown.test.js b/packages/astro/test/astro-markdown.test.js index 2bccf8d87..44315a510 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: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: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 () => { diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index e8a315ef1..3ea436795 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 5bee7ef6e..ebbe4032c 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, `