MarkView: in-app Markdown
MarkView is a small Markdown viewer that ships with ScriptWeaver. It renders
Markdown into a native Text widget — styled headings, code
blocks, tables, images, and clickable links — and adds page navigation with
back/forward history. It's what scriptweaver --help opens, and you can reuse
it in your own apps to show help, a README, release notes, or any Markdown
content, with no web engine.
Quick start
MarkView is built into the Player as a ready-to-use widget — like
Button, there's nothing to import, copy, or bundle.
Create it on any container with the MarkView factory:
const mv = app.MarkView({ base: 'docs' });
mv.pack.configure({ fill: 'both', expand: true });
mv.go('index.md');
That's a complete viewer: a scrolled Text with a Back /
Forward / Home toolbar, pointed at docs/index.md. Clicking a link to another
page loads it, and the toolbar and history follow along.
You can also import the class explicitly — handy to reach the lower-level
building blocks (below), or when you prefer an explicit dependency:
import { MarkView } from '//zipfs:/markview/index.js';
const mv = new MarkView(app, { base: 'docs' }); // app, a frame, a toplevel…
The MarkView widget
Create it as a child of any container — parent.MarkView({ ...opts }) (the
zero-import factory, like every other widget) or new MarkView(parent, opts)
after importing the class. Both take the same options:
| Option | Default | Meaning |
|---|---|---|
base |
'' |
Folder that page paths and images resolve against (disk or //zipfs:). |
toolbar |
true |
Show the Back / Forward / Home + title toolbar. false = a bare scrolled viewer. |
home |
'index.md' |
Page the Home button loads. |
onNavigate(info) |
— | Called after each move with { title, path, canBack, canForward }. |
openExternal(url) |
sw.sys.open |
Handler for http(s): / mailto: links. |
dark |
auto | Force the palette (true/false); omit to follow the active theme. |
| Method | Does |
|---|---|
go(target) |
Load a page relative to base ('guide.md', 'x.md#anchor'). Pushes history. |
back() / forward() |
Move through history (return false at the ends). |
reload() |
Re-render the current page — call after app.setTheme(...) so the viewer follows the new theme. |
renderString(md) |
Render a Markdown string directly, with no navigation or history. |
It also exposes mv.frame (the root widget), mv.text (the Text), mv.nav
(the Navigator), and — when the toolbar is on — mv.toolbar
(its frame, so you can dock your own controls into it, e.g.
mv.toolbar.Button({...}).pack.configure({ side: 'right' })), plus mv.pack /
mv.grid / mv.place layout proxies so it places like any widget. (The bundled
help viewer is itself an app.MarkView({...}) with a theme switcher added to
mv.toolbar — the flagship dogfood.)
The building blocks
The same module re-exports the lower-level pieces, for when you want to drive rendering yourself — still nothing to copy:
import { Renderer, Navigator, parseMarkdown } from '//zipfs:/markview/index.js';
| Export | What it provides |
|---|---|
parseMarkdown(src) |
Markdown → a document tree (the AST). |
Renderer |
Paints a parsed document into a Text widget. |
Navigator |
A browsable viewer (loads pages, resolves links, history). |
MarkView |
The ready-made shell above (toolbar + scrolled text + navigator). |
The worked example builds an equivalent viewer from
these blocks by hand. If you want to customize the viewer itself, MarkView's
source lives in the markview/ folder of the ScriptWeaver tree — it has no
dependencies beyond the Player, so you can copy and adapt it.
The Navigator
import { Navigator } from '//zipfs:/markview/index.js';
new Navigator(textWidget, {
base: 'docs', // folder that page paths are resolved against
onNavigate(info) {}, // { title, path, canBack, canForward } after each move
openExternal(url) {}, // for http/mailto links (default: sw.sys.open)
dark: true, // optional palette override; omit to follow the active theme
});
| Method | Does |
|---|---|
go(target) |
Load a page relative to base (e.g. 'guides/x.md' or 'x.md#anchor'). Pushes history. |
follow(href) |
Follow a link from the current page (relative ../api/Y.md, #anchor, or external). |
back() / forward() |
Move through history. Return false at the ends. |
canBack() / canForward() |
Whether a move is possible — handy for enabling toolbar buttons. |
Use onNavigate to update your window title and toolbar:
const nav = new Navigator(text, {
base: 'docs',
onNavigate(info) {
app.wm.title(info.title);
backBtn.state(info.canBack ? '!disabled' : 'disabled');
},
});
Pages load through the Tcl virtual file system, so the same code reads from a
folder on disk and from a mounted .zip — point base at 'docs' when
developing and '//zipfs:/app/docs' when bundled (or probe for whichever
opens). Relative links and #anchor fragments resolve against the current
page; http(s): / mailto: links go to openExternal. A page that fails to
load renders an in-viewer "not found" message instead of throwing.
Rendering without navigation
To render a single Markdown string (no link-following), use the Renderer
directly:
import { parseMarkdown, Renderer } from '//zipfs:/markview/index.js';
const r = new Renderer(text, {
onLink: (href) => sw.sys.open(href), // optional: handle link clicks yourself
});
r.render(parseMarkdown('# Hello\n\nSome **Markdown** text.'));
r.scrollToAnchor('hello'); // jump to a heading by its slug
parseMarkdown(src) returns the document tree if you want to inspect or
transform it before rendering.
What's supported
A pragmatic subset of GitHub-Flavored Markdown — enough for real documentation:
- ATX headings (
#…######), each linkable by its slug (page.md#a-heading) - paragraphs (soft-wrapped — single newlines in the source become spaces, so text reflows to the window width)
- bold, italic,
inline code, links, and images - fenced code blocks (
```) - blockquotes, ordered / unordered / nested lists
- pipe tables (rendered as a native table; click a cell to copy it, or Ctrl+C to copy the selected row) and horizontal rules
Not supported: raw HTML, and Markdown "hard breaks" (a line ending in two spaces). Colours follow the active theme automatically (readable on light and dark themes).
Images
Inline images use the usual syntax:

The src resolves like a link — relative to the current page, through the same
VFS the pages load from — so the same path works whether the docs are loose on
disk or bundled in a .zip (//zipfs:). Absolute and //zipfs: paths are
honored as-is.
- Formats. PNG and GIF load directly; JPEG is decoded by the Player and shown as well. (Decoding is a small native bridge, so JPEG works without bundling any extra image library.)
- Sizing. An image wider than the viewer is scaled down to fit, and re-fit when the window resizes; images are never enlarged past their natural size.
- Fallback. A missing, remote (
http(s):), or undecodable image degrades to its bracketed alt text so the page still reads sensibly — keepaltmeaningful. (Remote images are not fetched; bundle the picture with the docs.)
Live previews
A page can embed a live, interactive widget — the real thing, rendered inline
— instead of a screenshot. Tag a fenced code block swdemo and name a widget in
its body:
```swdemo
Button
```
MarkView builds a representative instance from a small registry (demos.js) and
drops it into the page, next to the prose that describes it. Because the viewer
shares one theme, the Theme switcher in the toolbar reskins these previews
live — so a widget's documentation shows how it actually looks, in any theme,
while you read. An unknown name (or a viewer bundled without demos.js) falls
back to rendering the block as ordinary code, so pages stay readable everywhere.
Demos are registered in demos.js as name → factory(parent); add entries
there to preview your own widgets. The themed widget reference pages — for
example Button — use this.
Theming
The viewer reads the Text widget's background and picks a light or dark
palette to match — links, code panels, quotes, and (in dark mode) the body text
colour all adapt. Nothing to configure; just run under --light or --dark.
scriptweaver --help also puts a Theme switcher at the east end of its
toolbar: choosing a theme calls app.setTheme(), repaints the
body to match, and restyles any live previews on the current page.
Packaging a help bundle
A self-contained help app is a .zip containing your viewer plus the MarkView
modules and your pages:
help.zip
├── main.js ← your viewer (imports ./nav.js)
├── parser.js
├── render.js
├── nav.js
├── demos.js ← optional: live previews (swdemo blocks)
└── docs/
├── index.md
└── … more .md pages …
Run it like any bundle: scriptweaver help.zip. Inside the zip the docs live at
//zipfs:/app/docs, so have main.js point base there (or probe both). This
is exactly how scriptweaver --help works — the Player embeds such a bundle of
these docs and opens it in MarkView. See Packaging apps for the
bundle format.
See also
- The MarkView example — a full viewer with a toolbar
- Text — the widget MarkView renders into
- Packaging apps — bundling your viewer and pages as a
.zip