Skip to content
Havoc7891Public

About

Havoc's single-file CSON (CoffeeScript Object Notation) library for C++.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

havCSON

Havoc's single-file CSON (CoffeeScript Object Notation) library for C++.

Table of Contents

Features

  • Single-header, dependency-free (just include havCSON.hpp)
  • Reading and writing CSON files (from strings or UTF-8 file paths)
    • Also supports generating CSON files from scratch
    • Atomic file writers safely replace an existing file after the new contents are flushed
  • Error-code and optional throwing parse APIs, including lossless parsing helpers
  • Duplicate object keys are rejected with ErrorCode::DuplicateKey
  • Optional source locations for parsed values and object keys, in both normal and lossless mode
  • Checked value access with logical paths, source-aware diagnostics, and safe integer conversions
  • Configurable nesting-depth and input-size limits for both parser modes and file input
  • Structured file-I/O errors with the filename, operation, and underlying system error
  • Finite double values use round-trip-safe parsing and formatting
  • Convert parsed data to JSON text via ToJsonString
  • Pretty-print output with controllable indent width and optional key sorting
  • Comment-aware round trips preserve comments (including comments after : or before a closing delimiter), blank lines, and member ordering while regenerating normalized CSON
  • Transactional lossless editing keeps semantic values and ordered nodes synchronized with configurable comment handling
  • Unicode / UTF-8 support with validation
  • Compile-time version constants (VersionMajor, VersionMinor, VersionPatch, and VersionString)

Getting Started

This library requires C++23. The library should be cross-platform, but has only been tested under Windows and Linux so far.

Installation

Copy the header file into your project folder and include the file like this:

#include "havCSON.hpp"

Usage

Here are some code examples demonstrating how to use the library. The iteration and lookup snippets reuse player from the array-construction example; the other snippets are independent.

Read CSON file

using namespace havCSON;

Value config;
Error error;

auto code = ParseFile("config.cson", config, &error);
if (code != ErrorCode::OK)
{
  std::cerr << "Failed to parse " << error.filename << ": line " << error.where.line
            << " col " << error.where.column
            << " (" << error.message << ")\n";
  return;
}

// Optional: throw on error
Value parsed = ParseOrThrow("foo: 1");

// Convert to JSON string without pretty-printing
std::string json;
if (!ToJsonString(config, json, &error))
{
  std::cerr << "JSON conversion failed: " << error.message << "\n";
}

Reject duplicate object keys

using namespace havCSON;

Value value;
Error error;
auto code = Parse("name: \"first\"\nname: \"second\"\n", value, &error);
if (code == ErrorCode::DuplicateKey)
{
  std::cerr << error.message << " at line " << error.where.line << "\n";
}

Duplicate keys are rejected in both normal and lossless parsing instead of silently replacing or ignoring a value.

Track source locations for validation

Source tracking is optional and disabled by default. Enable it when application-level validation needs to report where a value or key came from:

using namespace havCSON;

std::string source = "server:\n  port: \"not a number\"\n";
ParseOptions options{.trackSourceLocations = true};
Value config;
Error error;
if (Parse(source, config, &error, options) != ErrorCode::OK)
{
  std::cerr << error.message << "\n";
  return;
}

const Value& port = config.asObject().at("server").asObject().at("port");
if (!port.isNumber())
{
  const SourceInfo* location = port.Source();
  const SourcePosition& position = location->valueSpan.begin;
  std::cerr << "Expected a number at line " << position.line
            << ", column " << position.column << "\n"; // Line 2, column 9
}

The same final ParseOptions argument works with ParseLossless, ParseFile, and ParseFileLossless; throwing helpers take it after the input string or path. LosslessValue::Source() exposes the metadata of its semantic value. Source() returns nullptr when tracking is off or a node was created programmatically. Parse errors still include their usual location when tracking is off.

