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, "&nbsp;", " ")
 45  s = Regex.replace_all(s, "\\s+$", "")
 46  -- Decode common HTML entities
 47  s = Regex.replace_all(s, "&amp;", "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")