shelving
Version:
Toolkit for using data in JavaScript.
142 lines (141 loc) • 6.29 kB
JavaScript
import { isArray } from "./array.js";
import { isIterable } from "./iterate.js";
import { queryItems } from "./query.js";
/** Is an unknown value an element? */
export function isElement(value) {
return typeof value === "object" && value !== null && "type" in value;
}
/** Is an unknown value a collection of elements? */
export function isElements(value) {
return value === null || typeof value === "string" || isElement(value) || isArray(value);
}
/**
* Strip all tags from elements to produce a plain text string.
* - A `<br>` element becomes a newline (`\n`) — matching DOM `innerText`, so words either side of a line break don't fuse together.
*
* @param elements An element, a plain string, or null/undefined (or an array of those things).
* @returns The combined string made from the elements.
*
* @example `- Item with *strong*\n- Item with _em_` becomes `Item with strong Item with em`
*/
export function getElementText(elements) {
if (typeof elements === "string")
return elements;
if (isElement(elements)) {
// A `<br>` carries no children but renders as a line break — emit `\n` so adjacent words stay separated.
if (elements.type === "br")
return "\n";
return getElementText(elements.props.children);
}
// Iterate the collection directly — `walkElements()` skips loose strings, so it would drop text that sits alongside elements.
if (isIterable(elements)) {
let text = "";
for (const child of elements)
text += getElementText(child);
return text;
}
return "";
}
export function* walkElements(elements) {
if (isElement(elements))
yield elements;
else if (isIterable(elements))
for (const x of elements)
yield* walkElements(x);
}
/**
* Filter elements yielded by `walkElements()` using a `Query<Element>` object.
* - Supports any property query (e.g. `{ type: "tree-file" }`, `{ type: ["tree-file", "tree-directory"] }`), sorting, limiting — anything `queryItems()` accepts.
*/
export function queryElements(elements, query) {
return queryItems(walkElements(elements), query);
}
/** Filter elements yielded by `walkElements()` using a match function. */
export function* filterElements(elements, match) {
for (const element of walkElements(elements))
if (match(element))
yield element;
}
/**
* Resolve an element in a tree by walking a sequence of names from `root`.
* - The `root` element's own name is never matched against path segments — it's the container, not a step in the path.
* - A child's `name` may contain `/` separators, in which case it matches multiple consecutive path segments
* (e.g. a module named `"util/string"` matches the segments `["util", "string"]`).
* - If `path` is empty, returns `root` itself.
* - Returns `undefined` if no descendant matches at any level.
*
* Splitting the path:
* - We accept a raw `Segments` array, so callers can join paths later however they wish.
* - Element paths have no canonical string representation so we use `Segments` instead.
* - To split the names in `a.b.c` dotted data format use `mapItems(getElementPaths(root), splitDataPath)`
* - To split the names in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), splitAbsolutePath)`
*
* @param root The root element to walk from. Its own `name` is treated as a label, not a path segment.
* @param path An array of path segments naming descendants of `root`.
*
* @example resolveElementPath(root, ["util", "array"]) // Element with name "array" inside child with name "util"
* @example resolveElementPath(root, ["util", "string"]) // Module child with composite name "util/string"
* @example resolveElementPath(root, []) // `root` itself
*/
export function resolveElementPath(root, path) {
if (!path.length)
return root;
for (const el of walkElements(root.props.children)) {
const child = el;
const nameSegments = child.props.name.split("/");
if (nameSegments.length > path.length)
continue;
let matches = true;
for (let i = 0; i < nameSegments.length; i++) {
if (nameSegments[i] !== path[i]) {
matches = false;
break;
}
}
if (!matches)
continue;
const result = resolveElementPath(child, path.slice(nameSegments.length));
if (result)
return result;
}
return undefined;
}
/**
* Deeply iterate a tree from `root` and yield path segments for each reachable element.
* - Yields `[]` for `root` itself.
* - Yields `[name]` for each immediate child, `[name, name]` for grandchildren, etc.
* - The `root` element's own name never appears in any yielded path — it's the container.
* - Children with no `name` prop are skipped (and their descendants are not yielded).
*
* Joining the paths:
* - We return a `Segments` array for each element, so callers can join paths later however they wish.
* - Element paths have no canonical string representation so we use `Segments` instead.
* - To join the names in `a.b.c` dotted data format use `mapItems(getElementPaths(root), joinDataPath)`
* - To join the names in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), p => joinPath("/", p))`
*
* @param root The root element to walk from. Its own `name` is treated as a label, not a path segment.
* @param depth Controls how many levels of children to recurse into (defaults to infinite depth).
* - `depth=0` yields only `[]` (the root itself).
*
* @returns Iterable set of path segment arrays, each representing one descendant (or the root).
*/
export function getElementPaths(root, depth = Infinity) {
return _walkElementPaths(root, depth, []);
}
function* _walkElementPaths(element, depth, path) {
yield path;
if (depth <= 0)
return;
for (const child of walkElements(element.props.children)) {
// Skip elements with no `name` prop (and their descendants).
const name = child.props.name;
yield* _walkElementPaths(child, depth - 1, [...path, name]);
}
}
export function mergeElements(a, b) {
if (!a)
return b;
if (!b)
return a;
return [a, b];
}