@unified-semantics/mdast-util-semtree
Version:
Transformer utility for semantically nesting mdast nodes to their proper hierarchy
141 lines (140 loc) • 5.44 kB
JavaScript
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;
}