fix: propagate Maintainer field through dep graph and upgrade service (#2865)
* fix: propagate Maintainer field through dep graph and upgrade service Add Maintainer to InstallInfo and propagate it through the dependency graph (dep_resolve, dep_visual) and UpgradeService. Update docs generator with TOC support and Lua page builder. * lint
This commit is contained in:
9 files changed
+181
-24
No files matched your search
@@ -0,0 +1,71 @@
|
||||
-- Warn when an AUR package's maintainer changes between upgrades.
|
||||
--
|
||||
-- The known maintainer for each package is stored in a plain text cache file
|
||||
-- inside the yay cache directory (build_dir). On the first upgrade for a
|
||||
-- package the current maintainer is recorded without any warning. On
|
||||
-- subsequent upgrades:
|
||||
-- * same maintainer → debug "match correct"
|
||||
-- * different maintainer → error "new maintainer, double check build files"
|
||||
--
|
||||
-- The cache is updated whenever a new or changed maintainer is seen.
|
||||
--
|
||||
-- Cache file location: <build_dir>/maintainer_cache
|
||||
-- Format: one "pkgname=maintainer" entry per line.
|
||||
|
||||
local cache_file = yay.opt.build_dir .. "/maintainer_cache"
|
||||
|
||||
local function load_cache()
|
||||
local cache = {}
|
||||
local f = io.open(cache_file, "r")
|
||||
if not f then return cache end
|
||||
for line in f:lines() do
|
||||
local name, maintainer = line:match("^([^=]+)=(.*)$")
|
||||
if name then
|
||||
cache[name] = maintainer
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
return cache
|
||||
end
|
||||
|
||||
local function save_cache(cache)
|
||||
local f = assert(io.open(cache_file, "w"))
|
||||
for name, maintainer in pairs(cache) do
|
||||
f:write(name .. "=" .. maintainer .. "\n")
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
|
||||
yay.create_autocmd("UpgradeSelect", {
|
||||
desc = "warn on AUR maintainer changes",
|
||||
callback = function(event)
|
||||
yay.log.info("checking for AUR maintainer changes")
|
||||
local cache = load_cache()
|
||||
local dirty = false
|
||||
|
||||
for _, pkg in ipairs(event.data.upgrades) do
|
||||
if pkg.repository == "aur" and pkg.maintainer ~= "" then
|
||||
local cached = cache[pkg.name]
|
||||
if cached == nil then
|
||||
-- First time seeing this package: seed the cache silently.
|
||||
cache[pkg.name] = pkg.maintainer
|
||||
dirty = true
|
||||
elseif cached == pkg.maintainer then
|
||||
yay.log.debug("match correct: " .. pkg.name .. " " .. pkg.maintainer)
|
||||
else
|
||||
yay.log.error("new maintainer, double check build files: ", pkg.name,
|
||||
"(was: " .. cached .. ", now: " .. pkg.maintainer .. ")")
|
||||
cache[pkg.name] = pkg.maintainer
|
||||
dirty = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if dirty then
|
||||
yay.log.info("saving maintainer cache:", cache_file)
|
||||
save_cache(cache)
|
||||
end
|
||||
|
||||
return { exclude = {}, skip_menu = false }
|
||||
end,
|
||||
})
|
||||
@@ -52,10 +52,6 @@ yay.opt.debug = false -- Enable debug logging and local init.lua lookup convenie
|
||||
yay.opt.rpc = true -- Use AUR RPC for dependency/query operations.
|
||||
yay.opt.double_confirm = true -- Ask for confirmation before and after builds during upgrades.
|
||||
|
||||
-- Logging
|
||||
-- yay.log.info("loaded yay init.lua")
|
||||
-- yay.log.debug("build dir:", yay.opt.build_dir)
|
||||
|
||||
-- Hooks
|
||||
-- Run Lua before yay prints the upgrade exclusion menu. Return package names
|
||||
-- from event.data.upgrades to pre-exclude them. Set skip_menu = false, or omit
|
||||
|
||||
+10
@@ -11,6 +11,8 @@ yay can optionally load a Lua configuration file, `init.lua`. `init.lua` overlay
|
||||
|
||||
## Setting options with `yay.opt`
|
||||
|
||||
<p class="api-since">Available from yay v13.0.0</p>
|
||||
|
||||
Assign to keys on the `yay.opt` table using the exact option names shown
|
||||
below.
|
||||
|
||||
@@ -50,6 +52,8 @@ lives at [`doc/init.lua`](init.lua).
|
||||
|
||||
## Logging with `yay.log`
|
||||
|
||||
<p class="api-since">Available from yay v13.0.0</p>
|
||||
|
||||
Lua config and hooks can write through yay's normal logger:
|
||||
|
||||
```lua
|
||||
@@ -65,6 +69,8 @@ hook stops.
|
||||
|
||||
## Upgrade selection hooks
|
||||
|
||||
<p class="api-since">Available from yay v13.0.0</p>
|
||||
|
||||
`UpgradeSelect` runs during `yay -Syu` after yay has built and sorted the
|
||||
upgrade graph, and before the native "Packages to exclude" menu is printed.
|
||||
The hook can return package names to exclude. By default, yay still shows the
|
||||
@@ -143,6 +149,8 @@ native menu. `pulled_dependencies` entries are shown separately by yay and use
|
||||
|
||||
## AUR pre-install hooks
|
||||
|
||||
<p class="api-since">Available from yay v13.0.0</p>
|
||||
|
||||
`init.lua` can register hooks with a small autocmd API:
|
||||
|
||||
```lua
|
||||
@@ -249,6 +257,8 @@ yay.create_autocmd("AURPreInstall", {
|
||||
|
||||
## AUR post-download hooks
|
||||
|
||||
<p class="api-since">Available from yay v13.0.0</p>
|
||||
|
||||
`AURPostDownload` runs once per AUR package base, in sorted package-base order,
|
||||
after yay runs `makepkg --verifysource` for package sources and before
|
||||
compatibility checks, PGP key import prompts, builds, or package installs.
|
||||
|
||||
@@ -31,7 +31,8 @@ type InstallInfo struct {
|
||||
IsGroup bool
|
||||
Upgrade bool
|
||||
Devel bool
|
||||
LastModified int64 // Unix timestamp, non-zero only for AUR packages
|
||||
LastModified int64 // Unix timestamp, non-zero only for AUR packages
|
||||
Maintainer string // AUR maintainer username, empty for orphaned or non-AUR packages
|
||||
}
|
||||
|
||||
func (i *InstallInfo) String() string {
|
||||
@@ -470,6 +471,7 @@ func (g *Grapher) GraphFromAUR(ctx context.Context,
|
||||
Source: AUR,
|
||||
Version: aurPkg.Version,
|
||||
LastModified: int64(aurPkg.LastModified),
|
||||
Maintainer: aurPkg.Maintainer,
|
||||
})
|
||||
aurPkgsAdded = append(aurPkgsAdded, aurPkg)
|
||||
}
|
||||
@@ -727,6 +729,7 @@ func (g *Grapher) addNodes(
|
||||
AURBase: &aurPkg.PackageBase,
|
||||
Version: aurPkg.Version,
|
||||
LastModified: int64(aurPkg.LastModified),
|
||||
Maintainer: aurPkg.Maintainer,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -283,11 +283,11 @@ func TestGrapher_SplitPackages_Clion(t *testing.T) {
|
||||
}}
|
||||
|
||||
installInfos := map[string]*InstallInfo{
|
||||
"clion exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
|
||||
"clion-jre exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
|
||||
"clion-cmake exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
|
||||
"clion-gdb exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
|
||||
"clion-lldb exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120},
|
||||
"clion exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
|
||||
"clion-jre exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
|
||||
"clion-cmake exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
|
||||
"clion-gdb exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
|
||||
"clion-lldb exp": {Source: AUR, Reason: Explicit, Version: "2025.3.1.1-1", AURBase: ptrString("clion"), LastModified: 1768325120, Maintainer: "Zrax"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@@ -565,11 +565,11 @@ func TestGrapher_SplitPackages_NX(t *testing.T) {
|
||||
}}
|
||||
|
||||
installInfos := map[string]*InstallInfo{
|
||||
"nxproxy exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
|
||||
"nxagent exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
|
||||
"nx-x11 dep": {Source: AUR, Reason: Dep, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
|
||||
"libxcomp dep": {Source: AUR, Reason: Dep, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
|
||||
"libxcomp exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449},
|
||||
"nxproxy exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
|
||||
"nxagent exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
|
||||
"nx-x11 dep": {Source: AUR, Reason: Dep, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
|
||||
"libxcomp dep": {Source: AUR, Reason: Dep, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
|
||||
"libxcomp exp": {Source: AUR, Reason: Explicit, Version: "3.5.99.27-3", AURBase: ptrString("nx"), LastModified: 1693834449, Maintainer: "harrietobrien"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -167,6 +167,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) {
|
||||
Version: "10.8.8-1",
|
||||
AURBase: ptrString("jellyfin"),
|
||||
LastModified: 1669830147,
|
||||
Maintainer: "z3ntu",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -199,6 +200,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) {
|
||||
Version: "10.8.8-1",
|
||||
AURBase: ptrString("jellyfin"),
|
||||
LastModified: 1669830147,
|
||||
Maintainer: "z3ntu",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -208,6 +210,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) {
|
||||
Version: "10.8.8-1",
|
||||
AURBase: ptrString("jellyfin"),
|
||||
LastModified: 1669830147,
|
||||
Maintainer: "z3ntu",
|
||||
},
|
||||
"jellyfin-server": {
|
||||
Source: AUR,
|
||||
@@ -215,6 +218,7 @@ func TestGrapher_GraphFromTargets_jellyfin(t *testing.T) {
|
||||
Version: "10.8.8-1",
|
||||
AURBase: ptrString("jellyfin"),
|
||||
LastModified: 1669830147,
|
||||
Maintainer: "z3ntu",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -337,6 +341,7 @@ func TestGrapher_GraphProvides_androidsdk(t *testing.T) {
|
||||
Version: "26.1.1-2",
|
||||
AURBase: ptrString("android-sdk"),
|
||||
LastModified: 1647982720,
|
||||
Maintainer: "dreamingincode",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1022,6 +1027,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
|
||||
Version: "1.24.0.r37-1",
|
||||
AURBase: ptrString("gstreamer-git"),
|
||||
LastModified: 1700000000,
|
||||
Maintainer: "testmaint",
|
||||
},
|
||||
"gstreamer-git dep": {
|
||||
Source: AUR,
|
||||
@@ -1029,6 +1035,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
|
||||
Version: "1.24.0.r37-1",
|
||||
AURBase: ptrString("gstreamer-git"),
|
||||
LastModified: 1700000000,
|
||||
Maintainer: "testmaint",
|
||||
},
|
||||
"gst-plugins-base-libs-git exp": {
|
||||
Source: AUR,
|
||||
@@ -1036,6 +1043,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
|
||||
Version: "1.24.0.r37-1",
|
||||
AURBase: ptrString("gstreamer-git"),
|
||||
LastModified: 1700000000,
|
||||
Maintainer: "testmaint",
|
||||
},
|
||||
"gst-plugins-base-libs-git dep": {
|
||||
Source: AUR,
|
||||
@@ -1043,6 +1051,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
|
||||
Version: "1.24.0.r37-1",
|
||||
AURBase: ptrString("gstreamer-git"),
|
||||
LastModified: 1700000000,
|
||||
Maintainer: "testmaint",
|
||||
},
|
||||
"gst-plugins-good-git exp": {
|
||||
Source: AUR,
|
||||
@@ -1050,6 +1059,7 @@ func TestGrapher_GraphFromAUR_SplitPkgInternalDeps(t *testing.T) {
|
||||
Version: "1.24.0.r37-1",
|
||||
AURBase: ptrString("gstreamer-git"),
|
||||
LastModified: 1700000000,
|
||||
Maintainer: "testmaint",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ func (u *UpgradeService) upGraph(ctx context.Context, graph *topo.Graph[string,
|
||||
LocalVersion: up.LocalVersion,
|
||||
Version: up.RemoteVersion,
|
||||
LastModified: up.LastModified,
|
||||
Maintainer: aurPkg.Maintainer,
|
||||
})
|
||||
names.Add(up.Name)
|
||||
aurPkgsAdded = append(aurPkgsAdded, aurPkg)
|
||||
@@ -157,6 +158,7 @@ func (u *UpgradeService) upGraph(ctx context.Context, graph *topo.Graph[string,
|
||||
Version: up.RemoteVersion,
|
||||
LocalVersion: up.LocalVersion,
|
||||
LastModified: up.LastModified,
|
||||
Maintainer: aurPkg.Maintainer,
|
||||
})
|
||||
aurPkgsAdded = append(aurPkgsAdded, aurPkg)
|
||||
}
|
||||
@@ -224,6 +226,7 @@ func (u *UpgradeService) graphToUpSlice(graph *topo.Graph[string, *dep.InstallIn
|
||||
Reason: alpmReason,
|
||||
Extra: extra,
|
||||
LastModified: info.LastModified,
|
||||
Maintainer: info.Maintainer,
|
||||
})
|
||||
case dep.Sync:
|
||||
repoUp.Up = append(repoUp.Up, Upgrade{
|
||||
|
||||
@@ -125,6 +125,7 @@ func TestUpgradeService_GraphUpgrades(t *testing.T) {
|
||||
Version: "latest-commit",
|
||||
Upgrade: true,
|
||||
Devel: true,
|
||||
Maintainer: "morganamilo",
|
||||
}
|
||||
|
||||
newDepInfo := &dep.InstallInfo{
|
||||
@@ -145,6 +146,7 @@ func TestUpgradeService_GraphUpgrades(t *testing.T) {
|
||||
Version: "2.2.1.r69.g8a10460-1",
|
||||
Upgrade: true,
|
||||
Devel: false,
|
||||
Maintainer: "morganamilo",
|
||||
}
|
||||
|
||||
yayDepInfo := &dep.InstallInfo{
|
||||
@@ -155,6 +157,7 @@ func TestUpgradeService_GraphUpgrades(t *testing.T) {
|
||||
Version: "10.2.4",
|
||||
Upgrade: true,
|
||||
Devel: false,
|
||||
Maintainer: "jguer",
|
||||
}
|
||||
|
||||
coreDB := mock.NewDB("core")
|
||||
@@ -227,10 +230,11 @@ func TestUpgradeService_GraphUpgrades(t *testing.T) {
|
||||
mockAUR := &mockaur.MockAUR{
|
||||
GetFn: func(ctx context.Context, query *aur.Query) ([]aur.Pkg, error) {
|
||||
return []aur.Pkg{
|
||||
{Name: "yay", Version: "10.2.4", PackageBase: "yay"},
|
||||
{Name: "yay", Version: "10.2.4", PackageBase: "yay", Maintainer: "jguer"},
|
||||
{
|
||||
Name: "example-git", Version: "2.2.1.r69.g8a10460-1",
|
||||
PackageBase: "example", Depends: []string{"new-dep"},
|
||||
Maintainer: "morganamilo",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
|
||||
+68
-8
@@ -21,6 +21,7 @@ import (
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
bf "github.com/russross/blackfriday/v2"
|
||||
@@ -37,20 +38,21 @@ func main() {
|
||||
|
||||
// yay.8 (troff man page) → Markdown → HTML
|
||||
man8 := mustRead(filepath.Join(*docs, "yay.8"))
|
||||
writePage(*out, "man.html", "yay(8) Manual", mdToHTML(troff2md(man8)))
|
||||
writePage(*out, "man.html", pageData{Title: "yay(8) Manual", Body: template.HTML(mdToHTML(troff2md(man8)))})
|
||||
|
||||
// lua.md → HTML
|
||||
// lua.md → HTML with TOC and version badges
|
||||
luaMD := mustRead(filepath.Join(*docs, "lua.md"))
|
||||
writePage(*out, "lua.html", "Lua API", mdToHTML(luaMD))
|
||||
luaBody, luaTOC := buildLuaPage(mdToHTML(luaMD))
|
||||
writePage(*out, "lua.html", pageData{Title: "Lua API", Body: luaBody, TOC: luaTOC})
|
||||
|
||||
// init.lua source as a code block
|
||||
initLua := mustRead(filepath.Join(*docs, "init.lua"))
|
||||
initMD := "# init.lua template\n\n```lua\n" + string(initLua) + "```\n"
|
||||
writePage(*out, "init-lua.html", "init.lua template", mdToHTML([]byte(initMD)))
|
||||
writePage(*out, "init-lua.html", pageData{Title: "init.lua template", Body: template.HTML(mdToHTML([]byte(initMD)))})
|
||||
|
||||
// index / landing page
|
||||
indexMD := mustRead(filepath.Join(*docs, "index.md"))
|
||||
writePage(*out, "index.html", "yay", mdToHTML(indexMD))
|
||||
writePage(*out, "index.html", pageData{Title: "yay", Body: template.HTML(mdToHTML(indexMD))})
|
||||
|
||||
fmt.Printf("site written to %s\n", *out)
|
||||
}
|
||||
@@ -81,6 +83,15 @@ pre code{background:none;padding:0;font-size:.85em}
|
||||
a{color:#0067c0}
|
||||
strong{font-weight:600}
|
||||
footer{margin-top:3rem;padding-top:1rem;border-top:1px solid #e0e0e0;color:#666;font-size:.875rem}
|
||||
.page-toc{position:fixed;top:5rem;right:1rem;width:220px;max-height:calc(100vh - 6rem);overflow-y:auto;background:#f9f9f9;border:1px solid #e0e0e0;border-radius:4px;padding:.75rem 1rem}
|
||||
.page-toc strong{display:block;margin-bottom:.5rem;color:#555;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em}
|
||||
.page-toc ul{list-style:none;margin:0;padding:0}
|
||||
.page-toc li{margin:.15rem 0}
|
||||
.page-toc a{color:#0067c0;text-decoration:none;font-size:.8rem;line-height:1.4;display:block}
|
||||
.page-toc a:hover{text-decoration:underline}
|
||||
.toc-h3{padding-left:.75rem}
|
||||
@media(max-width:1200px){.page-toc{display:none}}
|
||||
.api-since{font-size:.8rem;color:#666;font-style:italic;margin:-.25rem 0 .75rem}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -90,6 +101,7 @@ footer{margin-top:3rem;padding-top:1rem;border-top:1px solid #e0e0e0;color:#666;
|
||||
<a href="lua.html">Lua API</a>
|
||||
<a href="init-lua.html">init.lua</a>
|
||||
</nav>
|
||||
{{if .TOC}}<aside class="page-toc">{{.TOC}}</aside>{{end}}
|
||||
<main>
|
||||
{{.Body}}
|
||||
</main>
|
||||
@@ -100,21 +112,70 @@ footer{margin-top:3rem;padding-top:1rem;border-top:1px solid #e0e0e0;color:#666;
|
||||
type pageData struct {
|
||||
Title string
|
||||
Body template.HTML
|
||||
TOC template.HTML // optional; non-empty → TOC sidebar is rendered
|
||||
}
|
||||
|
||||
func writePage(dir, name, title string, body []byte) {
|
||||
func writePage(dir, name string, data pageData) {
|
||||
path := filepath.Join(dir, name)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := pageTmpl.Execute(f, pageData{Title: title, Body: template.HTML(body)}); err != nil {
|
||||
if err := pageTmpl.Execute(f, data); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
fmt.Printf(" %s\n", path)
|
||||
}
|
||||
|
||||
// ── Lua page: TOC + version badges ────────────────────────────────────────────
|
||||
|
||||
// reHeading matches an h2 or h3 element with an id attribute as produced by
|
||||
// blackfriday's AutoHeadingIDs extension. The (?s) flag makes . match \n so
|
||||
// multi-line heading content (rare but possible) is covered.
|
||||
var reHeading = regexp.MustCompile(`(?s)<(h[23]) id="([^"]+)"[^>]*>(.*?)</h[23]>`)
|
||||
|
||||
// reTag strips HTML tags to obtain plain text for TOC labels.
|
||||
var reTag = regexp.MustCompile(`<[^>]+>`)
|
||||
|
||||
// buildLuaPage extracts headings for the table of contents and returns the
|
||||
// body HTML unchanged alongside the TOC HTML.
|
||||
func buildLuaPage(body []byte) (newBody template.HTML, toc template.HTML) {
|
||||
s := string(body)
|
||||
|
||||
// Collect headings in document order to build the TOC.
|
||||
type entry struct {
|
||||
level int
|
||||
id string
|
||||
label string // plain text, HTML-escaped when written into TOC
|
||||
}
|
||||
var entries []entry
|
||||
for _, m := range reHeading.FindAllStringSubmatch(s, -1) {
|
||||
lvl := 2
|
||||
if m[1] == "h3" {
|
||||
lvl = 3
|
||||
}
|
||||
// Strip inner tags (e.g. <code>) to get a plain-text label.
|
||||
label := strings.TrimSpace(reTag.ReplaceAllString(m[3], ""))
|
||||
entries = append(entries, entry{lvl, m[2], label})
|
||||
}
|
||||
|
||||
// Build the TOC HTML: h3 entries are indented via the .toc-h3 class.
|
||||
var b strings.Builder
|
||||
b.WriteString("<strong>On this page</strong>\n<ul>\n")
|
||||
for _, e := range entries {
|
||||
class := ""
|
||||
if e.level == 3 {
|
||||
class = ` class="toc-h3"`
|
||||
}
|
||||
fmt.Fprintf(&b, "<li%s><a href=\"#%s\">%s</a></li>\n",
|
||||
class, e.id, template.HTMLEscapeString(e.label))
|
||||
}
|
||||
b.WriteString("</ul>")
|
||||
|
||||
return template.HTML(s), template.HTML(b.String())
|
||||
}
|
||||
|
||||
// ── Markdown → HTML ───────────────────────────────────────────────────────────
|
||||
|
||||
func mdToHTML(src []byte) []byte {
|
||||
@@ -366,4 +427,3 @@ func fatal(err error) {
|
||||
fmt.Fprintln(os.Stderr, "gendocs:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user