Class reference

Path

The only name in a Document

A Path is how you name anything in a Document. There are no UUIDs. An element is its path; a slot is its path plus a property. Identity is hierarchy: /world/sphere is the sphere under world, and moving it changes its name. Because the parent is always a prefix of the child, cycles are structurally impossible.

def group "world" {
    def object "sphere" {
        float radius = 1.5          # the slot /world/sphere.radius
    }
}

Two path forms appear everywhere:

Path sphere("/world/sphere");                 // an element
Path radius("/world/sphere.radius");          // a slot on it

In Python a Path is just a string. Every API that takes a path takes a str, and every API that returns one returns a str. The C++ structure and its members below are the grammar those strings obey.

doc.append("/world/sphere", "object").set("radius", 1.5)   # paths are plain strings

The name grammar

A segment name and a property are [A-Za-z0-9_] — letters, digits, underscore. Nothing else. Hyphens are rejected: /my-thing will not parse. Use /my_thing. This rule is enforced at parse time, so a malformed name never enters a Document.

Path::parse("/world/sphere");      // ok
Path::parse("/my_thing");          // ok
Path::parse("/my-thing");          // std::nullopt — '-' is not a name char
Path::parse("world/sphere");       // std::nullopt — not absolute
Path::parse("/world/");            // std::nullopt — trailing slash

A path must be absolute (lead with /), have no empty or trailing segment, and carry at most one .property, on the last segment only.

parse — validate untrusted text

static std::optional<Path> parse(std::string_view raw);

Parse a path string. Returns std::nullopt if the string is malformed: not absolute, an empty or illegal segment, a misplaced or doubled ., a slot on the root (/.x), or an illegal character. Use parse for any input that may legitimately be wrong — a file, a network message, a user field — and handle the nullopt.

if (auto p = Path::parse(userInput))
    doc.append(*p, "object");
else
    reportBadPath(userInput);

Python never exposes parse: a path is a string, and a bad string fails loudly at the call that uses it. Check existence with doc.has(path).

if not doc.has("/world/sphere"):
    doc.append("/world/sphere", "object")

The constructors

Path();                                                   // the root "/"
explicit Path(std::string_view raw);                      // from a literal, throws if malformed
explicit Path(const char* raw);
explicit Path(std::vector<std::string> segments,
              std::optional<std::string> property = std::nullopt);  // from clean parts

The default Path() is the pseudo-root, /. The string constructor is the everyday form, Path("/world/lens"): it throws std::invalid_argument on a malformed literal, because a bad literal in your source is a programmer error — fail loud. For untrusted text, prefer parse. The parts constructor trusts its input: it performs no character validation, so callers pass already-sanitized names.

Path root;                              // "/"
Path lens("/world/lens");              // throws on a typo
Path lens2({"world", "lens"});         // no validation — you vouch for the parts

str — the canonical text

std::string str() const;

The path as text. Round-trips parse: Path::parse(p.str()) == p. The root is "/"; an element joins its segments with /; a slot appends .property.

Path("/world/sphere.radius").str();   // "/world/sphere.radius"
Path().str();                          // "/"

In Python this is the value — the string you already hold.

isRoot, segments, property, name — reading a path apart

bool isRoot() const;                                  // the "/" pseudo-root
const std::vector<std::string>& segments() const;     // the named/anonymous parts
const std::optional<std::string>& property() const;   // the slot, if any
std::optional<std::string> name() const;              // last segment, nullopt at root

isRoot is true only for /. segments is the ordered list between the slashes. property is the slot name, present only on a slot path. name is the last segment — the element's own name — and is std::nullopt at the root.

Path p("/world/sphere.radius");
p.isRoot();      // false
p.segments();    // {"world", "sphere"}
p.property();    // "radius"
p.name();        // "sphere"   — the slot is not part of the name

elementPath — drop the slot

Path elementPath() const;

The same path with any slot removed. On a slot path it returns the element that owns the slot; on an element path it returns itself. This is the difference between a slot path and an element path made one call: /world/sphere.radius becomes /world/sphere.

Path slot("/world/sphere.radius");
slot.elementPath();                    // "/world/sphere"
Path("/world/sphere").elementPath();   // "/world/sphere" — unchanged

In Python, slice the string at the last ., or just pass the slot to an API that wants an element — every slot path carries its element path as its prefix.

slot = "/world/sphere.radius"
element = slot.rsplit(".", 1)[0]       # "/world/sphere"

parent — climb one level

std::optional<Path> parent() const;

The parent element, or std::nullopt at the root. The parent drops the last segment and any slot, so the parent of a slot path is the element's parent, not the element.

Path("/world/sphere").parent();          // "/world"
Path("/world").parent();                 // "/"
Path("/").parent();                      // std::nullopt
Path("/world/sphere.radius").parent();   // "/world"

