On This Page
Path Utilities
Address a nested value by string path, and build, walk, and compare the paths themselves.
| Spelling | Addresses |
|---|---|
contact.taxId |
a field, dot joined |
lines.2 or lines[2] |
an array element by position |
lines[#a1] |
an array element by identity |
lines.*.cost |
a wildcard standing for any single segment |
A key rides between [# and ] and carries any character except ], the one character that closes the selector. Emails and compound ids address cleanly — users[#jack@semantic-ui.com].role, items[#200.40.50].qty — because a dot inside a bracket belongs to the key, never to the path.
Addressing
get
function get(obj, path = '', fields = elementKey.config.fields)Access a nested object field from a string path, like ‘a.b.c’.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to access |
| path | string | The path to the desired property |
| fields | string[] | Identity fields for keyed [#id] segments |
Returns
The value at the specified path, or undefined if not found.
Notes
A bracket segment whose body starts with # selects an array element by identity (items[#id]) rather than by position (items[0]). Identity is matched String-coerced (a number id of 7 matches [#7]), see elementKey. Returns the element when the path ends at the key, or undefined when no element matches.
An id may contain any character except ] — a dot inside a bracket belongs to the id, never the path. items[#jack@semantic-ui.com] and items[#200.40.50] are valid selectors.
A malformed bracket segment — an unclosed bracket, or a positional body that isn’t a whole index (items[abc]) — addresses nothing: get returns undefined, has returns false, set and unset no-op.
Example
import { get } from '@semantic-ui/utils';
const obj = { a: { b: { c: 42 } } };console.log(get(obj, 'a.b.c')); // 42console.log(get(obj, 'a.b.d')); // undefined
const doc = { items: [{ id: 'a', n: 1 }, { id: 'b', n: 2 }] };console.log(get(doc, 'items[#b].n')); // 2 (selected by identity, not position)
const team = { members: [{ id: 'jack@semantic-ui.com', role: 'owner' }] };console.log(get(team, 'members[#jack@semantic-ui.com].role')); // 'owner'has
function has(obj, path = '', fields = elementKey.config.fields)Existence twin of get. Returns true when the path resolves to a real location, even one holding undefined. get reports a missing path and a stored undefined the same way, so has is how you tell them apart.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to check |
| path | string | The path to test for |
| fields | string[] | Identity fields for keyed [#id] segments |
Returns
true when the path resolves to a stored location, false otherwise.
Notes
Understands the same path grammar as get: dotted keys, [0] indices, [#id] keyed segments (see elementKey), and literal dotted-key fallbacks. A non-object root or a non-string path returns false.
Example
import { has, get } from '@semantic-ui/utils';
const preferences = { theme: 'dark', notifications: undefined };console.log(get(preferences, 'notifications')); // undefinedconsole.log(has(preferences, 'notifications')); // true (key exists, value is undefined)console.log(has(preferences, 'timezone')); // false (never set)
const doc = { items: [{ id: 'a', done: undefined }] };console.log(has(doc, 'items[#a].done')); // true (resolves to a stored undefined)console.log(has(doc, 'items[#a].note')); // falseset
function set(obj, path, value, fields = elementKey.config.fields)Set a nested object field from a string path, the write twin of get. Creates missing intermediates — arrays when the next segment is a numeric index, objects otherwise.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to write into |
| path | string | The path string (e.g., ‘a.b.c’, ‘items.0.name’, ‘items[0].name’, or ‘items[#id]’) |
| value | any | The value to set at the path |
| fields | string[] | Identity fields for keyed [#id] segments |
Returns
The same object reference.
Notes
Paths from trackWrites and detectChanges apply back directly. Prototype-climbing segments (__proto__, constructor, prototype) are refused, and a non-string or empty path is a no-op.
A [#id] segment addresses an array element by identity (see elementKey): a present key replaces the element in place (or writes the field through it), an absent key appends a new element (a field write through an absent key is a no-op). The prototype-pollution guard is intentionally not extended to keyed bodies — a [#__proto__] value is only ===-compared against element identities, it is never used as a property name, so it appends an inert element rather than touching Object.prototype.
Example
import { set, get, trackWrites } from '@semantic-ui/utils';
console.log(set({}, 'a.b.c', 1)); // { a: { b: { c: 1 } } }console.log(set({}, 'items.0.name', 'first')); // { items: [{ name: 'first' }] }
// sync changes between objectsconst { paths } = trackWrites(source, mutator);paths.forEach((path) => set(replica, path, get(source, path)));
// keyed addressing — replace by identity, append when absentconst doc = { items: [{ id: 'a', n: 1 }] };set(doc, 'items[#a]', { id: 'a', n: 100 }); // replaces in placeset(doc, 'items[#b]', { id: 'b', n: 2 }); // appends, no element had id 'b'unset
function unset(obj, path, fields = elementKey.config.fields)Remove a nested object field from a string path, the delete twin of get and set.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to remove from |
| path | string | The path string (e.g., ‘a.b.c’, ‘items.0’, or ‘items[#id]’) |
| fields | string[] | Identity fields for keyed [#id] segments |
Returns
The same object reference.
Notes
A missing path is a no-op. A removed array index leaves a hole rather than splicing, so sibling index paths stay valid when applying several removals at once. Prototype-climbing segments (__proto__, constructor, prototype) are refused. Pairs with detectChanges — apply removed paths with unset and the rest with set.
A [#id] segment removes the matched array element by identity (see elementKey), splicing it out — there are no sibling index paths to keep valid, so the splice is safe.
Example
import { unset, set, get, detectChanges } from '@semantic-ui/utils';
const obj = { a: { b: 1, c: 2 } };unset(obj, 'a.b');console.log(obj); // { a: { c: 2 } }
// apply a full diffconst diff = detectChanges(before, after);[...diff.added, ...diff.changed].forEach((path) => set(replica, path, get(after, path)));diff.removed.forEach((path) => unset(replica, path));Element Identity
elementKey
function elementKey(item, fields = elementKey.config.fields)The identity of an array element — the value of the first present field in fields, or undefined for a scalar or an object carrying none of them. The same convention as reactivity’s Signal.id and the renderer’s getItemId, and what the keyed detectChanges mode and the keyed get / set / unset grammar match on.
Parameters
| Name | Type | Description |
|---|---|---|
| item | any | The array element to read identity from |
| fields | string[] | Candidate identity fields, first present wins |
Returns
The identity value, or undefined when none of the fields are present.
Notes
The value comes back raw: a number stays a number, and a string is returned whether or not it can ride the path grammar. Use pathKey wherever that identity becomes part of a path, since it String-coerces the value and returns null for the ids a path can’t carry. Read elementKey when you want the identity itself — comparing two elements, keying a Map, matching against a record.
Example
import { elementKey } from '@semantic-ui/utils';
console.log(elementKey({ id: 'a', _id: 'x' })); // 'a'console.log(elementKey({ name: 'n' })); // undefinedconsole.log(elementKey({ sku: 's1' }, ['sku'])); // 's1'elementKey.config
elementKey.config.fields // ['id', '_id', 'hash', 'key']The identity vocabulary for the whole keyed grammar, set once at app boot — every fields default across get/set/has/unset, keyedPath, pathKey, expandPath, and the keyed detectChanges/trackWrites/trackReads modes reads it live, and a per-call fields argument still wins.
import { elementKey } from '@semantic-ui/utils';
elementKey.config.fields.unshift('sku'); // sku now outranks id everywherepathKey
function pathKey(item, fields = elementKey.config.fields)An element’s key as it can appear in a path — the text that rides between [# and ] — or null when the element is unkeyed or its identity can’t ride the grammar.
Parameters
| Name | Type | Description |
|---|---|---|
| item | any | The array element to read identity from |
| fields | string[] | Candidate identity fields, first present wins |
Returns
The key as a string, or null when the element carries no identity or its identity carries ].
Notes
This is the item-to-path route. elementKey answers what an element’s identity is and hands back the raw value, which may be a number or a string carrying ]. pathKey answers how that identity spells inside a path, and refuses the values that would emit a path nothing can parse — so a keyed path that came through here always reads back through get.
A null result is the signal to address the element by position instead. Pair it with elementPath, which takes { key } or { index } and throws on exactly the keys pathKey rejects.
Example
import { elementPath, pathKey } from '@semantic-ui/utils';
console.log(pathKey({ id: 'jack@semantic-ui.com' })); // 'jack@semantic-ui.com'console.log(pathKey({ id: 7 })); // '7' (String-coerced)console.log(pathKey({ id: 'lot]7' })); // null (can't ride the grammar)console.log(pathKey({ name: 'n' })); // null (no identity field)
// the route from an item to its path, positional when there is no usable keyconst address = (item, index) => { const key = pathKey(item); return key === null ? elementPath('members', { index }) : elementPath('members', { key });};console.log(address({ id: 'jack@semantic-ui.com' }, 0)); // 'members[#jack@semantic-ui.com]'console.log(address({ name: 'anonymous' }, 3)); // 'members.3'isPathKey
function isPathKey(key)Whether a key can ride the path grammar — true for any value whose string form omits ].
Parameters
| Name | Type | Description |
|---|---|---|
| key | any | The candidate key, String-coerced before testing |
Returns
true when the key can appear between [# and ], false otherwise.
Notes
Dots, @, [, and a leading # all round-trip, so jack@semantic-ui.com and 200.40.50 are valid keys. Only ] is refused, since it closes the selector.
This is the guard for a key that exists before any item does — a user-typed id at a form boundary, an id arriving over the wire — where there is no element to hand pathKey yet.
Example
import { isPathKey } from '@semantic-ui/utils';
console.log(isPathKey('jack@semantic-ui.com')); // trueconsole.log(isPathKey('200.40.50')); // trueconsole.log(isPathKey('v 2.0 (beta)')); // trueconsole.log(isPathKey('lot]7')); // falseReading Paths
splitPath
function splitPath(path)Split a path into its dot separated segments, the textual read under every path walk.
Parameters
| Name | Type | Description |
|---|---|---|
| path | string | The path string to split |
Returns
An array of segment strings.
Notes
Only the dots outside brackets separate segments, so a dot inside a keyed id belongs to the id: users[#jack@semantic-ui.com].role splits in two, not three. Segments are contiguous substrings of the input, so joining them on . rebuilds the path exactly.
For what each segment means rather than where it starts and ends, use parsePath.
Example
import { splitPath } from '@semantic-ui/utils';
console.log(splitPath('order.customer.email'));// ['order', 'customer', 'email']
console.log(splitPath('users[#jack@semantic-ui.com].role'));// ['users[#jack@semantic-ui.com]', 'role']
const path = 'teams[#core.ui].members[#200.40.50].role';console.log(splitPath(path).join('.') === path); // trueparsePath
function parsePath(path)Parse a path into typed segments, the semantic read beside splitPath’s textual one.
Parameters
| Name | Type | Description |
|---|---|---|
| path | string | The path string to parse |
Returns
An array of segments, or null when the path doesn’t parse.
| Type | Shape | Spelling |
|---|---|---|
| field | { type: 'field', name } |
lines |
| key | { type: 'key', key } |
[#a1] |
| index | { type: 'index', index } |
2 or [2] |
| wildcard | { type: 'wildcard' } |
* |
Notes
Both index spellings parse to the same segment, so items[2].qty and items.2.qty are indistinguishable once parsed. A path that doesn’t parse — an unclosed bracket, a positional body that isn’t a whole index, an empty part, a non-string — returns null rather than a guess, so a relation over garbage reads as no relation rather than a false match. The empty path parses to no segments.
Results are cached and shared: repeated parses of one path hand back the same array, so treat segments as immutable. They are frozen in development to catch a write. The cache is bounded and sized through parsePath.config.cacheSize.
Example
import { parsePath } from '@semantic-ui/utils';
console.log(parsePath('lines[#a1].tax'));// [{ type: 'field', name: 'lines' }, { type: 'key', key: 'a1' }, { type: 'field', name: 'tax' }]
console.log(parsePath('lines.*.tax'));// [{ type: 'field', name: 'lines' }, { type: 'wildcard' }, { type: 'field', name: 'tax' }]
// a key carries any character except ']'console.log(parsePath('users[#jack@semantic-ui.com].role')[1]);// { type: 'key', key: 'jack@semantic-ui.com' }
// a path that doesn't parse is null, never a guessconsole.log(parsePath('lines[#a1')); // null (unclosed)console.log(parsePath('lines[abc]')); // null (positional body that isn't an index)eachPath
function eachPath(path, callback, { self = true } = {})Walk the paths a path passes through, shortest first — 'todos[#a].done' visits 'todos', 'todos[#a]', 'todos[#a].done'. Every visited value is itself a resolvable path: a bracket splits its segment into container and element, and a dot inside a keyed id belongs to the id, never a boundary.
Parameters
| Name | Type | Description |
|---|---|---|
| path | string | The path string to walk |
| callback | function | Called per containing path with (path, index, fullPath) |
| options | object | Optional configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| self | boolean | true | Include the full path itself as the final visit. Pass false to visit only the ancestors above the target |
Returns
The input path string.
Notes
The ancestors-only walk ({ self: false }) is what a subsumption check or an invalidation pass over path-addressed state needs — a write logged at any ancestor of a path covers that path. Returning false from the callback stops the walk early, like each.
Example
import { eachPath } from '@semantic-ui/utils';
eachPath('todos[#a].done', (path) => console.log(path));// 'todos'// 'todos[#a]'// 'todos[#a].done'
eachPath('todos[#a].done', (ancestor) => console.log(ancestor), { self: false });// 'todos'// 'todos[#a]'
// a dot inside a keyed id is identity, never a boundaryeachPath('users[#jack@semantic-ui.com].role', (path) => console.log(path));// 'users'// 'users[#jack@semantic-ui.com]'// 'users[#jack@semantic-ui.com].role'Building Paths
pathFrom
function pathFrom(segments)Join segments back into a path, the inverse of parsePath and of splitPath.
Parameters
| Name | Type | Description |
|---|---|---|
| segments | array | Parsed segments, plain segment strings, or a mix of both |
Returns
The path string.
Notes
Fields, positional indexes, and wildcards join with dots, and a key appends as [#key]. Positional indexes emit the dot form, so pathFrom(parsePath('items[2].qty')) normalizes to items.2.qty rather than preserving the bracket spelling. Keyed spellings round-trip exactly.
Accepting plain strings alongside parsed segments means a path can be assembled from a known prefix and a computed tail without parsing the prefix first.
Example
import { parsePath, pathFrom, splitPath } from '@semantic-ui/utils';
console.log(pathFrom(parsePath('users[#jack@semantic-ui.com].role')));// 'users[#jack@semantic-ui.com].role'
console.log(pathFrom(splitPath('teams[#core.ui].members')));// 'teams[#core.ui].members'
// segments and plain strings mix in one arrayconsole.log(pathFrom(['order.lines', { type: 'key', key: 'a1' }, 'qty']));// 'order.lines[#a1].qty'
// a positional index emits the dot formconsole.log(pathFrom(parsePath('lines[2].qty'))); // 'lines.2.qty'elementPath
function elementPath(listPath, { key, index } = {})The path of one element under a list path — by key in the [#key] form, by index in the dot form.
Parameters
| Name | Type | Description |
|---|---|---|
| listPath | string | The path of the array the element lives in |
| options | object | The element’s address, either key or index |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| key | string | number | undefined | Address by identity, emitting listPath[#key] |
| index | number | undefined | Address by position, emitting listPath.index |
Returns
The element’s path.
Notes
This is where a path is born from raw data, so the key contract is enforced here: a key carrying ] throws rather than emitting a path nothing can parse. Route items through pathKey first — it returns null for exactly the identities this rejects, leaving { index } as the fallback.
A key and an index are different address spaces. { key: '2' } gives a[#2], which matches the element whose id is '2', while { index: 2 } gives a.2, the third element whatever its id. An empty listPath addresses an array root. Passing neither key nor index throws.
Example
import { elementPath, get } from '@semantic-ui/utils';
console.log(elementPath('team.members', { key: 'a1' })); // 'team.members[#a1]'console.log(elementPath('team.members', { index: 1 })); // 'team.members.1'
// different address spaces — by id '1', not the second elementconsole.log(elementPath('team.members', { key: '1' })); // 'team.members[#1]'
const team = { members: [{ id: 'jack@semantic-ui.com', role: 'editor' }] };console.log(get({ team }, elementPath('team.members', { key: 'jack@semantic-ui.com' })));// { id: 'jack@semantic-ui.com', role: 'editor' }keyedPath
function keyedPath(obj, path, fields = elementKey.config.fields)The keyed spelling of a positional path — rewrites each positional array segment (items.0 or items[1]) to its [#id] form where the addressed element carries a path-safe identity, resolved against obj at call time. A positional address is only stable while the array doesn’t move, the keyed address survives a reorder — the same identity detectChanges emits in its keyed mode.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object the path addresses |
| path | string | The path string (e.g., ‘items.0.qty’ or ‘items[1]’) |
| fields | string[] | Identity fields for keyed [#id] segments |
Returns
The keyed spelling as a new string, or the input path itself (the same reference) when nothing rewrites — so keyedPath(obj, path) === path cheaply tells you the address was already stable.
Notes
Only a segment whose element carries a path-safe identity (any id without ], see pathKey) rewrites. Keyless arrays, inner value lists, unresolvable paths, and already-keyed spellings pass through unchanged. A leading index (the root itself being an array) stays positional, since get and set can’t parse a leading bracket. The emitted [#id] paths apply back through get/set/unset and match the keyed output of detectChanges.
Example
import { keyedPath, get } from '@semantic-ui/utils';
const doc = { items: [{ id: 'a', qty: 1 }, { id: 'b', qty: 2 }] };console.log(keyedPath(doc, 'items.0.qty')); // 'items[#a].qty'console.log(keyedPath(doc, 'items[1]')); // 'items[#b]'
// keyless and unresolvable paths pass through — same reference when nothing rewritesconst path = 'plain.0.n';console.log(keyedPath({ plain: [{ n: 1 }] }, path) === path); // true
// the keyed address survives a reorder that shifts every positional indexconst target = keyedPath(doc, 'items.1.qty'); // 'items[#b].qty'doc.items.unshift({ id: 'z', qty: 9 });console.log(get(doc, 'items.1.qty')); // 1 (positional now hits a)console.log(get(doc, target)); // 2 (keyed still hits b)wildcardPath
function wildcardPath(path)The wildcard path of a concrete path — lines[#a1].tax becomes lines.*.tax, the same address written at list grain.
Parameters
| Name | Type | Description |
|---|---|---|
| path | string | The concrete path |
Returns
The wildcard path, or the input path unchanged when it doesn’t parse.
Notes
Every element address becomes *, keyed and positional alike, so per-element paths share one spelling instead of one entry per element. The wildcard path always covers the path it came from, so it is still a usable subscription.
Example
import { pathCovers, wildcardPath } from '@semantic-ui/utils';
console.log(wildcardPath('lines[#jack@semantic-ui.com].tax')); // 'lines.*.tax'console.log(wildcardPath('lines.2.tax')); // 'lines.*.tax'console.log(wildcardPath('invoice.total')); // 'invoice.total'
// per-element writes group under one wildcard pathconst written = ['lines[#a1].tax', 'lines[#a2].tax', 'lines[#a1].qty'];console.log([...new Set(written.map(wildcardPath))]); // ['lines.*.tax', 'lines.*.qty']
// the wildcard path still covers the concrete path it came fromconsole.log(pathCovers(wildcardPath('lines[#a1].tax'), 'lines[#a1].tax')); // trueexpandPath
function expandPath(path, { from, doc, fields = elementKey.config.fields } = {})Expand a path spelling into the concrete paths it addresses, always as an array.
Parameters
| Name | Type | Description |
|---|---|---|
| path | string | The spelling to expand, which may lead with dots or carry wildcards |
| options | object | The context the spelling resolves against |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| from | string | undefined | The path a leading-dot spelling resolves against |
| doc | object | undefined | The value a * enumerates |
| fields | string[] | elementKey.config.fields |
Identity fields used while enumerating |
Returns
An array of concrete paths. A spelling with neither leading dots nor wildcards returns itself as the single element, so callers destructure uniformly.
Notes
A leading dot resolves against from, each dot stripping one trailing segment: .tax from lines[#a1].qty is lines[#a1].tax, ..total is lines.total. A keyed segment counts as one level like any other, whatever its key carries.
A * enumerates the array at that level of doc. An element with a pathKey takes the [#key] spelling, so a derived write respects a keyed override rather than a position that may move. A keyless element stays positional, and a level that isn’t an array contributes nothing — the expansion comes back short, or empty, rather than carrying a path that addresses nowhere. Both spellings compose in one call.
Throws when the spelling needs context it wasn’t given: a relative path without from, a wildcard without doc, or a path that doesn’t parse.
Example
import { expandPath } from '@semantic-ui/utils';
const invoice = { lines: [{ id: 'a1', cost: 10 }, { id: 'jack@semantic-ui.com', cost: 40 }], drafts: [{ qty: 1 }, { qty: 3 }],};
// no leading dots and no wildcard — the path addresses itselfconsole.log(expandPath('invoice.total')); // ['invoice.total']
// a leading dot resolves against from, one dot per trailing segmentconsole.log(expandPath('.cost', { from: 'lines[#a1].qty' })); // ['lines[#a1].cost']console.log(expandPath('..total', { from: 'lines[#a1].qty' })); // ['lines.total']
// a wildcard enumerates doc — keyed elements by key, keyless by positionconsole.log(expandPath('lines.*.cost', { doc: invoice }));// ['lines[#a1].cost', 'lines[#jack@semantic-ui.com].cost']console.log(expandPath('drafts.*.qty', { doc: invoice }));// ['drafts.0.qty', 'drafts.1.qty']
// both spellings compose, and a level that isn't an array contributes nothingconsole.log(expandPath('..*.cost', { from: 'lines[#a1].qty', doc: invoice }));// ['lines[#a1].cost', 'lines[#jack@semantic-ui.com].cost']console.log(expandPath('missing.*.x', { doc: invoice })); // []Path Relations
pathCovers
function pathCovers(a, b)Whether a covers b — b sitting at or under a, aligned segment by segment.
Parameters
| Name | Type | Description |
|---|---|---|
| a | string | The covering path |
| b | string | The path tested for sitting at or under a |
Returns
true when b lies at or under a, false otherwise.
Notes
Alignment is per segment, not per character, so contact covers contact.taxId but never contacts. The relation is directional: a parent covers its children, never the reverse. A path covers itself.
A wildcard covers any single segment, so lines.*.cost covers both lines[#a1].cost and lines.2.cost. Keyed and positional addresses stay apart — a[#7] never covers a.7.x, since an id of 7 and the eighth element are different things. A path that doesn’t parse has no relation to anything, in either position.
This is the one covering relation a projection, a write guard, and an invalidation check all read.
Example
import { pathCovers } from '@semantic-ui/utils';
console.log(pathCovers('contact', 'contact.taxId')); // trueconsole.log(pathCovers('contact', 'contacts')); // falseconsole.log(pathCovers('lines', 'lines[#a1].amount')); // trueconsole.log(pathCovers('lines[#a1].amount', 'lines')); // false (direction matters)
// a wildcard covers any single segmentconsole.log(pathCovers('lines.*.cost', 'lines[#a1].cost')); // trueconsole.log(pathCovers('lines.*.cost', 'lines[#a1].fee')); // false
// keyed and positional stay apartconsole.log(pathCovers('lines[#7]', 'lines[#7].cost')); // trueconsole.log(pathCovers('lines[#7]', 'lines.7.cost')); // falsepathsOverlap
function pathsOverlap(a, b)Whether two paths lie on one line — a at or under b, or b at or under a.
Parameters
| Name | Type | Description |
|---|---|---|
| a | string | The first path |
| b | string | The second path |
Returns
true when either path covers the other, false otherwise.
Notes
The symmetric read of pathCovers, for when direction doesn’t matter. A subscription on invoice.lines[#a1] is touched by a write to invoice.lines above it and by a write to invoice.lines[#a1].qty below it alike, but not by invoice.lines[#a2].qty beside it.
Segment alignment is the same, so items overlaps items[#r7].amount and never itemsLog.
Example
import { pathsOverlap } from '@semantic-ui/utils';
console.log(pathsOverlap('lines', 'lines[#a1].amount')); // trueconsole.log(pathsOverlap('lines[#a1].amount', 'lines')); // true (either direction)
// siblings never meet, and a prefix of the text is not a prefix of the pathconsole.log(pathsOverlap('lines[#a1].amount', 'lines[#a2].amount')); // falseconsole.log(pathsOverlap('lines', 'linesLog')); // false