Selection & path system: anchoring parts of a file
At a glance
- A
Pathis a doubly-linked list ofReferences, stored flat by id — not nested objects. - Two selection mechanisms feed it: free-text
mouseupselection (Pdf/Table/Text/Email) and click-driven tag/value selection (Xml/Json). getPath()formats the result differently perSchemaVariant(JSON/XML get special prefixes/suffixes).- Relative paths match by value-prefix equality, not identity —
tag1.0counts as a match fortag1.1.tag2.
This is the mechanism behind the node-inputs → file-viewer dependency: when a user clicks the clipboard icon on a text selector or cell selector input, what actually happens is a read from the store described here, via useFileInteraction.
The store: three slices
createContext builds one store per mounted FileProvider, combining three slices (packages/file-viewer/src/store/index.ts):
file(FileState) — the loadedFile, itsSchemaVariant,textContent, the currentselectionMode(TextorReference— defaulted per variant: Xml and Json start inReferencemode, everything else inTextmode), andhighlights(a list of{ head, tail, color, txt }ranges into the text —head/taildouble as row/column for table highlights, per an inline comment admitting the naming is overloaded and "should be changed later").path(internalPathState, not exported from the package) — the actual selection/anchoring state, covered below.ui(internalUIState) — just two modal-open booleans (configRefModalOpen,createRefModalOpen) for the Xml/Json reference-config and reference-create modals.
Two ways to select: text mode vs. reference mode
Pdf, Table, Text, and Email all call useTextSelect, which listens for the browser's mouseup and, when the current selectionMode is Text, takes window.getSelection()'s string and writes it into the path store. Xml and Json instead default to Reference mode and rely on their viewer's own click handlers (useXmlPath's createTagPath/createAttributePath/createValuePath, useJsonPath's createPath) — clicking a tag, attribute, value, or JSON key/row, rather than dragging a text selection. Both paths ultimately call the same store action, path.setAbsolutePath.
The path model: a linked list of references
A Path is Record<string, Reference> — each Reference has a type (an XmlSelector/TableSelector/TextSelector/JsonSelector "how is this value represented" tag, or an XmlOperator "value vs. attribute vs. array" tag), a value, an operation, and next/prev ids — a doubly linked list stored flat in a record rather than as nested objects, so any reference can be looked up by id in O(1) (getReference) while still being walkable in order (getOrderedIds, in utils/path.ts, walks from the node whose prev is null). renderReference turns one reference back into its string form (value, plus .operation if the reference has one).
Text-mode selection is a deliberate reuse of this same structure: useTextSelect writes the selected string in as a single XmlSelector.Base reference, even for a Pdf/Table/Text/Email selection that has nothing to do with XML.
The type tag lies in text mode
The source flags this itself — // TODO: refactor, misuse of path store. A text-mode reference's type is XmlSelector.Base regardless of the actual file variant, so don't read that field as "this came from an XML selection" — it doesn't mean what it says outside of Xml/Json's own reference-mode selections.
From a path to a template string: useFileInteraction
useFileInteraction's getPath() is what actually turns the stored Path into the string a node-input writes into a field. The format depends on the file's variant: JSON and XML paths are prefixed $inputdocument. (XML paths additionally get a trailing .@ to mean "the value at this reference"); every other variant just joins the ordered references with .. In PathMode.Relative (below), it instead prefixes the selected relative path's own reference string and appends only the new references beyond where that relative path left off (pathIds.slice(-diff)).
The trailing .@ is unconditional
getPath() appends .@ for every Xml-variant path, regardless of what the last reference in the path already contains. createValuePath (a click on a value/text node) already sets that reference's own operation to '@', and renderReference renders operation as a trailing .<operation> — so a value-click path ends up with the last reference's own .@ and getPath()'s unconditional one, i.e. a real, observed output like $inputdocument.Order.Customer.Name.@.@. Confirmed by actually driving the app, not just reading the source — a tag click (no operation set) produces a single trailing .@ instead.
getSelectedCellLocaion() is separate and much simpler — it just returns whatever path.cellLocation currently holds, set by the Table organism's handleCellClick as #<row>,<col>$get (see File viewer: rendering).
Relative paths: reusing an anchor
useRelativePaths implements the "extend an existing accessible path defined elsewhere in the template" mode described in the source's own PathMode doc comment. The available relative paths (SearchPathResult[], { reference, value } pairs — reference being some other field's own template expression) are set via useFileInteraction's setRelativePaths, which parses each one's value into the same Path shape via parseStringToPath — so a relative path can be compared against the current absolute selection using the same Path structure.
"Available" is determined by value equality of the path prefix, not identity: hasEqualValuePath checks whether a relative path's ordered reference values match the start of the current absolute path's values (ignoring operators) — so tag1.0 is considered a match for tag1.1.tag2, letting the UI offer a relative path even though the exact indices differ. When the absolute selection changes, useRelativePaths re-evaluates: if the currently-selected relative path no longer has an equal-value prefix, it looks for the longest one that still does (getLongestPartialEqualValuePath), and falls back to PathMode.Absolute if none qualify. RelativePathMenuBar is the UI for switching between the two modes — it renders nothing at all if there are no relative paths available for the current file.
The reference chip list UI
PathSelectionView renders the current absolute (or relative-plus-absolute) path as a horizontal row of segments, with left/right shift buttons that appear only when the list actually overflows its container — useListOverflow (a thin wrapper over the detect-element-overflow library, exposed generically enough that use-dynamic-ui.ts builds PathSelectionView's scroll/shift behavior on it) checks whether the first/last chip collides with the container's edge. The "+" button to add a new reference is conditionally shown by usePath's isCreateAvailable, which is variant-specific and re-derives the answer from the live document each time: for Xml it asks react-interactive-xml-viewer whether the currently-selected node has children; for Json it re-parses textContent and walks the current path into it to check whether the value found there is a plain object (not an array).