SourceInfo::valueSpan describes the original value token or container, and keySpan is present for object members. Positions use zero-based UTF-8 byte offsets and one-based lines and byte columns; spans are half-open (begin is inclusive; end is exclusive). File helpers also retain an owned filename. Byte offsets include any leading UTF-8 BOM, but the first content byte after the BOM still starts at column 1. LF, CRLF, and CR each count as a single newline.

Locations describe the original input, not subsequently edited or serialized text. Copying a node retains its original source locations; assigning a fresh Value removes that node's metadata. Use ClearSourceLocations(value) to clear a complete semantic or lossless tree after edits, or value.ClearSource() for one semantic node. Writers and value comparisons ignore locations. Tracking off avoids metadata and line-index allocations, although every Value still has a small metadata-pointer field.

Write CSON file atomically

Numbers are stored as double. Finite values are formatted so that writing and parsing them recovers the same value; the original numeric spelling is not retained.

using namespace havCSON;

Value root = Object{
  {"name", "Havoc"},
  {"level", 42.0}, // Numbers are double
  {"items", Array{1.0, 2.0, 3.0}},
};

WriteOptions options;
options.indentWidth = 2;
options.sortObjectKeys = true;

Error error;
if (!WriteFileAtomic("out.cson", root, options, &error))
{
  std::cerr << "Atomic write failed: " << error.message << "\n";
}

// Get the formatted string without touching disk
std::string text;
if (!ToString(root, text, options, &error))
{
  std::cerr << "Formatting failed: " << error.message << "\n";
}

WriteFileAtomic serializes to a temporary file in the destination directory, flushes it, and then replaces the destination. Use WriteFile for a direct write, WriteTextFileAtomic for text that is already serialized, or WriteFileLosslessAtomic for a LosslessValue.

Limit input size and nesting

using namespace havCSON;

ParseOptions options{
  .trackSourceLocations = true,
  .maxDepth = 64,
  .maxInputBytes = 4 * 1024 * 1024,
};
Value config;
Error error;
if (ParseFile("config.cson", config, &error, options) == ErrorCode::ResourceLimit)
{
  std::cerr << error.filename << ": " << error.message << "\n";
}

The default maximum container depth is 256. A root object or array counts as depth 1. Input size is unlimited by default. Setting either limit to 0 disables that limit. The same options apply to string/file, normal/lossless, and throwing parse APIs. A failed parse leaves the output unchanged. File reads enforce the byte cap incrementally, including if the file grows while being read.

Read checked values

using namespace havCSON;

Value config = ParseOrThrow("server: {port: 22}", {.trackSourceLocations = true});
auto port = ValueView(config).Member("server").Member("port").AsInteger<unsigned>(1, 65535);
if (!port)
{
  std::cerr << port.error().message << "\n";
  if (port.error().source)
  {
    std::cerr << "Line " << port.error().source->valueSpan.begin.line << "\n";
  }
}

ValueView also provides At(index), AtPath(path), and typed access to nulls, booleans, numbers, strings, arrays, and objects. Navigation preserves the first error. Paths contain literal keys and array indices, so keys containing dots are not ambiguous. Views and returned references borrow the document; AccessError owns its diagnostic data. Integer conversions reject fractional, non-finite, and out-of-range values before casting. Numbers remain double: these checks do not recover precision already lost in a large numeric literal.

Edit without losing comments

using namespace havCSON;

LosslessValue document = ParseLosslessOrThrow("# Server settings\nport: 21 # Connection port\n");
LosslessDocumentEditor editor(document);
Error error;
if (!editor.RenameMember({}, "port", "connectionPort", &error) ||
    !editor.ReplaceMember({}, "connectionPort", MakeLossless(Value{22.0}), &error))
{
  std::cerr << error.message << "\n";
  return;
}
std::cout << ToStringLossless(document); // Keeps both comments and the member position

