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:

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:

![alt text](images/diagram.png)

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.

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