diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8bd542a..deb595c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -94,7 +94,7 @@ Extracts the palette + scale from `ShellDocs.Components/wwwroot/shelldocs-theme. - **ShellUI integration path (NuGet install):** ShellUI's RCL detects `ShellDocs.Tokens` at runtime and skips emitting its own token file. Deferred to Phase 3 — needs a small opt-in flag on `AddShellUI()`. - Unit tests: token file emits, dark-mode class toggling, no duplicate declarations across bundles -### `feat/codeblock-shiki` +### ✅ `feat/codeblock-shiki` — shipped Ships to `ShellDocs.Components`. - `CodeBlock` component — takes `Language`, `Code`, `Filename`, `HighlightLines` diff --git a/examples/ShellDocs.Preview/Components/App.razor b/examples/ShellDocs.Preview/Components/App.razor index 3b4dda7..23ae0c1 100644 --- a/examples/ShellDocs.Preview/Components/App.razor +++ b/examples/ShellDocs.Preview/Components/App.razor @@ -9,8 +9,6 @@ - - - - - - - - - + diff --git a/src/ShellDocs.Components/Content/PreviewFrame.razor b/src/ShellDocs.Components/Content/PreviewFrame.razor index 7807917..41d14cd 100644 --- a/src/ShellDocs.Components/Content/PreviewFrame.razor +++ b/src/ShellDocs.Components/Content/PreviewFrame.razor @@ -37,8 +37,8 @@ } else { - @* Prism ships no razor grammar; markup/html grammar handles the tag+attr+string structure just fine. *@ -
@Preview!.Code
+ @* Shiki ships a razor grammar — full razor highlighting for previews. *@ +
@Preview!.Code
} diff --git a/src/ShellDocs.Components/wwwroot/shelldocs-theme.css b/src/ShellDocs.Components/wwwroot/shelldocs-theme.css index 4e4570a..8bfd23b 100644 --- a/src/ShellDocs.Components/wwwroot/shelldocs-theme.css +++ b/src/ShellDocs.Components/wwwroot/shelldocs-theme.css @@ -164,10 +164,10 @@ body { margin: 0; min-height: 100vh; } font-feature-settings: 'ss01', 'cv02'; } -/* Prism.js override — let Prism color tokens, keep our frame chrome */ -.shelldocs-codeblock pre[class*="language-"], -.shelldocs-prose pre[class*="language-"] { - background: transparent !important; +/* Shiki output — normalise its inline styles into our chrome. */ +.shelldocs-codeblock pre.shiki, +.shelldocs-prose pre.shiki, +.preview-body pre.shiki { margin: 0 !important; padding: 1rem 1.125rem !important; border: 0 !important; @@ -175,14 +175,21 @@ body { margin: 0; min-height: 100vh; } font-family: var(--font-mono) !important; font-size: 0.85rem !important; line-height: 1.7 !important; - text-shadow: none !important; + overflow-x: auto; } -.shelldocs-codeblock code[class*="language-"], -.shelldocs-prose code[class*="language-"] { +pre.shiki code { background: transparent !important; - border: 0 !important; padding: 0 !important; + border: 0 !important; font-family: var(--font-mono) !important; font-size: inherit !important; - text-shadow: none !important; } + +/* Dual-theme: Shiki emits --shiki-light + --shiki-dark on each token span + with defaultColor:false. Only set `color` — NEVER background on spans, or + every .line wrapper paints a bg strip across the block. */ +pre.shiki { background-color: transparent !important; } +pre.shiki, +pre.shiki span { color: var(--shiki-light); } +:root.dark pre.shiki, +:root.dark pre.shiki span { color: var(--shiki-dark); } diff --git a/src/ShellDocs.Components/wwwroot/shelldocs.js b/src/ShellDocs.Components/wwwroot/shelldocs.js index aaec54d..1884383 100644 --- a/src/ShellDocs.Components/wwwroot/shelldocs.js +++ b/src/ShellDocs.Components/wwwroot/shelldocs.js @@ -1,21 +1,51 @@ window.ShellDocs = window.ShellDocs || {}; -// Re-highlight all code blocks. Called from MarkdownContent after render. -window.shelldocsHighlight = function () { - if (window.Prism) { - try { window.Prism.highlightAll(); } catch (e) {} +/* Shiki-backed highlighters. Both functions replace

+   with Shiki's rendered 
 so we get VSCode-parity colouring. Idempotent —
+   a data-shiki flag prevents re-highlighting. */
+
+function langOf(codeEl) {
+    var cls = (codeEl.className || '').split(/\s+/);
+    for (var i = 0; i < cls.length; i++) {
+        if (cls[i].indexOf('language-') === 0) return cls[i].substring(9);
     }
-};
+    return null;
+}
 
-// Highlight a specific 
 element — used by PreviewFrame when the code tab
-// mounts, so we don't re-scan the whole page on every tab flip.
-window.shelldocsHighlightElement = function (preEl) {
-    if (!preEl || !window.Prism) return;
+function highlightOne(preEl) {
+    if (!preEl || !window.__shiki) return;
+    if (preEl.dataset.shiki === 'done') return;
     var code = preEl.querySelector('code');
     if (!code) return;
-    try { window.Prism.highlightElement(code); } catch (e) {}
+    var lang = langOf(code);
+    if (!lang) return;
+    /* Shiki doesn't know every Prism alias — silently fall back. */
+    if (!window.__shiki.getLoadedLanguages().includes(lang)) return;
+
+    var source = code.textContent;
+    try {
+        var html = window.__shiki.codeToHtml(source, {
+            lang: lang,
+            themes: { light: 'github-light', dark: 'github-dark' },
+            defaultColor: false
+        });
+        var tpl = document.createElement('template');
+        tpl.innerHTML = html.trim();
+        var newPre = tpl.content.firstElementChild;
+        if (!newPre) return;
+        newPre.dataset.shiki = 'done';
+        preEl.parentNode.replaceChild(newPre, preEl);
+    } catch (e) { /* skip on grammar error */ }
+}
+
+window.shelldocsHighlight = function () {
+    if (!window.__shiki) return;
+    document.querySelectorAll('pre:not([data-shiki]) > code[class*="language-"]')
+        .forEach(function (code) { highlightOne(code.parentElement); });
 };
 
+window.shelldocsHighlightElement = function (preEl) { highlightOne(preEl); };
+
 // Copy-to-clipboard for code blocks.
 window.shelldocsCopyCode = function (button) {
     var block = button.closest('.shelldocs-codeblock');