Skip to content

Selection & path system: anchoring parts of a file

At a glance

  • A Path is a doubly-linked list of References, stored flat by id — not nested objects.
  • Two selection mechanisms feed it: free-text mouseup selection (Pdf/Table/Text/Email) and click-driven tag/value selection (Xml/Json).
  • getPath() formats the result differently per SchemaVariant (JSON/XML get special prefixes/suffixes).
  • Relative paths match by value-prefix equality, not identity — tag1.0 counts as a match for tag1.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 loaded File, its SchemaVariant, textContent, the current selectionMode (Text or Reference — defaulted per variant: Xml and Json start in Reference mode, everything else in Text mode), and highlights (a list of { head, tail, color, txt } ranges into the text — head/tail double as row/column for table highlights, per an inline comment admitting the naming is overloaded and "should be changed later").
  • path (internal PathState, not exported from the package) — the actual selection/anchoring state, covered below.
  • ui (internal UIState) — 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).