main
1-- heading-slugs.lua — Replace org-generated IDs with readable slugs
2-- and add hover § anchor links (Crafting Interpreters style)
3--
4-- Before: <h2 id="org7f6f53b">Nix flakes and Go</h2>
5-- After: <h2 id="nix-flakes-and-go"><a class="anchor" href="#nix-flakes-and-go">§</a>Nix flakes and Go</h2>
6
7function lowercase(s)
8 s = Regex.replace_all(s, "A", "a")
9 s = Regex.replace_all(s, "B", "b")
10 s = Regex.replace_all(s, "C", "c")
11 s = Regex.replace_all(s, "D", "d")
12 s = Regex.replace_all(s, "E", "e")
13 s = Regex.replace_all(s, "F", "f")
14 s = Regex.replace_all(s, "G", "g")
15 s = Regex.replace_all(s, "H", "h")
16 s = Regex.replace_all(s, "I", "i")
17 s = Regex.replace_all(s, "J", "j")
18 s = Regex.replace_all(s, "K", "k")
19 s = Regex.replace_all(s, "L", "l")
20 s = Regex.replace_all(s, "M", "m")
21 s = Regex.replace_all(s, "N", "n")
22 s = Regex.replace_all(s, "O", "o")
23 s = Regex.replace_all(s, "P", "p")
24 s = Regex.replace_all(s, "Q", "q")
25 s = Regex.replace_all(s, "R", "r")
26 s = Regex.replace_all(s, "S", "s")
27 s = Regex.replace_all(s, "T", "t")
28 s = Regex.replace_all(s, "U", "u")
29 s = Regex.replace_all(s, "V", "v")
30 s = Regex.replace_all(s, "W", "w")
31 s = Regex.replace_all(s, "X", "x")
32 s = Regex.replace_all(s, "Y", "y")
33 s = Regex.replace_all(s, "Z", "z")
34 return s
35end
36
37function slugify(text)
38 -- Strip org tag block: everything from <span class="tag"> to end
39 -- (org tags are always last in the heading)
40 s = Regex.replace_all(text, "<span class=\"tag\">.*", "")
41 -- Strip remaining HTML tags
42 s = Regex.replace_all(s, "<[^>]+>", "")
43 -- Strip nbsp and trailing whitespace
44 s = Regex.replace_all(s, " ", " ")
45 s = Regex.replace_all(s, "\\s+$", "")
46 -- Decode common HTML entities
47 s = Regex.replace_all(s, "&", "and")
48 s = Regex.replace_all(s, "&#[0-9]+;", "")
49 s = Regex.replace_all(s, "&[a-z]+;", "")
50 -- Lowercase
51 s = lowercase(s)
52 -- Replace non-alphanumeric with hyphens
53 s = Regex.replace_all(s, "[^a-z0-9]+", "-")
54 -- Collapse multiple hyphens
55 s = Regex.replace_all(s, "-+", "-")
56 -- Trim leading/trailing hyphens
57 s = Regex.replace_all(s, "^-+", "")
58 s = Regex.replace_all(s, "-+$", "")
59 return s
60end
61
62-- Track used slugs to avoid duplicates
63used_slugs = {}
64
65function unique_slug(base)
66 if not used_slugs[base] then
67 used_slugs[base] = true
68 return base
69 end
70 n = 2
71 while used_slugs[base .. "-" .. tostring(n)] do
72 n = n + 1
73 end
74 slug = base .. "-" .. tostring(n)
75 used_slugs[slug] = true
76 return slug
77end
78
79function process_headings(selector)
80 headings = HTML.select(page, selector)
81 i = 1
82 while headings[i] do
83 h = headings[i]
84 old_id = HTML.get_attribute(h, "id")
85
86 -- Get slug text: use inner_html so we can strip org tag spans via regex
87 text = HTML.inner_html(h)
88 base_slug = slugify(text)
89
90 if base_slug ~= "" then
91 slug = unique_slug(base_slug)
92
93 -- Set the new ID on the heading
94 HTML.set_attribute(h, "id", slug)
95
96 -- Update parent section container if it references the old ID
97 if old_id then
98 sections = HTML.select(page, "section")
99 j = 1
100 while sections[j] do
101 cid = HTML.get_attribute(sections[j], "id")
102 if cid == "outline-container-" .. old_id then
103 HTML.set_attribute(sections[j], "id", "outline-container-" .. slug)
104 end
105 j = j + 1
106 end
107
108 -- Update outline-text div
109 divs = HTML.select(page, "div")
110 j = 1
111 while divs[j] do
112 did = HTML.get_attribute(divs[j], "id")
113 if did == "text-" .. old_id then
114 HTML.set_attribute(divs[j], "id", "text-" .. slug)
115 end
116 j = j + 1
117 end
118 end
119
120 -- Create § anchor link
121 anchor = HTML.create_element("a", "§")
122 HTML.set_attribute(anchor, "class", "anchor")
123 HTML.set_attribute(anchor, "href", "#" .. slug)
124
125 -- Prepend anchor as first child of heading
126 HTML.prepend_child(h, anchor)
127 end
128
129 i = i + 1
130 end
131end
132
133process_headings("h2")
134process_headings("h3")
135process_headings("h4")