Skip to content

Commit 68764c3

Browse files
committed
refactor: optimize plaintext converter algorithm
1 parent 078560d commit 68764c3

3 files changed

Lines changed: 80 additions & 98 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 5.8.3 - 2026-02-18
4+
5+
### Refactors
6+
7+
- Refactor plaintext converter
8+
39
## 5.8.2 - 2026-02-16
410

511
### Features

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mind-elixir",
3-
"version": "5.8.2",
3+
"version": "5.8.3",
44
"type": "module",
55
"description": "Mind elixir is a free open source mind map core.",
66
"keywords": [

src/utils/plaintextConverter.ts

Lines changed: 73 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { Summary } from '../summary'
44

55
interface ParseContext {
66
arrowLines: { content: string; parentChildren: NodeObj[] }[]
7-
summaryLines: { content: string; parentChildren: NodeObj[]; parentId: string }[]
87
nodeIdMap: Map<string, string> // maps [^id] to actual node id
98
}
109

@@ -39,7 +38,8 @@ export const plaintextExample = `- Root Node
3938
- Child Node 4-3 [^id8]
4039
- } Summary of all previous nodes
4140
- Child Node 4-4
42-
- > [^id1] <-Link position is not restricted, as long as the id can be found during rendering-> [^id8]
41+
- > [^id1] <-Link position is not restricted, as long as the id can be found during rendering-> [^id8]
42+
4343
`
4444

4545
/**
@@ -54,130 +54,106 @@ export const plaintextExample = `- Root Node
5454
* - Child 2 [^id2]
5555
* - > [^id1] <-label-> [^id2]
5656
*
57+
* When the plaintext contains more than one top-level node, a synthetic root
58+
* node is automatically created to wrap them as first-level children.
59+
*
5760
* @param plaintext - The plaintext string to convert
61+
* @param rootName - Optional name for the synthetic root node when multiple
62+
* top-level nodes are detected (defaults to 'Root')
5863
* @returns MindElixirData object
5964
*/
60-
export function plaintextToMindElixir(plaintext: string): MindElixirData {
65+
export function plaintextToMindElixir(plaintext: string, rootName = 'Root'): MindElixirData {
6166
const lines = plaintext.split('\n').filter(line => line.trim())
6267

68+
if (lines.length === 0) {
69+
throw new Error('Failed to parse plaintext: no root node found')
70+
}
71+
6372
const context: ParseContext = {
6473
arrowLines: [],
65-
summaryLines: [],
6674
nodeIdMap: new Map(),
6775
}
6876

69-
// First pass: parse nodes and collect arrows/summaries for later processing
70-
const root = parseNode(lines, 0, -2, context)
77+
const summaries: Summary[] = []
7178

72-
if (!root.node) {
73-
throw new Error('Failed to parse plaintext: no root node found')
74-
}
79+
// Stack tracks the current ancestry path: each entry holds the indent level and the node
80+
const stack: { indent: number; node: NodeObj }[] = []
81+
// Collect top-level nodes
82+
const topLevelNodes: NodeObj[] = []
7583

76-
// Second pass: process arrows and summaries
77-
const arrows = context.arrowLines.map(({ content }) => parseArrow(content, context)).filter((a): a is Arrow => a !== null)
84+
// Single pass: iterate through all lines once
85+
for (const line of lines) {
86+
const indent = getIndent(line)
87+
const parsed = parseLine(line)
7888

79-
const summaries = context.summaryLines
80-
.map(({ content, parentChildren, parentId }) => parseSummary(content, parentChildren, parentId))
81-
.filter((s): s is Summary => s !== null)
82-
83-
return {
84-
nodeData: root.node,
85-
arrows: arrows.length > 0 ? arrows : undefined,
86-
summaries: summaries.length > 0 ? summaries : undefined,
87-
}
88-
}
89+
// Pop the stack until we find the parent for this indent level
90+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
91+
stack.pop()
92+
}
8993

90-
interface ParseResult {
91-
node: NodeObj | null
92-
nextIndex: number
93-
}
94+
const parent = stack.length > 0 ? stack[stack.length - 1].node : null
95+
const parentChildren = parent ? (parent.children ??= []) : topLevelNodes
9496

95-
function parseNode(lines: string[], index: number, parentIndent: number, context: ParseContext): ParseResult {
96-
if (index >= lines.length) {
97-
return { node: null, nextIndex: index }
98-
}
97+
if (parsed.type === 'arrow') {
98+
context.arrowLines.push({
99+
content: parsed.content,
100+
parentChildren,
101+
})
102+
continue
103+
}
99104

100-
const line = lines[index]
101-
const indent = getIndent(line)
105+
if (parsed.type === 'summary') {
106+
const summary = parseSummary(parsed.content, parentChildren, parent?.id ?? '')
107+
if (summary) summaries.push(summary)
108+
continue
109+
}
102110

103-
if (indent <= parentIndent) {
104-
return { node: null, nextIndex: index }
105-
}
111+
// Create node
112+
const nodeId = generateId()
113+
const node: NodeObj = {
114+
topic: parsed.topic,
115+
id: nodeId,
116+
}
106117

107-
const parsed = parseLine(line)
118+
if (parsed.style) {
119+
node.style = parsed.style
120+
}
108121

109-
// If this line is an arrow or summary at the current level, we need to handle it
110-
// Note: We should only skip arrows/summaries when parsing a child recursively
111-
// But when encountered directly, collect them and skip creating a node
112-
if (parsed.type === 'arrow' || parsed.type === 'summary') {
113-
// We can't add to context here because we don't have the parent's children yet
114-
// Just skip and let the parent handle it
115-
return { node: null, nextIndex: index + 1 }
116-
}
122+
if (parsed.refId) {
123+
context.nodeIdMap.set(parsed.refId, nodeId)
124+
}
117125

118-
// Create the node
119-
const nodeId = generateId()
120-
const node: NodeObj = {
121-
topic: parsed.topic,
122-
id: nodeId,
123-
}
126+
// Attach to parent or top-level
127+
parentChildren.push(node)
124128

125-
if (parsed.style) {
126-
node.style = parsed.style
129+
// Push onto stack so subsequent deeper lines become children of this node
130+
stack.push({ indent, node })
127131
}
128132

129-
if (parsed.refId) {
130-
context.nodeIdMap.set(parsed.refId, nodeId)
133+
if (topLevelNodes.length === 0) {
134+
throw new Error('Failed to parse plaintext: no root node found')
131135
}
132136

133-
// Parse children
134-
const children: NodeObj[] = []
135-
let currentIndex = index + 1
136-
137-
while (currentIndex < lines.length) {
138-
const childLine = lines[currentIndex]
139-
const childIndent = getIndent(childLine)
140-
141-
// Not a child
142-
if (childIndent <= indent) {
143-
break
144-
}
145-
146-
// Direct child only
147-
if (childIndent === indent + 2) {
148-
const childParsed = parseLine(childLine)
149-
150-
if (childParsed.type === 'arrow') {
151-
context.arrowLines.push({
152-
content: childParsed.content,
153-
parentChildren: children,
154-
})
155-
currentIndex++
156-
} else if (childParsed.type === 'summary') {
157-
context.summaryLines.push({
158-
content: childParsed.content,
159-
parentChildren: children,
160-
parentId: nodeId, // Pass parent node ID
161-
})
162-
currentIndex++
163-
} else {
164-
const result = parseNode(lines, currentIndex, indent, context)
165-
if (result.node) {
166-
children.push(result.node)
167-
}
168-
currentIndex = result.nextIndex
169-
}
170-
} else {
171-
// Skip deeper indented lines (will be handled recursively)
172-
currentIndex++
137+
// If there are multiple top-level nodes, wrap them under a synthetic root
138+
let rootNode: NodeObj
139+
if (topLevelNodes.length === 1) {
140+
rootNode = topLevelNodes[0]
141+
} else {
142+
rootNode = {
143+
topic: rootName,
144+
id: generateId(),
145+
children: topLevelNodes,
173146
}
174147
}
175148

176-
if (children.length > 0) {
177-
node.children = children
178-
}
149+
// Process arrows (deferred because they depend on nodeIdMap which is built during the pass)
150+
const arrows = context.arrowLines.map(({ content }) => parseArrow(content, context)).filter((a): a is Arrow => a !== null)
179151

180-
return { node, nextIndex: currentIndex }
152+
return {
153+
nodeData: rootNode,
154+
arrows: arrows.length > 0 ? arrows : undefined,
155+
summaries: summaries.length > 0 ? summaries : undefined,
156+
}
181157
}
182158

183159
function getIndent(line: string): number {

0 commit comments

Comments
 (0)