The editor supports member and array insertion, replacement, removal, and reordering. Edits validate and update a temporary tree before committing, synchronizing all ancestors. Existing nodes retain their original source locations. These are not recalculated after an edit. Use MakeLossless for newly constructed semantic values. ValidateLosslessTree detects inconsistent manual edits. RebuildLosslessTree explicitly rebuilds semantic values from ordered children.

Paths use literal keys and zero-based array indices, such as ValuePath{"servers", std::size_t{0}}. An empty path ({}) selects the root. Replacements preserve comments, and removals preserve them by default. LosslessEditOptions controls removal and comment indentation policies. The document must outlive the editor. Reacquire borrowed views, pointers, and references to its children after a successful edit.

Handle file errors

File operations report ErrorCode::IoError separately from syntax errors and resource limits. Error::filename identifies the destination or input, operation identifies the failed step, and systemError retains the original native cause before cleanup. For example, test error.systemError == std::errc::permission_denied instead of searching the message text. Successful calls clear previous errors. Atomic failures before replacement preserve the old destination. A directory-sync failure after replacement reports that replacement already succeeded.

Parse from a string and mutate the data

using namespace havCSON;

// CoffeeScript-style inline array and object
Value value = ParseOrThrow("user: {name: \"Havoc\", tags: [\"dev\", \"cpp\"]}");

// Add a nested field
Object& obj = value.asObject().at("user").asObject();
obj["active"] = true;

// Push another tag
obj["tags"].asArray().push_back("oss");

Error error;
std::string formatted;
if (!ToString(value, formatted, {}, &error))
{
  std::cerr << "Formatting failed: " << error.message << "\n";
}
else
{
  std::cout << formatted << "\n";
}

Create an array and array entries

using namespace havCSON;

Array inventory;
inventory.emplace_back("Sword");
inventory.emplace_back("Shield");
inventory.emplace_back(3.0);

Value player = Object{
  {"name", "Havoc"},
  {"inventory", inventory},
};

Iterate over an array

const havCSON::Array& items = player.asObject().at("inventory").asArray();
for (const havCSON::Value& item : items)
{
  std::cout << "Item type index: " << item.index() << "\n";
}

Check if value exists

const auto& obj = player.asObject();
if (auto it = obj.find("name"); it != obj.end())
{
  std::cout << "Name: " << std::get<std::string>(it->second) << "\n";
}

Check value type

const havCSON::Value& numberValue = player.asObject().at("inventory").asArray().at(2);
if (numberValue.isNumber())
{
  double level = std::get<double>(numberValue);
  std::cout << "Level: " << level << "\n";
}

Handle multiline strings and comments losslessly

using namespace havCSON;

std::string src = R"(# Header comment
bio: """
  Multiline
  text preserved
"""
)";

LosslessValue lossless;
Error error;
if (ParseLossless(src, lossless, &error) != ErrorCode::OK)
{
  // Handle
}

// Preserve comments, blank lines, and ordering while normalizing formatting
std::string roundtrip;
if (!ToStringLossless(lossless, roundtrip, {}, &error))
{
  std::cerr << "Lossless formatting failed: " << error.message << "\n";
}

if (!WriteFileLosslessAtomic("out.cson", lossless, {}, &error))
{
  std::cerr << "Lossless atomic write failed: " << error.message << "\n";
}

Use ParseFileLossless to read directly from a UTF-8 file. ParseLosslessOrThrow is the throwing counterpart for string input. Lossless mode retains blank lines, member ordering, and comments in structural positions such as after key: and before closing delimiters. It regenerates normalized formatting rather than preserving the original bytes.

Contributing

Thank you for your interest! Suggestions for features and bug reports are always welcome via issues.

To maintain a consistent design and quality for this library, changes are implemented by the maintainer rather than via direct pull requests.

License

Copyright © 2025-2026 René Nicolaus

This library is released under the MIT license.

About

Havoc's single-file CSON (CoffeeScript Object Notation) library for C++.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages