Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Switch to mkdocs, mkdocs-material #1232

Merged
merged 3 commits into from
Aug 27, 2024
Merged

Conversation

abhinav
Copy link
Collaborator

@abhinav abhinav commented Aug 25, 2024

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
    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 Enable dark/night mode on documentation side #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.

Script to check URLs
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
}

# 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 evaluted these options,
mkdocs stood out as the closest winner,
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 uber-go#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"

        ```go
        ...
        ```

    === "Python"

        ```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
    ```
@abhinav abhinav requested a review from JacobOaks August 25, 2024 23:09
Copy link

codecov bot commented Aug 25, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.42%. Comparing base (f195420) to head (f24c4fe).
Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1232   +/-   ##
=======================================
  Coverage   98.42%   98.42%           
=======================================
  Files          35       35           
  Lines        2918     2918           
=======================================
  Hits         2872     2872           
  Misses         38       38           
  Partials        8        8           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@JacobOaks JacobOaks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome! Thanks @abhinav! Left one or two little questions but LGTM.

docs/ex/annotate/sample.go Show resolved Hide resolved
docs/ex/modules/module.go Show resolved Hide resolved
docs/ex/parameter-objects/define.go Show resolved Hide resolved
@abhinav
Copy link
Collaborator Author

abhinav commented Aug 27, 2024

Fixed all instances of duplicate markers. Used the following script to find them:

rg -g '*.go' --files ex | xargs -n1 perl -n -e 'if (/\[start:(.+?)\]/) {
  if ($seen{$1}) {
    print "$ARGV:$.:Duplicate start tag: $1\n";
  } else {
    $seen{$1}++;
  }
}'

@abhinav
Copy link
Collaborator Author

abhinav commented Aug 27, 2024

Will merge in 10-15 minutes if no objections, @JacobOaks

@abhinav abhinav merged commit 67ed361 into uber-go:master Aug 27, 2024
12 checks passed
@abhinav abhinav deleted the abg/mkdocs branch August 27, 2024 00:37
@abhinav
Copy link
Collaborator Author

abhinav commented Aug 27, 2024

It's up https://uber-go.github.io/fx/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Enable dark/night mode on documentation side
2 participants