From 3988bbcc9ead0b9af60b8a8749a0ad25c686bde3 Mon Sep 17 00:00:00 2001
From: Bjorn Lu <bjornlu.dev@gmail.com>
Date: Mon, 6 Nov 2023 17:58:42 +0800
Subject: [PATCH] Add shiki lang path compat (#8996)

---
 .changeset/strange-toes-remain.md             |   5 +
 packages/astro/components/Code.astro          |  24 +-
 packages/astro/src/core/config/schema.ts      |  26 +-
 .../astro/test/astro-markdown-shiki.test.js   |   8 +-
 .../langs/astro.config.mjs                    |   8 +
 .../langs/src/caddyfile.tmLanguage.json       | 365 ++++++++++++++++++
 .../langs/src/pages/index.md                  |   9 +
 7 files changed, 438 insertions(+), 7 deletions(-)
 create mode 100644 .changeset/strange-toes-remain.md
 create mode 100644 packages/astro/test/fixtures/astro-markdown-shiki/langs/src/caddyfile.tmLanguage.json

diff --git a/.changeset/strange-toes-remain.md b/.changeset/strange-toes-remain.md
new file mode 100644
index 0000000000..8c7141f5f3
--- /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 281b16fb40..0a4fff6b98 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 4ca70f85a8..289dfc698d 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 f9c1d0dc44..6315edbffb 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 130596b0c4..9a5f2eced5 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 0000000000..8a9f87c870
--- /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 d2d756b95d..cdd74060f3 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
+}
+```