Skip to content

Commit ca5ae75

Browse files
committed
✨ Add tree diagrams to docs
- Plain-text rig diagram was hard to read and had no links
1 parent 1c6feae commit ca5ae75

4 files changed

Lines changed: 280 additions & 12 deletions

File tree

‎src/docs/core-concepts/rigs/en.md‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ A **Rig** is the group of entities your Blueprint creates in Minecraft. Every ti
1111

1212
A rig is made of these entities:
1313

14-
```
15-
Root Entity (minecraft:item_display)
16-
├── Bones (minecraft:item_display) ← Groups with Cubes
17-
├── Item Displays (minecraft:item_display)
18-
├── Block Displays (minecraft:block_display)
19-
├── Text Displays (minecraft:text_display)
20-
│
21-
├╌╌ Locators (any entity type) ← only with Use Entity, free-floating
22-
├╌╌ Cameras (minecraft:item_display) ← free-floating
23-
└╌╌ Interactions (minecraft:interaction) ← free-floating
24-
```
14+
:::tree
15+
16+
- Root Entity `minecraft:item_display`
17+
- [Bones](/docs/nodes/groups) `minecraft:item_display` — Groups with Cubes
18+
- [Item Displays](/docs/nodes/item-displays) `minecraft:item_display`
19+
- [Block Displays](/docs/nodes/block-displays) `minecraft:block_display`
20+
- [Text Displays](/docs/nodes/text-displays) `minecraft:text_display`
21+
- ~ [Locators](/docs/nodes/locators) any entity type — only with Use Entity, free-floating
22+
- ~ [Cameras](/docs/nodes/cameras) `minecraft:item_display`
23+
- ~ [Interactions](/docs/nodes/interactions) `minecraft:interaction`
24+
25+
:::
2526

2627
:::tip
2728
The **E:** counter in the Outliner's toolbar shows how many entities one instance of your rig will use. Click it for a breakdown by node type.

