Skip to content

ktav (JavaScript / TypeScript)

npm CI License: MIT OR Apache-2.0 Playground

Universal JS/TS bindings for Ktav — a plain configuration format. JSON-shape, no required quotes, no commas, dotted keys. Powered by Rust under the hood, shipped as native N-API for Node and Bun, WebAssembly for Deno, browsers, and bundlers.

Languages: English · Русский · 简体中文

Playground: convert JSON / YAML / TOML / INI ⇄ Ktav in your browser at ktav-lang.github.io.

Specification: this package implements Ktav. The format is versioned and maintained independently of this package — see ktav-lang/spec for the formal document.


Install

npm install @ktav-lang/ktav

Naming note: the bare ktav name is blocked on npm's similarity filter (too close to koa, keyv, klaw, …), so the package ships under the @ktav-lang scope. Rust (ktav on crates.io) and Python (ktav on PyPI) keep the short form.

One package serves every target runtime:

Runtime Backend How it's loaded
Node ≥ 18, Bun N-API Platform-specific .node via optional dep
Deno, browser WASM web target, consumer awaits ready()
Webpack / Vite / Rollup / esbuild WASM bundler target, bundler resolves .wasm

Native binaries are prebuilt for Linux (x64/arm64, glibc + musl), macOS (x64/arm64), and Windows (x64/arm64); npm installs the one that matches the current host through optionalDependencies. If nothing matches, the loader throws early with a clear diagnostic.

Quick start

Parse — typed reads off the parsed object

import { loads, dumps } from "@ktav-lang/ktav";

interface DB { host: string; timeout: number; }
interface Config {
  service: string;
  port:    number;
  ratio:   number;
  tls:     boolean;
  tags:    string[];
  db:      DB;
}

const cfg = loads<Config>(`
service: web
port: 8080
ratio: 0.75
tls: true
tags: [
    prod
    eu-west-1
]
db.host: primary.internal
db.timeout: 30
`);

cfg.port;        // 8080 — typed as number
cfg.db.timeout;  // 30

Build & render — construct a document in code

const doc = {
  name:  "frontend",
  port:  8443,
  tls:   true,
  ratio: 0.95,
  upstreams: [
    { host: "a.example", port: 1080 },
    { host: "b.example", port: 1080 },
  ],
  notes: null,
};
const text = dumps(doc);
// name: frontend
// port: 8443
// tls: true
// ratio: 0.95
// upstreams: [
//     { host: a.example  port: 1080 }
//     { host: b.example  port: 1080 }
// ]
// notes: null

A complete runnable Node example lives in examples/node/index.mjs.

Format — comment-preserving formatter

import { format } from "@ktav-lang/ktav";

format(`
port: 8080

## the port

host: localhost
`);
// port: 8080
//
// ## the port
//
// host: localhost

format guarantees: every comment is preserved verbatim; runs of blank lines collapse to exactly one; key order is never changed; it is a fixed point (format(format(x)) === format(x)); and its output equals the canonical writer's exactly when the document has no comments and no blank lines.

WASM consumers (Deno, browser)

Call ready() once before the first loads / dumps — the wasm target defers instantiation:

import { ready, loads } from "@ktav-lang/ktav";
await ready();
loads("port: 8080\n");

Node / Bun consumers skip this — the native binary is loaded at import time.

Native FFI subexport (Deno, Bun) — @ktav-lang/ktav/ffi

For Deno users who want native speed without the WASM tax — and for Bun users who prefer bun:ffi over the N-API path — there's an opt-in subexport that talks directly to the C ABI shared library (ktav_cabi, the same binary used by the Java / Go / .NET bindings):

import { loads, loadsStrict, dumps } from "@ktav-lang/ktav/ffi";

// loads / dumps are ASYNC here (waiting on dlopen on first call)
const cfg = await loads("port: 8080\n");
await loadsStrict("port: 8080\n");
const text = await dumps({ port: 8443 });
Runtime Mechanism Permission flag
Deno Deno.dlopen --allow-ffi=<path-to-libktav_cabi> (or --allow-ffi for any FFI target)
Bun bun:ffi none — Bun trusts FFI
Node n/a throws — use the default import (already N-API native)
Browser n/a throws — use @ktav-lang/ktav/wasm instead

The library file ships in the matching @ktav-lang/js-<rid> optional dependency (same one that holds the .node binary), so npm install @ktav-lang/ktav is enough — no separate download. Override with KTAV_LIB_PATH for local cabi builds.

Trade-off: ~3–5× faster than WASM on parse / dump of large documents; requires a permission grant on Deno; loses Deno's "works in any sandbox" property. Stick with the default import unless you've measured a need.

Runnable examples: examples/deno/ffi.ts, examples/bun/ffi.ts.

Public API

function loads<T = KtavValue>(s: string): T;
function loadsStrict<T = KtavValue>(s: string): T;
function dumps<T extends KtavInput = KtavInput>(obj: T): string;
function stringifyForceStrings<T extends KtavInput = KtavInput>(obj: T): string;
function format(s: string): string;
function canonicalFromSource(s: string): string;
function emitCanonical<T extends KtavInput = KtavInput>(obj: T): string;

// web / Deno / browser only; Node + Bun ignore it
function ready(input?: URL | Response | ArrayBuffer): Promise<void>;

loadsStrict applies canonical-scalar validation and rejects lossy spellings while accepting forms emitted by the canonical writer.

