Code & syntax highlighting
@nextgensoftwares/folio-plugin-code adds syntax highlighting to code blocks and code groups: one example written in several languages, where the reader picks a language once and every group in the book follows. It is built for e-books that are read rather than downloaded, and it is an add-on, so the core stays small.
import { composePlugins, FolioEditor } from '@nextgensoftwares/folio-editor';
import { codePlugin } from '@nextgensoftwares/folio-plugin-code';
const code = codePlugin({ preload: ['python', 'javascript'] });
const composed = composePlugins(standardSchema, [code /* , other plugins */]);
const editor = new FolioEditor({
schema: composed.schema,
doc,
layout: composed.wrapLayout(hostLayout), // hostLayout passes composed.renderers to layoutDocument
measurer,
keymap: composed.keymap,
plugins: composed.pmPlugins,
// Plugin settings that change layout (preferred language, a grammar arriving) re-run it incrementally.
subscribeLayout: composed.subscribeLayout,
});<FolioView editor={editor} painters={composed.painters} ui={composed.ui} /> {/* the code header controls are a painter */}<FolioToolbar editor={editor} ui={composed.ui} /> {/* language pickers live in the "toolbar" slot */}With <FolioApp> all of that is one line: createFolioApp({ doc, measurer, plugins: [codePlugin()] }).
Try it
Look and layout
A code block is laid out by the plugin (measureCodeBlock) into ordinary fragments, so the screen, the PDF and the DOCX get the same component:
- Box. A hairline border and radius 8 (
theme.code.radius), drawn as fills: rounded caps at the real ends and a square body. When the paginator cuts the box at a page edge the cut stays square and open, and the next page starts with the header again, marked "(continued)" (style.repeatHeader). - Header. A language dot, the language (and an optional
title, e.g. a file name), a hairline divider. Groups show real tabs: the active one in the text colour with an accent underline. On screen the chip and Copy sit on the right. - Type.
theme.fonts.mono(Geist Mono in the playground) attheme.code.size(13px, ~0.9× body) with line height 1.6. Tabs advance to tab stops (style.tabWidth, default 4). Comments are italic, punctuation muted. - Long lines wrap (paper has no horizontal scroll) with a hanging indent;
wrapMarkerputs ↪ before continuation lines. - Optional, per block (all off by default):
lineNumbers(a muted gutter),highlight: "3-5,8"(a soft band with an accent bar),diff(see below),title. - Diff. Shows the block as a patch: a line starting with
+gets a green band (added), a line starting with-a red band (removed), each with a coloured accent bar and sign. Other lines are context. The text itself is not changed, so the signs stay in copies, the PDF and the DOCX (where the lines are shaded the same way). The block menu shows this as a tooltip and a help line under the option (DIFF_HELP).
Colours come from the code theme: presets github (default), one, solarized and monochrome (greys, for print), each with a light and dark variant (palette: 'one', 'solarized:dark'; auto picks by theme.code.background). A preset sets the box colours too; a custom CodePalette may leave them out to use theme.code. On screen everything goes through the document colour theme (ColorAdapter roles codeBackground, codeText, border, fill), so dark mode and custom themes keep code readable; the PDF and DOCX print the document colours.
The caret is coloured for what it sits on (code box, table cell fill, highlight): the adapter's caret colour for that background, or black/white when it would not reach 3:1. Selections over dark fills get a stronger tint.
Highlighting
The highlighter is a small regex tokenizer written for Folio (no WASM, no DOM): it runs inside layout, so the screen, the PDF and the DOCX show the same colours. Lines are measured with one shaping pass per source line and broken before tokens are applied; tokens only split each line's text into coloured items that tile it exactly. A grammar arriving (or switching code themes) therefore never changes line breaks or pagination; only colours do.
- Lazy grammars. Built-ins (JavaScript, TypeScript, Python, Java, Rust, Go, C/C++, SQL, Bash, JSON, CSS, HTML/XML) are separate chunks of about 1 KB each, loaded the first time a block uses them. Until then the block draws plain; when the grammar arrives the plugin bumps a generation and the host's
refreshLayout()re-measures only the blocks in that language. - Incremental. Flows are cached per node identity and tokens per (language, text); typing in one block re-highlights only that block (a 30-line block tokenizes in a few hundredths of a millisecond).
- Palettes.
LIGHT_PALETTEandDARK_PALETTEhold token colours as document colours. On screen, colour themes adapt them like any other code colour;codeTokenMap()gives a theme'smapthat shows the curated dark palette instead.setPalette()takes a customCodePalette. - Your own languages.
defineLanguage({ name, label, rules, keywords, … })andregisterLanguage()(or a lazy{ name, label, aliases, load }). - Your own highlighter.
setHighlighter()plugs in highlight.js, Prism, Shiki or anything withtokenize(code, lang)(sync or async), with scopes mapped onto the code theme. See Custom languages & highlighters.
On-block controls
The plugin paints the code header (painters['rect:code-header']) as the core does, plus screen-only controls that never move the caret:
- Variant tabs on a group showing one variant: the printed tab labels become buttons (
role="tablist"/role="tab", roving tabindex, ←/→, Home/End). Clicking a tab sets that group's own choice: thecodeGroup.attrs.activeattribute (an ordinary, undoable edit). Without an editor (a read-only layout) a tab sets the document-wide preference instead. - Language chip (top right, while an editor is mounted): opens a searchable list (type to filter by name or alias, ↑/↓, Enter, Escape). In a group the variant stays shown under its new language.
- Copy button: copies the block's text, with a check animation (
header: { copy: 'read' }shows it only in read-only views;falsehides it). - The chip's menu also edits the block: title, line numbers, highlighted lines, diff and ↪ markers (
setCodeAttrs). The title and line fields commit on Enter, on blur and when the menu closes (clicking anywhere else keeps the edit); Escape reverts the field, a second Escape closes the menu (CommitInputfrom@nextgensoftwares/folio-react). - Read-only views (
<FolioView readOnly>, i.e.usePaintEnv().readOnly, or no editor) keep the tabs and Copy and drop the chip; a tab there switches the reader's own preference, never the document. - An empty variant shows "Empty Python block: type or paste code" on screen (never printed), so a freshly inserted group doesn't look broken.
header: false drops the screen controls. PDF/DOCX print the tab labels (the active one bold), never the buttons.
Code groups
codeGroup is a new block holding several codeBlocks (content: 'codeBlock+'), one per language. A document-wide preferred language decides what each group shows. Precedence: the group's own choice (a clicked tab, active) → the document preference → the first variant. Choosing a language in the toolbar's Code: … picker is "apply to all": it sets the preference and clears every group's own choice (clearGroupVariants). "All languages" stacks every variant under its own label.
- Hidden variants are not measured, so they take no space: pagination follows what is shown, and switching re-lays out only the groups (their variants' flows are reused, only the stacking changes).
- The caret never enters a hidden variant (a ProseMirror guard moves it on in the direction it was going).
- With one variant shown, the group's header lists every language as tabs.
- Changing a variant's language (chip or toolbar), or adding a variant, sets the group's own choice to it, so the variant you edit stays visible.
The toolbar gets two slot items: Code language (while the caret is in a code block: language, "+ Language" to add a variant, "Remove …" in groups) and Code: … (once the document has groups: as written / a language / all languages). Slash menu: "Code block", "Code group (multi-language)". Keyboard: Mod-Alt-C turns the paragraph into a code block; Tab indents in code.
Export
- PDF draws
editor.layout, so it gets the token colours and exactly the shown variants with no exporter changes. - DOCX uses
exporters.docx.codeBlock: a shaded one-column table with a header row (title, language; Word repeats it on each page unlessstyle.repeatHeaderis off), the code in the mono font with coloured runs, tabs expanded to the same stops as on screen (style.tabWidth), line numbers as text and highlighted / diff lines shaded. Word wraps long lines itself, so ↪ markers are screen/PDF only. Word has no rounded corners.exporters.docx.codeGroupwrites the shown variant with the tabs in its header, or every variant withsetExportAll(true).docxis loaded with a dynamic import (optional peer). withAllVariants(fn)shows every variant whilefnruns, e.g. a PDF export of all languages, then restores the view.
Options
| Option | Default | Meaning |
|---|---|---|
preferred | null | initial preferred language (id or alias); null = each group's first variant |
display | 'preferred' | 'all' stacks every variant |
languages | extra or replacement languages | |
builtins | true | include the built-in grammars |
preload | grammars to fetch right away | |
highlighter, highlighterOptions | a pluggable highlighter and { mode, debounce, theme } (guide) | |
palette | 'auto' | code theme: github / one / solarized / monochrome (:light, :dark), auto / light / dark (GitHub), or a CodePalette |
exportAll | false | DOCX writes every variant |
picker, preferredPicker | on | the toolbar slot items ({ order } to place them, false to drop) |
groupLanguages | Python, JavaScript, Java | languages of a newly inserted group |
header | { chip: true, copy: 'always' } | on-block controls; false for none |
style | { tabWidth: 4, wrapMarker: false, repeatHeader: true } | document-wide code style (setStyle) |
settings | on | the "Code blocks" section (code theme, tab width, ↪, repeated headers) in the settings slot |
API reference: @nextgensoftwares/folio-plugin-code.