UNPKG

datocms-structured-text-utils

Version:

A set of Typescript types and helpers to work with DatoCMS Structured Text fields.

600 lines 26.7 kB
/** * Tree manipulation utilities for DatoCMS structured text documents. * * Provides a set of low-level utilities for visiting, transforming, and querying * structured text trees. Works with all tree variants (regular, request, nested). */ var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArrays = (this && this.__spreadArrays) || function () { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; }; /** * Check if a value is a structured text field value */ function isDocument(input) { return (typeof input === 'object' && input !== null && 'schema' in input && input.schema === 'dast' && 'document' in input); } /** * Extract the actual node from either a node or document wrapper */ function extractNode(input) { return isDocument(input) ? input.document : input; } /** * Check if a value has children property that is an array */ function hasChildren(node) { return (typeof node === 'object' && node !== null && 'children' in node && Array.isArray(node.children)); } /** * Visit every node in the Structured Text tree, calling the visitor function for each. * Uses pre-order traversal (parent is visited before its children). * * @template T - The type of the root node in the Structured Text tree * @param input - A structured text document, or a specific DAST node * @param visitor - Synchronous function called for each node. Receives the node, its parent, and path through the Structured Text tree */ export function forEachNode(input, visitor, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } var node = extractNode(input); // Visit current node visitor(node, parent, path); // Recursively visit children if (hasChildren(node)) { for (var index = 0; index < node.children.length; index++) { var child = node.children[index]; forEachNode(child, visitor, node, __spreadArrays(path, [ 'children', index, ])); } } } /** * Visit every node in the Structured Text tree, calling the visitor function for each. * Uses pre-order traversal (parent is visited before its children). * * @template T - The type of the root node in the Structured Text tree * @param input - A structured text document, or a specific DAST node * @param visitor - Asynchronous function called for each node. Receives the node, its parent, and path through the Structured Text tree * @returns Promise that resolves when all nodes have been visited */ export function forEachNodeAsync(input, visitor, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } return __awaiter(this, void 0, void 0, function () { var node, index, child; return __generator(this, function (_a) { switch (_a.label) { case 0: node = extractNode(input); // Visit current node return [4 /*yield*/, visitor(node, parent, path)]; case 1: // Visit current node _a.sent(); if (!hasChildren(node)) return [3 /*break*/, 5]; index = 0; _a.label = 2; case 2: if (!(index < node.children.length)) return [3 /*break*/, 5]; child = node.children[index]; return [4 /*yield*/, forEachNodeAsync(child, visitor, node, __spreadArrays(path, [ 'children', index, ]))]; case 3: _a.sent(); _a.label = 4; case 4: index++; return [3 /*break*/, 2]; case 5: return [2 /*return*/]; } }); }); } export function mapNodes(input, mapper, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } var node = extractNode(input); // Transform current node var transformedNode = mapper(node, parent, path); // If the original node has children, recursively transform them var result; if (hasChildren(node) && typeof transformedNode === 'object' && transformedNode !== null) { var transformedChildren = node.children.map(function (child, index) { return extractNode(mapNodes(child, mapper, node, __spreadArrays(path, [ 'children', index, ]))); }); result = __assign(__assign({}, transformedNode), { children: transformedChildren }); } else { result = transformedNode; } // If input was a document wrapper, return a document wrapper if (isDocument(input)) { return { schema: 'dast', document: result, }; } return result; } export function mapNodesAsync(input, mapper, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } return __awaiter(this, void 0, void 0, function () { var node, transformedNode, result, transformedChildren; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: node = extractNode(input); return [4 /*yield*/, mapper(node, parent, path)]; case 1: transformedNode = _a.sent(); if (!(hasChildren(node) && typeof transformedNode === 'object' && transformedNode !== null)) return [3 /*break*/, 3]; return [4 /*yield*/, Promise.all(node.children.map(function (child, index) { return __awaiter(_this, void 0, void 0, function () { var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = extractNode; return [4 /*yield*/, mapNodesAsync(child, mapper, node, __spreadArrays(path, [ 'children', index, ]))]; case 1: return [2 /*return*/, _a.apply(void 0, [_b.sent()])]; } }); }); }))]; case 2: transformedChildren = _a.sent(); result = __assign(__assign({}, transformedNode), { children: transformedChildren }); return [3 /*break*/, 4]; case 3: result = transformedNode; _a.label = 4; case 4: // If input was a document wrapper, return a document wrapper if (isDocument(input)) { return [2 /*return*/, { schema: 'dast', document: result, }]; } return [2 /*return*/, result]; } }); }); } export function collectNodes(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } var results = []; forEachNode(input, function (currentNode, currentParent, currentPath) { if (predicate(currentNode, currentParent, currentPath)) { results.push({ node: currentNode, path: currentPath }); } }, parent, path); return results; } export function collectNodesAsync(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } return __awaiter(this, void 0, void 0, function () { var results; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: results = []; return [4 /*yield*/, forEachNodeAsync(input, function (currentNode, currentParent, currentPath) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, predicate(currentNode, currentParent, currentPath)]; case 1: if (_a.sent()) { results.push({ node: currentNode, path: currentPath }); } return [2 /*return*/]; } }); }); }, parent, path)]; case 1: _a.sent(); return [2 /*return*/, results]; } }); }); } export function findFirstNode(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } var node = extractNode(input); // Check current node if (predicate(node, parent, path)) { return { node: node, path: path }; } // Recursively search children if (hasChildren(node)) { for (var index = 0; index < node.children.length; index++) { var child = node.children[index]; var result = findFirstNode(child, predicate, node, __spreadArrays(path, ['children', index])); if (result) { return result; } } } return null; } export function findFirstNodeAsync(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } return __awaiter(this, void 0, void 0, function () { var node, index, child, result; return __generator(this, function (_a) { switch (_a.label) { case 0: node = extractNode(input); return [4 /*yield*/, predicate(node, parent, path)]; case 1: // Check current node if (_a.sent()) { return [2 /*return*/, { node: node, path: path }]; } if (!hasChildren(node)) return [3 /*break*/, 5]; index = 0; _a.label = 2; case 2: if (!(index < node.children.length)) return [3 /*break*/, 5]; child = node.children[index]; return [4 /*yield*/, findFirstNodeAsync(child, predicate, node, __spreadArrays(path, ['children', index]))]; case 3: result = _a.sent(); if (result) { return [2 /*return*/, result]; } _a.label = 4; case 4: index++; return [3 /*break*/, 2]; case 5: return [2 /*return*/, null]; } }); }); } export function filterNodes(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } var node = extractNode(input); // If current node doesn't match predicate, return null if (!predicate(node, parent, path)) { return null; } // If node has no children, return it as-is var result; if (!hasChildren(node)) { result = node; } else { // Filter children recursively var childrenResults = node.children.map(function (child, index) { var childResult = filterNodes(child, predicate, node, __spreadArrays(path, ['children', index])); return childResult ? extractNode(childResult) : null; }); var prunedChildren = childrenResults.filter(function (child) { return child !== null; }); result = __assign(__assign({}, node), { children: prunedChildren }); } // If input was a document wrapper, return a document wrapper if (isDocument(input)) { return { schema: 'dast', document: result, }; } return result; } export function filterNodesAsync(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } return __awaiter(this, void 0, void 0, function () { var node, result, childrenResults, prunedChildren; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: node = extractNode(input); return [4 /*yield*/, predicate(node, parent, path)]; case 1: // If current node doesn't match predicate, return null if (!(_a.sent())) { return [2 /*return*/, null]; } if (!!hasChildren(node)) return [3 /*break*/, 2]; result = node; return [3 /*break*/, 4]; case 2: return [4 /*yield*/, Promise.all(node.children.map(function (child, index) { return __awaiter(_this, void 0, void 0, function () { var childResult; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, filterNodesAsync(child, predicate, node, __spreadArrays(path, ['children', index]))]; case 1: childResult = _a.sent(); return [2 /*return*/, childResult ? extractNode(childResult) : null]; } }); }); }))]; case 3: childrenResults = _a.sent(); prunedChildren = childrenResults.filter(function (child) { return child !== null; }); result = __assign(__assign({}, node), { children: prunedChildren }); _a.label = 4; case 4: // If input was a document wrapper, return a document wrapper if (isDocument(input)) { return [2 /*return*/, { schema: 'dast', document: result, }]; } return [2 /*return*/, result]; } }); }); } /** * Reduce the Structured Text tree to a single value by applying a reducer function to each node. * Uses pre-order traversal (parent is processed before its children). * The reducer function is called for each node with the current accumulator value. * * @template T - The type of nodes in the Structured Text tree * @template R - The type of the accumulated result * @param input - A structured text document, or a specific DAST node * @param reducer - Synchronous function that processes each node and updates the accumulator * @param initialValue - The initial value for the accumulator * @returns The final accumulated value */ export function reduceNodes(input, reducer, initialValue, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } var node = extractNode(input); var accumulator = initialValue; forEachNode(node, function (currentNode, currentParent, currentPath) { accumulator = reducer(accumulator, currentNode, currentParent, currentPath); }, parent, path); return accumulator; } /** * Reduce the Structured Text tree to a single value by applying a reducer function to each node. * Uses pre-order traversal (parent is processed before its children). * The reducer function is called for each node with the current accumulator value. * * @template T - The type of nodes in the Structured Text tree * @template R - The type of the accumulated result * @param input - A structured text document, or a specific DAST node * @param reducer - Asynchronous function that processes each node and updates the accumulator * @param initialValue - The initial value for the accumulator * @returns Promise that resolves to the final accumulated value */ export function reduceNodesAsync(input, reducer, initialValue, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } return __awaiter(this, void 0, void 0, function () { var node, accumulator; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: node = extractNode(input); accumulator = initialValue; return [4 /*yield*/, forEachNodeAsync(node, function (currentNode, currentParent, currentPath) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, reducer(accumulator, currentNode, currentParent, currentPath)]; case 1: accumulator = _a.sent(); return [2 /*return*/]; } }); }); }, parent, path)]; case 1: _a.sent(); return [2 /*return*/, accumulator]; } }); }); } /** * Check if any node in the Structured Text tree matches the predicate function. * Returns true as soon as the first matching node is found (short-circuit evaluation). * * @template T - The type of nodes in the Structured Text tree * @param input - A structured text document, or a specific DAST node * @param predicate - Synchronous function that tests each node. Should return true for matching nodes * @returns True if any node matches, false otherwise */ export function someNode(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } var node = extractNode(input); // Check current node if (predicate(node, parent, path)) { return true; } // Recursively check children if (hasChildren(node)) { for (var index = 0; index < node.children.length; index++) { var child = node.children[index]; if (someNode(child, predicate, node, __spreadArrays(path, [ 'children', index, ]))) { return true; } } } return false; } /** * Check if any node in the Structured Text tree matches the predicate function. * Returns true as soon as the first matching node is found (short-circuit evaluation). * * @template T - The type of nodes in the Structured Text tree * @param input - A structured text document, or a specific DAST node * @param predicate - Asynchronous function that tests each node. Should return true for matching nodes * @returns Promise that resolves to true if any node matches, false otherwise */ export function someNodeAsync(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } return __awaiter(this, void 0, void 0, function () { var node, index, child; return __generator(this, function (_a) { switch (_a.label) { case 0: node = extractNode(input); return [4 /*yield*/, predicate(node, parent, path)]; case 1: // Check current node if (_a.sent()) { return [2 /*return*/, true]; } if (!hasChildren(node)) return [3 /*break*/, 5]; index = 0; _a.label = 2; case 2: if (!(index < node.children.length)) return [3 /*break*/, 5]; child = node.children[index]; return [4 /*yield*/, someNodeAsync(child, predicate, node, __spreadArrays(path, [ 'children', index, ]))]; case 3: if (_a.sent()) { return [2 /*return*/, true]; } _a.label = 4; case 4: index++; return [3 /*break*/, 2]; case 5: return [2 /*return*/, false]; } }); }); } /** * Check if every node in the Structured Text tree matches the predicate function. * Returns false as soon as the first non-matching node is found (short-circuit evaluation). * * @template T - The type of nodes in the Structured Text tree * @param input - A structured text document, or a specific DAST node * @param predicate - Synchronous function that tests each node. Should return true for valid nodes * @returns True if all nodes match, false otherwise */ export function everyNode(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } var node = extractNode(input); return !someNode(node, function (currentNode, currentParent, currentPath) { return !predicate(currentNode, currentParent, currentPath); }, parent, path); } /** * Check if every node in the Structured Text tree matches the predicate function. * Returns false as soon as the first non-matching node is found (short-circuit evaluation). * * @template T - The type of nodes in the Structured Text tree * @param input - A structured text document, or a specific DAST node * @param predicate - Asynchronous function that tests each node. Should return true for valid nodes * @returns Promise that resolves to true if all nodes match, false otherwise */ export function everyNodeAsync(input, predicate, parent, path) { if (parent === void 0) { parent = null; } if (path === void 0) { path = []; } return __awaiter(this, void 0, void 0, function () { var node; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: node = extractNode(input); return [4 /*yield*/, someNodeAsync(node, function (currentNode, currentParent, currentPath) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, predicate(currentNode, currentParent, currentPath)]; case 1: return [2 /*return*/, !(_a.sent())]; } }); }); }, parent, path)]; case 1: return [2 /*return*/, !(_a.sent())]; } }); }); } //# sourceMappingURL=manipulation.js.map