child — descend by name or by index

Path child(std::string_view segment) const;   // a named child
Path child(std::size_t index) const;           // an anonymous child "[index]"

Append a child segment, dropping any slot first. The string overload adds a named child; the index overload adds an anonymous child of the form [N].

Path world("/world");
world.child("sphere");        // "/world/sphere"
world.child(0);               // "/world/[0]"

Python builds child paths the same way the C ABI does — by string — or lets the Document mint them for you. doc.append_child(parent, type) adds the next anonymous child and returns a handle at its path; doc.child(parent, index) resolves the index-th child in document order.

para = doc.append_child("/doc", "paragraph")   # mints "/doc/[0]", "/doc/[1]", …
first = doc.child("/doc", 0)                    # the index-th child, or None

Anonymous segments

A named segment is an authored name. An anonymous segment is [N] — a bracketed run of digits — naming a nameless element by its position among its siblings. Anonymous children carry ordered body content (the paragraphs of a converted document, the runs of a paragraph) so the file is not littered with names like "0", "1".

def document "doc" {
    def paragraph {}        # /doc/[0]
    def paragraph {}        # /doc/[1]
}
bool leafIsAnonymous() const;                        // is the last segment "[N]"?
static bool isAnonymousSegment(std::string_view s);  // is s the "[N]" form?

isAnonymousSegment is the test for one segment: true for [0], [42]; false for sphere, [], [x]. leafIsAnonymous applies that test to a path's last segment.

Path::isAnonymousSegment("[3]");                // true
Path::isAnonymousSegment("sphere");             // false
Path("/doc/[1]").leafIsAnonymous();             // true
Path("/world/sphere").leafIsAnonymous();        // false

A slot is never anonymous: /doc/[0].text is a valid slot on an anonymous element, but [0] can never be the property part.

appendingProperty — attach a slot

Path appendingProperty(std::string_view prop) const;

Turn an element path into a slot path by attaching a property to it.

Path sphere("/world/sphere");
sphere.appendingProperty("radius");     // "/world/sphere.radius"

In Python, write the slot directly: "/world/sphere.radius". Every value, connection, and eval call names its slot in this element.property form.

doc.eval("/world/sphere.radius")        # a slot path, as a string

isAncestorOf — ancestor and descendant queries

bool isAncestorOf(const Path& other) const;

True when this path is a prefix of other, ignoring slots. Reflexive: a path is its own ancestor, so a subtree query that starts at a root includes that root. This is the test behind remove-subtree and reparenting.

Path world("/world");
world.isAncestorOf(Path("/world/sphere"));        // true
world.isAncestorOf(Path("/world"));               // true  — reflexive
world.isAncestorOf(Path("/other"));               // false
world.isAncestorOf(Path("/world/sphere.radius")); // true  — slot ignored

In Python the equivalent check is a segment-aware prefix test; for selecting a subtree, reach for the glob in find.

subtree = doc.find("/world/**")         # every descendant of /world

reparented — move a subtree

Path reparented(const Path& oldPrefix, const Path& newPrefix) const;

If this path lies under oldPrefix, swap that prefix for newPrefix, keeping any slot; otherwise return the path unchanged. This is how a whole subtree's paths shift when its root moves.

Path p("/world/sphere.radius");
p.reparented(Path("/world"), Path("/scene"));   // "/scene/sphere.radius"
p.reparented(Path("/other"), Path("/scene"));   // unchanged — not under /other

In Python, move an element with doc.rename(frm, to); the Document reparents every descendant path for you and returns the new path.

new_path = doc.rename("/world/sphere", "/scene/sphere")

Comparison and hashing

bool operator==(const Path&) const = default;
auto operator<=>(const Path&) const = default;   // stable total order

Paths compare by value and order lexicographically by segments then property, giving a stable sort for diff-clean output. std::hash<kinogaki::Path> is specialized with FNV-1a over str(), so the hash is stable across runs — unlike a seeded std::hash — and a Path drops straight into an unordered_map or unordered_set.

std::unordered_map<Path, Element> byPath;        // hashable out of the box
std::vector<Path> paths = /* … */;
std::sort(paths.begin(), paths.end());           // stable, deterministic

In Python, paths are strings: they compare, sort, and hash as strings, and key any dict or set directly.

find — selecting paths with a glob

The Python API resolves paths in bulk with a glob. doc.find(pattern) returns every Element a pattern selects, in document order. * matches one segment or part of a name; ** matches any depth.

doc.find("/parts/*")              # every direct child of /parts
doc.find("/parts/bolt_*")         # names beginning bolt_
doc.find("/world/**/camera")      # a camera at any depth under /world
for el in doc.find("/world/**"):  # edit each match through its path
    doc.edit(el.path).set("visible", True)

A glob is a read-side convenience over the same paths described above — it never changes the one rule that identity is the path.