‎src/lib/docs/rehype-tree.ts‎

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import type { Element, ElementContent, Root, RootContent, Text } from 'hast'
2+
import type { Plugin } from 'unified'
3+
import { visit } from 'unist-util-visit'
4+
5+
const MCDP_ICON_URL = 'https://github.com/FuncFusion/mc-dp-icons-assets/blob/main/icons/current'
6+
const NOTE_SEPARATOR = ' — '
7+
8+
/** Resolves `mcdp:<name>` to an icon in the MCDP set (github.com/FuncFusion/mc-dp-icons-assets). */
9+
export function resolveTreeIconUrl(src: string): string {
10+
if (!src.startsWith('mcdp:')) return src
11+
return `${MCDP_ICON_URL}/${src.slice('mcdp:'.length)}.svg?raw=true`
12+
}
13+
14+
function isWhitespaceText(node: RootContent): boolean {
15+
return node.type === 'text' && node.value.trim().length === 0
16+
}
17+
18+
function isElement(node: RootContent | undefined, tagName: string): node is Element {
19+
return node?.type === 'element' && node.tagName === tagName
20+
}
21+
22+
function isParagraphWithText(node: RootContent | undefined, exp: RegExp): boolean {
23+
if (!isElement(node, 'p') || node.children.length !== 1) return false
24+
const [child] = node.children
25+
return child.type === 'text' && exp.test(child.value)
26+
}
27+
28+
function nextContentIndex(children: RootContent[], from: number): number {
29+
let index = from
30+
while (index < children.length && isWhitespaceText(children[index])) index += 1
31+
return index
32+
}
33+
34+
/** The fence's closing `:::` lazily continues into the last list item, so strip it from there. */
35+
function stripTrailingFence(node: Element): boolean {
36+
for (let index = node.children.length - 1; index >= 0; index -= 1) {
37+
const child = node.children[index]
38+
39+
if (child.type === 'text') {
40+
if (child.value.trim().length === 0) continue
41+
42+
const match = /^([\s\S]*?)\s*:::\s*$/.exec(child.value)
43+
if (!match) return false
44+
45+
child.value = match[1]
46+
return true
47+
}
48+
49+
if (child.type === 'element') return stripTrailingFence(child)
50+
51+
return false
52+
}
53+
54+
return false
55+
}
56+
57+
/** Splits a trailing ` — note` off the label into its own muted span. */
58+
function splitNote(children: ElementContent[]): ElementContent[] {
59+
const textIndex = children.findIndex(
60+
child => child.type === 'text' && child.value.includes(NOTE_SEPARATOR)
61+
)
62+
if (textIndex < 0) return children
63+
64+
const text = children[textIndex] as Text
65+
const separatorIndex = text.value.indexOf(NOTE_SEPARATOR)
66+
const before = text.value.slice(0, separatorIndex)
67+
const after = text.value.slice(separatorIndex + NOTE_SEPARATOR.length)
68+
69+
const note: Element = {
70+
type: 'element',
71+
tagName: 'span',
72+
properties: { className: ['tree-note'] },
73+
children: [{ type: 'text', value: after }, ...children.slice(textIndex + 1)],
74+
}
75+
76+
return [...children.slice(0, textIndex), { type: 'text', value: before }, note]
77+
}
78+
79+
function transformItem(item: Element): Element {
80+
const sublists: Element[] = []
81+
let label: ElementContent[] = []
82+
83+
for (const child of item.children) {
84+
if (isElement(child, 'ul') || isElement(child, 'ol')) {
85+
sublists.push(transformList(child))
86+
} else if (isElement(child, 'p')) {
87+
// Loose lists wrap item content in paragraphs.
88+
label.push(...child.children)
89+
} else {
90+
label.push(child)
91+
}
92+
}
93+
94+
while (label.length > 0 && isWhitespaceText(label[0])) label.shift()
95+
while (label.length > 0 && isWhitespaceText(label[label.length - 1])) label.pop()
96+
97+
let floating = false
98+
const first = label[0]
99+
if (first?.type === 'text' && first.value.startsWith('~')) {
100+
floating = true
101+
first.value = first.value.slice(1).trimStart()
102+
if (first.value.length === 0) label.shift()
103+
}
104+
105+
const icon = label[0]
106+
if (isElement(icon, 'img')) {
107+
icon.properties = {
108+
...icon.properties,
109+
className: ['tree-icon'],
110+
src: resolveTreeIconUrl(String(icon.properties.src ?? '')),
111+
alt: icon.properties.alt ?? '',
112+
}
113+
const afterIcon = label[1]
114+
if (afterIcon?.type === 'text') afterIcon.value = afterIcon.value.trimStart()
115+
}
116+
117+
label = splitNote(label)
118+
119+
return {
120+
type: 'element',
121+
tagName: 'li',
122+
properties: { className: floating ? ['tree-item', 'tree-item-floating'] : ['tree-item'] },
123+
children: [
124+
{
125+
type: 'element',
126+
tagName: 'span',
127+
properties: { className: ['tree-label'] },
128+
children: label,
129+
},
130+
...sublists,
131+
],
132+
}
133+
}
134+
135+
function transformList(list: Element): Element {
136+
return {
137+
type: 'element',
138+
tagName: 'ul',
139+
properties: {},
140+
children: list.children.filter(child => isElement(child, 'li')).map(transformItem),
141+
}
142+
}
143+
144+
/**
145+
* Turns a `:::tree` fence around a nested list into a tree diagram.
146+
* Items starting with `~` get a dashed branch, a leading image becomes the item's icon,
147+
* and text after ` — ` becomes a muted note.
148+
*/
149+
const rehypeTree: Plugin<[], Root> = () => {
150+
return (tree: Root) => {
151+
visit(tree, 'element', (node: Element, index, parent) => {
152+
if (!parent || typeof index !== 'number') return
153+
if (!isParagraphWithText(node, /^\s*:::\s*tree\s*$/i)) return
154+
155+
const listIndex = nextContentIndex(parent.children, index + 1)
156+
const list = parent.children[listIndex]
157+
if (!isElement(list, 'ul')) return
158+
159+
let closingIndex = nextContentIndex(parent.children, listIndex + 1)
160+
if (!isParagraphWithText(parent.children[closingIndex], /^\s*:::\s*$/)) {
161+
if (!stripTrailingFence(list)) return
162+
closingIndex = listIndex
163+
}
164+
165+
const treeNode: Element = {
166+
type: 'element',
167+
tagName: 'div',
168+
properties: { className: ['tree'] },
169+
children: [transformList(list)],
170+
}
171+
172+
parent.children.splice(index, closingIndex - index + 1, treeNode)
173+
return index + 1
174+
})
175+
}
176+
}
177+
178+
export default rehypeTree

