alpha
Login
or
Join now
utensil.tngl.sh
/
forest
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Utensil's Zettelkasten-style forest of evergreen notes on math and tech.
utensil.tngl.sh/forest/
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
Port scripts not committed by agent
author
utensil
date
2 months ago
(May 9, 2026, 4:10 PM +0800)
commit
46523f90
46523f902379c09dbc3abef03d44de9f20946541
parent
9c20a36f
9c20a36fc169c9b1d45aeec670e8ed0265a2d626
change-id
nylnlzuo
nylnlzuolqlytpxvpwnrurrunllrpvuz
+259
2 changed files
Expand all
Collapse all
Unified
Split
port_interests.py
port_posts.py
+110
port_interests.py
View file
Reviewed
···
1
1
+
#!/usr/bin/env -S uv run
2
2
+
# /// script
3
3
+
# requires-python = ">=3.11"
4
4
+
# dependencies = []
5
5
+
# ///
6
6
+
7
7
+
"""
8
8
+
Port forest interest nodes (uts-0030 to uts-0165) to garden Markdown files.
9
9
+
Each tree file uses the md macro with a GitHub issue header; content is
10
10
+
extracted and written with YAML frontmatter (title, date, tags, aliases, URL).
11
11
+
"""
12
12
+
13
13
+
import re
14
14
+
from pathlib import Path
15
15
+
16
16
+
17
17
+
def slugify(title: str, max_words: int = 6) -> str:
18
18
+
title = title.lower()
19
19
+
title = re.sub(r"[^\w\s-]", "", title)
20
20
+
title = re.sub(r"\s+", "-", title.strip())
21
21
+
title = re.sub(r"-+", "-", title).strip("-")
22
22
+
parts = title.split("-")
23
23
+
if len(parts) > max_words:
24
24
+
title = "-".join(parts[:max_words])
25
25
+
return title or "untitled"
26
26
+
27
27
+
28
28
+
def extract_md_content(tree_text: str) -> str:
29
29
+
md_start = tree_text.find("\\md{\n")
30
30
+
if md_start == -1:
31
31
+
return ""
32
32
+
33
33
+
content = tree_text[md_start + len("\\md{\n"):]
34
34
+
lines = content.split("\n")
35
35
+
36
36
+
# strip trailing blank lines and closing "}"
37
37
+
while lines and lines[-1].strip() == "":
38
38
+
lines.pop()
39
39
+
if lines and lines[-1].strip() == "}":
40
40
+
lines.pop()
41
41
+
42
42
+
content = "\n".join(lines)
43
43
+
44
44
+
# strip the "#### [user] opened issue at [...]:" header + following blank line
45
45
+
content = re.sub(
46
46
+
r"^####[^\n]*opened issue at[^\n]*\n\n?", "", content, flags=re.MULTILINE
47
47
+
)
48
48
+
return content.strip()
49
49
+
50
50
+
51
51
+
def convert(tree_id: str, tree_text: str) -> tuple[str, str]:
52
52
+
title_m = re.search(r"\\title\{([^}]+)\}", tree_text)
53
53
+
title = title_m.group(1) if title_m else tree_id
54
54
+
55
55
+
date_m = re.search(r"\\date\{([^}]+)\}", tree_text)
56
56
+
date = date_m.group(1) if date_m else ""
57
57
+
58
58
+
content = extract_md_content(tree_text)
59
59
+
slug = slugify(title)
60
60
+
source = f"https://utensil.github.io/forest/{tree_id}/"
61
61
+
62
62
+
# escape quotes in title for YAML
63
63
+
title_yaml = title.replace('"', '\\"')
64
64
+
65
65
+
frontmatter = (
66
66
+
f'---\n'
67
67
+
f'title: "{title_yaml}"\n'
68
68
+
f'date: {date}\n'
69
69
+
f'tags: [interest]\n'
70
70
+
f'aliases: [{tree_id}]\n'
71
71
+
f'source: "{source}"\n'
72
72
+
f'---'
73
73
+
)
74
74
+
return slug, f"{frontmatter}\n\n{content}\n"
75
75
+
76
76
+
77
77
+
def main() -> None:
78
78
+
trees_dir = Path("/Users/utensil/projects/forest/trees")
79
79
+
output_dir = Path("/Users/utensil/projects/garden-interest-port/content/interests")
80
80
+
output_dir.mkdir(parents=True, exist_ok=True)
81
81
+
82
82
+
used_slugs: dict[str, str] = {}
83
83
+
converted = 0
84
84
+
85
85
+
for num in range(30, 166):
86
86
+
tree_id = f"uts-{num:04d}"
87
87
+
tree_file = trees_dir / f"{tree_id}.tree"
88
88
+
if not tree_file.exists():
89
89
+
print(f" missing: {tree_id}")
90
90
+
continue
91
91
+
92
92
+
text = tree_file.read_text()
93
93
+
slug, md = convert(tree_id, text)
94
94
+
95
95
+
# disambiguate slug collisions
96
96
+
base_slug = slug
97
97
+
if slug in used_slugs:
98
98
+
slug = f"{base_slug}-{num}"
99
99
+
used_slugs[slug] = tree_id
100
100
+
101
101
+
out = output_dir / f"{slug}.md"
102
102
+
out.write_text(md)
103
103
+
print(f" {tree_id} → interests/{slug}.md")
104
104
+
converted += 1
105
105
+
106
106
+
print(f"\nConverted {converted} nodes → {output_dir}")
107
107
+
108
108
+
109
109
+
if __name__ == "__main__":
110
110
+
main()
+149
port_posts.py
View file
Reviewed
···
1
1
+
#!/usr/bin/env -S uv run
2
2
+
# /// script
3
3
+
# requires-python = ">=3.11"
4
4
+
# dependencies = []
5
5
+
# ///
6
6
+
7
7
+
"""
8
8
+
Port forest post nodes (mdnote) to garden Markdown files in content/posts/.
9
9
+
Handles both plain mdnote{title}{content} and mdnote{title}{verb>>>|content>>>} forms.
10
10
+
"""
11
11
+
12
12
+
import re
13
13
+
from pathlib import Path
14
14
+
15
15
+
16
16
+
NODES = [
17
17
+
"uts-0167",
18
18
+
"uts-016A",
19
19
+
"uts-016B",
20
20
+
"uts-016C",
21
21
+
"uts-016E",
22
22
+
"uts-016J",
23
23
+
"uts-016K",
24
24
+
"uts-016L",
25
25
+
# uts-016M skipped: jj.md already exists in garden with complete content
26
26
+
"uts-016N",
27
27
+
"uts-016O",
28
28
+
"uts-016P",
29
29
+
]
30
30
+
31
31
+
32
32
+
def slugify(title: str, max_words: int = 6) -> str:
33
33
+
title = title.lower()
34
34
+
title = re.sub(r"[^\w\s-]", "", title)
35
35
+
title = re.sub(r"\s+", "-", title.strip())
36
36
+
title = re.sub(r"-+", "-", title).strip("-")
37
37
+
parts = title.split("-")
38
38
+
if len(parts) > max_words:
39
39
+
title = "-".join(parts[:max_words])
40
40
+
return title or "untitled"
41
41
+
42
42
+
43
43
+
def extract_mdnote(text: str) -> tuple[str, str]:
44
44
+
m = re.search(r"\\mdnote\{([^}]+)\}\{", text)
45
45
+
if not m:
46
46
+
return "", ""
47
47
+
48
48
+
title = m.group(1)
49
49
+
rest = text[m.end():]
50
50
+
51
51
+
# verb-wrapped: \verb>>>|...\n>>>}
52
52
+
verb_m = re.match(r"\\verb>>>\|(.*?)>>>\}", rest, re.DOTALL)
53
53
+
if verb_m:
54
54
+
return title, verb_m.group(1).strip()
55
55
+
56
56
+
# plain content: brace-balanced extraction
57
57
+
depth = 1
58
58
+
i = 0
59
59
+
while i < len(rest) and depth > 0:
60
60
+
if rest[i] == "{":
61
61
+
depth += 1
62
62
+
elif rest[i] == "}":
63
63
+
depth -= 1
64
64
+
i += 1
65
65
+
66
66
+
return title, rest[: i - 1].strip()
67
67
+
68
68
+
69
69
+
def parse_tags(text: str) -> list[str]:
70
70
+
return re.findall(r"\\tag\{([^}]+)\}", text)
71
71
+
72
72
+
73
73
+
def parse_meta(text: str, key: str) -> str:
74
74
+
m = re.search(rf"\\meta\{{{re.escape(key)}\}}\{{([^}}]+)\}}", text)
75
75
+
return m.group(1) if m else ""
76
76
+
77
77
+
78
78
+
def parse_field(text: str, field: str) -> str:
79
79
+
m = re.search(rf"\\{field}\{{([^}}]+)\}}", text)
80
80
+
return m.group(1) if m else ""
81
81
+
82
82
+
83
83
+
def build_frontmatter(title: str, date: str, tags: list[str], aliases: str, external: str) -> str:
84
84
+
lines = ["---", f"title: {title}"]
85
85
+
if date:
86
86
+
lines.append(f"date: {date}")
87
87
+
if len(tags) == 1:
88
88
+
lines.append(f"tag: {tags[0]}")
89
89
+
elif tags:
90
90
+
lines.append("tags:")
91
91
+
for t in tags:
92
92
+
lines.append(f" - {t}")
93
93
+
lines.append(f"aliases: [{aliases}]")
94
94
+
lines.append(f'source: "https://utensil.github.io/forest/{aliases}/"')
95
95
+
if external:
96
96
+
lines.append(f'external: "{external}"')
97
97
+
lines.append("---")
98
98
+
return "\n".join(lines)
99
99
+
100
100
+
101
101
+
def convert(tree_id: str, tree_text: str) -> tuple[str, str]:
102
102
+
title, content = extract_mdnote(tree_text)
103
103
+
if not title:
104
104
+
return "", ""
105
105
+
106
106
+
date = parse_field(tree_text, "date")
107
107
+
tags = parse_tags(tree_text)
108
108
+
external = parse_meta(tree_text, "external")
109
109
+
110
110
+
slug = slugify(title)
111
111
+
fm = build_frontmatter(title, date, tags, tree_id, external)
112
112
+
return slug, f"{fm}\n\n{content}\n"
113
113
+
114
114
+
115
115
+
def main() -> None:
116
116
+
trees_dir = Path("/Users/utensil/projects/forest/trees")
117
117
+
output_dir = Path("/Users/utensil/projects/garden-posts-port/content/posts")
118
118
+
output_dir.mkdir(parents=True, exist_ok=True)
119
119
+
120
120
+
used_slugs: dict[str, str] = {}
121
121
+
converted = 0
122
122
+
123
123
+
for tree_id in NODES:
124
124
+
tree_file = trees_dir / f"{tree_id}.tree"
125
125
+
if not tree_file.exists():
126
126
+
print(f" missing: {tree_id}")
127
127
+
continue
128
128
+
129
129
+
text = tree_file.read_text()
130
130
+
slug, md = convert(tree_id, text)
131
131
+
if not slug:
132
132
+
print(f" no mdnote: {tree_id}")
133
133
+
continue
134
134
+
135
135
+
base_slug = slug
136
136
+
if slug in used_slugs:
137
137
+
slug = f"{base_slug}-{tree_id[-3:]}"
138
138
+
used_slugs[slug] = tree_id
139
139
+
140
140
+
out = output_dir / f"{slug}.md"
141
141
+
out.write_text(md)
142
142
+
print(f" {tree_id} → posts/{slug}.md")
143
143
+
converted += 1
144
144
+
145
145
+
print(f"\nConverted {converted} nodes → {output_dir}")
146
146
+
147
147
+
148
148
+
if __name__ == "__main__":
149
149
+
main()