mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Editor: view-scoped Extract-to-Dataset for inline and self-defined data
This commit is contained in:
@@ -164,35 +164,18 @@ export function libraryRefName(data: unknown, selfDefined: ReadonlySet<string>):
|
||||
|
||||
References appear in several places — top-level `data`, per-layer `data`, `data`
|
||||
inside `spec`/`facet`/`hconcat`/`vconcat`, and a lookup transform's `from.data`.
|
||||
Rather than enumerate the grammar, each pass walks the spec recursively but
|
||||
**prunes two keys**: a `data` object's payload (its `values`/rows) and the
|
||||
top-level `datasets` map hold user data, not nested specs. Without the prune, a
|
||||
data _row_ carrying a field literally named `data: { name: "x" }` is misread as a
|
||||
reference.
|
||||
Rather than enumerate the grammar, the walk recurses the spec but **prunes two
|
||||
keys**: a `data` object's payload (its `values`/rows) and the top-level `datasets`
|
||||
map hold user data, not nested specs. Without the prune, a data _row_ carrying a
|
||||
field literally named `data: { name: "x" }` is misread as a reference.
|
||||
|
||||
```ts
|
||||
// src/core/spec-refs.ts — the recursive walk; classification routes through spec-data
|
||||
|
||||
export function extractDatasetRefs(spec: Json): string[] {
|
||||
const root = typeof spec === 'string' ? safeParse(spec) : spec; // unparseable → no refs
|
||||
const selfDefined = selfDefinedNames(root);
|
||||
const names = new Set<string>();
|
||||
const walk = (node: Json): void => {
|
||||
if (Array.isArray(node)) return void node.forEach(walk);
|
||||
if (node && typeof node === 'object') {
|
||||
const obj = node as Record<string, Json>;
|
||||
const refName = libraryRefName(obj.data, selfDefined);
|
||||
if (refName !== null) names.add(refName);
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (key === 'data' || key === 'datasets') continue; // prune user-data payloads
|
||||
walk(obj[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(root);
|
||||
return [...names];
|
||||
}
|
||||
```
|
||||
That walk is **one shared pair** in `core/spec-data`, not re-implemented per pass:
|
||||
`forEachDataBinding(spec, visit)` (read-only, `visit` returns `true` to stop early)
|
||||
and `mapDataBindings(spec, mapData)` (returns a copy). Both compute the spec's
|
||||
self-defined names once and hand them to the callback, so a caller writes only its
|
||||
own rule — _what is a reference_ (`libraryRefName`) or _what to rewrite_ — never the
|
||||
walk. `extractDatasetRefs` collects through `forEachDataBinding`; `renameDatasetInSpec`
|
||||
and `promoteSelfDefinedDataset` (§3.2) rewrite through `mapDataBindings`.
|
||||
|
||||
`recomputeDatasetRefs` is the thin sorted/de-duped wrapper stored on
|
||||
`snippet.datasetRefs`, run on every draft change and on publish.
|
||||
@@ -211,22 +194,50 @@ export function extractDatasetRefs(spec: Json): string[] {
|
||||
programmatic extract/revert rewrites, never on raw keystrokes. That keeps the
|
||||
links in step with the edited draft while never recomputing from a
|
||||
transiently-invalid spec.
|
||||
- Keep the three ref walks in lockstep — extraction here, `renameDatasetInSpec`,
|
||||
and the renderer's `resolveDatasetRefs` (`src/core/rendering.ts`). They agree
|
||||
because they share both halves: the `libraryRefName` classifier (what is a
|
||||
reference) and the prune of the **same two keys** (`data`, `datasets`). If one
|
||||
classified differently, or descended into data payloads while another didn't, a
|
||||
row field named `data` would get counted, rewritten, or throw
|
||||
`DatasetNotFoundError`.
|
||||
- Route every binding pass through the shared `forEachDataBinding` /
|
||||
`mapDataBindings`. They share both halves that must agree — the `libraryRefName`
|
||||
classifier (what is a reference) and the prune of the **same two keys** (`data`,
|
||||
`datasets`) — so extraction, rename, and the reverse extraction cannot drift. If
|
||||
one classified differently, or descended into data payloads while another didn't,
|
||||
a row field named `data` would get counted, rewritten, or throw
|
||||
`DatasetNotFoundError`. (The renderer's `resolveDatasetRefs` mutates in place and
|
||||
can throw mid-walk, so it keeps its own copy of the walk — the one exception, held
|
||||
in step by the same prune-two-keys rule.)
|
||||
|
||||
**Don't**
|
||||
|
||||
- Don't let two code paths each have their own idea of "referenced names".
|
||||
Renamer, ref-recomputer, and renderer must use the same walk shape.
|
||||
- Don't add a fresh prune-walk for a new binding pass — reuse the shared pair. A
|
||||
hand-written copy is one classifier tweak away from disagreeing with the others.
|
||||
- Don't "enumerate the grammar" (scope the walk to a fixed list of container
|
||||
keys) to fix the payload-descent problem — pruning the two data-bearing keys
|
||||
stays correct as Vega-Lite's composition grammar grows; an allow-list rots.
|
||||
|
||||
### 3.2 Extracting embedded data into a dataset — the reverse (`spec-inline-data.ts`, `spec-refs.ts`)
|
||||
|
||||
Extract-to-Dataset is the inverse of a reference: it lifts a view's **embedded**
|
||||
data into a stored dataset and rewrites the spec to reference it by name. It is a
|
||||
cursor-scoped editor action — `services/extract-action` resolves the focused view's
|
||||
binding (`dataBindingAtPath`), seeds the modal, then opens it; the gate
|
||||
`specHasExtractableData` hides the toolbar action when no view carries liftable
|
||||
data. Two embedded shapes lift, both routing through the same `spec-data` classifier
|
||||
so they never disagree with reference detection:
|
||||
|
||||
- **Inline `values`** — `inlineValuesOf` captures the payload verbatim (a CSV/TSV
|
||||
string is kept as-is); confirm rewrites that view's `data` block, at its anchor
|
||||
path, to `{ name }`.
|
||||
- **A self-defined `datasets` entry** the view references — `selfDefinedPayloadOf`
|
||||
reads the named rows and `promoteSelfDefinedDataset` drops the `datasets` entry
|
||||
(and the map when it empties). Keeping the name needs no reference rewrite — it
|
||||
un-shadows onto the new library dataset; renaming rewrites every matching
|
||||
reference. This is the reverse direction of the self-defined-vs-library
|
||||
precedence in §3.1.
|
||||
|
||||
A `lookup` transform's inline `from.data` lifts like any view binding —
|
||||
`dataBindingAtPath` finds it. A `url`, a generator, or an existing library
|
||||
reference carries nothing to lift.
|
||||
|
||||
---
|
||||
|
||||
## 4. Reverse lookup: who uses this dataset?
|
||||
|
||||
Reference in New Issue
Block a user