|
35 | 35 | --shadow-sm: 0 1px 2px rgba(15,15,15,0.04); |
36 | 36 | --shadow: 0 1px 2px rgba(15,15,15,0.04), 0 1px 4px rgba(15,15,15,0.04); |
37 | 37 | --shadow-pop:0 12px 32px rgba(15,15,15,0.10), 0 0 0 1px rgba(15,15,15,0.05); |
| 38 | + /* C# syntax highlighting — matches the docs site's Prism "github" light theme. */ |
| 39 | + --syn-plain: #393a34; |
| 40 | + --syn-comment: #999988; |
| 41 | + --syn-string: #e3116c; |
| 42 | + --syn-keyword: #00009f; |
| 43 | + --syn-number: #36acaa; |
| 44 | + --syn-func: #d73a49; |
| 45 | + --syn-type: #6f42c1; |
38 | 46 | } |
39 | 47 | :root[data-theme="dark"] { |
40 | 48 | --bg: #0a0a0a; |
|
65 | 73 | --shadow-sm: 0 1px 2px rgba(0,0,0,0.4); |
66 | 74 | --shadow: 0 1px 2px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.02); |
67 | 75 | --shadow-pop:0 24px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04); |
| 76 | + /* C# syntax highlighting — matches the docs site's Prism "dracula" dark theme. */ |
| 77 | + --syn-plain: #f8f8f2; |
| 78 | + --syn-comment: #6272a4; |
| 79 | + --syn-string: #ff79c6; |
| 80 | + --syn-keyword: #bd93f9; |
| 81 | + --syn-number: #bd93f9; |
| 82 | + --syn-func: #50fa7b; |
| 83 | + --syn-type: #8be9fd; |
68 | 84 | } |
69 | 85 |
|
70 | 86 | *, *::before, *::after { box-sizing: border-box; } |
|
587 | 603 | user-select: none; min-width: 3ch; text-align: right; |
588 | 604 | color: var(--text-dim); opacity: 0.5; margin-right: 16px; flex-shrink: 0; |
589 | 605 | } |
590 | | - .source-snippet-code .line-content { white-space: pre; color: var(--text); } |
| 606 | + .source-snippet-code .line-content { white-space: pre; color: var(--syn-plain); } |
| 607 | + /* Baked-in "good enough" C# syntax highlighting. Token colors are theme-aware |
| 608 | + and switch with the light/dark toggle via the --syn-* custom properties. */ |
| 609 | + .source-snippet-code .tok-comment { color: var(--syn-comment); font-style: italic; } |
| 610 | + .source-snippet-code .tok-string { color: var(--syn-string); } |
| 611 | + .source-snippet-code .tok-keyword { color: var(--syn-keyword); } |
| 612 | + .source-snippet-code .tok-number { color: var(--syn-number); } |
| 613 | + .source-snippet-code .tok-func { color: var(--syn-func); } |
| 614 | + .source-snippet-code .tok-type { color: var(--syn-type); } |
591 | 615 |
|
592 | 616 | /* ============================== run view ============================== */ |
593 | 617 | .run-wrap { max-width: 1280px; margin: 0 auto; padding: 28px 28px 80px; } |
@@ -1684,6 +1708,125 @@ <h2>Categories</h2> |
1684 | 1708 | const tmpl = (t.source.endLine && t.source.endLine > t.source.line && sl.rangeUrl) ? sl.rangeUrl : sl.lineUrl; |
1685 | 1709 | return fillSourceTemplate(tmpl, t); |
1686 | 1710 | } |
| 1711 | +// ── Baked-in "good enough" C# syntax highlighter ────────────────────────── |
| 1712 | +// A small hand-rolled tokenizer. Not a full C# parser — it deliberately trades |
| 1713 | +// edge-case accuracy for zero dependencies and a tiny footprint. Output token |
| 1714 | +// classes map to theme-aware --syn-* colors so highlighting follows the toggle. |
| 1715 | +const CS_KEYWORDS = new Set(( |
| 1716 | + 'abstract as async await base bool break byte case catch char checked class const ' + |
| 1717 | + 'continue decimal default delegate do double else enum event explicit extern false ' + |
| 1718 | + 'finally fixed float for foreach goto if implicit in int interface internal is lock ' + |
| 1719 | + 'long namespace new null object operator out override params private protected public ' + |
| 1720 | + 'readonly ref return sbyte sealed short sizeof stackalloc static string struct switch ' + |
| 1721 | + 'this throw true try typeof uint ulong unchecked unsafe ushort using virtual void ' + |
| 1722 | + 'volatile while ' + |
| 1723 | + // Contextual keywords — limited to ones that are nearly always keywords, so we |
| 1724 | + // don't miscolor common identifiers like `value`, `from`, `select`, `on`, `by`. |
| 1725 | + 'and dynamic file get global init nameof nint not notnull nuint or ' + |
| 1726 | + 'partial record required scoped set unmanaged var when where with yield' |
| 1727 | +).split(' ')); |
| 1728 | + |
| 1729 | +function highlightCSharpLines(code) { |
| 1730 | + // Tokenize the whole snippet (so multi-line comments / verbatim strings are |
| 1731 | + // handled), then split tokens across newlines into one HTML string per line. |
| 1732 | + const lines = [[]]; |
| 1733 | + const push = (cls, text) => { |
| 1734 | + const parts = text.split('\n'); |
| 1735 | + for (let i = 0; i < parts.length; i++) { |
| 1736 | + if (i > 0) lines.push([]); |
| 1737 | + if (parts[i] === '') continue; |
| 1738 | + const safe = esc(parts[i]); |
| 1739 | + lines[lines.length - 1].push(cls ? `<span class="${cls}">${safe}</span>` : safe); |
| 1740 | + } |
| 1741 | + }; |
| 1742 | + const n = code.length; |
| 1743 | + let i = 0; |
| 1744 | + while (i < n) { |
| 1745 | + const c = code[i]; |
| 1746 | + const c2 = code[i + 1]; |
| 1747 | + // line comment / xml-doc comment |
| 1748 | + if (c === '/' && c2 === '/') { |
| 1749 | + let j = i + 2; while (j < n && code[j] !== '\n') j++; |
| 1750 | + push('tok-comment', code.slice(i, j)); i = j; continue; |
| 1751 | + } |
| 1752 | + // block comment |
| 1753 | + if (c === '/' && c2 === '*') { |
| 1754 | + let j = i + 2; while (j < n && !(code[j] === '*' && code[j + 1] === '/')) j++; |
| 1755 | + j = Math.min(n, j + 2); |
| 1756 | + push('tok-comment', code.slice(i, j)); i = j; continue; |
| 1757 | + } |
| 1758 | + // raw string literal: """ ... """ (optionally interpolated: $"""), 3+ quotes |
| 1759 | + if (c === '"' && c2 === '"' && code[i + 2] === '"') { |
| 1760 | + let q = 0; while (code[i + q] === '"') q++; |
| 1761 | + const fence = '"'.repeat(q); |
| 1762 | + let j = code.indexOf(fence, i + q); |
| 1763 | + j = j === -1 ? n : j + q; |
| 1764 | + push('tok-string', code.slice(i, j)); i = j; continue; |
| 1765 | + } |
| 1766 | + // verbatim / interpolated-verbatim string: @"...", $@"...", @$"..." |
| 1767 | + if (c === '@' && c2 === '"') { i = readVerbatim(code, i, 1, push); continue; } |
| 1768 | + if ((c === '$' && c2 === '@') || (c === '@' && c2 === '$')) { |
| 1769 | + if (code[i + 2] === '"') { i = readVerbatim(code, i, 2, push); continue; } |
| 1770 | + } |
| 1771 | + // regular or interpolated string: "..." / $"..." |
| 1772 | + if (c === '"' || (c === '$' && c2 === '"')) { |
| 1773 | + const start = c === '$' ? i + 1 : i; |
| 1774 | + let j = start + 1; |
| 1775 | + while (j < n && code[j] !== '"' && code[j] !== '\n') { |
| 1776 | + if (code[j] === '\\') j++; |
| 1777 | + j++; |
| 1778 | + } |
| 1779 | + j = Math.min(n, j + 1); |
| 1780 | + push('tok-string', code.slice(i, j)); i = j; continue; |
| 1781 | + } |
| 1782 | + // char literal |
| 1783 | + if (c === "'") { |
| 1784 | + let j = i + 1; |
| 1785 | + while (j < n && code[j] !== "'" && code[j] !== '\n') { |
| 1786 | + if (code[j] === '\\') j++; |
| 1787 | + j++; |
| 1788 | + } |
| 1789 | + j = Math.min(n, j + 1); |
| 1790 | + push('tok-string', code.slice(i, j)); i = j; continue; |
| 1791 | + } |
| 1792 | + // number |
| 1793 | + if (c >= '0' && c <= '9') { |
| 1794 | + const m = /^(?:0[xX][0-9a-fA-F_]+|0[bB][01_]+|[0-9][0-9_]*(?:\.[0-9_]+)?(?:[eE][+-]?[0-9]+)?)[fFdDmMuUlL]*/.exec(code.slice(i)); |
| 1795 | + const tok = m ? m[0] : c; |
| 1796 | + push('tok-number', tok); i += tok.length; continue; |
| 1797 | + } |
| 1798 | + // identifier / keyword (allow leading @ verbatim identifier) |
| 1799 | + if (/[A-Za-z_@]/.test(c)) { |
| 1800 | + const m = /^@?[A-Za-z_][A-Za-z0-9_]*/.exec(code.slice(i)); |
| 1801 | + const word = m[0]; |
| 1802 | + const bare = word[0] === '@' ? word.slice(1) : word; |
| 1803 | + let k = i + word.length; while (k < n && /\s/.test(code[k])) k++; |
| 1804 | + let cls = ''; |
| 1805 | + if (CS_KEYWORDS.has(bare)) cls = 'tok-keyword'; |
| 1806 | + else if (code[k] === '(') cls = 'tok-func'; // call / declaration |
| 1807 | + else if (/^[A-Z]/.test(bare)) cls = 'tok-type'; // PascalCase ⇒ type |
| 1808 | + push(cls, word); i += word.length; continue; |
| 1809 | + } |
| 1810 | + // anything else (punctuation, operators, whitespace) → plain |
| 1811 | + push('', c); i++; |
| 1812 | + } |
| 1813 | + return lines.map(parts => parts.join('')); |
| 1814 | +} |
| 1815 | +function readVerbatim(code, i, prefix, push) { |
| 1816 | + // prefix = number of chars before the opening quote (@ ⇒ 1, $@/@$ ⇒ 2). |
| 1817 | + // In verbatim strings a quote is escaped by doubling it (""). |
| 1818 | + const n = code.length; |
| 1819 | + let j = i + prefix + 1; |
| 1820 | + while (j < n) { |
| 1821 | + if (code[j] === '"') { |
| 1822 | + if (code[j + 1] === '"') { j += 2; continue; } |
| 1823 | + j++; break; |
| 1824 | + } |
| 1825 | + j++; |
| 1826 | + } |
| 1827 | + push('tok-string', code.slice(i, Math.min(n, j))); |
| 1828 | + return Math.min(n, j); |
| 1829 | +} |
1687 | 1830 | const _snippetCache = new Map(); |
1688 | 1831 | const SNIPPET_FALLBACK_LINES = 40; |
1689 | 1832 | function loadSourceSnippet(detail) { |
@@ -1712,10 +1855,14 @@ <h2>Categories</h2> |
1712 | 1855 | const snippet = lines.slice(from, to); |
1713 | 1856 | const fileName = path.split('/').pop(); |
1714 | 1857 | const rangeLabel = `lines ${startLine}–${from + snippet.length}`; |
| 1858 | + // Highlight C# files; fall back to plain (escaped) text for anything else. |
| 1859 | + const htmlLines = /\.cs$/i.test(fileName) |
| 1860 | + ? highlightCSharpLines(snippet.join('\n')) |
| 1861 | + : snippet.map(esc); |
1715 | 1862 | container.innerHTML = |
1716 | 1863 | `<div class="source-snippet-header"><span>${esc(fileName)} (${rangeLabel})</span></div>` + |
1717 | 1864 | `<div class="source-snippet-code">${snippet.map((l, i) => |
1718 | | - `<div class="line"><span class="line-num">${from + i + 1}</span><span class="line-content">${esc(l)}</span></div>` |
| 1865 | + `<div class="line"><span class="line-num">${from + i + 1}</span><span class="line-content">${htmlLines[i] || ''}</span></div>` |
1719 | 1866 | ).join('')}</div>`; |
1720 | 1867 | }; |
1721 | 1868 |
|
|
0 commit comments