#!/usr/bin/env oak { println: println map: map append: append } := import('std') { join: join } := import('str') fs := import('fs') fmt := import('fmt') cli := import('cli') path := import('path') md := import('md') Cli := cli.parse() Cli.args := [Cli.verb] |> append(Cli.args) [srcPath, destPath] := Cli.args // Exit early if incorrect usage if srcPath = ? | destPath = ? -> { println('Usage: ./compile.oak ') exit(1) } fn nblog(xs...) println('[notebook]', xs...) // highlightOak syntax-highlightings an Oak code snippet. Under the hood, this // calls out to the `oak cat` command to accomplish this. Note that this // function is synchronous, while `compileOakToJS` below is not. fn highlightOak(panelIndex, prog) { evt := exec(Cli.exe, ['cat', '--html', '--stdin'], prog) if evt.type { :error -> { nblog('Could not syntax-highlight panel ', panelIndex) prog } _ -> evt.stdout } } // compileOakToJS compiles an Oak program into JavaScript, internally calling // out to `oak build --web` under the hood. Note that this function is // asynchronous and returns the result in a callback, in contrast to // `highlightOak` above, which is synchronous and blocking. fn compileOakToJS(prog, withJS) { buildArgs := ['build' '--entry', '/tmp/in.oak' '--output', '/tmp/out.js' '--web'] with fs.writeFile('/tmp/in.oak', prog) fn(res) if res { ? -> withJS(?) _ -> with exec(Cli.exe, buildArgs, '') fn(evt) if { evt.type = :error -> withJS(?) evt.status != 0 -> { nblog(evt.stdout) withJS(?) } _ -> with fs.readFile('/tmp/out.js') fn(jsProg) if jsProg { ? -> withJS(?) _ -> withJS(jsProg) } } } } with fs.readFile(path.resolve(srcPath)) fn(file) if file { ? -> nblog('Could not read file', srcPath) _ -> { runners := [] // calls to initialize each panel snippets := [] // panel definitions content := md.parse(file) |> map(fn(block, i) if { // syntax-highlight Oak code snippets block.tag = :pre & block.children.(0).lang = 'oak' -> { block.children.(0).children.0 := { tag: :rawHTML children: [highlightOak(i, block.children.(0).children.0)] } block } // compile panel definition snippets out of the document, and // replace them with panel placeholder
s block.tag = :pre & block.children.(0).lang = 'notebook' -> { runners << 'panel_{{0}}()' |> fmt.format(i) snippets << 'fn panel_{{0}} { nb := Notebook({{ 0 }}), {{1}}, nb.register(display), nb.display() }' |> fmt.format(i, block.children.(0).children.0) { tag: :rawHTML children: ['
' |> fmt.format(i)] } } _ -> block }) |> md.compile() // the final Oak script to be included in the page includes a "prelude" // with the Oak Notebook runtime, definitions of all the panels, and // calls to initialize each panel panelProg := [ Prelude snippets |> join(',') runners |> join(',') ] |> join(',') // compile and save the notebook HTML page with compileOakToJS(panelProg) fn(script) if script { ? -> nblog('Could not compile script') _ -> with fs.writeFile( path.resolve(destPath) Template |> fmt.format({ content: content script: script }) ) fn(res) if res { ? -> nblog('Could not save file!') _ -> nblog('Saved.') } } } } Prelude := '// oak notebook std := import(\'std\') str := import(\'str\') math := import(\'math\') sort := import(\'sort\') fmt := import(\'fmt\') datetime := import(\'datetime\') debug := import(\'debug\') md := import(\'md\') // global display in case display is not defined locally in a panel display := ? _AllNotebooks := [] CustomWidget := { customWidgets := [] { widgets: fn() customWidgets define: fn define(name, f) { customWidgets << { name: string(name) f: f } _AllNotebooks |> with std.each() fn(nb) { nb.(string(name)) := fn(args...) f(nb, args...) } } } } fn Notebook(i) { el := document.querySelector(\'.oak-notebook-panel.panel-\' + string(i)) displayEl := el displayHostEl := { displayEl := document.createElement(\'div\') displayEl.className := \'oak-notebook-display\' displayEl } displayOutputs := fn {} fn _wrapInLabel(labelName, inputEl) { labelText := document.createElement(\'div\') labelText.className := \'labelText\' labelText.textContent := labelName labelEl := document.createElement(\'label\') labelEl.appendChild(labelText) labelEl.appendChild(inputEl) labelEl } fn _input(type, labelName, value, attrs) { inputEl := document.createElement(if type { :textarea -> \'textarea\' _ -> \'input\' }) if type != :textarea -> { inputEl.setAttribute(\'type\', string(type)) } inputEl.value := value attrs |> std.default({}) |> std.entries() |> with std.each() fn(entry) { [attr, val] := entry if val != ? -> inputEl.setAttribute(attr, val) } with inputEl.addEventListener(\'input\') fn { display() } labelEl := _wrapInLabel(labelName, inputEl) labelEl.classList.add(\'type-\' + string(type)) el.appendChild(labelEl) inputEl } fn number(labelName, value, min, max, step) { _input(:number, labelName, value, { min: min max: max step: step }) } fn scrubbable(fmtSpec, value, min, max, step) { step := step |> std.default(1) fmtSpec := fmtSpec |> std.default([\'\', \'\']) inputEl := document.createElement(\'span\') inputEl.className := \'oak-notebook-scrubbable\' inputEl.tabIndex := 0 inputEl.textContent := value resetButton := document.createElement(\'button\') resetButton.classList.add(\'resetButton\') resetButton.textContent := \'reset\' with resetButton.addEventListener(\'click\') fn { value <- initialValue renderValue() } fn clientX(evt) if evt.touches { ? -> evt.clientX _ -> evt.touches.(0).clientX } fn clipByStep(n) { min + int((n + step / 2 - min) / step) * step } initialValue := value startX := ? startValue := ? scrubObj := { value: value } fn renderValue { inputEl.textContent := string(value) scrubObj.value := value if { value = initialValue & formatParagraph.contains(resetButton) -> formatParagraph.removeChild(resetButton) value != initialValue & !formatParagraph.contains(resetButton) -> formatParagraph.appendChild(resetButton) } display() } with inputEl.addEventListener(\'keydown\') fn(evt) if evt.key { \'ArrowUp\', \'ArrowRight\' -> { evt.preventDefault() value <- math.clamp(value + step, min, max) renderValue() } \'ArrowDown\', \'ArrowLeft\' -> { evt.preventDefault() value <- math.clamp(value - step, min, max) renderValue() } } [ [\'mousedown\', \'mouseup\', \'mousemove\'] [\'touchstart\', \'touchend\', \'touchmove\'] ] |> with std.each() fn(events) { [begin, end, move] := events fn handleMove(evt) { evt.preventDefault() value <- clipByStep( startValue + (clientX(evt) - startX) |> math.scale(0, math.min(window.innerWidth, 20 * (max - min) / step), 0, max - min) ) |> math.clamp(min, max) renderValue() } with inputEl.addEventListener(begin) fn(evt) { evt.preventDefault() startX <- clientX(evt) startValue <- value document.body.classList.add(\'scrubbing\') document.body.addEventListener(move, handleMove) with document.body.addEventListener(end) fn { evt.preventDefault() document.body.classList.remove(\'scrubbing\') document.body.removeEventListener(move, handleMove) } } } formatParagraph := document.createElement(\'p\') [before, after] := fmtSpec |> std.map(fn(text) document.createTextNode(text)) formatParagraph.appendChild(before) formatParagraph.appendChild(inputEl) formatParagraph.appendChild(after) el.appendChild(formatParagraph) scrubObj } fn checkbox(labelName) { _input(:checkbox, labelName) } fn text(labelName, value) { _input(:text, labelName, value) } fn prose(labelName, value) { _input(:textarea, labelName, value) } fn select(labelName, options) { fn createOption(value) { opt := document.createElement(\'option\') opt.value := value opt.textContent := value } selectEl := document.createElement(\'select\') selectEl.append(options |> std.map(createOption)...) with selectEl.addEventListener(\'change\') fn [ display() ] el.appendChild(_wrapInLabel(labelName, selectEl)) selectEl } fn button(value) { buttonObj := { clicks: [] } b := document.createElement(\'button\') b.classList.add(\'button\') b.textContent := value with b.addEventListener(\'click\') fn { buttonObj.clicks << time() display() } el.appendChild(b) buttonObj } fn label(s...) { html(s |> std.map(string) |> str.join(\' \') |> md.transform()) } fn table(entries) if type(entries.0) { :null, :object -> { columns := {} |> std.merge(entries...) |> keys() tb := document.createElement(\'table\') header := document.createElement(\'tr\') tb.appendChild(header) columns |> with std.each() fn(col) { th := document.createElement(\'th\') th.textContent := col header.appendChild(th) } entries |> with std.each() fn(entry) { tr := document.createElement(\'tr\') columns |> with std.each() fn(col) { td := document.createElement(\'td\') val := entry.(col) td.textContent := if type(val) { :null -> \'\' :int, :float, :string -> string(val) _ -> debug.inspect(val) } tr.appendChild(td) } tb.appendChild(tr) } tbContainer := document.createElement(\'div\') tbContainer.classList.add(\'table-container\') tbContainer.appendChild(tb) displayEl.appendChild(tbContainer) } _ -> table(entries |> std.map(fn(x) { value: x })) } fn plot(f, domain, range) { // TODO: use canvas } fn html(innerHTML) { template := document.createElement(\'template\') template.innerHTML := String(innerHTML) displayEl.appendChild(template.content) } fn register(display) { if display != ? -> displayOutputs <- display } fn display { with requestAnimationFrame() fn { displayEl <- displayHostEl if !el.contains(displayEl) -> el.appendChild(displayEl) displayEl.textContent := \'\' evt := try(displayOutputs) if evt.type = :error -> std.println(\'Error:\', evt.error) displayEl <- el } } nb := { number: number checkbox: checkbox scrubbable: scrubbable prose: prose text: text button: button select: select label: label table: table plot: plot html: html register: register display: display } with CustomWidget.widgets() |> std.each() fn(widget) { nb.(widget.name) := fn(args...) widget.f(nb, args...) } _AllNotebooks << nb nb }' Template := ' Oak Notebook
{{ content }}
'