Skip to content
NisokuPublic

About

Sakko, the modern DSL (Design Sub-Language)

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Sakko

CI Deploy License: Apache-2.0

The modern DSL (Design Sub-Language) for describing UI trees.

Sakko is a bracket-based markup language for describing UI trees. Write concise, readable markup. Get a typed AST and a diagnostic report from the mature parser and typechecker, or use it as a standalone parser. (JavaScript codegen is under development.)

What does it look like?

<card {
  heading: "Hello, world"
  text: "This compiles to an AST."
  button: "Get Started"
}>

That is the entire source. Sakko tokenizes it, parses it, and produces a structured AST. The AST can then be transformed into anything: Sazami web components, React VNodes, JSON, you name it.

Here is a more complex example:

<player {
  card(row center) {
    coverart(round): "album.jpg"
    details {
      text(bold): "Midnight City"
      text(dim): "M83"
    }
    controls {
      icon-btn: previous
      icon-btn(accent large): play
      icon-btn: next
    }
  }
}>

Getting started

Install

cargo add sakko

Use

use sakko::{parse_sakko, tokenize};

fn main() -> sakko::Result<()> {
    // Tokenize source to tokens (useful for debugging)
    let tokens = tokenize("button(accent): Click me")?;

    // Parse to AST
    let ast = parse_sakko(r#"
<card {
  heading: "Hello"
  button: "Click"
}>
"#)?;

    println!("{ast:#?}");
    Ok(())
}

The structure parser is low-alloc: AST nodes borrow their text straight from the source via spans, and token/line values are Cow borrows of that same source. Parsing large templates allocates little beyond those token vectors.

AST Structure

The parser produces one of four node types:

Type Fields Description
root name, modifiers, children Top-level container
element name, modifiers, children Block element with children
inline name, modifiers, value Leaf element with text value
list items Comma-separated group

Modifiers are either flags or key-value pairs:

Modifier::Flag { value: "accent".into() }
Modifier::Pair { key: "cols".into(), value: "3".into() }

Every token and AST node carries a Span (byte offsets into the source), that is resolvable to line/column via LineIndex.

Syntax overview

Root blocks

Every Sakko document has one root block wrapped in angle brackets:

<page {
  ...children
}>

The root is implicit: parse_sakko wraps a file's top-level markup in a generated root node, so ports, lints and tooling all see a single unnamed root above the content.

Block elements

Elements with children use curly braces:

card {
  heading: "Title"
  text: "Description"
}

Inline elements

Elements without children use a colon:

text: Hello world
button(accent): Click me
icon: play

Modifiers

Parenthesized flags or key-value pairs after the element name:

button(primary large): Submit
grid(cols 3 gap medium): [...]
card(row center curved): { ... }
input(placeholder "Email"): ""

Lists

Comma-separated elements in square brackets:

row: [button: A, button: B, button: C]

Comments

Single-line comments with //:

// This is a comment
card {
  text: Hello  // inline comment
}

Reactive State

Declare reactive state with @state:

<counter {
  @state {
    count = 0
    step = 1
  }

  button @on:click { count++ }: "+"
  text: "Count: {count}"
}>

Compiles to Sairin signals. Read values with {name} interpolation.

Effects

Run side effects with @effect:

<app {
  @state { count = 0 }

  @effect {
    console.log("Count changed:", count)
    js { document.title = `Count: ${count}` }
  }

  button @on:click { count++ }: "Increment"
}>

Derived State

Compute derived values with @derived:

<app {
  @state { items = [] }

  @derived {
    count = items.length
    isEmpty = items.length == 0
  }

  text: "{count} items"
}>

Event Handlers

Handle events with @on:event:

button @on:click { count++ }: "Click"
input @on:input { value = e.target.value as string }: ""
div @on:mouseenter { isHovered = true }: "Hover me"

The event parameter e has type unknown: navigate and call freely, but assert with as before using it as a typed value.

Raw JavaScript: js { ... }

Saho is deliberately small. When you need the full platform, escape with a raw js block. The bytes pass through untouched, so any valid JavaScript works:

<dash {
  @state {
    theme = js { return localStorage.getItem("theme") } ?? "dark"
    width = js { return window.innerWidth } as number
  }

  button @on:click {
    js { document.title = "Dashboard" }
  }: "Focus"
}>

Rules:

  • A js {} block always has type unknown, wherever it appears.
  • unknown navigates (.prop, [i]) and calls freely; results stay unknown.
  • Typed consumption - arithmetic, comparisons, assigning into a typed slot - requires an assertion first (as number, as string, ...).
  • Bodies are never checked semantically; every occurrence is recorded in the compile report for audits.

Type Assertions

Cast with postfix as. Supported types: number, string, boolean, null, undefined, unknown, arrays (T[]), and one nullable suffix (T | null, T | undefined). Provably impossible casts are errors:

@state {
  n = raw as number          // unknown -> number: trusted
  bad = 5 as string          // error SKT014: cannot cast 'number' to 'string'
}

Two-way Binding

Bind inputs with @bind:

<form {
  input @bind="username": ""
  input(type password) @bind="password": ""
  text: "Hello, {username}!"
}>

Interpolation

Use {expression} in text values:

text: "Hello, {name}!"
text: "{a} + {b} = {a + b}"
text: "Items: {items.map(i => i.name).join(', ')}"

Expressions are parsed by Saho (sakko::saho), Sakko's embedded strict-expression language: Pratt-precedence parsing, template literals with nested substitutions, arrows, spread, optional chaining, the works. Every interpolation round-trips byte-for-byte through parse and lower.

Compiling & Running

Sakko's target output is pre-compiled JavaScript bound to the Sairin reactive runtime: components ship as plain JS plus script tags, with zero parser code in the browser.

The compiler pipeline is under active development:

Stage Status
Tokenizer + structure parser done
Saho expression module done
Typecheck pass (sakko::typecheck) done
Codegen (AST to JS) planned
Sakumi CLI (sakumi build) planned

Once the CLI lands, building a component will be:

sakumi build counter.sakko -o dist/

This emits JS you drop into a page alongside the Sairin runtime.

Project structure

crates/sakko/        Rust implementation crate
  src/saho/          Expression language (lexer + Pratt parser)
  src/syntax/        Template tokenizer + structure parser
  src/typecheck/     Typecheck pass
crates/sakko-wasm/   WASM bindings
Tests/sakko/         Integration test suites
Examples/            Example .sako files
Docs/                Documentation site (powered by DocMD)

Development

Requires rustup. The configured toolchain is selected automatically.

cargo build      # build
cargo test       # run all suites
cargo clippy     # lint (CI treats warnings as errors)
cargo fmt        # format

Documentation

Document Summary
Language Reference Full Sakko syntax guide
docs.rs/sakko Rust API reference (once published)

Run docs locally

cd Docs
npm install
npm run dev

License

Apache License v2.0

About

Sakko, the modern DSL (Design Sub-Language)

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages