Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: Switch to mkdocs, mkdocs-material (#1232)
# Background When we originally set up the documentation website, there were a couple core requirements: - Inter-linking of documentation pages should be "checked". That is, instead of writing "../foo.html" and hoping that that generated page exists, we wanted to write "../foo.md", have the system check that it exists, and turn it into "foo.html" or equivalent as needed. - It must be possible to include code snippets in documentation from sections of real code. We didn't find anything at the time that met the latter requirement well enough, so we resorted to a custom solution: we used `mdox` and a custom shell script to extract code snippets from source and include them in the documentation. This has worked well okay, but it is a bit annoying to have around. At the time we evaluated options, mkdocs almost won, except that its snippets plugin only supported line numbers, not named regions of code the way we wanted. The mkdocs snippets plugin has since added support for including named regions of code: https://facelessuser.github.io/pymdown-extensions/extensions/snippets/#snippet-sections This unblocks us from using mkdocs for our documentation. # Description This PR deletes all custom setup, mdox integration, etc. in favor of using mkdocs to generate documentation for Fx. It migrates all documentation as-is over from VuePress to mkdocs. High-level notes: - Navigation tree which was previously specified in docs/.vuepress/config.js is now specified in docs/mkdocs.yml. (It was always weird to have it in a hidden directory anyway.) - mkdocs is configured to verify validity of all interlinking, including links to headers within a page. The build will fail if any links are invalid. - We're using [uv](https://docs.astral.sh/uv/) to manage our Python dependencies. This will also manage the Python version as needed. - There is no longer need for a custom page redirect component. We can use the mkdocs-redirects plugin for this purpose. - The existing Google Analytics tag has been carried over. - The mkdocs-material theme supports a fair bit of customization, including a dark/light mode toggle. This resolves #1071. ## Syntax changes - Admonitions were previously specified as: ``` ::: tip This is a tip. ::: ``` They are now specified as: ``` !!! tip This is a tip. ``` - Tabbed blocks were previously specified as: ``` <code-group> <code-block title="Go"> ... </code-block> <code-block title="Python"> ... </code-block> </code-group> ``` They are now specified as: ``` === "Go" ... === "Python" ... ``` - Code snippets were previously included as: ``` go mdox-exec='region ex/path/to/file.go snippet-name' ``` They are now included as: ``` --8<-- "path/to/file.go:snippet-name" ``` - Code regions were previously marked in code as: ``` // region snippet-name ... // endregion snippet-name ``` They are now marked as: ``` // --8<-- [start:snippet-name] ... // --8<-- [end:snippet-name] ``` The snippets are processed at Markdown file-read time, so they don't result in code duplicated into the Markdown file. - Multi-paragraph list items must be indented 4 spaces instead of just aligning to the list item text (which was 2 spaces for `-` and 3 spaces for `1. `). So, before: ``` - First paragraph Second paragraph - Next item ``` Becomes: ``` - First paragraph Second paragraph - Next item ``` The following is also valid: ``` - First paragraph Second paragraph - Next item ``` # Demo A demo site is available at https://abhinav.github.io/fx/. It will be deleted if/when this PR is merged. # Backwards compatibility mkdocs has been configured to generate file URLs instead of directory URLs. foo.md -> foo.html foo/index.md -> foo/index.html This matches the configuration we had for VuePress. ## Testing To test this, I hacked together a quick script to crawl the old website, and verify that all links still work if the host is changed to the demo site. <details> <sumamry>Script to check URLs</summary> ```go package main import ( "cmp" "container/list" "fmt" "iter" "log" "net/http" "net/url" "path" "golang.org/x/net/html" "golang.org/x/net/html/atom" ) const ( _oldHost = "uber-go.github.io" _newHost = "abhinav.github.io" ) func main() { log.SetFlags(0) for link, err := range oldLinks() { if err != nil { panic(err) } if err := checkNewLink(link); err != nil { log.Printf(" not migrated (%v): %s", link, err) } } } func checkNewLink(oldLink string) error { u, err := url.Parse(oldLink) if err != nil { return err } u.Host = _newHost log.Println("Checking:", u.String()) res, err := http.Get(u.String()) if err != nil { return err } if res.StatusCode != http.StatusOK { return fmt.Errorf("status: %s", res.Status) } return nil } func oldLinks() iter.Seq2[string, error] { seen := make(map[string]struct{}) pending := list.New() // list[string] pending.PushBack("https://" + _oldHost + "/fx") return func(yield func(string, error) bool) { for pending.Len() > 0 { u := pending.Remove(pending.Front()).(string) if _, ok := seen[u]; ok { continue } seen[u] = struct{}{} if !yield(u, nil) { return } log.Println("Fetching:", u) res, err := http.Get(u) if err != nil { yield("", err) return } if res.StatusCode != http.StatusOK { log.Println(" skipping:", res.Status) continue } url, err := url.Parse(u) if err != nil { yield("", err) return } links, err := extractLocalLinks(url.Path, res) _ = res.Body.Close() if err != nil { yield("", err) return } for _, link := range links { pending.PushBack(link) } } } } func extractLocalLinks(fromPath string, res *http.Response) ([]string, error) { doc, err := html.Parse(res.Body) if err != nil { return nil, err } var links []string var visit func(*html.Node) visit = func(n *html.Node) { if n.Type == html.ElementNode && n.DataAtom == atom.A { for _, a := range n.Attr { if a.Key != "href" { continue } if a.Val == "" { continue } u, err := url.Parse(a.Val) if err != nil { continue } u.Host = cmp.Or(u.Host, _oldHost) u.Scheme = cmp.Or(u.Scheme, "https") u.Fragment = "" if u.Host != _oldHost { continue // external link } if !path.IsAbs(u.Path) { u.Path = path.Join(path.Dir(fromPath), u.Path) } links = append(links, u.String()) break } } for c := n.FirstChild; c != nil; c = c.NextSibling { visit(c) } } visit(doc) return links, nil } ``` </details>
- Loading branch information