UNPKG

@unified-semantics/mdast-util-semtree

Version:

Transformer utility for semantically nesting mdast nodes to their proper hierarchy

141 lines (140 loc) 5.44 kB
import { visit } from 'unist-util-visit'; /** * The default options for the semantic tree transformer */ const defaultOptions = { preserveListStructure: true }; /** * Creates a transformer function that organizes mdast nodes into a semantic hierarchy * based on their heading levels */ export function semtree(options = {}) { const opts = { ...defaultOptions, ...options }; return function transformer(tree) { // Create a new tree with the same type as the original const newTree = { type: 'root', children: [] }; // Track the current parent nodes at each heading level const parents = { 1: newTree, 2: newTree, 3: newTree, 4: newTree, 5: newTree, 6: newTree }; // Keep track of the current heading level let currentLevel = 1; // Process each node in the original tree tree.children.forEach(node => { if (node.type === 'heading') { const level = node.depth; currentLevel = level; // Determine the parent for this heading based on its level let parentNode = newTree; // Initialize with root as default if (level === 1) { // H1 headings are direct children of the root parentNode = newTree; } else { // Find the nearest heading of higher level to be the parent for (let i = level - 1; i >= 1; i--) { if (parents[i] !== newTree) { parentNode = parents[i]; break; } } } // Clone the heading node const headingNode = { ...node, children: [...node.children] }; // If the heading doesn't have a children array, add one if (!('children' in headingNode)) { headingNode.children = []; } // Add the heading to its parent parentNode.children.push(headingNode); // Set this heading as the current parent for its level and all lower levels parents[level] = headingNode; for (let i = level + 1; i <= 6; i++) { parents[i] = headingNode; } } else if (node.type === 'list' && opts.preserveListStructure) { // For lists, we want to preserve the list->list item structure // Clone the list and its items const listNode = { ...node, children: node.children.map(item => { if (item.type === 'listItem') { return { ...item, children: [...item.children] }; } return item; }) }; // Add the list to the current parent based on heading level parents[currentLevel].children.push(listNode); } else { // For other content (paragraphs, code blocks, etc.) // Clone the node const contentNode = { ...node }; // If it has children, clone those too if ('children' in node && Array.isArray(node.children)) { contentNode.children = [...node.children]; } // Add to the current parent based on heading level parents[currentLevel].children.push(contentNode); } }); return newTree; }; } /** * Utility function to extract content belonging to a specific heading */ export function extractHeadingContent(tree, headingText) { const result = []; // Find the heading node by its text content visit(tree, 'heading', (node) => { // Check if the heading text matches const headingTextContent = node.children .filter(child => child.type === 'text') .map(child => child.value) .join(''); if (headingTextContent === headingText) { // Return the children of this heading, which represent its content if ('children' in node) { result.push(...node.children.filter(child => child.type !== 'heading')); } } }); return result; } /** * Creates a valid mdast tree from a node and its ancestors */ export function createSubtree(tree, headingText) { const newTree = { type: 'root', children: [] }; // Find the heading node and its content let foundHeading = false; visit(tree, 'heading', (node) => { if (foundHeading) return; // Check if the heading text matches const headingTextContent = node.children .filter(child => child.type === 'text') .map(child => child.value) .join(''); if (headingTextContent === headingText) { foundHeading = true; // Add this heading and all its content to the new tree if ('children' in node) { newTree.children.push(node); } } }); return newTree; }