Skip to content

Quick start

Astrolabe is one package, astrolabe, with everything on by default: every built-in component, the default look plus playful and notebook, English and Polish, and TipTap as the editor. Each way to use it is one snippet; every default is one option away.

Terminal window
npm install astrolabe

The script-tag bundles need no install: they are the files of dist/cdn/ in the package, on any CDN.

One script tag, astrolabe.js: the page compiles the exercise and grades it in the browser, so the answers are in the page source. This is examples/static-html:

index.html
<script type="module" src="https://cdn.jsdelivr.net/npm/astrolabe@0.1/dist/cdn/astrolabe.js"></script>
<style>astl-player:not(:defined) { visibility: hidden; }</style>
<astl-player locale="en" persist>
<astl-doc lang="en">
<astl-page id="p1" label="Animals">
<p>An elephant is <astl-gap id="g1" answer="grey"><astl-alt>gray</astl-alt></astl-gap>, and a trout is a <astl-gap id="g2" answer="fish"></astl-gap>.</p>
<astl-choice id="q1" multiple scoring="per-option">
<astl-prompt>Which animals are mammals?</astl-prompt>
<astl-option id="a" correct>Otter</astl-option>
<astl-option id="b">Trout</astl-option>
<astl-option id="c" correct>Mole</astl-option>
</astl-choice>
</astl-page>
<astl-page id="p2" label="Numbers">
<p>Half of one is <astl-gap id="g3" type="number" answer="0.5"></astl-gap>.</p>
<astl-choice id="q2">
<astl-prompt>How many legs does a spider have?</astl-prompt>
<astl-option id="a">Six</astl-option>
<astl-option id="b" correct>Eight</astl-option>
<astl-option id="c">Ten</astl-option>
</astl-choice>
</astl-page>
</astl-doc>
</astl-player>

locale is the language of the buttons and messages, lang the language of the exercise (it decides how answers are compared), and persist keeps progress in the browser. Serve the folder over http: (browsers load module scripts and WebCrypto only from real origins, not file:); pnpm dev:static serves this example. Answer and press "Check":

An elephant is gray, and a trout is a .

Which animals are mammals? Otter Trout Mole

Half of one is .

How many legs does a spider have? Six Eight Ten

pnpm astl validate examples/static-html/index.html --locale en prints every issue of the source in the terminal. More: HTML with answers.

Your server compiles the source into a public view and a private answer key, serves the view and grades checks and submits. astrolabe/server has the engine with every built-in and the grading endpoint:

server.ts
import { createEngine, createGradingHandler, createMemoryAttemptStore, PRACTICE_POLICY } from 'astrolabe/server';
// At least 16 bytes and stable: it decides every view. Only compile needs it; there is no default.
const engine = createEngine({ secret: process.env.ASTL_SECRET });
const result = await engine.compile(source, { salt: 'exercise-42' });
if (!result.ok) throw new Error(result.issues.map((i) => engine.describe(i, 'en')).join('\n'));
// result.view: public HTML, result.viewHash: its SHA-256, result.key: private, result.metadata: public facts
const attempts = createMemoryAttemptStore(); // your database in production (AttemptStore)
const grading = createGradingHandler({ engine, attempts });
const attempt = await attempts.create({ key: result.key, policy: PRACTICE_POLICY });
// In your router, after checking that the signed-in student owns the attempt:
// POST /api/attempts/:id/:action -> grading(request, { attemptId: id })

The page loads astrolabe-player.js, which contains no engine and no answers; integrity is the viewHash, so the player refuses a view that changed on the way:

exercise page
<script type="module" src="https://cdn.jsdelivr.net/npm/astrolabe@0.1/dist/cdn/astrolabe-player.js"></script>
<astl-player locale="en" src="/v/3b1f....html" integrity="3b1f..." endpoint="/api/attempts/123"></astl-player>

With a bundler, call definePlayer() from astrolabe/player instead of the script tag. pnpm dev:server starts examples/node-server (set ASTL_SECRET first: export ASTL_SECRET="$(openssl rand -base64 32)"). More: HTML + backend, with Node, Hono and Cloudflare Workers snippets.

<astl-editor> is the exercise editor: TipTap with an insert menu, an inspector for answers and settings, a live issues list and a preview that grades. value is the source, astl-editor-change says it changed, and components picks the types authors can insert:

editor page
<astl-editor locale="en" components="astl-gap astl-choice"></astl-editor>
<script type="module">
import { defineEditor } from 'astrolabe/editor';
defineEditor();
const editor = document.querySelector('astl-editor');
editor.value = '<astl-doc lang="en"><p>An elephant is <astl-gap id="g1" answer="grey"></astl-gap>.</p></astl-doc>';
editor.addEventListener('astl-editor-change', () => save(editor.value)); // save: your own code
</script>

A bundler resolves astrolabe/editor; without one, load astrolabe-editor.js from the CDN instead, which defines the editor on load and exports defineEditor and the rest. TipTap is a chunk of its own, loaded only when the editor creates its own surface: with your editor in slot="surface" and its adapter, it is never loaded. The kit never publishes: validate the source (editor.authoring.validate(editor.value)) and store it. More: Editor and the TipTap example.

Every option replaces its default and is never merged with it. The built-in lists are exported from the entries (builtInDefinitions, builtInElements, builtInEditors, builtInStyleTemplates, builtInManifests): extend one with [...builtInElements, mine], restrict it with .filter(...). English and Polish messages, the built-in slot kinds and DEFAULT_PROFILE are always on; catalogs, slotKinds and profile add to them.

Option (where) Default, and how to change it
components (createEngine, defineEditor) builtInDefinitions. Extend or filter it.
elements (definePlayer, defineEditor) builtInElements (lazy); in defineEditor, those of its components. Extend or filter them.
editors (defineEditor) The builtInEditors of its components. Extend or filter them.
styleTemplates (all three) builtInStyleTemplates: playful, notebook. Extend or filter them; [] keeps only the default look.
catalogs (all three) None: English and Polish are always on. [myWording] adds yours, and its keys win.
slotKinds, profile, urlPolicy (createEngine, defineEditor; urlPolicy in definePlayer too) None, DEFAULT_PROFILE, isSafeUrl. Slot kinds add, profile fields override, urlPolicy replaces: give every part the same one (URLs).
secret (createEngine) None: compile throws. Pass { secret: process.env.ASTL_SECRET }.
compileSource (definePlayer) None: views only. createSourceCompiler() from astrolabe/engine compiles sources in the page.
locale, theme (<astl-player>, <astl-editor>) Own lang, else the browser, else en; auto. Set the attribute.
style-template (<astl-player>) None: the default look. playful, notebook or your own.
components, adapter (<astl-editor>) Every registered type; TipTap. A tag list; any adapter with a slot="surface" child.

createEngine is in astrolabe/engine and astrolabe/server, definePlayer in astrolabe/player, defineEditor in astrolabe/editor. The Looks guide shows the three looks and how to write your own.

  • Concepts: source, view, key, slots, policies, pages and the two languages.
  • Content format: every element and attribute you can write.
  • Playground: edit a source and see its issues, view, key and the player side by side.