Sign in

    yxl

    A YAML-driven Excel (.xlsx) compiler and CLI — manage spreadsheets as version-controllable YAML with reuse/dedup, built on bobzhang/mbtexcel.

    yaml
    excel
    xlsx
    spreadsheet
    cli
    compiler
    Download zip
    Author
    Version
    0.6.1
    License
    Apache-2.0
    Last updated
    1 hour ago
    Downloads
    4

    #yxl

    English | 日本語

    License

    Manage Excel spreadsheets as version-controllable YAML. yxl is a MoonBit command-line compiler: you write a declarative *.yxl.yaml spec, and yxl build turns it into a real, Excel-compatible .xlsx.

    Spreadsheets-as-code, with the properties code has:

    • Diffable & reviewable — a workbook is text under Git.
    • DRY / single-source-of-truth — a value, formula, or style written once and referenced many times is managed in one place, and compiles down to Excel's native sharing (shared strings, defined names, shared formulas, a single style id). Change it once, it changes everywhere.
    • Flexible structure — split data and formatting across files with $include, feed a region straight from a CSV or JSON table, or inline everything in one file. Same result either way.
    • One spec, many workbooks — declare params: and override them per build with --set region=EMEA.
    • A single native command — yxl init for a first spec, then yxl build in its directory.

    The .xlsx bytes are produced by the mature moonbitlang/mbtexcel library (a MoonBit port of Go's excelize); yxl is the language, the reuse/dedup engine, the validator, and the CLI on top. It is not another spreadsheet library — it is declarative authoring for people who'd rather edit YAML than write code.

    ⚠️ Status: pre-release, schema not yet frozen. yxl build works today — values, formulas, dates, durations, rich text, styles, layout, print setup, multi-file specs, external CSV/JSON data, parameters, one-off overrides, validations, conditional formatting, hyperlinks, notes, protection, Excel tables, charts, images, shapes, sheet backgrounds, sparklines, form controls, table slicers, and pivot tables all compile, and the CLI has init, --check, --set, and stable exit codes. Still ahead: performance work, import, and the schema freeze — the schema may change until v1.0. See ROADMAP.md for the phase plan and the living changelog.

    #A taste

    # report.yxl.yaml params: # overridable per build with --set region: APAC quarter: Q3 defs: # declared once, referenced by name styles: base: { font: { name: Calibri, size: 11 } } title: { extends: base, font: { bold: true, size: 16 } } header: { extends: base, font: { bold: true, color: "FFFFFF" }, fill: "1F3864" } total: { extends: base, font: { bold: true } } sheets: - name: "${region}" freeze: A4 # the header rows stay put while the data scrolls merges: [A1:B1] columns: - at: A width: 18 - at: B width: 14 format: "#,##0" # a whole-column default format cells: A1: { value: "${quarter} ${region} sales", style: title } A3: { value: Region, style: header } B3: { value: Revenue, style: header } A7: { value: Total, style: total } B7: { formula: "SUM(B4:B6)", style: total } data: - at: A4 csv: data/sales.csv # rows from a file the spec never restates — # or write them here with `values:` instead print: area: A1:B7 orientation: landscape fit: { width: 1 } # shrink to one page across

    yxl build report.yxl.yaml -o q3-apac.xlsx yxl build report.yxl.yaml -o q4-emea.xlsx --set region=EMEA --set quarter=Q4

    Each declared style compiles to a single Excel style id however many cells wear it, and each defs.values entry to a defined name — so editing it in Excel updates every reference. An unknown key, a bad cell reference, a dangling $ref, or a cycle among includes, styles, or parameters fails the build with a diagnostic naming the file, never a silently dropped value.

    #Install

    Linux / macOS:

    curl -fsSL https://raw.githubusercontent.com/t-ujiie-g/yxl/main/install.sh | sh

    Windows (PowerShell):

    irm https://raw.githubusercontent.com/t-ujiie-g/yxl/main/install.ps1 | iex

    Either fetches the latest release for your platform, verifies its SHA-256, and installs yxl — into ~/.local/bin, or %LOCALAPPDATA%\yxl\bin on Windows. Pin a version or choose a directory with YXL_VERSION and YXL_INSTALL_DIR:

    YXL_VERSION=0.1.0 YXL_INSTALL_DIR=/usr/local/bin \ curl -fsSL https://raw.githubusercontent.com/t-ujiie-g/yxl/main/install.sh | sh

    Prebuilt binaries cover Linux x86_64, macOS arm64 (Apple silicon), and Windows x86_64. On an Intel Mac, or any other platform, run it with moonx or build from source below. Piping a script into a shell is worth doing deliberately — read install.sh first if you would rather, or install by hand from the release assets, or build from source:

    #Without installing: moonx

    yxl is published on mooncakes, so with MoonBit installed you can run it without installing anything else:

    moonx t-ujiie-g/yxl/cmd/main@0.6.1 build report.yxl.yaml moonx t-ujiie-g/yxl/cmd/main@0.6.1 extract existing.xlsx -o spec.yxl.yaml

    Every command and option is the same as yxl's. moonx runs it as WebAssembly, and its workbooks are byte-identical to the native binary's. It is slower to start, so for daily use install the binary. @latest instead of a version fetches the newest release.

    #As a MoonBit library

    moon add t-ujiie-g/yxl

    Import t-ujiie-g/yxl/cli in your package's moon.pkg. Two functions are the supported surface, and neither touches the disk:

    // spec text -> .xlsx bytes
    let bytes = @cli.compile(spec_text, file="report.yxl.yaml")
    // .xlsx bytes -> spec text, plus what could not be carried
    let result = @cli.extract(bytes, file="existing.xlsx")
    println(result.spec)

    compile takes resolve= and read_bytes= to supply $included files, CSVs and images, and params= for --set. @cli.error_message(err) renders a failure the way the CLI prints it. The packages under internal/ are not importable.

    #From source

    With MoonBit installed:

    git clone https://github.com/t-ujiie-g/yxl.git cd yxl moon build --target native --release install -m 755 _build/native/release/build/cmd/main/main.exe ~/.local/bin/yxl

    macOS marks downloaded binaries as quarantined; if Gatekeeper objects, clear it with xattr -d com.apple.quarantine ~/.local/bin/yxl.

    Paths may use either separator, so yxl build specs\report.yaml works on Windows and a spec's own $include: data/x.yaml stays portable.

    One Windows limitation: the command line cannot carry non-ASCII today — yxl build 売上\report.yaml fails, because the runtime reads arguments as UTF-8 while Windows hands them over in the system code page (upstream: "TODO: Handle other encodings"). Paths inside a spec are unaffected — $include: 表/theme.yaml and csv: 売上/data.csv work, since those come from the file, which is UTF-8. So name the spec you pass on the command line in ASCII; everything it refers to can be in any script.

    #Using it

    yxl init -o sheet.yxl.yaml # a first spec: one empty sheet yxl build # compile the one *.yxl.yaml here yxl build report.yxl.yaml # compile → report.xlsx yxl build report.yxl.yaml -o out.xlsx # … to a path of your choosing yxl build report.yxl.yaml --check # validate, write nothing yxl build report.yxl.yaml -o r.xlsx --set region=EMEA yxl extract legacy.xlsx -o legacy.yxl.yaml # existing workbook → starting spec yxl version # print the version yxl help # full usage

    init writes somewhere to start typing — one empty sheet, not a worked example you would have to delete first. It names your own file in the build line it carries, and refuses to overwrite an existing spec unless you pass --force.

    Without -o, build writes where the spec's own output: key says, or else beside the spec under its name. Without a spec, it builds the only *.yxl.yaml in the current directory.

    The full schema is in docs/spec.md; extract — a one-way migration aid, not a round trip — is its §22.

    In an editor, a JSON Schema generated from that page gives completion on every key, the reference's own sentence on hover, and a squiggle under a key that does not exist. In VS Code, install the YAML extension once:

    code --install-extension redhat.vscode-yaml

    Then name the schema in a spec, as its first line —

    # yaml-language-server: $schema=https://raw.githubusercontent.com/t-ujiie-g/yxl/main/docs/yxl.schema.json

    — or once for every spec in a project, in .vscode/settings.json (create the file if the project has none; the same block works in your user settings, and JetBrains IDEs take the equivalent under Languages & Frameworks → Schemas):

    { "yaml.schemas": { "https://raw.githubusercontent.com/t-ujiie-g/yxl/main/docs/yxl.schema.json": "*.yxl.yaml" } }

    That glob is why the *.yxl.yaml naming is worth keeping to: a file pulled in by $include is a part of a spec rather than one, so it must not match — it would be reported as a document missing its sheets. Nothing else needs configuring; the modeline is yaml-language-server's own, so Neovim and Helix read it as VS Code does. §24 covers pointing at a local checkout, and what the schema deliberately leaves to yxl build.

    Exit codes are stable across releases:

    CodeMeaning
    0success
    1the spec is invalid, or a file could not be read or written
    2the command line itself was wrong

    #Examples

    examples/ is a worked cookbook, and CI compiles every file in it so the pages cannot drift from the compiler:

    ExampleShows
    quickstart.yxl.yamlcell kinds, number formats, a formula, a filled formula column, a block of rows written inline
    styling.yxl.yamldeclare-once styles, extends, defined names, rich text, conditional formatting
    layout.yxl.yamlmerges, frozen headers, sized and grouped bands, sheet visibility, print setup, document properties, an image, a watermark background
    modular.yxl.yaml$include, CSV / JSON data:, an Excel table over the region they fill, a slicer filtering it
    parameters.yxl.yamlparams: with ${} and --set
    interactive.yxl.yamldrop-downs and other validations, an auto filter saved with a criterion, hyperlinks, notes, sheet protection, a check box and spin button
    charts.yxl.yamlcolumn, pie, and bar charts, series named from cells, one plotting another sheet
    pivots.yxl.yamltwo pivot tables: rows, columns, a filters: axis, and two aggregations
    shapes.yxl.yamla cloud stamp, a chevron with two text lines, a pinned star
    sparklines.yxl.yamla trend line per row with markers, and win/loss cells plotting another sheet
    overrides.yxl.yamlone-off exceptions written down as exceptions: a row of a filled formula range that does not follow it, a corrected value, a style alone — each with its reason
    columns.yxl.yamla year-on-year report whose columns are named, not lettered: layouts: with a defs.blocks group placed once per item, a two-row merged header, a totals row, {{name}} formulas, per-column conditional formatting, and rows that fill only the input columns
    subtotals.yxl.yamla store list with subtotals beneath it — per branch, per channel within each branch, and a grand total — as live SUMIFS formulas in a fixed order; a named layout another sheet sums by name, a chart placed under the list and plotting it by name, a store drop-down fed by it, and an override that finds its row by the data's values
    workbook.yxl.yamlthe layout to keep a real workbook in: a sheet per file, styles named once, one store master every sheet references, and a month's data swapped with --set

    yxl build examples/quickstart.yxl.yaml -o quickstart.xlsx

    #AI skills

    skills/ holds Agent Skills — workflow checklists an AI coding agent (Claude Code, Codex, Cursor, OpenCode, …) can follow:

    SkillCovers
    yxl-authoringthe general workflow: the default architecture to build unless told otherwise (a sheet per file, styles named once, one master per shared list), writing a spec from scratch, editing an existing one, and operating it month to month
    extract-to-specmigration: yxl extract gives a starting point from an existing workbook, and this is the rewrite that makes it maintainable

    npx skills add t-ujiie-g/yxl # any agent — installs from skills/

    Claude Code users can instead add this repository as a plugin marketplace (/plugin → Add Marketplace → t-ujiie-g/yxl) and install yxl-skills.

    #How it works

    report.yxl.yaml → parse (YAML → document tree) → load + validate + resolve references (typed model, diagnostics naming the file) → emit: intern shared styles, strings, and defined names ← the DRY engine via a swappable backend (moonbitlang/mbtexcel) → report.xlsx

    The pipeline core is filesystem-free and unit-testable on strings and bytes; only the CLI touches disk. The Excel backend sits behind a seam so it can be swapped (see ROADMAP.md §7, ADR-002).

    #Packages

    Three packages are public; the compiler itself is under internal/ (ADR-028).

    PackagePurpose
    clicompile and extract, I/O-free, plus argument parsing and exit codes
    includeThe resolver types compile takes for included files
    cmd/mainThe yxl executable: file I/O around cli
    internal/diagDiagnostics + subdomain errors with source spans
    internal/unitsType-safe cell references, colors, dates
    internal/yamlYAML source → document tree
    internal/modelTyped intermediate representation
    internal/loaderDocument tree → model: schema validation, reference resolution
    internal/emitModel → .xlsx bytes (mbtexcel-backed), style/string interning
    internal/read.xlsx bytes → model, the mirror of emit (for yxl extract)
    internal/renderModel → document tree, the mirror of loader, where extracted styles get their names
    examplesTest-only: compiles the examples/ cookbook and asserts on it

    #Development

    moon check --deny-warn # type check moon test # tests moon fmt # format moon info # regenerate the .mbti interface files moon build --target native # build the CLI

    main is protected: land changes through a pull request, and CI must be green. Tagging a commit vX.Y.Z builds the binaries and publishes the release — the tag, moon.mod, and the version yxl version reports must agree, or the release job stops before building.

    Direction, phase scope, architecture decisions (ADRs), and the changelog all live in the single source of truth, ROADMAP.md. Contributor and AI-agent conventions are in AGENTS.md (CLAUDE.md is a symlink to it); the spec format is docs/spec.md.

    #License

    Apache-2.0. See LICENSE.