Utensil's Zettelkasten-style forest of evergreen notes on math and tech.
utensil.tngl.sh/forest/
1#!/usr/bin/env -S uv run
2# /// script
3# requires-python = ">=3.11"
4# dependencies = []
5# ///
6
7"""
8Port forest post nodes (mdnote) to garden Markdown files in content/posts/.
9Handles both plain mdnote{title}{content} and mdnote{title}{verb>>>|content>>>} forms.
10"""
11
12import re
13from pathlib import Path
14
15
16NODES = [
17 "uts-0167",
18 "uts-016A",
19 "uts-016B",
20 "uts-016C",
21 "uts-016E",
22 "uts-016J",
23 "uts-016K",
24 "uts-016L",
25 # uts-016M skipped: jj.md already exists in garden with complete content
26 "uts-016N",
27 "uts-016O",
28 "uts-016P",
29]
30
31
32def slugify(title: str, max_words: int = 6) -> str:
33 title = title.lower()
34 title = re.sub(r"[^\w\s-]", "", title)
35 title = re.sub(r"\s+", "-", title.strip())
36 title = re.sub(r"-+", "-", title).strip("-")
37 parts = title.split("-")
38 if len(parts) > max_words:
39 title = "-".join(parts[:max_words])
40 return title or "untitled"
41
42
43def extract_mdnote(text: str) -> tuple[str, str]:
44 m = re.search(r"\\mdnote\{([^}]+)\}\{", text)
45 if not m:
46 return "", ""
47
48 title = m.group(1)
49 rest = text[m.end():]
50
51 # verb-wrapped: \verb>>>|...\n>>>}
52 verb_m = re.match(r"\\verb>>>\|(.*?)>>>\}", rest, re.DOTALL)
53 if verb_m:
54 return title, verb_m.group(1).strip()
55
56 # plain content: brace-balanced extraction
57 depth = 1
58 i = 0
59 while i < len(rest) and depth > 0:
60 if rest[i] == "{":
61 depth += 1
62 elif rest[i] == "}":
63 depth -= 1
64 i += 1
65
66 return title, rest[: i - 1].strip()
67
68
69def parse_tags(text: str) -> list[str]:
70 return re.findall(r"\\tag\{([^}]+)\}", text)
71
72
73def parse_meta(text: str, key: str) -> str:
74 m = re.search(rf"\\meta\{{{re.escape(key)}\}}\{{([^}}]+)\}}", text)
75 return m.group(1) if m else ""
76
77
78def parse_field(text: str, field: str) -> str:
79 m = re.search(rf"\\{field}\{{([^}}]+)\}}", text)
80 return m.group(1) if m else ""
81
82
83def build_frontmatter(title: str, date: str, tags: list[str], aliases: str, external: str) -> str:
84 lines = ["---", f"title: {title}"]
85 if date:
86 lines.append(f"date: {date}")
87 if len(tags) == 1:
88 lines.append(f"tag: {tags[0]}")
89 elif tags:
90 lines.append("tags:")
91 for t in tags:
92 lines.append(f" - {t}")
93 lines.append(f"aliases: [{aliases}]")
94 lines.append(f'source: "https://utensil.github.io/forest/{aliases}/"')
95 if external:
96 lines.append(f'external: "{external}"')
97 lines.append("---")
98 return "\n".join(lines)
99
100
101def convert(tree_id: str, tree_text: str) -> tuple[str, str]:
102 title, content = extract_mdnote(tree_text)
103 if not title:
104 return "", ""
105
106 date = parse_field(tree_text, "date")
107 tags = parse_tags(tree_text)
108 external = parse_meta(tree_text, "external")
109
110 slug = slugify(title)
111 fm = build_frontmatter(title, date, tags, tree_id, external)
112 return slug, f"{fm}\n\n{content}\n"
113
114
115def main() -> None:
116 trees_dir = Path("/Users/utensil/projects/forest/trees")
117 output_dir = Path("/Users/utensil/projects/garden-posts-port/content/posts")
118 output_dir.mkdir(parents=True, exist_ok=True)
119
120 used_slugs: dict[str, str] = {}
121 converted = 0
122
123 for tree_id in NODES:
124 tree_file = trees_dir / f"{tree_id}.tree"
125 if not tree_file.exists():
126 print(f" missing: {tree_id}")
127 continue
128
129 text = tree_file.read_text()
130 slug, md = convert(tree_id, text)
131 if not slug:
132 print(f" no mdnote: {tree_id}")
133 continue
134
135 base_slug = slug
136 if slug in used_slugs:
137 slug = f"{base_slug}-{tree_id[-3:]}"
138 used_slugs[slug] = tree_id
139
140 out = output_dir / f"{slug}.md"
141 out.write_text(md)
142 print(f" {tree_id} → posts/{slug}.md")
143 converted += 1
144
145 print(f"\nConverted {converted} nodes → {output_dir}")
146
147
148if __name__ == "__main__":
149 main()