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:
- An element path names a node:
/world/sphere. - A slot path names one property on that node:
/world/sphere.radius. The slot is the trailing.property.
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.