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.

It's plain JavaScript — a few small modules you bundle with your app:

Module What it provides
parser.js parseMarkdown(src) → a document tree (the AST)
render.js Renderer — paints a parsed document into a Text widget
nav.js Navigator — a browsable viewer (loads pages, resolves links, history)
demos.js optional — live widget previews (swdemo blocks, below)

Copy those files next to your app's main.js (they have no dependencies beyond the Player itself), then import them. demos.js is needed only if your pages embed live previews.

Quick start

A complete viewer is a Text widget, a scrollbar, and a Navigator pointed at a folder of .md files:

import { Navigator } from './nav.js';

const text = app.Text({ wrap: 'word', borderwidth: 0 });
const sb = app.TScrollbar({ orient: 'vertical' });
__native_tcl(text._id, 'configure', '-yscrollcommand', sb._id + ' set');
__native_tcl(sb._id, 'configure', '-command', text._id + ' yview');
sb.pack.configure({ side: 'right', fill: 'y' });
text.pack.configure({ side: 'left', fill: 'both', expand: true });

const nav = new Navigator(text, { base: 'docs' });
nav.go('index.md');

That renders docs/index.md; clicking a link to another page loads it, and nav.back() / nav.forward() move through history. See the worked example for a viewer with a Back / Forward / Home toolbar.

The Navigator

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)
});
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 } from './parser.js';
import { Renderer } from './render.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 Azure light and dark).

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
TButton
```

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 TButton — 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