Improve documentation navigation and code blocks
This commit is contained in:
@@ -35,8 +35,10 @@ function renderNav(items, prefix = []) {
|
|||||||
<div class={playground || frontpage ? 'layout playground-layout' : 'layout'}>
|
<div class={playground || frontpage ? 'layout playground-layout' : 'layout'}>
|
||||||
{!playground && !frontpage && (
|
{!playground && !frontpage && (
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<a class="sidebar-title" href="/docs/">Manual</a>
|
<div class="sidebar-content">
|
||||||
<nav set:html={renderNav(nav)} />
|
<a class="sidebar-title" href="/docs/">Manual</a>
|
||||||
|
<nav set:html={renderNav(nav)} />
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
<main class={playground ? 'playground' : frontpage ? 'frontpage' : ''}>
|
<main class={playground ? 'playground' : frontpage ? 'frontpage' : ''}>
|
||||||
@@ -47,5 +49,20 @@ function renderNav(items, prefix = []) {
|
|||||||
<span>2026 Hare</span>
|
<span>2026 Hare</span>
|
||||||
<a href="https://gitea.hareworks.net/Hare/Decodal">Source</a>
|
<a href="https://gitea.hareworks.net/Hare/Decodal">Source</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
<script>
|
||||||
|
const sidebar = document.querySelector('.sidebar-content');
|
||||||
|
|
||||||
|
if (sidebar instanceof HTMLElement) {
|
||||||
|
const updateStickyOffset = () => {
|
||||||
|
const stickyTop = Math.min(0, window.innerHeight - sidebar.offsetHeight);
|
||||||
|
sidebar.style.setProperty('--sidebar-sticky-top', `${stickyTop}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(updateStickyOffset);
|
||||||
|
observer.observe(sidebar);
|
||||||
|
window.addEventListener('resize', updateStickyOffset);
|
||||||
|
updateStickyOffset();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,6 +2,23 @@ const DECODAL_KEYWORDS = new Set(['let', 'in', 'fn', 'match', 'import', 'default
|
|||||||
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool', 'Unknown']);
|
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool', 'Unknown']);
|
||||||
const DECODAL_LITERALS = new Set(['true', 'false']);
|
const DECODAL_LITERALS = new Set(['true', 'false']);
|
||||||
|
|
||||||
|
const RUST_KEYWORDS = new Set([
|
||||||
|
'as', 'const', 'crate', 'dyn', 'else', 'enum', 'extern', 'fn', 'for', 'if', 'impl',
|
||||||
|
'in', 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self',
|
||||||
|
'Self', 'static', 'struct', 'super', 'trait', 'type', 'unsafe', 'use', 'where',
|
||||||
|
'while', 'async', 'await',
|
||||||
|
]);
|
||||||
|
const RUST_LITERALS = new Set(['true', 'false', 'None', 'Some', 'Ok', 'Err']);
|
||||||
|
|
||||||
|
const JAVASCRIPT_KEYWORDS = new Set([
|
||||||
|
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
||||||
|
'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'from',
|
||||||
|
'function', 'get', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'of', 'return',
|
||||||
|
'set', 'static', 'switch', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with',
|
||||||
|
'yield',
|
||||||
|
]);
|
||||||
|
const JAVASCRIPT_LITERALS = new Set(['true', 'false', 'null', 'undefined']);
|
||||||
|
|
||||||
const HTML_ESCAPE = {
|
const HTML_ESCAPE = {
|
||||||
'&': '&',
|
'&': '&',
|
||||||
'<': '<',
|
'<': '<',
|
||||||
@@ -22,6 +39,9 @@ export function highlightCode(code, language = '') {
|
|||||||
const normalized = language.toLowerCase();
|
const normalized = language.toLowerCase();
|
||||||
if (normalized === 'dcdl' || normalized === 'decodal') return highlightDecodal(code);
|
if (normalized === 'dcdl' || normalized === 'decodal') return highlightDecodal(code);
|
||||||
if (normalized === 'sh' || normalized === 'bash' || normalized === 'shell') return highlightShell(code);
|
if (normalized === 'sh' || normalized === 'bash' || normalized === 'shell') return highlightShell(code);
|
||||||
|
if (normalized === 'rs' || normalized === 'rust') return highlightRust(code);
|
||||||
|
if (normalized === 'js' || normalized === 'javascript') return highlightJavaScript(code);
|
||||||
|
if (normalized === 'toml') return highlightToml(code);
|
||||||
return escapeHtml(code);
|
return escapeHtml(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +193,139 @@ function highlightShellCode(source) {
|
|||||||
.replace(/(^|\s)(--?[A-Za-z0-9][A-Za-z0-9-]*)/g, '$1<span class="tok-operator">$2</span>');
|
.replace(/(^|\s)(--?[A-Za-z0-9][A-Za-z0-9-]*)/g, '$1<span class="tok-operator">$2</span>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function highlightRust(source) {
|
||||||
|
return highlightGeneric(source, {
|
||||||
|
keywords: RUST_KEYWORDS,
|
||||||
|
literals: RUST_LITERALS,
|
||||||
|
lineComment: '//',
|
||||||
|
blockComments: true,
|
||||||
|
rawStrings: true,
|
||||||
|
typeNames: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightJavaScript(source) {
|
||||||
|
return highlightGeneric(source, {
|
||||||
|
keywords: JAVASCRIPT_KEYWORDS,
|
||||||
|
literals: JAVASCRIPT_LITERALS,
|
||||||
|
lineComment: '//',
|
||||||
|
blockComments: true,
|
||||||
|
quotes: ['"', "'", '`'],
|
||||||
|
typeNames: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightToml(source) {
|
||||||
|
return highlightGeneric(source, {
|
||||||
|
literals: new Set(['true', 'false']),
|
||||||
|
lineComment: '#',
|
||||||
|
classifyIdentifier(_identifier, start, end) {
|
||||||
|
const before = source.slice(0, start).trimEnd().at(-1);
|
||||||
|
const after = source.slice(end).match(/^\s*(.)/)?.[1];
|
||||||
|
if (before === '[') return 'type';
|
||||||
|
if (after === '=') return 'keyword';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightGeneric(source, options) {
|
||||||
|
const {
|
||||||
|
keywords = new Set(),
|
||||||
|
literals = new Set(),
|
||||||
|
lineComment = '',
|
||||||
|
blockComments = false,
|
||||||
|
rawStrings = false,
|
||||||
|
quotes = ['"', "'"],
|
||||||
|
typeNames = false,
|
||||||
|
classifyIdentifier,
|
||||||
|
} = options;
|
||||||
|
let html = '';
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
while (index < source.length) {
|
||||||
|
if (lineComment && source.startsWith(lineComment, index)) {
|
||||||
|
const end = readUntilLineEnd(source, index);
|
||||||
|
html += token('comment', source.slice(index, end));
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockComments && source.startsWith('/*', index)) {
|
||||||
|
const close = source.indexOf('*/', index + 2);
|
||||||
|
const end = close < 0 ? source.length : close + 2;
|
||||||
|
html += token('comment', source.slice(index, end));
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawStrings && source[index] === 'r' && /^r#*"/.test(source.slice(index))) {
|
||||||
|
const hashes = source.slice(index + 1).match(/^#*/)?.[0] ?? '';
|
||||||
|
const openLength = 2 + hashes.length;
|
||||||
|
const close = source.indexOf(`"${hashes}`, index + openLength);
|
||||||
|
const end = close < 0 ? source.length : close + 1 + hashes.length;
|
||||||
|
html += token('string', source.slice(index, end));
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quotes.includes(source[index])) {
|
||||||
|
const end = readQuoted(source, index, source[index]);
|
||||||
|
html += token('string', source.slice(index, end));
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[0-9]/.test(source[index])) {
|
||||||
|
const match = source.slice(index).match(/^(?:0x[0-9a-fA-F_]+|0b[01_]+|\d[\d_]*(?:\.\d[\d_]*)?)/);
|
||||||
|
const value = match?.[0] ?? source[index];
|
||||||
|
html += token('number', value);
|
||||||
|
index += value.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[A-Za-z_$]/.test(source[index])) {
|
||||||
|
const match = source.slice(index).match(/^[A-Za-z_$][A-Za-z0-9_$-]*/);
|
||||||
|
const identifier = match?.[0] ?? source[index];
|
||||||
|
const end = index + identifier.length;
|
||||||
|
let kind = '';
|
||||||
|
if (keywords.has(identifier)) kind = 'keyword';
|
||||||
|
else if (literals.has(identifier)) kind = 'literal';
|
||||||
|
else if (classifyIdentifier) kind = classifyIdentifier(identifier, index, end);
|
||||||
|
else if (typeNames && /^[A-Z]/.test(identifier)) kind = 'type';
|
||||||
|
html += kind ? token(kind, identifier) : escapeHtml(identifier);
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('&=<>!|:;,.{}()[]+-*/?@#'.includes(source[index])) {
|
||||||
|
let end = index + 1;
|
||||||
|
while (end < source.length && '&=<>!|:+-*?'.includes(source[end])) end += 1;
|
||||||
|
html += token('operator', source.slice(index, end));
|
||||||
|
index = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += escapeHtml(source[index]);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readQuoted(source, start, quote) {
|
||||||
|
let index = start + 1;
|
||||||
|
while (index < source.length) {
|
||||||
|
if (source[index] === '\\') {
|
||||||
|
index += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (source[index] === quote) return index + 1;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
return source.length;
|
||||||
|
}
|
||||||
|
|
||||||
function token(kind, value) {
|
function token(kind, value) {
|
||||||
return `<span class="tok-${kind}">${escapeHtml(value)}</span>`;
|
return `<span class="tok-${kind}">${escapeHtml(value)}</span>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { highlightCode } from '../lib/highlight.js';
|
||||||
|
|
||||||
|
test('highlights Rust examples in the embedding guide', () => {
|
||||||
|
const html = highlightCode('use decodal::Engine;\nlet mut engine = Engine::new();', 'rust');
|
||||||
|
|
||||||
|
assert.match(html, /tok-keyword">use/);
|
||||||
|
assert.match(html, /tok-keyword">let/);
|
||||||
|
assert.match(html, /tok-type">Engine/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('highlights JavaScript examples in the embedding guide', () => {
|
||||||
|
const html = highlightCode("import init from 'decodal-wasm';\nconst enabled = true;", 'js');
|
||||||
|
|
||||||
|
assert.match(html, /tok-keyword">import/);
|
||||||
|
assert.match(html, /tok-string">'decodal-wasm'/);
|
||||||
|
assert.match(html, /tok-literal">true/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('highlights TOML examples in the packages guide', () => {
|
||||||
|
const html = highlightCode('[dependencies]\ndecodal = "0.4"', 'toml');
|
||||||
|
|
||||||
|
assert.match(html, /tok-type">dependencies/);
|
||||||
|
assert.match(html, /tok-keyword">decodal/);
|
||||||
|
assert.match(html, /tok-string">"0.4"/);
|
||||||
|
});
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
--inline-code-text: #3730a3;
|
--inline-code-text: #3730a3;
|
||||||
--editor-bg: #0f172a;
|
--editor-bg: #0f172a;
|
||||||
--editor-text: #e5e7eb;
|
--editor-text: #e5e7eb;
|
||||||
|
--code-outline: #475569;
|
||||||
--danger: #b91c1c;
|
--danger: #b91c1c;
|
||||||
--error: #fecaca;
|
--error: #fecaca;
|
||||||
--selection: rgb(59 130 246 / 0.35);
|
--selection: rgb(59 130 246 / 0.35);
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
--shadow: 0 8px 24px rgb(0 0 0 / 0.25);
|
--shadow: 0 8px 24px rgb(0 0 0 / 0.25);
|
||||||
--inline-code-bg: #1e293b;
|
--inline-code-bg: #1e293b;
|
||||||
--inline-code-text: #bfdbfe;
|
--inline-code-text: #bfdbfe;
|
||||||
|
--code-outline: #64748b;
|
||||||
--danger: #fca5a5;
|
--danger: #fca5a5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,10 @@
|
|||||||
body {
|
body {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -69,6 +74,7 @@ a:hover {
|
|||||||
background: var(--topbar-bg);
|
background: var(--topbar-bg);
|
||||||
color: var(--topbar-text);
|
color: var(--topbar-text);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
@@ -94,6 +100,7 @@ a:hover {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
color: var(--subtle);
|
color: var(--subtle);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -102,8 +109,9 @@ a:hover {
|
|||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
flex: 1 0 auto;
|
||||||
grid-template-columns: 300px minmax(0, 1fr);
|
grid-template-columns: 300px minmax(0, 1fr);
|
||||||
min-height: calc(100vh - 104px);
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-layout {
|
.playground-layout {
|
||||||
@@ -113,8 +121,12 @@ a:hover {
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
overflow: auto;
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
position: sticky;
|
||||||
padding: 22px 18px;
|
padding: 22px 18px;
|
||||||
|
top: var(--sidebar-sticky-top, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
@@ -188,6 +200,7 @@ main.playground {
|
|||||||
.markdown pre,
|
.markdown pre,
|
||||||
.output-pane pre {
|
.output-pane pre {
|
||||||
background: var(--editor-bg);
|
background: var(--editor-bg);
|
||||||
|
border: 1px solid var(--code-outline);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
color: var(--editor-text);
|
color: var(--editor-text);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -632,6 +645,10 @@ button:disabled {
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
max-height: 260px;
|
max-height: 260px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.sidebar-content {
|
||||||
|
position: static;
|
||||||
}
|
}
|
||||||
.playground-shell,
|
.playground-shell,
|
||||||
.home-grid,
|
.home-grid,
|
||||||
|
|||||||
Reference in New Issue
Block a user