UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

142 lines (141 loc) 6.29 kB
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]; }