import type {HTMLMarkdownElement} from '../../MarkdownTextInput.web';
import type {MarkdownRange, MarkdownType} from '../../commonTypes';
type NodeType = MarkdownType | 'line' | 'text' | 'br' | 'block' | 'root';
type TreeNode = Omit & {
element: HTMLMarkdownElement;
parentNode: TreeNode | null;
childNodes: TreeNode[];
type: NodeType;
orderIndex: string;
isGeneratingNewline: boolean;
};
function createRootTreeNode(target: HTMLMarkdownElement, length = 0): TreeNode {
return {
element: target,
start: 0,
length,
parentNode: null,
childNodes: [],
type: 'root',
orderIndex: '',
isGeneratingNewline: false,
};
}
function addNodeToTree(element: HTMLMarkdownElement, parentTreeNode: TreeNode, type: NodeType, length: number | null = null) {
const contentLength = length || (element.nodeName === 'BR' || type === 'br' ? 1 : element.value?.length) || 0;
const isGeneratingNewline = type === 'line' && !(element.childNodes.length === 1 && (element.childNodes[0] as HTMLElement)?.getAttribute?.('data-type') === 'br');
const parentChildrenCount = parentTreeNode?.childNodes.length || 0;
let startIndex = parentTreeNode.start;
if (parentChildrenCount > 0) {
const lastParentChild = parentTreeNode.childNodes[parentChildrenCount - 1];
if (lastParentChild) {
startIndex = lastParentChild.start + lastParentChild.length;
startIndex += lastParentChild.isGeneratingNewline || (type !== 'block' && element.style.display === 'block') ? 1 : 0;
}
}
const item: TreeNode = {
element,
parentNode: parentTreeNode,
childNodes: [],
start: startIndex,
length: contentLength,
type,
orderIndex: parentTreeNode.parentNode === null ? `${parentChildrenCount}` : `${parentTreeNode.orderIndex},${parentChildrenCount}`,
isGeneratingNewline,
};
element.setAttribute('data-id', item.orderIndex);
parentTreeNode.childNodes.push(item);
return item;
}
function updateTreeElementRefs(treeRoot: TreeNode, element: HTMLMarkdownElement) {
const stack: TreeNode[] = [treeRoot];
const treeElements = element.querySelectorAll('[data-id]') as NodeListOf;
const dataIDToElementMap: Record = {};
treeElements.forEach((el) => {
const dataID = el.getAttribute('data-id');
if (!dataID) {
return;
}
dataIDToElementMap[dataID] = el;
});
while (stack.length > 0) {
const node = stack.pop() as TreeNode;
stack.push(...node.childNodes);
const currentElement = dataIDToElementMap[node.orderIndex];
if (currentElement) {
node.element = currentElement;
}
}
return treeRoot;
}
function findHTMLElementInTree(treeRoot: TreeNode, element: HTMLElement): TreeNode | null {
if (element.hasAttribute?.('contenteditable')) {
return treeRoot;
}
if (!element || !element.hasAttribute?.('data-id')) {
return null;
}
const indexes = element.getAttribute('data-id')?.split(',');
let el: TreeNode | null = treeRoot;
while (el && indexes && indexes.length > 0) {
const index = Number(indexes.shift() || -1);
if (index < 0) {
break;
}
if (el) {
el = el.childNodes[index] || null;
}
}
return el;
}
function getTreeNodeByIndex(treeRoot: TreeNode, index: number): TreeNode | null {
let el: TreeNode | null = treeRoot;
let i = 0;
let newLineGenerated = false;
if (treeRoot.length === 0) {
return treeRoot;
}
while (el && el.childNodes.length > 0 && i < el.childNodes.length) {
const child = el.childNodes[i] as TreeNode;
if (!child) {
break;
}
if (index >= child.start && index < child.start + child.length) {
if (child.childNodes.length === 0) {
return child;
}
el = child;
i = 0;
} else if ((child.isGeneratingNewline || newLineGenerated || i === el.childNodes.length - 1) && index === child.start + child.length) {
newLineGenerated = true;
if (child.childNodes.length === 0) {
return child;
}
el = child;
i = 0;
} else {
i++;
}
}
return null;
}
export {addNodeToTree, findHTMLElementInTree, getTreeNodeByIndex, updateTreeElementRefs, createRootTreeNode};
export type {TreeNode, NodeType};