Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions builder/highlight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,27 @@ import { loadHighlightTheme } from "./highlight-theme.mjs";

// Fenced-info aliases that select the bundled tB grammar.
const TB_ALIASES = new Set(["tb", "twinbasic", "vb", "vba"]);
const SHIKI_BUNDLED_LANGS = [
"js", "json", "ruby", "html", "yaml", "xml", "sql", "sh", "cpp", "c", "liquid",

// Fence labels that explicitly disclaim highlighting -- never warn for these.
// Empty info string lands as wrapperLang `plaintext` (see renderCodeBlock).
const SILENT_LANGS = new Set(["plaintext", "text", "txt", ""]);

// Shiki grammars to load alongside the tB grammar. Restricted to labels
// actually used in docs/ -- the highlighter warns at build time for any
// unknown label, so adding a new fence language is a deliberate step:
// extend this list, run the build, verify no warning. Aliases are
// recognized automatically (shiki registers both canonical and alias
// names, so loading `js` accepts `javascript` too, `yaml` accepts `yml`,
// `batch` accepts `bat`). Counts from the last survey are noted.
const SHIKI_LANGS = [
"js", // 56 blocks (CEF/WebView2 interop tutorials)
"yaml", // 13 blocks (config snippets)
"json", // 7 blocks
"c", // 3 blocks (Win32 API examples, comment style demos)
"html", // 2 blocks (transitively loads css + javascript)
"xml", // 1 block
"sql", // 1 block
"batch", // 1 block (Windows .bat examples)
];

// Phase 11 (B5) server-side copy-button: emitted inside the wrapper
Expand All @@ -38,7 +57,7 @@ const COPY_BUTTON_HTML =

let cached = null;

export async function initHighlighter({ copyButton = true } = {}) {
export async function initHighlighter() {
if (cached) return cached;

const theme = await loadHighlightTheme();
Expand All @@ -50,21 +69,31 @@ export async function initHighlighter({ copyButton = true } = {}) {
const tbGrammar = JSON.parse(grammarText);
shiki = await createHighlighter({
themes: [],
langs: [tbGrammar, ...SHIKI_BUNDLED_LANGS],
langs: [tbGrammar, ...SHIKI_LANGS],
});
} catch (err) {
if (err.code !== "ENOENT") throw err;
}

const copyButtonHtml = copyButton ? COPY_BUTTON_HTML : "";
// Dedup unknown-language warnings per init. SILENT_LANGS suppresses
// explicit plaintext intent (text, txt, plaintext) and empty fences.
const warned = new Set();
const warn = (lang) => {
if (warned.has(lang)) return;
warned.add(lang);
console.warn(
`highlight: unknown fence language "${lang}" -- falling back to plain text. ` +
`Add it to highlight.mjs's SHIKI_LANGS to enable highlighting.`);
};

cached = {
render: (code, lang) => renderCodeBlock(shiki, theme, copyButtonHtml, code, lang),
render: (code, lang) => renderCodeBlock(shiki, theme, code, lang, warn),
themeCss: theme.css,
};
return cached;
}

function renderCodeBlock(shiki, theme, copyButtonHtml, code, lang) {
function renderCodeBlock(shiki, theme, code, lang, warn) {
const lower = (lang || "").toLowerCase();
const isTb = TB_ALIASES.has(lower);
// The wrapper class is `language-<as-typed>`; keep `vb` / `vba` /
Expand All @@ -82,6 +111,12 @@ function renderCodeBlock(shiki, theme, copyButtonHtml, code, lang) {
}
}

// Warn for non-silent fence labels that don't resolve to a loaded
// grammar. SILENT_LANGS covers explicit plaintext intent.
if (!shikiLang && !SILENT_LANGS.has(lower) && warn) {
warn(lower);
}

// The trailing \n inside <code> matches the rouge / kramdown shape:
// GFM strips the user's trailing newline; one is re-added here.
const codeBody = code.endsWith("\n") ? code : code + "\n";
Expand All @@ -97,7 +132,7 @@ function renderCodeBlock(shiki, theme, copyButtonHtml, code, lang) {
tokenizedHtml = escapeHtml(codeBody);
}

return `<div class="language-${wrapperLang} highlighter-rouge">${copyButtonHtml}<div class="highlight"><pre class="highlight"><code>${tokenizedHtml}</code></pre></div></div>`;
return `<div class="language-${wrapperLang} highlighter-rouge">${COPY_BUTTON_HTML}<div class="highlight"><pre class="highlight"><code>${tokenizedHtml}</code></pre></div></div>`;
}

// Shiki's `codeToTokensBase` with `includeExplanation` returns
Expand Down
4 changes: 1 addition & 3 deletions builder/tbdocs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,7 @@ export async function runBuild(opts) {
// Build the shared markdown-it instance up front so Phase 2's SEO
// pass and Phase 3's body renderer use the same configured renderer.
// initHighlighter overlaps with the running git shell-outs above.
const highlighter = await initHighlighter({
copyButton: config.enable_copy_code_button !== false,
});
const highlighter = await initHighlighter();
const linkTables = buildLinkTables(pages);
const baseurl = String(config.baseurl || "");
const staticFileSet = new Set(staticFiles.map((s) => s.srcRel));
Expand Down
7 changes: 3 additions & 4 deletions builder/template.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -270,19 +270,18 @@ const SVG_SYMBOLS_COPY = `<!-- Bootstrap Icons. MIT License: https://github.com/
</svg>
</symbol>`;

// Port of theme's _includes/icons/icons.html. Search and copy-code-button
// icons are conditional.
// Port of theme's _includes/icons/icons.html. The search icon is
// conditional; the copy-code icons ship unconditionally.
function buildSvgSprites(config) {
const searchEnabled = config.search_enabled !== false;
const copyEnabled = config.enable_copy_code_button !== false;
const parts = [
` <svg xmlns="http://www.w3.org/2000/svg" class="d-none">`,
SVG_SYMBOL_LINK,
SVG_SYMBOL_MENU,
SVG_SYMBOL_EXPAND,
];
if (searchEnabled) parts.push(SVG_SYMBOL_DOC);
if (copyEnabled) parts.push(SVG_SYMBOLS_COPY);
parts.push(SVG_SYMBOLS_COPY);
parts.push(`</svg>`);
return parts.join("\n");
}
Expand Down
4 changes: 2 additions & 2 deletions docs/Documentation/Pipeline-Stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ Returns `{ book: <parsed YAML> }`, or `{}` when the file is absent. The orchestr
Before Phase 2 completes, the orchestrator builds the shared markdown-it instance that both Phase 2's SEO pass and Phase 3's render pass reuse. Three functions from `render.mjs` are called in sequence:

```js
const highlighter = await initHighlighter({ copyButton: boolean });
const highlighter = await initHighlighter();
const linkTables = buildLinkTables(pages);
const markdown = createMarkdownIt({ highlighter, linkTables, baseurl, staticFiles });
```
Expand Down Expand Up @@ -298,7 +298,7 @@ Renders each page's `rawContent` to `page.renderedContent` using the shared `sit
|---|---|---|
| `renderPhase` | `(pages, site, staticFiles?) → Promise<void>` | Main entry point. |
| `createMarkdownIt` | `({ highlighter, linkTables, baseurl, staticFiles }) → MarkdownIt` | Configures and returns a markdown-it instance with all plugins and render-rule overrides applied. See [Extending the Builder](Extending) for how to add a plugin here. |
| `initHighlighter` | `({ copyButton?: boolean }) → Promise<{ render, themeCss }>` | Initialises Shiki with the bundled twinBASIC grammar (delegates to `highlight.mjs` internally). `render(code, lang)` returns highlighted HTML; `themeCss` is the generated `tb-highlight.css` string or `null` when no theme was loaded. |
| `initHighlighter` | `() → Promise<{ render, themeCss }>` | Initialises Shiki with the bundled twinBASIC grammar (delegates to `highlight.mjs` internally). `render(code, lang)` returns highlighted HTML; `themeCss` is the generated `tb-highlight.css` string or `null` when no theme was loaded. |
| `buildLinkTables` | `(pages: Page[]) → { byPath, byUrl, byRedirect }` | Builds lookup tables keyed by `srcRel`, `permalink`, and `redirect_from` entries. Used by the relative-links plugin to resolve in-source `[X](Y.md)` links to absolute URLs at render time. |
| `kramdownSlug` | `(text: string) → string` | Converts heading text to a kramdown-compatible anchor slug: lowercase, strip non-word characters, deduplicate with `-1`, `-2`, and so on. |
| `rewriteAdmonitions` | `(src: string) → string` | Pre-render text pass: converts GFM `> [!NOTE]` / `[!IMPORTANT]` / `[!WARNING]` / `[!TIP]` / `[!CAUTION]` blocks to the `markdown-alert markdown-alert-<type>` class structure. |
Expand Down
2 changes: 1 addition & 1 deletion docs/Documentation/Tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ The build pipeline also reads a handful of declarative files. They are not execu

| File | Effect |
|---|---|
| `docs/_config.yml` | Site config. `tbdocs` reads `url`, `baseurl`, `title`, `logo`, `also_build_offline`, `also_build_pdf`, `offline_exclude`, `exclude`, `enable_copy_code_button`, the footer / aux-link knobs, the GitHub edit-link knobs, and the offline-download-link knobs. Jekyll-only keys (`markdown`, `kramdown`, `theme`, `highlighter`, the `defaults` block, the `compress_html` block) are ignored. |
| `docs/_config.yml` | Site config. `tbdocs` reads `url`, `baseurl`, `title`, `logo`, `also_build_offline`, `also_build_pdf`, `offline_exclude`, `exclude`, the footer / aux-link knobs, the GitHub edit-link knobs, and the offline-download-link knobs. Jekyll-only keys (`markdown`, `kramdown`, `theme`, `highlighter`, the `defaults` block, the `compress_html` block) are ignored. |
| `docs/_book.yml` | The PDF book's chapter manifest. Entries are resolved to pages via the selector schema (`page` / `pages` / `nav_page` / `nav_pages` / `no_descent`) and control PDF outline behaviour via `landing_page:`, `landing_is_target:`, `no_outline_entry:`, `no_heading_shift:`, and `outline_closed:`. Full schema is documented in the file header. Phase 2 resolves chapter arrays; Phase 8 assembles `book.html`. |
| `builder/themes/Light.theme`, `Dark.theme`, `Classic.theme` | twinBASIC IDE theme files, vendored from the BETA installer. `builder/highlight-theme.mjs` parses them into a Symbol-keyed palette that drives both the renderer's scope-to-class mapping and the generated `tb-highlight.css`. Refresh from the installer when the IDE adds new palette entries. |
| `builder/twinbasic.tmLanguage.json` | TextMate grammar for the twinBASIC language. Shiki uses it to tokenise every ` ```tb ` code block. |
8 changes: 4 additions & 4 deletions docs/Features/Advanced/API-Declarations.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ With `cdecl` calling convention fully supported, twinBASIC can also handle varia

Using the [given C/C++ prototype](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-wsprintfw):

```cpp
```c
int WINAPIV wsprintfW(
[out] LPWSTR unnamedParam1,
[in] LPCWSTR unnamedParam2,
...
/* [out] */ LPWSTR unnamedParam1,
/* [in] */ LPCWSTR unnamedParam2,
/* ... */
);
```

Expand Down
3 changes: 0 additions & 3 deletions docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ logo: favicon.png
logo_with_title: true
url: "https://docs.twinbasic.com"

# For copy button on code
enable_copy_code_button: true

# Aux links for the upper right navigation
aux_links:
"twinBASIC Home":
Expand Down