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.