Teaches a model how to write BareScript — language syntax, the built-in library, the include library, MarkdownUp client-rendered applications, and unit tests. Use this skill whenever the user asks you to write, read, modify, or review BareScript (`.bare`) code, a MarkdownUp Markdown application, a `markdown-script` code block, or any code that uses BareScript built-in functions (`array*`, `object*`, `string*`, `data*`, `markdownPrint`, `elementModelRender`, `drawNew`, etc.). Also use it when asked to write unit tests for BareScript code. Apply this skill even if the user does not say the word "BareScript" — recognize it from the `.bare` extension, the `markdown-script` fenced code block, or the function-naming style above.
Resources
12Install
npx skillscat add craigahobbs/bare-script-py Install via the SkillsCat registry.
BareScript Skill
This file teaches a language model how to write correct, idiomatic
BareScript. It is plain
Markdown with concrete examples — any frontier model (Claude, GPT, Gemini,
Llama, …) can read it.
When to use this skill
Use it whenever you write, modify, review, or debug BareScript — .bare files,markdown-script fenced blocks, MarkdownUp apps, or their unit tests. Recognize
BareScript even when it isn't named: the .bare extension, markdown-script
code fences, function names like arrayLength / objectGet / markdownPrint /systemFetch, or the function: … endfunction / if: … endif /for … in …: … endfor block syntax.
What BareScript is (one paragraph)
BareScript is a small, embeddable scripting language with Python-flavored
syntax. It runs in two implementations (JavaScript and Python) that share
100% of their test suite. There are no classes, no modules, no exceptions, no
imports — just functions, a literal-only data model (number, string, boolean,
null, array, object, datetime, regex, function), and ~200 library functions.
Long-running scripts can be executed in a synchronous interpreter; scripts
that do I/O (systemFetch, includes that fetch URLs) run in the async
interpreter and use async function declarations.
1. Language syntax (must-know)
Statements end at newline
There are no semicolons. To continue a long statement onto the next line, end
the previous line with a backslash \:
colors = [ \
'red', \
'green', \
'blue' \
]Comments
# to end of line. There are no block comments.
# This is a comment
x = 1 # Trailing comments are also fineValues
null,true,false(special variables; cannot be reassigned)- Numbers: integer, float, scientific (
1.5e10), hex (0xFF) - Strings:
'single'or"double"— concatenate with+ - Arrays:
[1, 2, 3] - Objects:
{'name': 'Alice', 'age': 30} - Datetimes: produced by
datetimeNew(...),datetimeISOParse(...), etc. - Regex: produced by
regexNew('[0-9]+') - Functions are first-class values
Variables and assignment
x = 5
name = 'Alice'
items = [1, 2, 3]Top-level assignments create globals. Assignments inside a function create
locals. Variable names with non-alphanumeric characters must be wrapped in
brackets: [Height (ft)].
Critical: there is no bracket access for arrays/objects
obj['key'] and arr[0] are NOT valid BareScript. (Brackets are only used
for array literals and for variable names with special characters.) Always
use the library functions:
| You want… | Use this |
|---|---|
arr[i] |
arrayGet(arr, i) |
arr[i] = v |
arraySet(arr, i, v) |
obj['k'] |
objectGet(obj, 'k') |
obj['k'] = v |
objectSet(obj, 'k', v) |
obj['k'] with default |
objectGet(obj, 'k', defaultVal) |
| check key existence | objectHas(obj, 'k') |
| array length | arrayLength(arr) |
| object keys | objectKeys(obj) |
This is the single most common mistake when models write BareScript. If you
catch yourself typing [, stop and check whether you mean a literal.
Control flow
Every block-opening statement ends with : and every block has an explicitendif / endwhile / endfor / endfunction:
if x < 0:
y = -1
elif x > 0:
y = 1
else:
y = 0
endif
i = 0
while i < 10:
i = i + 1
endwhile
for value in items:
systemLog(value)
endfor
# With index
for value, ixValue in items:
systemLog(ixValue + ': ' + value)
endfor
break # exits the nearest while/for
continue # next iteration of the nearest while/forjump label / jumpif (expr) label / label: exist for hot loops but are
rarely used outside the standard library — prefer while/for.
Functions
function double(n):
return n * 2
endfunction
# Async required if the body calls any async function (e.g., systemFetch,
# included libraries that fetch URLs). The closer is always `endfunction` —
# there is no separate `endasync` keyword.
async function fetchJson(url):
return jsonParse(systemFetch(url))
endfunctionA function that calls any async function must itself be declared async.
Sync code cannot call async code.
return without an expression returns null. A function with no return
also returns null.
The built-in if(test, a, b) expression function
This is special: only the chosen branch is evaluated.
v = if(a == b, fn1(), fn2())Use this for inline conditional values; use if/elif/endif for statements.
Operators
Arithmetic: + - * / % **
Comparison: == != < <= > >=
Logical: && || (short-circuit; && returns left if falsy else right;|| returns left if truthy else right)
Bitwise: & | ^ << >> ~
Unary: ! - ~
+ concatenates strings and coerces the other operand to string if either is
a string. Datetimes support + number (milliseconds) and datetime - datetime
(milliseconds).
Operator on incompatible types returns null. Library functions also returnnull on invalid arguments rather than raising.
Truthiness
if, &&, ||, !, and the if(test, a, b) expression function all use
the same boolean coercion. Falsy values:
nullfalse0''(empty string)[](empty array)
Everything else is truthy — including the empty object {}.
This differs from JavaScript: empty arrays are falsy here. The practical
consequence: if !arr: is not equivalent to if arr == null: because the
former also triggers on []. Only collapse X == null to !X when you can
prove X is either null or guaranteed-truthy (an object, or a string the
producer never returns as empty).
Short-circuit evaluation
&&, ||, and the if(test, a, b) expression function all short-circuit —
the unused branch is never evaluated:
# regexMatch only runs when stringStartsWith returns true
m = stringStartsWith(s, '"') && regexMatch(quotedFieldRe, s)
# Equivalent using the if expression — same short-circuit behavior
m = if(stringStartsWith(s, '"'), regexMatch(quotedFieldRe, s))Library functions don't short-circuit their arguments. Use the operators orif() when you need to guard an expensive call behind a cheap predicate.
Includes
include 'util.bare' # relative path (or URL)
include <unittest.bare> # system include — resolved against the
# interpreter's system-include prefixinclude evaluates the file's top-level statements in the global scope,
making its functions available. The order matters: include before you call.
In the CLI, non-URL paths read from the local filesystem; same for non-URLsystemFetch calls.
Multiline statements
Use a trailing backslash. Common in object and array literals:
person = { \
'name': 'Alice', \
'age': 30, \
'city': 'New York' \
}What is NOT in the language
- No classes, no
new, nothis. - No imports/modules — only
include. - No exceptions, no
try/catch. Functions returnnullon bad input. - No
++/--/+=/ compound assignment. Writei = i + 1. - No bracket access (covered above — the most important "no").
- No string interpolation. Concatenate with
+. - No
for (i = 0; i < n; i = i + 1)C-style loops — usewhileorfor in. - No destructuring, no spread.
- No
nullcoalescing operator. UseobjectGet(obj, 'k', default)orif(x == null, default, x).
Performance
These hold up when writing or optimizing any BareScript. Each is a starting
hypothesis, not a guarantee — a-priori estimates of perf impact are routinely
off by 5–10× in both directions, so measure a candidate against a baseline
before committing to it.
- Prefer native built-ins over interpreted replacements.
regexMatch,jsonStringify/jsonParse,arrayJoin,stringEncoderun in the host
language; don't reimplement them as BareScript loops to "skip overhead."jsonParse(jsonStringify(x))deep-clones faster than a per-elementarrayCopyloop. - Hoist loop invariants, and split a loop on a function-arg invariant
rather than re-checking it per iteration (if variables != null:outside,
two specialized loop bodies inside, beats one body with a per-iterif(variables != null, ...)). systemBoolean(X)inside anifis redundant —ifalready coerces.- Precompute per-field metadata as tuples (e.g.
[field, attr, isMarkdown])
once before a row loop when the same metadata is read across many rows. - Per-row dispatch is expensive. Pre-group by function so a
elif fn == 'sum': … elif fn == 'avg': …chain runs once per group, not once
per row. When dispatch is unavoidable, an inlineif/elifchain beats afor-loop sweep over candidate keys — it drops the loop-variable bookkeeping
and lets each branch assign a string literal directly (~25% on one inner loop). - For single-key / member dispatch, avoid
arrayGet(objectKeys(obj), 0)—
it allocates a fresh keys array per call; use directobjectGet(obj, 'key')
truthy chains. For "which alternation member of a regex matched," wrap each
member as(?<name>...)at compile time and identify it with oneobjectGet(match.groups, name); chain lazily
(x = !prev && objectGet(groups, 'x')) so later lookups short-circuit once an
earlier sibling matched. regexMatchAll+ anixSearchindex beatswhile + regexMatch + stringSlicefor iterating matches — one scan with a pointer vs a fresh
re-scan each iteration — and it's the idiomatic form (seemarkdownParserParagraphSpans,markdownHighlightElements). Caveat: with themflag,^/$in inner patterns match true line boundaries in theregexMatchAllform (usually what you want).- Don't assume "precompute into a lookup map" is free — an
objectGetinto
a map can cost more than the work it replaces (e.g. a concat of string
literals). Likewise, don't gateregexMatch(re, line)withstringIndexOf
pre-checks; the engine fails fast on anchored mismatches, and the gate usually
costs more than it saves.
2. The built-in library
All functions are global — no namespacing. Names are camelCase, grouped by
prefix. Library functions validate arguments and return null on type
mismatch (with a debug log if debug mode is on).
Full reference: https://craigahobbs.github.io/bare-script/library/ — also
published as a single-page Markdown document that can be fetched directly into
context: https://craigahobbs.github.io/bare-script/library/barescript-library.md.
What follows is what you need to recall the right name without searching.
Array (array*)
arrayCopy(arr) · arrayDelete(arr, i) · arrayExtend(arr, other) ·arrayFlat(arr, depth?) · arrayGet(arr, i) ·arrayIndexOf(arr, value, start?) · arrayJoin(arr, sep) ·arrayLastIndexOf(arr, value, start?) · arrayLength(arr) ·arrayNew(values...) · arrayNewSize(size, fill?) · arrayPop(arr) ·arrayPush(arr, values...) · arrayReverse(arr) · arraySet(arr, i, v) ·arrayShift(arr) · arraySlice(arr, start?, end?) · arraySort(arr, cmp?)
nums = [3, 1, 4, 1, 5]
arrayPush(nums, 9, 2, 6)
sorted = arraySort(arrayCopy(nums)) # don't mutate the original
first = arrayGet(sorted, 0)arrayGet out of bounds is not free. Unlike objectGet (which takes a
default and quietly returns it for a missing key), arrayGet with an index>= arrayLength(arr) is an invalid argument: it returns null but emits a
debug log in debug mode — so you can't use it to "peek" at an optional
trailing element. Guard the length, and let short-circuiting skip the read:
desc = arrayGet(sort, 1) # BAD: logs when sort is just [field]
desc = if(arrayLength(sort) >= 2, arrayGet(sort, 1)) # GOOD: null when absentPrefer if() when binding the optional value to a variable — a missing
element becomes null, the natural "absent" value. && works too
(arrayLength(sort) >= 2 && arrayGet(sort, 1)) but yields false rather thannull for the short tuple, so reserve it for feeding a boolean test. This is
the array analog of guarding with objectHas before a no-default objectGet.
Object (object*)
objectAssign(target, source) · objectCopy(obj) · objectDelete(obj, k) ·objectGet(obj, k, default?) · objectHas(obj, k) · objectKeys(obj) ·objectNew(k1, v1, ...) · objectSet(obj, k, v)
Iterate keys with for:
for key in objectKeys(obj):
systemLog(key + ' = ' + objectGet(obj, key))
endforString (string*)
stringCharAt(s, i) · stringCharCodeAt(s, i) · stringDecode(bytes) ·stringEncode(s) · stringEndsWith(s, suffix) · stringFromCharCode(...) ·stringIndexOf(s, sub, start?) · stringLastIndexOf(s, sub, start?) ·stringLength(s) · stringLower(s) · stringNew(value) ·stringRepeat(s, n) · stringReplace(s, find, replace) ·stringSlice(s, start, end?) · stringSplit(s, sep) ·stringSplitLines(s) · stringStartsWith(s, prefix) · stringTrim(s) ·stringUpper(s)
There is no string interpolation. Use +:
greeting = 'Hello, ' + name + '! You are ' + age + ' years old.'Number (number*)
numberParseFloat(s) · numberParseInt(s, radix?) ·numberToFixed(n, decimals?, trimZeros?) · numberToString(n)
Math (math*)
mathAbs · mathAcos · mathAsin · mathAtan · mathAtan2(y, x) ·mathCeil · mathCos · mathE() · mathFloor · mathLn · mathLog(x, base?) ·mathMax(...) · mathMin(...) · mathPi() · mathRandom() · mathRound ·mathSign · mathSin · mathSqrt · mathTan
Regex (regex*)
regexEscape(s) · regexMatch(re, s) · regexMatchAll(re, s) ·regexNew(pattern, flags?) · regexReplace(re, s, replace) ·regexSplit(re, s)
Regex literals are not part of the syntax — always call regexNew(...).regexMatch returns null on no match, otherwise {'index': N, 'groups': {'0': '...', '1': '...'}}.
JSON (json*)
jsonParse(text) · jsonStringify(value, indent?)
Datetime (datetime*)
datetimeDay · datetimeHour · datetimeISOFormat(dt, isDate?) ·datetimeISOParse(s) · datetimeMillisecond · datetimeMinute ·datetimeMonth · datetimeNew(year, month, day, hour?, min?, sec?, ms?) ·datetimeNow() · datetimeSecond · datetimeToday() · datetimeYear
Add/subtract milliseconds: tomorrow = today + 86400000.
URL (url*)
urlEncode(s) · urlEncodeComponent(s)
Schema (schema*) — Schema Markdown
schemaParse(line1, line2, ...) · schemaParseEx(lines, types?, filename?) ·schemaTypeModel() · schemaValidate(types, typeName, value) ·schemaValidateTypeModel(types)
System (system*)
systemBoolean(v) · systemCompare(a, b) · systemFetch(url|request|urls) ·systemGlobalGet(name, default?) · systemGlobalSet(name, value) ·systemIs(a, b) · systemLog(msg) · systemLogDebug(msg) ·systemPartial(fn, args...) · systemType(v)
systemFetch is the only I/O primitive — and it's async. Any function
that calls it (directly or transitively) must be async.
async function loadDocs():
return jsonParse(systemFetch('https://example.com/data.json'))
endfunctionsystemType returns one of 'null', 'boolean', 'number', 'string','datetime', 'array', 'object', 'function', 'regex'.
BareScript (barescript*)
barescriptParseExpression(text) · barescriptEvaluateExpression(expr, globals?)
Use these to evaluate user-authored formula expressions (e.g. for a
spreadsheet-style data filter).
3. The include library (include <name.bare>)
Pure-BareScript libraries that ship with the package. Always include before
calling. Each is documented in detail at
https://craigahobbs.github.io/bare-script/include/.
| Include | Purpose | Key functions |
|---|---|---|
args.bare |
Parse MarkdownUp URL args, build links | argsParse, argsLink, argsURL, argsValidate, argsHelp |
data.bare |
Tabular data manipulation | dataParseCSV, dataFilter, dataSort, dataAggregate, dataJoin, dataCalculatedField, dataTop, dataValidate |
dataTable.bare |
Render data array as Markdown table | dataTable, dataTableMarkdown, dataTableElements, dataTableValidate |
dataLineChart.bare |
Render line charts as SVG | dataLineChart, dataLineChartElements, dataLineChartValidate |
diff.bare |
Line diff between strings/arrays | diffLines |
draw.bare |
Imperative SVG drawing | drawNew, drawRect, drawCircle, drawEllipse, drawLine, drawMove, drawClose, drawPathRect, drawArc, drawText, drawTextStyle, drawTextWidth, drawTextHeight, drawImage, drawStyle, drawOnClick, drawWidth, drawHeight, drawHLine, drawVLine, drawRender, drawElements |
elementModel.bare |
Validate / stringify element models | elementModelValidate, elementModelToString |
forms.bare |
Form-control element-model helpers | formsTextElements, formsLinkElements, formsLinkButtonElements |
markdown.bare |
Markdown utilities | markdownEscape, markdownHeaderId, markdownTitle, markdownParagraphText, markdownValidate |
markdownParser.bare |
Markdown text → Markdown model | markdownParse |
markdownElements.bare |
Markdown model → element model | markdownElements, markdownElementsAsync |
markdownHighlight.bare |
Code-block syntax highlighting | markdownHighlightElements, markdownHighlightCodeBlockElements, markdownHighlightCodeBlockElementsAsync, markdownHighlightCompileHighlightModels |
markdownUp.bare |
Stub implementations of the MarkdownUp runtime functions. Loaded automatically by bare -m / -l; never include it yourself — see Section 4. |
markdownPrint, elementModelRender, documentSetTitle, documentInputValue, documentURL, documentSetFocus, documentSetKeyDown, documentSetReset, documentFontSize, windowWidth, windowHeight, windowKeyState, windowPlaySound, windowSetLocation, windowSetResize, windowSetTimeout, windowURLObject, windowClipboardRead, windowClipboardWrite, localStorageGet/Set/Remove/Clear, sessionStorageGet/Set/Remove/Clear |
pager.bare |
Multi-page MarkdownUp app shell | pagerMain, pagerValidate |
qrcode.bare |
Render QR codes | qrcodeDraw, qrcodeElements, qrcodeMatrix |
schemaDoc.bare |
Schema-markdown documentation app | schemaDocMain, schemaDocMarkdown |
unittest.bare |
Unit-test framework | unittestRunTest, unittestEqual, unittestDeepEqual, unittestCoverageStart, unittestCoverageStop, unittestReport |
unittestMock.bare |
Mock library functions during tests | unittestMockAll, unittestMockOne, unittestMockOneGeneric, unittestMockEnd |
baredoc.bare / baredocCLI.bare |
Generate library.json from doc comments |
baredocMain, baredocCLIMain |
data.bare quick examples
include <data.bare>
rows = [ \
{'name': 'Alice', 'age': 30, 'city': 'NY'}, \
{'name': 'Bob', 'age': 25, 'city': 'BOS'}, \
{'name': 'Cara', 'age': 35, 'city': 'NY'} \
]
# Filter rows using an expression evaluated against each row
adults = dataFilter(rows, 'age >= 30')
# Sort: array of [field, descending?] pairs
sorted = dataSort(rows, [['city', false], ['age', true]])
# Add a calculated field IN PLACE
dataCalculatedField(rows, 'label', 'name + " (" + city + ")"')
# Aggregate (categories = group-by; measures = aggregations)
summary = dataAggregate(rows, { \
'categories': ['city'], \
'measures': [ \
{'field': 'age', 'function': 'average', 'name': 'avgAge'}, \
{'field': 'age', 'function': 'count', 'name': 'n'} \
] \
})
# Join two data arrays on a shared key
cities = [{'city': 'NY', 'state': 'NY'}, {'city': 'BOS', 'state': 'MA'}]
joined = dataJoin(rows, cities, 'city')draw.bare quick example
include <draw.bare>
drawNew(400, 300)
drawStyle('black', 2, '#88ccff')
drawRect(20, 20, 360, 260)
drawTextStyle(24, 'black', true)
drawText('Hello', 200, 150)
drawRender() # prints SVG via markdownPrint in MarkdownUpelementModel.bare: element models
Element models are plain BareScript objects shaped like:
{ \
'html': 'div', # or 'svg': '...' for SVG
'attr': {'id': 'main', 'class': 'box'}, \ # optional attributes
'elem': [ \ # children: object | array | {'text': '...'}
{'html': 'h1', 'elem': {'text': 'Hi'}}, \
{'html': 'p', 'elem': {'text': 'World'}} \
], \
'callback': {'click': onClickFn} \ # optional event handlers
}Render with elementModelRender(model) (MarkdownUp), or stringify withelementModelToString(model) for use as raw HTML/SVG.
4. MarkdownUp client-rendered applications
MarkdownUp is a Markdown viewer
that executes BareScript embedded in fenced markdown-script code blocks. A
MarkdownUp app is a Markdown file plus one or more .bare files.
Minimum app
app.md
~~~ markdown-script
include 'app.bare'
~~~app.bare
function appMain():
documentSetTitle('Hello App')
markdownPrint('# Hello, MarkdownUp!', '')
i = 0
while i < 5:
markdownPrint('- Item ' + (i + 1))
i = i + 1
endwhile
endfunction
appMain()The runtime functions you have
When code runs inside MarkdownUp (in the browser), these "document" / "window"
/ "storage" functions are provided by the runtime:
- Output:
markdownPrint(line1, line2, ...)— print Markdown lines
(empty string = blank line / paragraph break);elementModelRender(model)— render an element model. - Document:
documentSetTitle(s),documentURL(url),documentInputValue(id),documentFontSize(),documentSetFocus(id),documentSetKeyDown(fn),documentSetReset(id). - Window:
windowWidth(),windowHeight(),windowSetLocation(url),windowSetResize(fn),windowSetTimeout(fn, ms),windowURLObject(url),windowClipboardRead(),windowClipboardWrite(s),windowKeyState(code, ctrl?, shift?, alt?, meta?),windowPlaySound(name). - Storage:
localStorageGet/Set/Remove/Clear,sessionStorageGet/Set/Remove/Clear.
Notes on a few of these:
documentSetKeyDown(fn)— the event passed tofncarries.key(the
logical key, e.g.'r'or'ArrowLeft'),.code(the physical key,
e.g.'KeyR'or'ArrowLeft'), and.ctrlKey/.shiftKey/.altKey/.metaKey.documentSetReset(id)— tells the runtime to clear everything below the
element with this id before the next re-render. Pair with an invisible<div>you render once so the MarkdownUp menu DOM stays stable while
your canvas repaints (see the Animation apps section below).windowKeyState(code, ...)— polls the current keyboard state for a
physical key (e.g.'ArrowLeft','Space','KeyR'). For game /
animation loops, polling inside the tick is preferable to adocumentSetKeyDownhandler — see gotcha #5 below.windowPlaySound(name)— built-in SFX, sourced frommarkdownUp.bare:- UI:
beep,click,error,success,warning - Arcade:
coin,jump,laser,explosion,powerup,powerdown,hit,blip,gameover - Notes:
noteC4…noteC5(including sharpsnoteCs4,noteDs4,noteFs4,noteGs4,noteAs4) - Drums:
drumKick,drumSnare,drumHihat,drumOpenhat,drumTomLow/Mid/High,drumClap,drumCrash,drumRide
- UI:
In the browser, MarkdownUp itself provides these functions. Under the plainbare CLI, pass -m (or -l for HTML output) and the CLI automatically
prepends include <markdownUp.bare> for you — a small library of stub
implementations:
bare -m app.bare # run the app, output Markdown text
bare -l app.bare # run the app, output an HTML document
bare -m -x app.bare # lint / static-analyze a MarkdownUp app⚠ Never write
include <markdownUp.bare>yourself — not in app code,
test code, orincluded helpers. In the real browser runtime these
functions are built-in, and the include would overwrite them with the
logging stubs, silently turning rendering into call-recording. Underbare -m/-l(and unit tests) the CLI already prepends it for you, so
a manual include is at best redundant. Either way it's wrong.
In tests, the stubs let you assert what the app would have rendered.
URL arguments with args.bare
include <args.bare>
arguments = [ \
{'name': 'name', 'default': 'World'}, \
{'name': 'count', 'type': 'int', 'default': 3} \
]
function appMain():
args = argsParse(arguments)
name = objectGet(args, 'name')
count = objectGet(args, 'count')
documentSetTitle('Hello ' + name)
markdownPrint('# Hello, ' + markdownEscape(name) + '!', '')
i = 0
while i < count:
markdownPrint('- Greeting ' + (i + 1))
i = i + 1
endwhile
# Links that round-trip current args, overriding 'count'
markdownPrint( \
'', argsLink(arguments, 'More', {'count': count + 1}), \
'', argsLink(arguments, 'Less', {'count': count - 1}), \
'', argsLink(arguments, 'Reset', null, true) \
)
endfunction
appMain()Interactive forms with forms.bare
include <args.bare>
include <forms.bare>
arguments = [{'name': 'value', 'default': ''}]
function appMain():
args = argsParse(arguments)
documentSetTitle('Echo')
markdownPrint('# Echo', '')
elementModelRender(formsTextElements('input', objectGet(args, 'value'), 30, onEnter))
markdownPrint('', 'You typed: ' + markdownEscape(objectGet(args, 'value')))
endfunction
function onEnter():
value = documentInputValue('input')
windowSetLocation(argsURL(arguments, {'value': value}))
endfunction
appMain()Multi-page apps with pager.bare
include <pager.bare>
function pageHello(args):
markdownPrint('# Hello', '', 'On the Hello page.')
endfunction
function pageAbout(args):
markdownPrint('# About', '', 'This is an example.')
endfunction
pagerMain({ \
'pages': [ \
{'name': 'Hello', 'type': {'function': {'function': pageHello}}}, \
{'name': 'About', 'type': {'function': {'function': pageAbout}}} \
] \
})Animation apps (the tick pattern)
Animation in MarkdownUp is a tick function that runs every N ms and
reschedules itself via windowSetTimeout. Two non-obvious pieces make it
work cleanly:
- An invisible "reset anchor"
<div>rendered once +documentSetReset(id)
before each redraw. Without it, the SVG fights the MarkdownUp burger
menu on every redraw. With it, the runtime preserves everything up to the
anchor element and replaces only what's below — so the menu DOM stays
stable while the canvas repaints. - Polled input via
windowKeyState, notdocumentSetKeyDown. See
gotcha #5 below: a keydown handler that doesn't itself callwindowSetTimeoutsilently kills the animation loop on the next
keypress.
A minimal skeleton:
include <draw.bare>
appAnchorId = 'app-anchor'
appTickMs = 50
appState = {'x': 100, 'spaceWasDown': false}
function appMain():
documentSetTitle('Animated')
elementModelRender([{'html': 'div', 'attr': {'id': appAnchorId, 'style': 'display: none;'}}])
appRenderAndSchedule()
endfunction
function appRenderAndSchedule():
documentSetReset(appAnchorId)
appDraw()
windowSetTimeout(appTick, appTickMs)
endfunction
function appTick():
# Continuous: held-down motion
if windowKeyState('ArrowRight'):
objectSet(appState, 'x', objectGet(appState, 'x') + 2)
endif
# Edge-triggered: fire only on rising edge of Space
spaceNow = windowKeyState('Space')
if spaceNow && !objectGet(appState, 'spaceWasDown'):
windowPlaySound('laser')
endif
objectSet(appState, 'spaceWasDown', spaceNow)
appRenderAndSchedule()
endfunction
function appDraw():
drawNew(200, 200)
drawStyle('#000', 0, '#000')
drawRect(0, 0, 200, 200)
drawStyle('#0f0', 0, '#0f0')
drawRect(objectGet(appState, 'x'), 100, 20, 20)
drawRender()
endfunctionFor apps that need to survive a hamburger toggle or hash-param change
(which tear down all BareScript globals), persist the state object withsessionStorageSet(key, jsonStringify(appState)) at the end of each tick
and restore it in appMain — Tunguska is the canonical example. Version
the saved blob so a schema change ignores stale saves rather than
producing partial state.
MarkdownUp gotchas
- Always escape user input (URL args, fetched data, anything string-y)
withmarkdownEscapebeforemarkdownPrint. Markdown is rendered as HTML;
unescaped<and&will produce broken output or XSS. - Anything that calls
systemFetchmust beasync, including event
handlers wired throughforms.barecallbacks. markdownPrint(a, b)with no empty-string separator printsaandb
on consecutive lines — same paragraph in Markdown. Pass''between them
to force a paragraph break.- The MarkdownUp runtime is single-threaded JavaScript — there is no
coroutine cancellation. Long synchronous loops will freeze the browser.
For animation, schedule withwindowSetTimeout(fn, ms). - MarkdownUp cancels the pending
windowSetTimeoutat the end of every
script invocation. After adocumentSetKeyDownhandler, awindowSetResizehandler, or any other event callback returns, the
runtime clears whatever timer was armed before the callback ran and
only re-installs one if this invocation calledwindowSetTimeout(...). Practical consequence: adocumentSetKeyDown
handler that doesn't itself reschedule will silently kill an animation
loop on the next keypress. The safest pattern for game / animation apps
is to poll all input viawindowKeyStateinside the tick and skipdocumentSetKeyDownentirely — the tick is then the only invocation
that runs, and it always reschedules itself.
Real-world example applications
Complete, working MarkdownUp apps to study — each is a single .bare file
exercising the patterns above. Grouped by what they demonstrate (full gallery:
https://craigahobbs.github.io/MarkdownUpApplications.md):
Canvas drawing & animation (draw.bare)
- Mandelbrot Set Explorer — fractal rendering, zoom/pan via URL args.
App ·
Source - Conway's Game of Life —
windowSetTimeoutanimation loop withforms.barecontrols.
App ·
Source - Chaos Balls — animated bouncing-ball physics;
forms.bare+schemaDoc.baresettings.
App ·
Source - Color Ramp — color-space drawing driven by form controls.
App ·
Source - The Fruit Fly Trap Maker — generates a printable diagram from inputs.
App ·
Source - Happy Holidays! — a seasonal greeting drawing; custom message via URL
arg, full-screen mode, redraws on resize.
App ·
Source - Snack Attack — a Galaxian-like arcade game;
windowSetTimeoutgame loop,windowKeyStateinput,windowPlaySoundeffects, and title/play/game-over
screens with full-windowdrawNew.
App ·
Source - Tunguska Event Simulation — the largest example (~1200 lines), a pure
draw.baresimulation.
App ·
Source
Data dashboards (data.bare + dataTable.bare / dataLineChart.bare, often pager.bare)
- Solar Dashboard — multi-page dashboard of fetched data; charts + tables.
App ·
Source - Sunrise Dashboard — sunrise/sunset-time dashboard; pager + charts + tables.
App ·
Source - Day Hikes Dashboard — multi-page hike data with charts and tables.
App ·
Source - Money — financial-data dashboard; pager + charts +
schemaDoc.bare.
App ·
Source - Package Downloads Dashboard — package download stats as charts/tables.
App ·
Source - npm Dependency Explorer — fetches and aggregates the npm registry;
forms.bare-driven.
App ·
Source
Forms, widgets & documentation viewers (forms.bare, schemaDoc.bare)
- QR Code — generates QR codes with
qrcode.bare+elementModel.bare.
App ·
Source - Emoji — searchable emoji table;
forms.bare+dataTable.bare.
App ·
Source - The Hobbs Family Cookbook — a "Markdown book" reader app.
App ·
Source - BareScript Library Documentation Viewer — renders library JSON docs with
schemaDoc.bare+markdown.bare(this skill's own library reference).
App ·
Source - Chisel Documentation Viewer — smallest example (~90 lines): renders API
schema docs viaschemaDoc.bare.
App ·
Source
5. Unit testing BareScript
Layout
myapp/
├── app.bare
└── test/
├── runTests.md # entry point for MarkdownUp / browser
├── runTests.bare # entry point for `bare -m`
└── testApp.baretest/runTests.md
~~~ markdown-script
include 'runTests.bare'
~~~test/runTests.bare
include <unittest.bare>
include <unittestMock.bare> # only if you need mocking
unittestCoverageStart()
include 'testApp.bare'
unittestCoverageStop()
return unittestReport({'coverageMin': 100})unittestReport({'coverageMin': N}) fails the run when coverage falls below
the floor, separately from assertion failures — so "every assertion passed"
and "the suite is green" are two different checks. A new branch (an addedif/elif, a new objectGet default path) with no fixture that exercises it
turns the suite red on coverage even when every test passes; new code paths
need new tests.
test/testApp.bare — basic assertions
include '../app.bare'
function testAddNumbers():
unittestEqual(addNumbers(1, 2), 3)
unittestEqual(addNumbers(0, 0), 0)
endfunction
unittestRunTest('testAddNumbers')
function testFlatten():
unittestDeepEqual( \
flatten([[1, 2], [3, [4, 5]]]), \
[1, 2, 3, 4, 5] \
)
endfunction
unittestRunTest('testFlatten')Use unittestEqual for primitives and identity, unittestDeepEqual for
arrays and objects. Both log a failure and continue; unittestReport totals
them up.
Running tests
bare -m test/runTests.bare # exits non-zero on any failure
bare -m -x test/runTests.bare # lint + run (lint warnings printed alongside test results)-m automatically prepends include <markdownUp.bare> — your test code must
not include it itself (see Section 4).
Mocking the MarkdownUp runtime
markdownPrint, documentSetTitle, systemFetch, windowSetLocation, etc.
all have side effects you don't want in unit tests.
unittestMock.bare
intercepts every library function and records calls.
include '../app.bare'
include <unittestMock.bare>
function testAppMain():
unittestMockAll()
appMain()
unittestDeepEqual( \
unittestMockEnd(), \
[ \
['documentSetTitle', ['Hello App']], \
['markdownPrint', ['# Hello, MarkdownUp!', '']], \
['markdownPrint', ['- Item 1']], \
['markdownPrint', ['- Item 2']] \
] \
)
endfunction
unittestRunTest('testAppMain')Patterns:
unittestMockAll(data?)at the top of the test — mocks every MarkdownUp
runtime function andsystemFetch/systemLog. Each call is recorded as[name, [args...]]. The optionaldataargument supplies per-function
mock inputs; the two supported keys are:'documentInputValue': object mapping input element id → value'systemFetch': object mapping URL → response text
Example:unittestMockAll({'systemFetch': {'data.json': '{"a": 1}'}}).systemFetchalso accepts an array of URLs and returns an array of
responses (a batched, parallel fetch), recorded as['systemFetch', [[url1, url2]]]. CallingsystemFetchwith an empty array
succeeds and returns[], so consider guarding withif arrayLength(urls):
before fetching to skip a wasted no-op call.
unittestMockOne(funcName, mockFunc)— replace one library function
withmockFunc(a function value).mockFuncis called instead of the
real implementation; its return value is what callers see. The call is
also recorded.unittestMockOneGeneric(funcName)— record-only mock; calls returnnull. Useful for side-effect-only functions.unittestMockEnd()returns the recorded call log as an array of[name, [args...]]entries, and stops mocking.
Test conventions used in this repo
- One test function per behavior, named
test<Subject><Behavior>. - Last line of each test is
unittestRunTest('testName')— the runner picks
it up by name. - Async tests use
async functionand the runner awaits them. - For functions with side effects, always
unittestMockAll()and assert
on the call log — do not mock partially. - Reset the globals a test sets. Apps read URL args from
v<Name>globals
(systemGlobalSet('vName', 'x')); a test that sets them should set each back
tonullat the end so state never leaks into the next test. Prefer
unsetting exactly what the test set over a blanket "clear everything" helper.
Builtin debug logs are NOT mockable
When a builtin fails on an invalid argument (e.g. an out-of-bounds arrayGet),
the runtime emits its debug log by calling the interpreter's log sink
directly — not through systemLog / systemLogDebug. SounittestMockAll() / unittestMockOne('systemLog', ...) cannot capture it:
the mock records nothing while the message still prints. A test asserting "no
debug log" by mocking systemLog passes whether or not the bug is present, so
don't write one. (Mocking the failing builtin itself doesn't work either — the
unittest framework calls that same builtin.)
To prove a change produces no spurious debug output, run the suite in debug
mode (bare -d …; the include-test runner already passes -d) — a failing
builtin then prints … BareScript: Function "X" failed with error: …. This
repo's convention: a test that intentionally triggers an expected failure
precedes it with systemLogDebug('NOTICE: The following "X" error is expected:'). So every failed with error line must be immediately preceded
by a NOTICE: line — grep -B1 "failed with error" over the output makes
that a one-look check; any un-NOTICE'd line is unintended logging.
Asserting rendered output
Apps render through markdownPrint and elementModelRender; underunittestMockAll() you assert the recorded call log. Three things make those
assertions tractable:
unittestDeepEqualcomparesjsonStringifyoutput, and that one fact
drives the rest. Object keys serialize sorted, so key order never has to
match. Every function serializes to the string'<function>'(and every
regex to'<regex>'), so to assert an element model that carries an
event-handler callback — e.g.formsTextElementsstoressystemPartial(formsTextOnKeyup, onEnter)undercallback.keyup— just write
the string'<function>'in that slot and the deep-equal matches. There is no
literal for the anonymous partial, but none is needed and no scrubbing of the
call log is needed either. Trade-off: all functions collapse to the same
sentinel, so the assertion proves a callback is present, not which one.
(jsonStringify(value, 4)on its own is also handy for dumping a captured log
into a stable, diffable literal.)
6. Running BareScript
CLI (bare)
bare script.bare # run a plain BareScript script
bare -m app.bare # run a MarkdownUp app (Markdown text output)
bare -l app.bare # run a MarkdownUp app (HTML output)
bare -c 'expr' # run an inline expression
bare -d script.bare # debug mode: log invalid library args
bare -v K=V script.bare # set global K to JSON value V
bare -x script.bare # run AND statically analyze (lint warnings + execution)
bare -s script.bare # static analysis only — parse + lint, no execution
bare -m -x app.bare # run + lint a MarkdownUp app (must combine -m with -x)
bare -m -s app.bare # lint-only check of a MarkdownUp appNote: -x is static analysis with execution — it runs the script and
also reports lint warnings. Use -s for parse/lint without execution. For
MarkdownUp apps and their tests, always combine -m (or -l) with -x or-s so the runtime stubs are available (see Section 4).
In the CLI, systemFetch('local/path.json') reads a file; with a request
body it writes the body to the file.
Embedding (JavaScript)
import {parseScript} from 'bare-script/lib/parser.js';
import {executeScript} from 'bare-script/lib/runtime.js';
import {executeScriptAsync} from 'bare-script/lib/runtimeAsync.js';
const script = parseScript(`return N * 2`);
// Sync — no async functions used
console.log(executeScript(script, {globals: {N: 21}})); // 42
// Async — required if the script uses systemFetch or any async include
console.log(await executeScriptAsync(script, {fetchFn: fetch}));Embedding (Python)
Identical API in the
`bare-script` Python package.
7. Idioms and pitfalls — the model's checklist
Before declaring code "done," scan it for these. They are the failures models
most commonly produce when writing BareScript for the first time.
- No bracket access: no
obj['k'], noarr[0]. UseobjectGet,arrayGet. (Brackets appear only in[1, 2, 3]literals and[Variable Name]-style identifiers.) -
[],'', and0are all falsy. To tellnullfrom an empty
array/string, testx == nullandsystemType(x)/arrayLength(x)
explicitly — don't lean onif x:orif !x:(see Truthiness). - No
+=,++,--. Writei = i + 1. - Every
functionhasendfunction, everyifhasendif, everywhilehasendwhile, everyforhasendfor. -
asyncis on the function declaration (notawaitat call sites
— there is noawaitkeyword). If a function callssystemFetchor
any other async function, it must beasync function fn(...). - Multiline continuation uses trailing
\, including inside object
and array literals. - Strings are concatenated with
+— no f-strings, no${...}. - Invalid arguments return
null, not an exception. Defensively checkif x == null:before chaining. - Regex literals don't exist — always
regexNew(pattern, flags?). -
markdownPrintarguments are separate lines. Pass''between
blocks to get a Markdown paragraph break. - Escape Markdown with
markdownEscapebefore printing user content. - Includes go at the top, before any code that uses their functions.
- Never
include <markdownUp.bare>(see Section 4). Run/lint
MarkdownUp apps withbare -m -x app.bare, notbare -x app.bare. - Use
for value, ixValue in items:to get both value and index;
don't reinvent withwhile.
8. Notes for the model
- Examples beat prose. Pattern-match on the concrete examples above, and
match their style — 4-space indent, trailing\for continuation inside
literals,function/endfunction, lowercase camelCase names. - Recall names before inventing them; grep before guessing. If you're
unsure of a name, recall its camelCase prefix (array*,object*,string*,data*,draw*,markdown*,unittest*,systemFetch, …) and
pick the function that literally describes the operation. If it isn't in
this file, searchlib/library.js(built-ins) andlib/include/*.bare
(includes) — both stable and 100%-covered. - Don't translate from JavaScript or Python without re-checking. The
syntactic similarity hides three pitfalls — bracket access, augmented
assignment, and exceptions — none of which exist here. - Don't introduce abstractions. BareScript apps are small; inline the
logic, oneappMain()is fine. - For tests, use the
unittest.bare+unittestMock.barepattern from
Section 5 — don't invent your own assertions.
9. Links
- Language: https://craigahobbs.github.io/bare-script/language/
- Built-in library: https://craigahobbs.github.io/bare-script/library/
- Include library: https://craigahobbs.github.io/bare-script/include/
- Expression library: https://craigahobbs.github.io/bare-script/library/expression.html
- Built-in library, single-page Markdown (fetch for the full reference): https://craigahobbs.github.io/bare-script/library/barescript-library.md
- Library models, single-page Markdown: https://craigahobbs.github.io/bare-script/library/barescript-library-model.md
- MarkdownUp: https://craigahobbs.github.io/markdown-up/
- MarkdownUp example applications (fetchable Markdown): https://craigahobbs.github.io/MarkdownUpApplications.md
- JS source: https://github.com/craigahobbs/bare-script
- Python source: https://github.com/craigahobbs/bare-script-py