Skip to content

Commit 94f60e4

Browse files
committed
feat(plaintext): support preserving arrow delta coordinates (x,y)
1 parent 70754d5 commit 94f60e4

4 files changed

Lines changed: 40 additions & 16 deletions

File tree

skills/plaintext-format/SKILL.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,16 @@ Create connections between nodes using the `>` prefix and arrow syntax:
5252
- Root
5353
- Node A [^id1]
5454
- Node B [^id2]
55-
- > [^id1] <-Link Label-> [^id2]
55+
- > [^id1] (10,20) <-Link Label-> (-10,-20) [^id2]
5656
```
5757

58-
**Format:** `> [^sourceId] <-Label-> [^targetId]`
58+
**Format:** `> [^sourceId] (delta1X,delta1Y) <-Label-> (delta2X,delta2Y) [^targetId]`
59+
60+
- `(delta1X,delta1Y)`: Offset of the control point from the **start** node.
61+
- `(delta2X,delta2Y)`: Offset of the control point from the **end** node.
62+
63+
> [!TIP]
64+
> **Optional Coordinates**: When manually writing or generating plaintext (e.g., via AI), you can **omit** the `(x,y)` coordinates. Mind Elixir will automatically calculate default balanced offsets when rendering. However, when you export data back to plaintext, these coordinates will be included to preserve any manual adjustments made in the UI.
5965
6066
#### Unidirectional Links
6167

@@ -129,7 +135,7 @@ Create summary nodes that visually group previous siblings:
129135
- Phase 3: Launch [^phase3]
130136
- Marketing
131137
- Deployment
132-
- > [^phase1] >-Leads to-> [^phase2]
138+
- > [^phase1] (50,0) >-Leads to-> (-50,0) [^phase2]
133139
- > [^phase2] >-Leads to-> [^phase3]
134140
```
135141

@@ -271,8 +277,8 @@ function safeParse(plaintext: string): MindElixirData | null {
271277
| Node | `- Topic` | `- My Node` |
272278
| Node with ID | `- Topic [^id]` | `- Node A [^id1]` |
273279
| Node with Style | `- Topic {"prop": "value"}` | `- Node {"color": "#ff0000"}` |
274-
| Bidirectional Link | `> [^id1] <-Label-> [^id2]` | `> [^a] <-connects-> [^b]` |
275-
| Forward Link | `> [^id1] >-Label-> [^id2]` | `> [^a] >-leads to-> [^b]` |
280+
| Bidirectional Link | `> [^id1] (x,y) <-Label-> (x,y) [^id2]` | `> [^a] (10,20) <-connects-> (-10,-20) [^b]` |
281+
| Forward Link | `> [^id1] (x,y) >-Label-> (x,y) [^id2]` | `> [^a] (10,20) >-leads to-> (-10,-20) [^b]` |
276282
| Summary (all) | `} Summary text` | `} Overview` |
277283
| Summary (N nodes) | `}:N Summary text` | `}:3 Last three items` |
278284

src/dev.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,6 @@ document.querySelector('#ssr')!.innerHTML = renderSSRHTML(layoutSSR(window.m.nod
200200
const convertedData = plaintextToMindElixir(plaintextExample)
201201
console.log('convertedData', convertedData)
202202
mind.refresh(convertedData)
203-
203+
mind.toCenter()
204204
const plaintext = mindElixirToPlaintext(mind.getData())
205205
console.log('plaintext', plaintext)

src/utils/mindElixirToPlaintext.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ export function mindElixirToPlaintext(data: MindElixirData): string {
109109
if (el.type === 'arrow') {
110110
const a = el.arrow
111111
const connector = a.bidirectional ? `<-${a.label ?? ''}->` : `>-${a.label ?? ''}->`
112-
lines.push(`${childIndent}- > [^${getRefId(a.from)}] ${connector} [^${getRefId(a.to)}]`)
112+
const d1 = a.delta1 ? ` (${a.delta1.x},${a.delta1.y})` : ''
113+
const d2 = a.delta2 ? ` (${a.delta2.x},${a.delta2.y})` : ''
114+
lines.push(`${childIndent}- > [^${getRefId(a.from)}]${d1} ${connector}${d2} [^${getRefId(a.to)}]`)
113115
} else if (el.type === 'summary') {
114116
const s = el.summary
115117
const count = s.end - s.start + 1
@@ -129,7 +131,9 @@ export function mindElixirToPlaintext(data: MindElixirData): string {
129131
// Emit root-level arrows
130132
for (const a of rootArrows) {
131133
const connector = a.bidirectional ? `<-${a.label ?? ''}->` : `>-${a.label ?? ''}->`
132-
lines.push(`- > [^${getRefId(a.from)}] ${connector} [^${getRefId(a.to)}]`)
134+
const d1 = a.delta1 ? ` (${a.delta1.x},${a.delta1.y})` : ''
135+
const d2 = a.delta2 ? ` (${a.delta2.x},${a.delta2.y})` : ''
136+
lines.push(`- > [^${getRefId(a.from)}]${d1} ${connector}${d2} [^${getRefId(a.to)}]`)
133137
}
134138

135139
return lines.join('\n') + '\n'

src/utils/plaintextToMindElixir.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -245,34 +245,48 @@ function parseLine(line: string): ParsedLine {
245245
}
246246

247247
function parseArrow(content: string, context: ParseContext): Arrow | null {
248-
// Bidirectional: [^id1] <-label-> [^id2]
249-
const bidirectionalMatch = content.match(/\[\^([\w-]+)\]\s*<-([^-]*)->\s*\[\^([\w-]+)\]/)
248+
// Bidirectional: [^id1] (x,y) <-label-> (x,y) [^id2]
249+
const bidirectionalMatch = content.match(
250+
/\[\^([\w-]+)\](?:\s*\(([\d.-]+),([\d.-]+)\))?\s*<-([^-]*)->(?:\s*\(([\d.-]+),([\d.-]+)\))?\s*\[\^([\w-]+)\]/
251+
)
250252
if (bidirectionalMatch) {
251253
const fromRefId = bidirectionalMatch[1]
252-
const label = bidirectionalMatch[2].trim()
253-
const toRefId = bidirectionalMatch[3]
254+
const d1x = bidirectionalMatch[2]
255+
const d1y = bidirectionalMatch[3]
256+
const label = bidirectionalMatch[4].trim()
257+
const d2x = bidirectionalMatch[5]
258+
const d2y = bidirectionalMatch[6]
259+
const toRefId = bidirectionalMatch[7]
254260

255261
return {
256262
id: generateUUID(),
257263
label,
258264
from: context.nodeIdMap.get(fromRefId) || fromRefId,
259265
to: context.nodeIdMap.get(toRefId) || toRefId,
266+
delta1: d1x && d1y ? { x: Number(d1x), y: Number(d1y) } : undefined,
267+
delta2: d2x && d2y ? { x: Number(d2x), y: Number(d2y) } : undefined,
260268
bidirectional: true,
261269
} as Arrow
262270
}
263271

264-
// Forward: [^id1] >-label-> [^id2]
265-
const forwardMatch = content.match(/\[\^([\w-]+)\]\s*>-([^-]*)->\s*\[\^([\w-]+)\]/)
272+
// Forward: [^id1] (x,y) >-label-> (x,y) [^id2]
273+
const forwardMatch = content.match(/\[\^([\w-]+)\](?:\s*\(([\d.-]+),([\d.-]+)\))?\s*>-([^-]*)->(?:\s*\(([\d.-]+),([\d.-]+)\))?\s*\[\^([\w-]+)\]/)
266274
if (forwardMatch) {
267275
const fromRefId = forwardMatch[1]
268-
const label = forwardMatch[2].trim()
269-
const toRefId = forwardMatch[3]
276+
const d1x = forwardMatch[2]
277+
const d1y = forwardMatch[3]
278+
const label = forwardMatch[4].trim()
279+
const d2x = forwardMatch[5]
280+
const d2y = forwardMatch[6]
281+
const toRefId = forwardMatch[7]
270282

271283
return {
272284
id: generateUUID(),
273285
label,
274286
from: context.nodeIdMap.get(fromRefId) || fromRefId,
275287
to: context.nodeIdMap.get(toRefId) || toRefId,
288+
delta1: d1x && d1y ? { x: Number(d1x), y: Number(d1y) } : undefined,
289+
delta2: d2x && d2y ? { x: Number(d2x), y: Number(d2y) } : undefined,
276290
} as Arrow
277291
}
278292

0 commit comments

Comments
 (0)