‎src/lib/styles/global.css‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,94 @@ code {
181181
color: #66e3ff;
182182
}
183183

184+
.docs-content .tree {
185+
--tree-line: 2px solid var(--minecraft-button-secondary-text-color);
186+
--tree-branch-y: 14px;
187+
188+
margin: 16px 0;
189+
padding: 12px 16px;
190+
overflow-x: auto;
191+
background-color: var(--minecraft-background-color);
192+
box-shadow: 0 0 0 4px var(--minecraft-border-color);
193+
}
194+
195+
.docs-content .tree ul {
196+
margin: 0 0 0 8px;
197+
padding-left: 0;
198+
}
199+
200+
.docs-content .tree > ul {
201+
margin: 0;
202+
}
203+
204+
.docs-content .tree li {
205+
position: relative;
206+
margin: 0;
207+
padding: 2px 0 2px 24px;
208+
line-height: 24px;
209+
white-space: nowrap;
210+
}
211+
212+
.docs-content .tree > ul > li {
213+
padding-left: 0;
214+
}
215+
216+
/* Vertical connector, reusing the list bullet's ::before. */
217+
.docs-content .tree li::before {
218+
content: '';
219+
position: absolute;
220+
top: 0;
221+
bottom: 0;
222+
left: 0;
223+
width: 0;
224+
height: auto;
225+
margin: 0;
226+
border-left: var(--tree-line);
227+
background: none;
228+
outline: none;
229+
box-shadow: none;
230+
transform: none;
231+
}
232+
233+
.docs-content .tree li:last-child::before {
234+
bottom: auto;
235+
height: var(--tree-branch-y);
236+
}
237+
238+
.docs-content .tree li::after {
239+
content: '';
240+
position: absolute;
241+
top: var(--tree-branch-y);
242+
left: 0;
243+
width: 16px;
244+
border-top: var(--tree-line);
245+
}
246+
247+
.docs-content .tree .tree-item-floating::after {
248+
border-top-style: dashed;
249+
}
250+
251+
.docs-content .tree > ul > li::before,
252+
.docs-content .tree > ul > li::after {
253+
display: none;
254+
}
255+
256+
.docs-content .tree-icon {
257+
width: 16px;
258+
height: 16px;
259+
margin: 0 6px 0 0;
260+
vertical-align: -3px;
261+
background: none;
262+
box-shadow: none;
263+
image-rendering: pixelated;
264+
}
265+
266+
.docs-content .tree-note {
267+
margin-left: 12px;
268+
color: var(--minecraft-text-subtle-color);
269+
font-style: italic;
270+
}
271+
184272
.docs-content a.link {
185273
text-decoration: underline;
186274
}

‎svelte.config.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import rehypeSlug from 'rehype-slug'
66
import remarkDirective from 'remark-directive'
77
import remarkGfm from 'remark-gfm'
88
import remarkAdmonitions from './src/lib/docs/remark-admonitions.ts'
9+
import rehypeTree from './src/lib/docs/rehype-tree.ts'
910
import './src/lib/prismjs/mcfunction.ts'
1011

1112
/**
@@ -18,7 +19,7 @@ const config = {
1819
mdsvex({
1920
extensions: ['.md'],
2021
layout: resolve('./src/lib/docs/docLayout.svelte'),
21-
rehypePlugins: [rehypeSlug, remarkAdmonitions],
22+
rehypePlugins: [rehypeSlug, remarkAdmonitions, rehypeTree],
2223
remarkPlugins: [remarkGfm, remarkDirective],
2324
highlight: {
2425
highlighter: (code, lang) => {

0 commit comments

Comments
 (0)