Skip to content

Commit ae2c441

Browse files
committed
design: add syntax highlighting to code examples
1 parent 34bec5c commit ae2c441

3 files changed

Lines changed: 213 additions & 6 deletions

File tree

app.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,174 @@ function setupCopyButtons() {
2626
}
2727
}
2828

29+
function escapeHtml(value) {
30+
return value
31+
.replaceAll("&", "&")
32+
.replaceAll("<", "&lt;")
33+
.replaceAll(">", "&gt;");
34+
}
35+
36+
function applyPatternSegments(input, pattern, className) {
37+
const segments = [];
38+
let output = "";
39+
let lastIndex = 0;
40+
let match;
41+
42+
const regex = new RegExp(pattern.source, pattern.flags);
43+
44+
while ((match = regex.exec(input)) !== null) {
45+
const [fullMatch] = match;
46+
const startIndex = match.index;
47+
const endIndex = startIndex + fullMatch.length;
48+
49+
output += input.slice(lastIndex, startIndex);
50+
const token = `__TOKEN_${segments.length}__`;
51+
segments.push(`<span class="${className}">${fullMatch}</span>`);
52+
output += token;
53+
lastIndex = endIndex;
54+
55+
if (fullMatch.length === 0) {
56+
regex.lastIndex += 1;
57+
}
58+
}
59+
60+
output += input.slice(lastIndex);
61+
62+
return {
63+
output,
64+
segments,
65+
};
66+
}
67+
68+
function restorePatternSegments(input, segments) {
69+
return segments.reduce(
70+
(restored, segment, index) => restored.replace(`__TOKEN_${index}__`, segment),
71+
input,
72+
);
73+
}
74+
75+
function highlightShellLine(line) {
76+
if (/^\s*#/.test(line)) {
77+
return `<span class="token-comment">${escapeHtml(line)}</span>`;
78+
}
79+
80+
let html = escapeHtml(line);
81+
82+
const protectedSegments = [
83+
{ pattern: /\$\{\{[^}]+\}\}/g, className: "token-variable" },
84+
{ pattern: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, className: "token-string" },
85+
{ pattern: /\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*/g, className: "token-variable" },
86+
];
87+
88+
const segmentGroups = [];
89+
90+
for (const entry of protectedSegments) {
91+
const result = applyPatternSegments(html, entry.pattern, entry.className);
92+
html = result.output;
93+
segmentGroups.push(result.segments);
94+
}
95+
96+
html = html
97+
.replace(/(^|\s)(--[A-Za-z0-9-]+)/g, '$1<span class="token-flag">$2</span>')
98+
.replace(
99+
/(^|\s)(curl|bash|mkdir|printf|chmod|rsync|echo|if|then|fi|do|done|for|in)(?=\s|$)/g,
100+
'$1<span class="token-keyword">$2</span>',
101+
)
102+
.replace(/(^|\s)([A-Z][A-Z0-9_]*)(=)/g, '$1<span class="token-key">$2</span><span class="token-punctuation">$3</span>');
103+
104+
for (let index = segmentGroups.length - 1; index >= 0; index -= 1) {
105+
html = restorePatternSegments(html, segmentGroups[index]);
106+
}
107+
108+
return html;
109+
}
110+
111+
function highlightEnvLine(line) {
112+
const escapedLine = escapeHtml(line);
113+
const match = escapedLine.match(/^([A-Z][A-Z0-9_]*)(=)(.*)$/);
114+
115+
if (!match) {
116+
return escapedLine;
117+
}
118+
119+
const [, key, separator, value] = match;
120+
const highlightedValue = value.replace(
121+
/(\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*)/g,
122+
'<span class="token-variable">$1</span>',
123+
);
124+
125+
return `<span class="token-key">${key}</span><span class="token-punctuation">${separator}</span><span class="token-string">${highlightedValue}</span>`;
126+
}
127+
128+
function highlightJsonLine(line) {
129+
let html = escapeHtml(line);
130+
131+
html = html
132+
.replace(
133+
/^(\s*)"([^"]+)"(\s*:)/,
134+
'$1<span class="token-key">"$2"</span><span class="token-punctuation">$3</span>',
135+
)
136+
.replace(/:\s*"([^"]*)"/g, ': <span class="token-string">"$1"</span>')
137+
.replace(/\b(true|false|null)\b/g, '<span class="token-constant">$1</span>')
138+
.replace(/\b-?\d+(?:\.\d+)?\b/g, '<span class="token-number">$&</span>');
139+
140+
return html;
141+
}
142+
143+
function highlightYamlLine(line) {
144+
if (/^\s*#/.test(line)) {
145+
return `<span class="token-comment">${escapeHtml(line)}</span>`;
146+
}
147+
148+
let html = escapeHtml(line);
149+
150+
html = html
151+
.replace(/(\$\{\{[^}]+\}\})/g, '<span class="token-variable">$1</span>')
152+
.replace(
153+
/^(\s*-?\s*)([A-Za-z0-9_.-]+)(:\s*)/,
154+
'$1<span class="token-key">$2</span><span class="token-punctuation">$3</span>',
155+
)
156+
.replace(/:\s*"([^"]*)"/g, ': <span class="token-string">"$1"</span>')
157+
.replace(/:\s*'([^']*)'/g, ": <span class=\"token-string\">'$1'</span>")
158+
.replace(/\b(true|false|null)\b/g, '<span class="token-constant">$1</span>')
159+
.replace(/\b\d+\b/g, '<span class="token-number">$&</span>')
160+
.replace(/\b(actions\/checkout@v\d+|actions\/setup-node@v\d+|Selflify\/[A-Za-z0-9-]+@v\d+)\b/g, '<span class="token-string">$1</span>');
161+
162+
return html;
163+
}
164+
165+
function highlightCodeBlock(code) {
166+
const language = code.getAttribute("data-lang");
167+
168+
if (!language) {
169+
return;
170+
}
171+
172+
const raw = code.textContent ?? "";
173+
const lines = raw.split("\n");
174+
175+
const highlighter =
176+
language === "json"
177+
? highlightJsonLine
178+
: language === "yaml"
179+
? highlightYamlLine
180+
: language === "env"
181+
? highlightEnvLine
182+
: highlightShellLine;
183+
184+
code.innerHTML = lines.map((line) => highlighter(line)).join("\n");
185+
}
186+
187+
function setupSyntaxHighlighting() {
188+
const blocks = document.querySelectorAll("pre code[data-lang]");
189+
190+
for (const block of blocks) {
191+
if (block instanceof HTMLElement) {
192+
highlightCodeBlock(block);
193+
}
194+
}
195+
}
196+
29197
function setupExpandableCodeExamples() {
30198
const containers = document.querySelectorAll("[data-expandable-code]");
31199

@@ -50,6 +218,7 @@ function setupExpandableCodeExamples() {
50218
}
51219

52220
document.addEventListener("DOMContentLoaded", () => {
221+
setupSyntaxHighlighting();
53222
setupCopyButtons();
54223
setupExpandableCodeExamples();
55224
});

index.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ <h2>Fresh server to running panel in one command</h2>
277277
<button class="copy-button" type="button" data-copy-target="bootstrap-command">
278278
Copy
279279
</button>
280-
<pre><code id="bootstrap-command">curl -fsSL https://github.com/Selflify/Selflify/releases/latest/download/install-selflify.sh | bash</code></pre>
280+
<pre><code id="bootstrap-command" data-lang="shell">curl -fsSL https://github.com/Selflify/Selflify/releases/latest/download/install-selflify.sh | bash</code></pre>
281281
</div>
282282

283283
<div class="bootstrap-notes">
@@ -458,7 +458,7 @@ <h2>Configure Selflify through one <code>.env</code> file</h2>
458458
<button class="copy-button" type="button" data-copy-target="environment-vars">
459459
Copy
460460
</button>
461-
<pre><code id="environment-vars">AUTH_SECRET=replace-me-with-a-long-random-string
461+
<pre><code id="environment-vars" data-lang="env">AUTH_SECRET=replace-me-with-a-long-random-string
462462
SELFLIFY_CONFIG_PATH=./.dev/selflify.config.json
463463
SELFLIFY_CADDY_CONFIG_PATH=./.dev/Caddyfile
464464
SELFLIFY_CADDY_ADMIN_ADDRESS=http://caddy:2019
@@ -596,7 +596,7 @@ <h3>Add the site map file</h3>
596596
<button class="copy-button" type="button" data-copy-target="github-preview-sites">
597597
Copy
598598
</button>
599-
<pre><code id="github-preview-sites">{
599+
<pre><code id="github-preview-sites" data-lang="json">{
600600
"include": [
601601
{
602602
"site": "marketing",
@@ -641,7 +641,7 @@ <h3>Add the preview workflow</h3>
641641
<button class="copy-button" type="button" data-copy-target="github-preview-workflow">
642642
Copy
643643
</button>
644-
<pre><code id="github-preview-workflow">name: Preview
644+
<pre><code id="github-preview-workflow" data-lang="yaml">name: Preview
645645

646646
on:
647647
pull_request:
@@ -753,7 +753,7 @@ <h3>Feed the preview URL into e2e</h3>
753753
<button class="copy-button" type="button" data-copy-target="github-preview-e2e">
754754
Copy
755755
</button>
756-
<pre><code id="github-preview-e2e">jobs:
756+
<pre><code id="github-preview-e2e" data-lang="yaml">jobs:
757757
e2e:
758758
needs: deploy
759759
runs-on: ubuntu-latest
@@ -842,7 +842,7 @@ <h3>Upload the build to the preview directory</h3>
842842
<button class="copy-button" type="button" data-copy-target="github-preview-rsync">
843843
Copy
844844
</button>
845-
<pre><code id="github-preview-rsync">SITE=marketing
845+
<pre><code id="github-preview-rsync" data-lang="shell">SITE=marketing
846846
DEPLOY_NAME="pr-${PR_NUMBER}"
847847
OUTPUT_DIR="dist"
848848
TARGET_DIR="/var/www/${SITE}/${DEPLOY_NAME}"

styles.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,44 @@ img {
451451
color: rgba(255, 241, 244, 0.92);
452452
}
453453

454+
.code-block code {
455+
display: block;
456+
white-space: pre;
457+
}
458+
459+
.code-block .token-key {
460+
color: #ffb3ca;
461+
}
462+
463+
.code-block .token-string {
464+
color: #f2c688;
465+
}
466+
467+
.code-block .token-variable {
468+
color: #8dd9ff;
469+
}
470+
471+
.code-block .token-keyword {
472+
color: #c5a4ff;
473+
}
474+
475+
.code-block .token-number,
476+
.code-block .token-constant {
477+
color: #9fe19b;
478+
}
479+
480+
.code-block .token-flag {
481+
color: #ff86ad;
482+
}
483+
484+
.code-block .token-comment {
485+
color: rgba(255, 240, 244, 0.38);
486+
}
487+
488+
.code-block .token-punctuation {
489+
color: rgba(255, 240, 244, 0.62);
490+
}
491+
454492
.copy-button {
455493
position: absolute;
456494
top: 1rem;

0 commit comments

Comments
 (0)