stringifyForceStrings renders like dumps but flattens every leaf scalar — integer, float, boolean, null — to its textual form via the raw marker (::). Compounds keep their structure, and the result parses back through loads as the same set of String scalars.

Three functions produce canonical output and they are not interchangeable:

input comments scalar spelling
format(s) source text kept preserved
canonicalFromSource(s) source text dropped preserved
emitCanonical(obj) a JS value none to keep may change

canonicalFromSource is text in, canonical text out, with no JavaScript value in between — so 1.0, 1e9 and -0.0 survive byte-exactly. emitCanonical cannot promise that: a JS number cannot express Ktav's Integer/Float distinction, so 1.0 arrives indistinguishable from 1. Use emitCanonical when you have a value, canonicalFromSource when you have a document.

The generic parameter on loads is an unchecked cast — use it when you know the shape for IDE autocomplete. Pass nothing for the structural KtavValue type.

Errors

Every error thrown by the bindings is a typed KtavError carrying the ten fields of ktav::ErrorEnvelope: error (class, e.g. "UnclosedCompound"), reason (stable writer-time code), line, line_text, span ({start, end} — byte offsets into the UTF-8 source, not UTF-16 indices), path (array of exact decoded key segments, never a joined string), body, canonical, spec_section, and message. The message is taken verbatim, never reassembled from the other fields, and never contains raw JSON. Fields a particular error doesn't carry are null.

import { loads } from "@ktav-lang/ktav";

try {
  loads("a: [");
} catch (e) {
  e.name;          // "KtavError"
  e.error;         // "UnclosedCompound"
  e.line_text;     // "a: ["
  e.span;          // { start: 3, end: 4 } — UTF-8 byte offsets
  e.spec_section;  // "§6.1"
  e.message;       // "Syntax error: Unclosed array at end of input"
}

Type mapping

Ktav JavaScript
null null
true / false boolean
bare integer number (safe range) / bigint (larger)
bare decimal number
other scalar string
[ ... ] Array
{ ... } plain object (insertion-ordered)

Ktav types numbers by lexical form — a bare port: 8080 is a number, ratio: 0.5 a float, and anything that isn't a bare number stays a string. Force a numeric-looking value to stay a string with :: (zip:: 01007).

On encode, Number.isInteger(x) decides integer vs decimal output; bigint always encodes as a bare integer. NaN and ±Infinity are rejected — Ktav does not represent them.

Key escaping

Bare key segments can escape structural characters with a backslash. For keys that need spaces, quotes, or other characters that are awkward in bare form, quote an individual segment with "...", '...', or `...`. Quoted segments support escapes such as \uXXXX for a Unicode code point (spec 0.8.0, § 3.7.1):

"service name": web
"a.b".child: v
"caf\u00E9": yes

A literal . or : in a bare segment is escaped as \. or \:; a literal backslash is \\. A dot between segments remains the path separator.

Single-file browser build

dist/wasm/web/ktav.inline.js is a variant with the WASM binary base64-embedded — drop it into any <script type="module"> without a sibling .wasm file and without any HTTP server. Works over file://.

<script type="module">
    import init, { loads, dumps } from "https://unpkg.com/ktav/dist/wasm/web/ktav.inline.js";
    await init();
    console.log(loads("hello: world\n"));
</script>

Trade-off: ≈ 35 % bigger uncompressed, ≈ 5 % after gzip — base64 compresses well against the wasm's near-random bytes.

Philosophy

Ktav is intentionally small. Its five design principles (from spec/CONTRIBUTING.md):

  1. Locality — a line's meaning does not depend on another line.
  2. One sentence — any new rule fits in one sentence of the spec.
  3. No whitespace sensitivity (line breaks aside).
  4. No magic types — the format never decides "8080" means a number.
  5. Explicit over clever — :: is verbose on purpose.

These bindings honour that: no schema inference, no auto-casting, no defaulting. If you want typing, do it at the boundary with your own tool (Zod, io-ts, hand-written validators) against the native structures this library returns.

Related projects

  • ktav-lang/spec — canonical format specification and language-agnostic conformance test suite.
  • ktav-lang/rust — reference Rust implementation. The N-API crate and the WASM crate both wrap it.
  • ktav-lang/python — Python bindings (PyO3) over the same crate.

Versioning

Follows Semantic Versioning with the pre-1.0 convention that a MINOR bump is breaking. Package version and the ktav crate version move together.

Development

See CONTRIBUTING.md for the dev setup, the runtime test matrix, and the contribution workflow.

Support the project

The author has many ideas that could be broadly useful to IT worldwide — not limited to Ktav. Realizing them requires funding. If you'd like to help, please reach out at phpcraftdream@gmail.com.

License

MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.

Other Ktav implementations

  • spec — specification + conformance suite
  • rust — reference Rust crate (cargo add ktav)
  • csharp — C# / .NET (dotnet add package Ktav)
  • golang — Go (go get github.com/ktav-lang/golang)
  • java — Java / JVM (io.github.ktav-lang:ktav on Maven Central)
  • php — PHP (composer require ktav-lang/ktav)
  • python — Python (pip install ktav)

About

Universal JS/TS bindings for Ktav — a plain configuration format with three rules, zero indentation, and zero quoting. WASM-backed, ships for Node, Deno, Bun, and browsers from a single package.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages