diff --git a/.changeset/strange-toes-remain.md b/.changeset/strange-toes-remain.md new file mode 100644 index 0000000000000..8c7141f5f3e08 --- /dev/null +++ b/.changeset/strange-toes-remain.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Adds compatibility for shiki languages with the `path` property diff --git a/packages/astro/components/Code.astro b/packages/astro/components/Code.astro index 281b16fb4018b..0a4fff6b98d98 100644 --- a/packages/astro/components/Code.astro +++ b/packages/astro/components/Code.astro @@ -1,4 +1,7 @@ --- +import path from 'node:path'; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; import type { BuiltinLanguage, BuiltinTheme, @@ -56,8 +59,25 @@ const { // shiki -> shikiji compat if (typeof lang === 'object') { - // `id` renamed to `name - if ((lang as any).id && !lang.name) { + // shikiji does not support `path` + // https://github.com/shikijs/shiki/blob/facb6ff37996129626f8066a5dccb4608e45f649/packages/shiki/src/loader.ts#L98 + const langPath = (lang as any).path; + if (langPath) { + // shiki resolves path from within its package directory :shrug: + const astroRoot = fileURLToPath(new URL('../', import.meta.url)); + const normalizedPath = path.isAbsolute(langPath) ? langPath : path.resolve(astroRoot, langPath); + try { + const content = fs.readFileSync(normalizedPath, 'utf-8'); + const parsed = JSON.parse(content); + Object.assign(lang, parsed); + } catch (e) { + throw new Error(`Unable to find language file at ${normalizedPath}`, { + cause: e, + }); + } + } + // `id` renamed to `name` (always override) + if ((lang as any).id) { lang.name = (lang as any).id; } // `grammar` flattened to lang itself diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 4ca70f85a81d7..289dfc698df19 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -10,7 +10,8 @@ import type { AstroUserConfig, ViteUserConfig } from '../../@types/astro.js'; import type { OutgoingHttpHeaders } from 'node:http'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; +import fs from 'node:fs'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { z } from 'zod'; import { appendForwardSlash, prependForwardSlash, removeTrailingForwardSlash } from '../path.js'; @@ -247,8 +248,27 @@ export const AstroConfigSchema = z.object({ for (const lang of langs) { // shiki -> shikiji compat if (typeof lang === 'object') { - // `id` renamed to `name - if ((lang as any).id && !lang.name) { + // shikiji does not support `path` + // https://github.com/shikijs/shiki/blob/facb6ff37996129626f8066a5dccb4608e45f649/packages/shiki/src/loader.ts#L98 + const langPath = (lang as any).path; + if (langPath) { + // shiki resolves path from within its package directory :shrug: + const astroRoot = fileURLToPath(new URL('../../../', import.meta.url)); + const normalizedPath = path.isAbsolute(langPath) + ? langPath + : path.resolve(astroRoot, langPath); + try { + const content = fs.readFileSync(normalizedPath, 'utf-8'); + const parsed = JSON.parse(content); + Object.assign(lang, parsed); + } catch (e) { + throw new Error(`Unable to find language file at ${normalizedPath}`, { + cause: e, + }); + } + } + // `id` renamed to `name` (always override) + if ((lang as any).id) { lang.name = (lang as any).id; } // `grammar` flattened to lang itself diff --git a/packages/astro/test/astro-markdown-shiki.test.js b/packages/astro/test/astro-markdown-shiki.test.js index f9c1d0dc44b33..6315edbffbad3 100644 --- a/packages/astro/test/astro-markdown-shiki.test.js +++ b/packages/astro/test/astro-markdown-shiki.test.js @@ -93,8 +93,12 @@ describe('Astro Markdown Shiki', () => { expect(segments[0].attribs.style).to.be.equal('color:#79B8FF'); expect(segments[1].attribs.style).to.be.equal('color:#E1E4E8'); - const unknownLang = $('.astro-code').last(); - expect(unknownLang.attr('style')).to.contain('background-color:#24292e;color:#e1e4e8;'); + const unknownLang = $('.astro-code').get(1); + expect(unknownLang.attribs.style).to.contain('background-color:#24292e;color:#e1e4e8;'); + + const caddyLang = $('.astro-code').last(); + const caddySegments = caddyLang.find('.line'); + expect(caddySegments.get(1).children[0].attribs.style).to.contain('color:#B392F0'); }); }); diff --git a/packages/astro/test/fixtures/astro-markdown-shiki/langs/astro.config.mjs b/packages/astro/test/fixtures/astro-markdown-shiki/langs/astro.config.mjs index 130596b0c4c53..9a5f2eced5523 100644 --- a/packages/astro/test/fixtures/astro-markdown-shiki/langs/astro.config.mjs +++ b/packages/astro/test/fixtures/astro-markdown-shiki/langs/astro.config.mjs @@ -13,6 +13,14 @@ export default { grammar: riGrammar, aliases: ['ri'], }, + { + id: 'caddy', + scopeName: 'source.Caddyfile', + // shiki compat: resolves from astro package directory. + // careful as astro is linked, this relative path is based on astro/packages/astro. + // it's weird but we're testing to prevent regressions. + path: './test/fixtures/astro-markdown-shiki/langs/src/caddyfile.tmLanguage.json', + }, ], }, }, diff --git a/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/caddyfile.tmLanguage.json b/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/caddyfile.tmLanguage.json new file mode 100644 index 0000000000000..8a9f87c870cb1 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/caddyfile.tmLanguage.json @@ -0,0 +1,365 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + + "name": "Caddyfile", + "fileTypes": ["Caddyfile"], + "scopeName": "source.Caddyfile", + + "patterns": [ + { "include": "#comments" }, + { "include": "#strings" }, + { "include": "#domains" }, + { "include": "#status_codes" }, + { "include": "#path" }, + { "include": "#global_options" }, + { "include": "#matchers" }, + { "include": "#directive" }, + { "include": "#site_block_common" } + ], + + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.line.Caddyfile", + "match": "\\s#.*" + }, + { + "name": "comment.line.Caddyfile", + "match": "^#.*" + } + ] + }, + + "strings": { + "patterns": [ + { + "comment": "Double Quoted Strings", + "begin": "\"", + "end": "\"", + "name": "string.quoted.double.Caddyfile", + "patterns": [ + { + "name": "constant.character.escape.Caddyfile", + "match": "\\\\\"" + } + ] + }, + { + "comment": "Backtick Strings", + "begin": "`", + "end": "`", + "name": "string.quoted.single.Caddyfile" + } + ] + }, + + "status_codes": { + "patterns": [ + { + "name": "constant.numeric.decimal", + "match": "\\s[0-9]{3}(?!\\.)" + } + ] + }, + + "path": { + "patterns": [ + { + "name": "keyword.control.caddyfile", + "match": "(unix/)*/[a-zA-Z0-9_\\-./*]+" + }, + { + "name": "variable.other.property.caddyfile", + "match": "\\*.[a-z]{1,5}" + }, + { + "name": "variable.other.property.caddyfile", + "match": "\\*/?" + }, + { + "name": "variable.other.property.caddyfile", + "match": "\\?/" + } + ] + }, + + "domains": { + "patterns": [ + { + "comment": "Domains and URLs", + "name": "keyword.control.caddyfile", + "match": "(https?://)*[a-z0-9-\\*]*(?:\\.[a-zA-Z]{2,})+(:[0-9]+)*\\S*" + }, + { + "comment": "localhost", + "name": "keyword.control.caddyfile", + "match": "localhost(:[0-9]+)*" + }, + { + "comment": "IPv4", + "name": "keyword.control.caddyfile", + "match": "((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + }, + { + "comment": "IPv6", + "name": "keyword.control.caddyfile", + "match": "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))" + }, + { + "comment": "Ports", + "name": "keyword.control.caddyfile", + "match": ":[0-9]+" + } + ] + }, + + "global_options": { + "patterns": [ + { + "begin": "^(\\{)$", + "end": "^(\\})$", + + "beginCaptures": { + "0": { "name": "punctuation.definition.dictionary.begin" } + }, + + "endCaptures": { + "0": { "name": "punctuation.definition.dictionary.end" } + }, + + "patterns": [ + { "include": "#comments" }, + { + "name": "support.constant.Caddyfile", + "match": "^\\s*(debug|https?_port|default_bind|order|storage|storage_clean_interval|renew_interval|ocsp_interval|admin|log|grace_period|shutdown_delay|auto_https|email|default_sni|local_certs|skip_install_trust|acme_ca|acme_ca_root|acme_eab|acme_dns|on_demand_tls|key_type|cert_issuer|ocsp_stapling|preferred_chains|servers|pki|events)" + } + ] + } + ] + }, + + "site_block_common": { + "patterns": [{ "include": "#placeholders" }, { "include": "#block" }] + }, + + "matchers": { + "patterns": [ + { + "comment": "Matchers", + "name": "support.function.Caddyfile", + "match": "@[^\\s]+(?=\\s)" + } + ] + }, + + "placeholders": { + "patterns": [ + { + "name": "keyword.control.Caddyfile", + "match": "\\{[\\[\\]\\w.\\$+-]+\\}" + } + ] + }, + + "directive": { + "patterns": [ + { + "name": "entity.name.function.Caddyfile", + "match": "^\\s*[a-zA-Z_\\-+]+" + }, + { "include": "#content_types" }, + { "include": "#heredoc" } + ] + }, + + "content_types": { + "patterns": [ + { + "comment": "Content Types", + "name": "variable.other.property.caddyfile", + "match": "(application|audio|example|font|image|message|model|multipart|text|video)/[a-zA-Z0-9*+\\-.]+;* *[a-zA-Z0-9=\\-]*" + } + ] + }, + + "block": { + "patterns": [ + { + "begin": "\\{", + "end": "\\}", + + "patterns": [{ "include": "#block_content" }] + } + ] + }, + + "block_content": { + "patterns": [ + { + "patterns": [ + { "include": "#comments" }, + { "include": "#strings" }, + { "include": "#domains" }, + { "include": "#status_codes" }, + { "include": "#path" }, + { "include": "#matchers" }, + { "include": "#placeholders" }, + { "include": "#directive" }, + { "include": "#block" } + ] + } + ] + }, + + "heredoc": { + "patterns": [ + { + "begin": "(?i)(?=<<\\s*([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)\\s*$)", + "end": "(?!\\G)", + "name": "string.unquoted.heredoc.caddyfile", + "patterns": [{ "include": "#heredoc_interior" }] + } + ] + }, + + "heredoc_interior": { + "patterns": [ + { + "comment": "CSS", + + "name": "meta.embedded.css", + "contentName": "source.css", + + "begin": "(<<)\\s*(CSS)(\\s*)$", + "beginCaptures": { + "0": { "name": "punctuation.section.embedded.begin.caddyfile" }, + "1": { "name": "punctuation.definition.string.begin" }, + "2": { "name": "keyword.operator.heredoc.caddyfile" }, + "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" } + }, + + "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])", + "endCaptures": { + "0": { "name": "punctuation.section.embedded.end.caddyfile" }, + "1": { "name": "keyword.operator.heredoc.caddyfile" } + }, + + "patterns": [{ "include": "source.css" }] + }, + + { + "comment": "HTML", + + "name": "meta.embedded.html", + "contentName": "text.html", + + "begin": "(<<)\\s*(HTML)(\\s*)$", + "beginCaptures": { + "0": { "name": "punctuation.section.embedded.begin.caddyfile" }, + "1": { "name": "punctuation.definition.string.begin" }, + "2": { "name": "keyword.operator.heredoc.caddyfile" }, + "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" } + }, + + "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])", + "endCaptures": { + "0": { "name": "punctuation.section.embedded.end.caddyfile" }, + "1": { "name": "keyword.operator.heredoc.caddyfile" } + }, + + "patterns": [{ "include": "text.html.basic" }] + }, + + { + "comment": "JavaScript", + + "name": "meta.embedded.js", + "contentName": "source.js", + + "begin": "(<<)\\s*(JAVASCRIPT|JS)(\\s*)$", + "beginCaptures": { + "0": { "name": "punctuation.section.embedded.begin.caddyfile" }, + "1": { "name": "punctuation.definition.string.begin" }, + "2": { "name": "keyword.operator.heredoc.caddyfile" }, + "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" } + }, + + "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])", + "endCaptures": { + "0": { "name": "punctuation.section.embedded.end.caddyfile" }, + "1": { "name": "keyword.operator.heredoc.caddyfile" } + }, + + "patterns": [{ "include": "source.js" }] + }, + + { + "comment": "JSON", + + "name": "meta.embedded.json", + "contentName": "source.json", + + "begin": "(<<)\\s*(JSON)(\\s*)$", + "beginCaptures": { + "0": { "name": "punctuation.section.embedded.begin.caddyfile" }, + "1": { "name": "punctuation.definition.string.begin" }, + "2": { "name": "keyword.operator.heredoc.caddyfile" }, + "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" } + }, + + "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])", + "endCaptures": { + "0": { "name": "punctuation.section.embedded.end.caddyfile" }, + "1": { "name": "keyword.operator.heredoc.caddyfile" } + }, + + "patterns": [{ "include": "source.json" }] + }, + + { + "comment": "XML", + + "name": "meta.embedded.xml", + "contentName": "text.xml", + + "begin": "(<<)\\s*(XML)(\\s*)$", + "beginCaptures": { + "0": { "name": "punctuation.section.embedded.begin.caddyfile" }, + "1": { "name": "punctuation.definition.string.begin" }, + "2": { "name": "keyword.operator.heredoc.caddyfile" }, + "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" } + }, + + "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])", + "endCaptures": { + "0": { "name": "punctuation.section.embedded.end.caddyfile" }, + "1": { "name": "keyword.operator.heredoc.caddyfile" } + }, + + "patterns": [{ "include": "text.xml" }] + }, + + { + "comment": "Any other heredoc", + + "begin": "(?i)(<<)\\s*([a-z_\\x{7f}-\\x{10ffff}]+[a-z0-9_\\x{7f}-\\x{10ffff}]*)(\\s*)", + "beginCaptures": { + "0": { "name": "punctuation.section.embedded.begin.caddyfile" }, + "1": { "name": "punctuation.definition.string.caddyfile" }, + "2": { "name": "keyword.operator.heredoc.caddyfile" }, + "4": { "name": "invalid.illegal.trailing-whitespace.caddyfile" } + }, + + "end": "^\\s*(\\2)(?![A-Za-z0-9_\\x{7f}-\\x{10ffff}])", + "endCaptures": { + "0": { "name": "punctuation.section.embedded.end.caddyfile" }, + "1": { "name": "keyword.operator.heredoc.caddyfile" } + }, + + "patterns": [] + } + ] + } + } +} diff --git a/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md b/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md index d2d756b95dc1f..cdd74060f3a89 100644 --- a/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md +++ b/packages/astro/test/fixtures/astro-markdown-shiki/langs/src/pages/index.md @@ -24,3 +24,12 @@ fin ```unknown This language does not exist ``` + +```caddy +example.com { + root * /var/www/wordpress + encode gzip + php_fastcgi unix//run/php/php-version-fpm.sock + file_server +} +```