datocms-structured-text-utils
Version:
A set of Typescript types and helpers to work with DatoCMS Structured Text fields.
271 lines • 10.5 kB
JavaScript
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;
};
import { hasChildren, isBlock, isCode, isDocument, isHeading, isInlineBlock, isItemLink, isLink, isList, isParagraph, isSpan, } from './guards';
/**
* Default configuration values.
*/
var DEFAULT_CONFIG = {
/** Maximum width for inspect output */
MAX_WIDTH: 80,
};
/**
* Type guard to check if a node has children and provides proper type narrowing.
* This replaces the need for unsafe type assertions when accessing children.
*
* @param node - The node to check
* @returns True if the node has children, with proper type narrowing
*/
function nodeHasChildren(node) {
return hasChildren(node);
}
/**
* Default block formatter that displays items in a structured format.
*
* @template BlockItemType - Type of block items
* @template InlineBlockItemType - Type of inline block items
* @param item - The item to format
* @param maxLineWidth - Suggested maximum line width (unused in default formatter)
* @returns Formatted string representation
*/
function defaultBlockFormatter(item) {
return "(item: " + JSON.stringify(item) + ")";
}
/**
* Safely extracts the root node from either a Document or Node input.
*
* @template BlockItemType - Type of block items
* @template InlineBlockItemType - Type of inline block items
* @param input - Either a Document or Node
* @returns The root node
*/
function extractNode(input) {
return isDocument(input)
? input.document
: input;
}
/**
* Inspects and renders a structured text document or node as a tree structure.
* Skips the root node and returns its children directly.
*
* @template BlockItemType - Type of block items (defaults to BlockId)
* @template InlineBlockItemType - Type of inline block items (defaults to BlockId)
* @param input - The structured text document or node to inspect
* @param options - Configuration options for the inspector
* @returns A TreeNode representation of the structure (without root node)
*
* @example
* ```typescript
* const tree = inspectionTreeNodes(document, {
* blockFormatter: (item) => typeof item === 'string' ? item : item.title
* });
* console.log(formatAsTree(tree));
* ```
*/
export function inspectionTreeNodes(input, options) {
if (options === void 0) { options = {}; }
var rootNode = extractNode(input);
var rootTreeNode = buildTreeNode(rootNode, options);
// Skip the root node and return a wrapper containing its children
if (rootTreeNode.nodes && rootTreeNode.nodes.length > 0) {
// If there's only one child, return it directly
if (rootTreeNode.nodes.length === 1) {
return rootTreeNode.nodes[0];
}
// If there are multiple children, create a wrapper node with empty label
return {
label: '',
nodes: rootTreeNode.nodes,
};
}
// Fallback: return empty wrapper if no children
return { label: '' };
}
/**
* Inspects and formats a structured text document or node as a tree string.
* This is a convenience function that combines inspectionTreeNodes and formatAsTree.
*
* @template BlockItemType - Type of block items (defaults to BlockId)
* @template InlineBlockItemType - Type of inline block items (defaults to BlockId)
* @param input - The structured text document or node to inspect
* @param options - Configuration options for the inspector
* @returns A formatted tree string representation
*
* @example
* ```typescript
* const treeString = inspect(document, {
* blockFormatter: (item) => typeof item === 'string' ? item : item.title
* });
* console.log(treeString);
* ```
*/
export function inspect(input, options) {
if (options === void 0) { options = {}; }
return formatAsTree(inspectionTreeNodes(input, options));
}
/**
* Recursively builds a TreeNode representation of a node and its children.
*
* @template BlockItemType - Type of block items
* @template InlineBlockItemType - Type of inline block items
* @param node - The current node to process
* @param options - Configuration options
* @returns TreeNode representation of the node and its subtree
*/
function buildTreeNode(node, options) {
var _a;
var _b;
// Calculate available width for block content
// Mimicking the old calculation: prefix + connector + space (typically around 6-8 characters for deeper nodes)
var maxWidth = (_b = options.maxWidth) !== null && _b !== void 0 ? _b : DEFAULT_CONFIG.MAX_WIDTH;
var availableWidth = Math.max(20, maxWidth - 8);
var nodeLabel = buildNodeLabel(node, options, availableWidth);
// Create the TreeNode with the label
var treeNode = {
label: nodeLabel,
};
// Add children if the node has them
if (nodeHasChildren(node)) {
var children = node.children;
if (children.length > 0) {
treeNode.nodes = [];
for (var _i = 0, children_1 = children; _i < children_1.length; _i++) {
var child = children_1[_i];
treeNode.nodes.push(buildTreeNode(child, options));
}
}
}
// Handle TreeNode returns from blockFormatter for block/inlineBlock nodes
if ((isBlock(node) || isInlineBlock(node)) && options.blockFormatter) {
var formattedContent = options.blockFormatter(node.item, availableWidth);
if (typeof formattedContent !== 'string') {
// Initialize nodes array if it doesn't exist
if (!treeNode.nodes) {
treeNode.nodes = [];
}
// Handle single TreeNode or array of TreeNodes
if (Array.isArray(formattedContent)) {
(_a = treeNode.nodes).push.apply(_a, formattedContent);
}
else {
treeNode.nodes.push(formattedContent);
}
}
}
return treeNode;
}
/**
* Builds a descriptive label for a node based on its type and properties.
*
* @template BlockItemType - Type of block items
* @template InlineBlockItemType - Type of inline block items
* @param node - The node to create a label for
* @param options - Configuration options including block formatter
* @param availableWidth - Available width for content formatting
* @returns A formatted string label for the node
*/
function buildNodeLabel(node, options, availableWidth) {
var metaInfo = [];
var content = '';
if (isSpan(node)) {
if (node.marks && node.marks.length > 0) {
metaInfo.push("marks: " + node.marks.join(', '));
}
content = " \"" + truncateText(node.value, availableWidth) + "\"";
}
else if (isCode(node)) {
if (node.language) {
metaInfo.push("language: \"" + node.language + "\"");
}
content = " \"" + truncateText(node.code, availableWidth) + "\"";
}
else if (isHeading(node)) {
metaInfo.push("level: " + node.level);
}
else if (isList(node)) {
metaInfo.push("style: " + node.style);
}
else if (isLink(node)) {
metaInfo.push("url: \"" + node.url + "\"");
if (node.meta && node.meta.length > 0) {
var metaEntries = node.meta
.map(function (m) { return m.id + "=\"" + m.value + "\""; })
.join(', ');
metaInfo.push("meta: {" + metaEntries + "}");
}
}
else if (isItemLink(node)) {
metaInfo.push("item: \"" + node.item + "\"");
if (node.meta && node.meta.length > 0) {
var metaEntries = node.meta
.map(function (m) { return m.id + "=\"" + m.value + "\""; })
.join(', ');
metaInfo.push("meta: {" + metaEntries + "}");
}
}
else if (isBlock(node) || isInlineBlock(node)) {
var formatter = options.blockFormatter || defaultBlockFormatter;
var formattedContent = formatter(node.item, availableWidth);
// Handle string returns - always treat as single-line (strip newlines)
if (typeof formattedContent === 'string') {
var singleLineContent = formattedContent.replace(/\n/g, ' ').trim();
content = " " + singleLineContent;
}
// TreeNode/TreeNode[] returns will be handled in buildTreeNode function
}
else if (isParagraph(node)) {
if (node.style) {
metaInfo.push("style: \"" + node.style + "\"");
}
}
var metaString = metaInfo.length > 0 ? " (" + metaInfo.join(', ') + ")" : '';
return "" + node.type + metaString + content;
}
function truncateText(text, maxLength) {
if (text.length <= maxLength) {
return text;
}
return text.substring(0, maxLength - 3) + '...';
}
export function formatAsTree(input) {
var root = typeof input === 'string' ? { label: input } : input;
var out = [];
function render(node, ancestorsHasNext, isLast) {
var label = node.label.trim();
var children = node.nodes || [];
// ⬇️ Skip wrapper nodes (no label but has children)
if (!label && children.length > 0) {
children.forEach(function (child, idx) {
var childIsLast = idx === children.length - 1;
render(child, ancestorsHasNext, childIsLast);
});
return;
}
// Build ancestor prefix from booleans: true -> '│ ', false -> ' '
var ancestorPrefix = ancestorsHasNext
.map(function (h) { return (h ? '│ ' : ' '); })
.join('');
var branch = isLast ? '└' : '├';
var lines = label.split('\n');
// First line
out.push("" + ancestorPrefix + branch + " " + lines[0]);
// Continuations
var contPrefix = ancestorPrefix + (isLast ? ' ' : '│ ');
for (var i = 1; i < lines.length; i++) {
out.push("" + contPrefix + lines[i]);
}
// Recurse children
children.forEach(function (child, idx) {
var childIsLast = idx === children.length - 1;
render(child, __spreadArrays(ancestorsHasNext, [!isLast]), childIsLast);
});
}
// Root as a top element
render(root, [], true);
return out.join('\n') + '\n';
}
//# sourceMappingURL=inspector.js.map