report/src/main.nika

// Reads a stock list and writes it as an HTML page, largest count first.
//
//     printf 'tools;Bolts & Nuts;12\npaper;<plain> A4;40\n' > stock.csv
//     nikaia run -- stock.csv report.html
//     2 items, 52 in total
//
// The input has one `category;item;count` per line, as above. The page lands
// in report.html.
//
// Shows: a `grammar` with `@frame`, sorting by a key, and a `dsl html` page
// written to a file with `fs::write`.
//
// A package: `nikaia run` in this directory runs it, and `nikaia test` checks
// what it prints and the file it writes against `tests/` (ADR-247).

use std::fs
use std::cli
use std::html

grammar Stock {

    // Up to the next `;` or the end of the line. A line missing its last field
    // then stops at the newline instead of eating the line after it.
    rule FIELD -> String = s:until(";" | frame_end) -> { s }

    rule COUNT -> i64 = n:dec[i64](digit+) -> { n }

    // `@frame` says an entry ends at a newline. The rule is uppercase, so it is
    // lexical: no whitespace is skipped, and ` Bolts` stays different from `Bolts`.
    @frame(boundary: "\n")
    rule ENTRY -> Entry =
        category:FIELD ";" => item:FIELD ";" count:COUNT frame_end
        -> { Entry { category, item, count } }

    entry rule file -> Vec[Entry] = entries:ENTRY* -> { entries }
}

// `String`: the category and the item are slices of the input file, not
// copies.
pub struct Entry {
    category: String,
    item: String,
    count: i64,
}

// Every hole in the template is escaped, so the `&`, `<` and `"` in item names
// from the file arrive on the page as `&amp;`, `&lt;` and `&quot;`.
fn page(entries: Vec[Entry], total: i64) -> String {
    return dsl html {
        <html>
        <head><title>Stock</title></head>
        <body>
        <h1>Stock</h1>
        <table>
        <tr><th>Category</th><th>Item</th><th>Count</th></tr>
        <for e in :entries><tr><td>{e.category}</td><td>{e.item}</td><td>{e.count}</td></tr></for>
        </table>
        <p>{total} in total</p>
        </body>
        </html>
    } eod
}

fn main() throws {
    let path = cli::args().nth(1) ?? "stock.csv"
    let out = cli::args().nth(2) ?? "report.html"

    let data = fs::map(ref path, fs::Root::Anywhere) catch {
        eprintln(f"cannot open {path}: {error}")
        return
    }

    let mut entries = Stock::file(data) catch {
        eprintln(f"{path}: {error}")
        return
    }

    // Largest first. The sort is stable: items with the same count keep their
    // order from the file.
    entries.sort_by_key fn(entry) { -entry.count }

    let mut total = 0
    for e in entries { total += e.count }

    let count = entries.len()
    let markup = page(entries, total)

    // If the page cannot be written, say so and stop, rather than report success.
    fs::write(ref out, fs::Root::Anywhere, ref markup) catch {
        eprintln(f"cannot write {out}: {error}")
        return
    }

    println(f"{count} items, {total} in total")
}

The file itself · all the examples

This site is open source. Improve this page.