├── favicon.ico ├── static ├── css │ ├── input.css │ ├── index.css │ └── output.css ├── img │ └── logo.svg └── js │ └── index.js ├── .gitignore ├── html ├── components │ ├── overlay.html │ ├── footer.html │ ├── pagenav.html │ ├── header.html │ ├── pageturn.html │ └── sitenav.html └── templates │ └── base.html ├── godocument.config.json ├── go.mod ├── internal ├── util │ └── util.go ├── handler │ └── handler.go ├── middleware │ └── middleware.go ├── stypes │ └── stypes.go ├── contentrouter │ └── contentrouter.go ├── config │ └── config.go └── filewriter │ └── filewriter.go ├── .air.toml ├── tailwind.config.js ├── docs ├── installation.md ├── building.md ├── introduction.md ├── custom-components.md ├── scripting.md ├── configuration.md └── theming.md ├── README.md ├── main.go └── go.sum /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phillip-England/godocument/main/favicon.ico -------------------------------------------------------------------------------- /static/css/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the environment variables file 2 | .env 3 | 4 | # Ignore the temporary files directory 5 | /tmp/ 6 | -------------------------------------------------------------------------------- /html/components/overlay.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /godocument.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Introduction": "/introduction.md", 4 | "Installation": "/installation.md", 5 | "Configuration": "/configuration.md", 6 | "Theming": "/theming.md", 7 | "Custom Components": "/custom-components.md", 8 | "Scripting": "/scripting.md", 9 | "Building": "/building.md" 10 | }, 11 | "meta": { 12 | "title": "Godocument" 13 | } 14 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module godocument 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/iancoleman/orderedmap v0.3.0 7 | github.com/joho/godotenv v1.5.1 8 | github.com/tdewolff/minify/v2 v2.20.19 9 | github.com/yuin/goldmark v1.7.1 10 | ) 11 | 12 | require ( 13 | github.com/PuerkitoBio/goquery v1.9.1 // indirect 14 | github.com/andybalholm/cascadia v1.3.2 // indirect 15 | github.com/tdewolff/parse/v2 v2.7.12 // indirect 16 | golang.org/x/net v0.21.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /html/components/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // Function to generate a random string of length n 9 | func RandomString(n int) string { 10 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 11 | src := rand.NewSource(time.Now().UnixNano()) 12 | r := rand.New(src) 13 | b := make([]rune, n) 14 | for i := range b { 15 | b[i] = letters[r.Intn(len(letters))] 16 | } 17 | return string(b) 18 | } 19 | -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | # air.toml 2 | # This is a TOML file. 3 | 4 | root = "." 5 | tmp_dir = "tmp" 6 | 7 | [build] 8 | cmd = "go build -o ./tmp/main ." 9 | bin = "./tmp/main" 10 | full_bin = "" 11 | include_ext = ["go", "tpl", "tmpl", "html", "css"] 12 | exclude_dir = ["assets", "tmp", "vendor"] 13 | exclude_file = ["html/components/sitenav.html"] 14 | exclude_regex = [] 15 | include_dir = [] 16 | include_file = [] 17 | follow_symlink = false 18 | 19 | [log] 20 | time = true 21 | 22 | [serve] 23 | command = "" 24 | port = 3000 25 | browser_reload = false 26 | 27 | [watcher] 28 | interval = 1000 29 | signal = "SIGTERM" 30 | -------------------------------------------------------------------------------- /internal/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "path/filepath" 6 | ) 7 | 8 | func ServeFavicon(w http.ResponseWriter, r *http.Request) { 9 | filePath := "favicon.ico" 10 | fullPath := filepath.Join(".", ".", filePath) 11 | http.ServeFile(w, r, fullPath) 12 | } 13 | 14 | func ServeStaticFiles(w http.ResponseWriter, r *http.Request) { 15 | filePath := r.URL.Path[len("/static/"):] 16 | fullPath := filepath.Join(".", "static", filePath) 17 | http.ServeFile(w, r, fullPath) 18 | } 19 | 20 | func ServeOutFiles(w http.ResponseWriter, r *http.Request) { 21 | filePath := r.URL.Path[len("/"):] 22 | fullPath := filepath.Join(".", "out", filePath) 23 | http.ServeFile(w, r, fullPath) 24 | } 25 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'selector', 4 | content: ["./html/**/*.html", "./internal/**/*.go", "./static/js/**/*.js"], 5 | theme: { 6 | extend: { 7 | fontFamily: { 8 | 'body': ['Nunito'] 9 | }, 10 | screens: { 11 | 'xs': '480px', // Extra small devices (phones) 12 | 'sm': '640px', // Small devices (tablets) 13 | 'md': '768px', // Medium devices (small laptops) 14 | 'lg': '1024px', // Large devices (laptops/desktops) 15 | 'xl': '1280px', // Extra large devices (large desktops) 16 | '2xl': '1536px' // Bigger than extra large devices 17 | } 18 | }, 19 | }, 20 | plugins: [], 21 | } 22 | 23 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Installation 5 | 6 | ## The Repo 7 | 8 | You can find a blank Godocument template at [https://github.com/phillip-england/godocument](https://github.com/phillip-england/godocument). 9 | 10 | All the commands in this guide assume you are using a Unix-based terminal. 11 | 12 | To get started, create a directory and clone the repo within it: 13 | 14 | ```bash 15 | mkdir 16 | cd 17 | git clone https://github.com/phillip-england/godocument . 18 | ``` -------------------------------------------------------------------------------- /html/components/pagenav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "text/template" 8 | "time" 9 | ) 10 | 11 | type CustomContext struct { 12 | context.Context 13 | StartTime time.Time 14 | Templates *template.Template 15 | } 16 | 17 | type CustomHandler func(cc *CustomContext, w http.ResponseWriter, r *http.Request) 18 | type CustomMiddleware func(cc *CustomContext, w http.ResponseWriter, r *http.Request) error 19 | 20 | func Chain(w http.ResponseWriter, r *http.Request, templates *template.Template, handler CustomHandler, middleware ...CustomMiddleware) { 21 | cc := &CustomContext{ 22 | Context: context.Background(), 23 | StartTime: time.Now(), 24 | Templates: templates, 25 | } 26 | for _, mw := range middleware { 27 | err := mw(cc, w, r) 28 | if err != nil { 29 | return 30 | } 31 | } 32 | handler(cc, w, r) 33 | Log(cc, w, r) 34 | } 35 | 36 | func Log(cc *CustomContext, w http.ResponseWriter, r *http.Request) error { 37 | elapsedTime := time.Since(cc.StartTime) 38 | formattedTime := time.Now().Format("2006-01-02 15:04:05") 39 | fmt.Printf("[%s] [%s] [%s] [%s]\n", formattedTime, r.Method, r.URL.Path, elapsedTime) 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /html/components/header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/stypes/stypes.go: -------------------------------------------------------------------------------- 1 | package stypes 2 | 3 | import "godocument/internal/middleware" 4 | 5 | // represents our base template 6 | type BaseTemplate struct { 7 | Title string 8 | Content string 9 | Prev *MarkdownNode 10 | Next *MarkdownNode 11 | MarkdownHeaders []MarkdownHeader 12 | MetaTags []MarkdownMetaTag 13 | } 14 | 15 | // a slice of DocConfig representing the structured data 16 | type DocConfig []DocNode 17 | 18 | // each line in the godocument.config.json under the "docs" section is a DocNode 19 | type DocNode interface{} 20 | 21 | // all DocConfig should implement this type in their struct 22 | type BaseNodeData struct { 23 | Depth int 24 | Parent string 25 | Name string 26 | NavHTML string 27 | } 28 | 29 | // MarkdownNode represents a leaf node in the structured data 30 | type MarkdownNode struct { 31 | BaseNodeData *BaseNodeData 32 | MarkdownFile string 33 | RouterPath string 34 | StaticAssetPath string 35 | Sequence int 36 | Next *MarkdownNode 37 | Prev *MarkdownNode 38 | HandlerName string 39 | HandlerUniqueString string 40 | HandlerFunc middleware.CustomHandler 41 | } 42 | 43 | // MarkdownHeader represents a header in the markdown file 44 | type MarkdownHeader struct { 45 | Line string 46 | Link string 47 | DepthClass string 48 | } 49 | 50 | // MarkdownMetaTag represets a meta tag found within a markdown file 51 | type MarkdownMetaTag struct { 52 | Tag string 53 | } 54 | 55 | // ObjectNode represents a non-leaf node in the structured data 56 | type ObjectNode struct { 57 | BaseNodeData *BaseNodeData 58 | Children DocConfig 59 | } 60 | -------------------------------------------------------------------------------- /html/components/pageturn.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /html/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ if .MetaTags }} 7 | {{ range .MetaTags }} 8 | {{ .Tag }} 9 | {{ end }} 10 | {{ end }} 11 | 12 | 13 | 14 | 15 | 16 | {{ .Title }} 17 | 18 | 19 |
20 | {{ template "header.html" . }} 21 |
22 | {{ template "sitenav.html" . }} 23 |
24 |
25 | {{ .Content }} 26 | {{ template "pageturn.html" . }} 27 |
28 | {{ template "pagenav.html" . }} 29 |
30 |
31 | {{ template "footer.html" . }} 32 | {{ template "overlay.html" .}} 33 |
34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/building.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Building 5 | 6 | ## go run main.go --build 7 | 8 | This command will build all of your static assets and place them in `/out`. 9 | 10 | Godocument will take all the relative paths you use during development and modify them into absolute paths for production. 11 | 12 | Using relative paths during development is a requirement. Not doing so will result in unexpected behaviour when building. 13 | 14 | ## Providing an Absolute Path 15 | 16 | When you are ready to build your site for production, you will need to provide an absolute path. 17 | 18 | ```bash 19 | go run main.go --build 20 | ``` 21 | 22 | If an absolute path is not provided, Godocument will serve your assets on whatever port 8080. This is useful for testing your application prior to deployment. If you do provide an absolute path, Godocument will not serve the assets locally. 23 | 24 | For example, let's say I wanted to build for `godocument.dev`, I would run: 25 | 26 | ```bash 27 | go run main.go --build godocument.dev 28 | ``` 29 | 30 | Absolute paths should not include a '/' at the end, this will result in a panic. 31 | 32 | ## Bundling Stylesheets 33 | 34 | Godocument will bundle your stylesheets to reduce the number of network calls needed on the initial page load. During development, two network calls are made to introduce styling into your pages. Let's look at the `` links in `/html/templates/base.html`: 35 | 36 | ```html 37 | 38 | 39 | ``` 40 | 41 | After bundling, these will be converted into: 42 | 43 | ```html 44 | 45 | ``` 46 | 47 | The vanilla CSS will be stacked on top of the Tailwind CSS, giving Tailwind priority. 48 | 49 | ## Minification 50 | 51 | Godocument uses [Minify](https://github.com/tdewolff/minify) to compress and minify static assets, which helps to reduce the file sizes of HTML, CSS, and Javascript files. These optimizations improves loading times and bandwidth usage in production. -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Introduction 5 | 6 | ## What is Godocument? 7 | Godocument is a static site generator inspired by [Docusaurus](https://docusaurus.io/) and powered by [Htmx](https://htmx.org). Documenting your code should be *simple*. 8 | 9 | Godocument requires Go version 1.22.0 or greater 10 | 11 | ## Hello, World 12 | 13 | 14 | A Godocument website can be created using the following steps: 15 | 16 | 17 | - Make a directory 18 | 19 | ```bash 20 | mkdir 21 | cd 22 | ``` 23 | 24 | - Clone the repo 25 | 26 | ```bash 27 | git clone https://github.com/phillip-england/godocument . 28 | ``` 29 | 30 | - Reset the project 31 | 32 | ```bash 33 | go run main.go --reset 34 | ``` 35 | 36 | - Add some new entries to `godocument.config.json`: 37 | 38 | ```json 39 | { 40 | "docs": { 41 | "Introduction": "/introduction.md", 42 | "First Page": "/first-page.md", 43 | "First Section": { 44 | "Second Page": "/first-section/second-page.md" 45 | } 46 | }, 47 | "meta": { 48 | "title": "My Website" 49 | } 50 | } 51 | ``` 52 | 53 | - Inside of `/docs`, create `first-page.md` 54 | 55 | ```bash 56 | touch /docs/first-page.md 57 | ``` 58 | 59 | - Add the following lines to `/docs/first-page.md` 60 | 61 | ```md 62 | 63 | 64 | # First Page 65 | 66 | ## Hello, World 67 | 68 | This is the first page I've created using Godocument! 69 | ``` 70 | 71 | - Inside of `/docs` create a directory called `/first-section` 72 | 73 | ```bash 74 | mkdir /docs/first-section 75 | ``` 76 | 77 | - Inside of `/docs/first-section`, create a file called `second-page.md` 78 | 79 | ```bash 80 | touch /docs/first-section/second-page.md 81 | ``` 82 | 83 | - Add the following lines to `/docs/first-section/second-page.md` 84 | 85 | ```md 86 | 87 | 88 | # Second Page 89 | 90 | ## Hello, World 91 | 92 | This is the second page I've created using Godocument! 93 | ``` 94 | 95 | - From your application's root directory, run the following command to view the results on `localhost:8080`: 96 | 97 | ```bash 98 | go run main.go 99 | ``` 100 | 101 | - To test your static assets locally, run: 102 | 103 | ```bash 104 | go run main.go --build 105 | ``` 106 | 107 | -- To build for production, run: 108 | 109 | ```bash 110 | go run main.go --build 111 | ``` 112 | 113 | That's it! Your example is deployment-ready and can be found at `/out`. You can easily deploy on Github Pages, Amazon S3, or a CDN of your choice. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godocument 2 | 3 | ## Documentation 4 | 5 | To read the full documentation, check out [godocument.dev](https://godocument.dev) 6 | 7 | 8 | 9 | 10 | # Introduction 11 | 12 | ## What is Godocument? 13 | Godocument is a static site generator inspired by [Docusaurus](https://docusaurus.io/) and powered by [Htmx](https://htmx.org). Documenting your code should be *simple*. 14 | 15 | Godocument requires Go version 1.22.0 or greater 16 | 17 | ## Hello, World 18 | 19 | 20 | A Godocument website can be created using the following steps: 21 | 22 | 23 | - Make a directory 24 | 25 | ```bash 26 | mkdir 27 | cd 28 | ``` 29 | 30 | - Clone the repo 31 | 32 | ```bash 33 | git clone https://github.com/phillip-england/godocument . 34 | ``` 35 | 36 | - Reset the project 37 | 38 | ```bash 39 | go run main.go --reset 40 | ``` 41 | 42 | - Add some new entries to `godocument.config.json`: 43 | 44 | ```json 45 | { 46 | "docs": { 47 | "Introduction": "/introduction.md", 48 | "First Page": "/first-page.md", 49 | "First Section": { 50 | "Second Page": "/first-section/second-page.md" 51 | } 52 | }, 53 | "meta": { 54 | "title": "My Website" 55 | } 56 | } 57 | ``` 58 | 59 | - Inside of `/docs`, create `first-page.md` 60 | 61 | ```bash 62 | touch /docs/first-page.md 63 | ``` 64 | 65 | - Add the following lines to `/docs/first-page.md` 66 | 67 | ```md 68 | 69 | 70 | # First Page 71 | 72 | ## Hello, World 73 | 74 | This is the first page I've created using Godocument! 75 | ``` 76 | 77 | - Inside of `/docs` create a directory called `/first-section` 78 | 79 | ```bash 80 | mkdir /docs/first-section 81 | ``` 82 | 83 | - Inside of `/docs/first-section`, create a file called `second-page.md` 84 | 85 | ```bash 86 | touch /docs/first-section/second-page.md 87 | ``` 88 | 89 | - Add the following lines to `/docs/first-section/second-page.md` 90 | 91 | ```md 92 | 93 | 94 | # Second Page 95 | 96 | ## Hello, World 97 | 98 | This is the second page I've created using Godocument! 99 | ``` 100 | 101 | - From your application's root directory, run the following command to view the results on `localhost:8080`: 102 | 103 | ```bash 104 | go run main.go 105 | ``` 106 | 107 | - To test your static assets locally, run: 108 | 109 | ```bash 110 | go run main.go --build 111 | ``` 112 | 113 | - To build for production, run: 114 | 115 | ```bash 116 | go run main.go --build 117 | ``` 118 | 119 | That's it! Your example is deployment-ready and can be found at `/out`. You can easily deploy on Github Pages, Amazon S3, or a CDN of your choice. -------------------------------------------------------------------------------- /docs/custom-components.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # Custom Components 6 | 7 | ## Creating a New Component 8 | 9 | All Javascript for Godocument is located in `/static/js/index.js`. 10 | 11 | To create a new custom component, first create a class to represent the component. 12 | 13 | Let's call this component, `simple-component`. The constructor should take in a parameter called `customComponent`, which will be an object built from the `CustomComponent` class. 14 | 15 | The `CustomComponent` class is already included in Godocument. 16 | 17 | ```js 18 | class SimpleComponent { 19 | constructor(customComponent) { 20 | 21 | } 22 | } 23 | ``` 24 | 25 | In `onLoad()`, be sure to instantiate both `CustomComponent` and `SimpleComponent`: 26 | 27 | ```js 28 | function onLoad() { 29 | // ... 30 | 31 | // defining custom components 32 | let customComponents = new CustomComponent() 33 | new SimpleComponent(customComponents) 34 | } 35 | ``` 36 | 37 | In the constructor of `SimpleComponent` call `customComponent.registerComponent(className, htmlContent)` as follows: 38 | 39 | ```js 40 | class SimpleComponent { 41 | constructor(customComponent) { 42 | customComponent.registerComponent("simple-component", ` 43 |
44 |

Hello, World

45 |

{text}

46 |
47 | `) 48 | } 49 | } 50 | ``` 51 | 52 | Take note of this line: 53 | 54 | ```js 55 |

{text}

56 | ``` 57 | 58 | Register component will replace `{text}` with the `innerHTML` of the component. 59 | 60 | To utilize `simple-component`, place the following markup in any of your `.md` files: 61 | 62 | ```md 63 | I am a simple component! 64 | ``` 65 | 66 | ## Included Components 67 | 68 | Godocument comes with a few components already built. Here they are: 69 | 70 | ### MdImportant 71 | 72 | I am created by using this markup in a `.md` file: `I am important!` 73 | 74 | ### MdWarning 75 | 76 | I am created by using this markup in a `.md` file: `I am a warning!` 77 | 78 | ### MdCorrect 79 | 80 | I am created by using this markup in a `.md` file: `That is correct!` 81 | 82 | ## Goldmark html.WithUnsafe 83 | 84 | Since we are using [Goldmark](https://github.com/yuin/goldmark) to convert `.md` files into workable HTML, we have to use the `html.WithUnsafe()` renderer option when instantiating Goldmark in our project. This will allow us to place HTML elements directly in our `.md` files. 85 | 86 | This is only considered *unsafe* if the content within our `.md` files is controlled by our users. In our case, since we will be the ones writing the markup directly, it is not considered unsafe. 87 | 88 | Removing `html.WithUnsafe()` from Goldmark's rendering options will cause Goldmark to ignore any HTML markup within our `.md` files. -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "godocument/internal/config" 7 | "godocument/internal/contentrouter" 8 | "godocument/internal/filewriter" 9 | "godocument/internal/handler" 10 | "godocument/internal/middleware" 11 | "net/http" 12 | "os" 13 | "path/filepath" 14 | "text/template" 15 | "time" 16 | ) 17 | 18 | var templates *template.Template 19 | 20 | func main() { 21 | 22 | args := os.Args 23 | port := "8080" 24 | 25 | if len(args) > 1 && args[1] == "--reset" { 26 | fmt.Println("WARNING: Resetting the project cannot be undone. All progress will be lost.") 27 | fmt.Println("Are you sure you want to reset the project?") 28 | fmt.Println("type 'reset' to confirm") 29 | var response string 30 | fmt.Scanln(&response) 31 | if response == "reset" { 32 | fmt.Println("resetting project...") 33 | filewriter.ResetProject() 34 | } 35 | return 36 | } 37 | 38 | if len(args) > 1 && args[1] == "--build" { 39 | absolutePath := "" 40 | testLocally := false 41 | if len(args) > 2 { 42 | absolutePath = args[2] 43 | } 44 | if absolutePath == "" { 45 | fmt.Println("No absolute path provided, defaulting to localhost:8080") 46 | testLocally = true 47 | absolutePath = "http://localhost:8080" 48 | } 49 | if absolutePath[len(absolutePath)-1:] == "/" { 50 | panic("The absolute path should not end with a '/'") 51 | } 52 | serverDone := make(chan bool) 53 | shutdownComplete := make(chan bool) 54 | go func() { 55 | if err := runDevServer(serverDone, shutdownComplete, port); err != nil { 56 | panic(err) 57 | } 58 | }() 59 | build(absolutePath, port) 60 | serverDone <- true 61 | <-shutdownComplete 62 | if testLocally { 63 | runStaticServer(port) 64 | } 65 | return 66 | } 67 | 68 | err := runDevServer(nil, nil, port) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | } 74 | 75 | // builds out static assets using godocument.config.json 76 | func build(absolutePath string, port string) { 77 | cnf := config.GetDocConfig() 78 | filewriter.GenerateStaticAssets(cnf, absolutePath, port) 79 | } 80 | 81 | // serves static files for testing prior to deployment 82 | func runStaticServer(port string) { 83 | mux := http.NewServeMux() 84 | mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { 85 | middleware.Chain(w, r, nil, func(cc *middleware.CustomContext, w http.ResponseWriter, r *http.Request) { 86 | handler.ServeOutFiles(w, r) 87 | }) 88 | }) 89 | fmt.Println("Static server is running on port: " + port) 90 | err := http.ListenAndServe(":"+port, mux) 91 | if err != nil { 92 | fmt.Println(err) 93 | } 94 | } 95 | 96 | // runs the development server 97 | func runDevServer(serverDone chan bool, shutdownComplete chan bool, port string) error { 98 | mux := http.NewServeMux() 99 | mux.HandleFunc("GET /favicon.ico", handler.ServeFavicon) 100 | mux.HandleFunc("GET /static/", handler.ServeStaticFiles) 101 | cnf := config.GetDocConfig() 102 | filewriter.GenerateDynamicNavbar(cnf) 103 | parseTemplates() 104 | contentrouter.GenerateRoutes(mux, templates) 105 | server := &http.Server{Addr: ":" + port, Handler: mux} 106 | go func() { 107 | if serverDone != nil { 108 | <-serverDone 109 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 110 | defer cancel() 111 | server.Shutdown(ctx) 112 | fmt.Println("Development server is shutting down...") 113 | if shutdownComplete != nil { 114 | shutdownComplete <- true // Signal that the shutdown is complete 115 | } 116 | } 117 | }() 118 | fmt.Println("Development server is running on port:", port) 119 | if err := server.ListenAndServe(); err != http.ErrServerClosed { 120 | return err 121 | } 122 | return nil 123 | } 124 | 125 | // parses all html templates in the html directory 126 | func parseTemplates() { 127 | templates = template.New("") 128 | err := filepath.Walk("./html", func(path string, info os.FileInfo, err error) error { 129 | if err != nil { 130 | return err 131 | } 132 | if !info.IsDir() && filepath.Ext(path) == ".html" { 133 | _, err := templates.ParseFiles(path) 134 | if err != nil { 135 | return err 136 | } 137 | } 138 | return nil 139 | }) 140 | if err != nil { 141 | fmt.Println("Error parsing templates") 142 | fmt.Println(err) 143 | panic(err) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /html/components/sitenav.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= 2 | github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= 3 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= 4 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= 5 | github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= 6 | github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= 7 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 8 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 9 | github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo= 10 | github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM= 11 | github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ= 12 | github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= 13 | github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= 14 | github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= 15 | github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= 16 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 17 | github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= 18 | github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 19 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 20 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 21 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 22 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 23 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 24 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 25 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 26 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 27 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 28 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 29 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 30 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 34 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 41 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 42 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 43 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 45 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 46 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 47 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 48 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 49 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 50 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 51 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 52 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 53 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 54 | -------------------------------------------------------------------------------- /docs/scripting.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Scripting 5 | 6 | ## Single Page Application 7 | 8 | Godocument uses [Htmx](https://htmx.org) to provide a single page application user experience. Htmx enables us to do this without the complexity of a Javascript framework. 9 | 10 | This functionality is enabled by a single attribute on our `` tag, [hx-boost](https://htmx.org/attributes/hx-boost). 11 | 12 | ```html 13 | 14 | ``` 15 | 16 | Basically, when you click on a navigational link within your website, Htmx will take over and the following will happen: 17 | 18 | 1. An AJAX request will be sent to the `href` of the clicked ``. 19 | 2. When the response is received, the `` from the response will be isolated. 20 | 3. Our page's current `` will be replaced with the newly received ``. 21 | 22 | All of this will be done without a full-page refresh. Since we are generating static pages, the wait time between when a request is clicked and when the response is receieved will be minimal, giving the illusion of a single page experience to the user. 23 | 24 | ## Htmx Implications 25 | 26 | Using Htmx's `hx-boost` attribute has implications on how we need to think about using Javascript in our application. 27 | 28 | Since the `` is the only thing changed when using `hx-boost`, the Javascript located in the `` of our document will only be loaded once. However, Javascript located in the `` will be ran on each request. 29 | 30 | This can create issues when declaring functions, declaring variables, and mounting event listeners. 31 | 32 | ## loaded attribute 33 | 34 | Godocument makes use of an attribute, `loaded`, on the `` tag to avoid reinstantiating variables multiple times. 35 | 36 | ```html 37 | 38 | ``` 39 | 40 | After the page is loaded on the initial visit, this attribute is set to `true`. This will prevent our variables and functions from being instantiated more than once. 41 | 42 | Failing to set `loaded="true"` on `` will result in unexpected behavior 43 | 44 | ## onLoad function 45 | 46 | Godocument makes use of the function `onLoad()` to run the appropriate Javascript on all page loads. 47 | 48 | ```js 49 | function onLoad() { 50 | 51 | // elements 52 | const body = qs(document, 'body') 53 | const sitenav = qs(document, '#sitenav') 54 | const sitenavItems = qsa(sitenav, '.item') 55 | const sitenavDropdowns = qsa(sitenav, '.dropdown') 56 | const pagenav = qs(document, '#pagenav') 57 | const pagenavLinks = qsa(pagenav, 'a') 58 | const article = qs(document, '#article') 59 | const articleTitles = qsa(article, 'h2, h3, h4, h5, h6') 60 | const header = qs(document, '#header') 61 | const headerBars = qs(header, '#bars') 62 | const overlay = qs(document, '#overlay') 63 | const sunIcons = qsa(document, '.sun') 64 | const moonIcons = qsa(document, '.moon') 65 | const htmlDocument = qs(document, 'html') 66 | 67 | // hooking events and running initializations 68 | window.scrollTo(0, 0, { behavior: 'auto' }) 69 | new SiteNav(sitenav, sitenavItems, sitenavDropdowns, header, overlay) 70 | new PageNav(pagenav, pagenavLinks, articleTitles) 71 | new Header(headerBars, overlay, sitenav) 72 | new Theme(sunIcons, moonIcons, htmlDocument) 73 | 74 | // web components 75 | doOnce(() => { 76 | customElements.define('md-important', MdImportant) 77 | customElements.define('md-warning', MdWarning) 78 | customElements.define('md-correct', MdCorrect) 79 | }) 80 | 81 | // init 82 | Prism.highlightAll(); 83 | 84 | // reveal body 85 | zez.applyState(body, 'loaded') 86 | 87 | } 88 | ``` 89 | 90 | `onLoad()` is mounted to the `window` and `` using the `DOMContentLoaded` and `htmx:afterOnLoad` events. Click [here](https://htmx.org/events/) to read more about Htmx events. 91 | 92 | ```js 93 | eReset(window, 'DOMContentLoaded', onLoad) // initial page load 94 | eReset(document.getElementsByTagName('body')[0], "htmx:afterOnLoad", onLoad) // after htmx swaps 95 | ``` 96 | 97 | `DOMContentLoaded` will handle the initial page load, while `hmtx:afterOnLoad` will handle all other navigations. 98 | 99 | ## Managing Events 100 | 101 | Since the page is never refreshed using Htmx, we need to make sure we are unmounting events and remounting them on every navigation. `eReset()` is a handy function that does just that. 102 | 103 | ```js 104 | function eReset(node, eventType, callback) { 105 | node.removeEventListener(eventType, callback) 106 | node.addEventListener(eventType, callback) 107 | } 108 | ``` 109 | 110 | Instead of calling `element.addEventListener()`, it is better to use `eReset()` to ensure events are properly managed between page navigations. 111 | 112 | Failing to unhook events upon navigation will result in the same events being hooked multiple times to the target element, which can have unexpected consequences and lead to poor memory management. 113 | 114 | 115 | -------------------------------------------------------------------------------- /internal/contentrouter/contentrouter.go: -------------------------------------------------------------------------------- 1 | package contentrouter 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "text/template" 11 | 12 | "godocument/internal/config" 13 | "godocument/internal/middleware" 14 | "godocument/internal/stypes" 15 | 16 | "github.com/yuin/goldmark" 17 | "github.com/yuin/goldmark/parser" 18 | "github.com/yuin/goldmark/renderer/html" 19 | ) 20 | 21 | // GenerateRoutes generates code for application routes based on the ./godocument.config.json file "docs" section 22 | // this function populates ./internal/generated/generated.go 23 | func GenerateRoutes(mux *http.ServeMux, templates *template.Template) { 24 | cnf := config.GetDocConfig() 25 | assignHandlers(cnf) 26 | hookDocRoutes(mux, templates, cnf) 27 | } 28 | 29 | // hookDocRoutes links our routes to the http.ServeMux 30 | func hookDocRoutes(mux *http.ServeMux, templates *template.Template, cnf stypes.DocConfig) { 31 | config.WorkOnMarkdownNodes(cnf, func(m *stypes.MarkdownNode) { 32 | if m.BaseNodeData.Parent == config.DocRoot && m.BaseNodeData.Name == config.IntroductionString { 33 | mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { 34 | if r.URL.Path != "/" { 35 | http.NotFound(w, r) 36 | return 37 | } 38 | middleware.Chain(w, r, templates, m.HandlerFunc) 39 | }) 40 | return 41 | } 42 | mux.HandleFunc("GET "+m.RouterPath, func(w http.ResponseWriter, r *http.Request) { 43 | middleware.Chain(w, r, templates, m.HandlerFunc) 44 | }) 45 | }) 46 | } 47 | 48 | func assignHandlers(cnf stypes.DocConfig) { 49 | config.WorkOnMarkdownNodes(cnf, func(m *stypes.MarkdownNode) { 50 | m.HandlerFunc = func(cc *middleware.CustomContext, w http.ResponseWriter, r *http.Request) { 51 | md := goldmark.New( 52 | goldmark.WithParserOptions( 53 | parser.WithAutoHeadingID(), 54 | ), 55 | goldmark.WithRendererOptions( 56 | html.WithUnsafe(), 57 | ), 58 | ) 59 | mdContent, err := os.ReadFile(m.MarkdownFile) 60 | if err != nil { 61 | // Handle error (e.g., file not found) 62 | http.Error(w, "File not found", http.StatusNotFound) 63 | return 64 | } 65 | var mdBuf bytes.Buffer 66 | if err := md.Convert(mdContent, &mdBuf); err != nil { 67 | http.Error(w, "Error converting markdown", http.StatusInternalServerError) 68 | return 69 | } 70 | 71 | // reading all headers from the markdown file content and parsing out any tags 72 | file, err := os.Open(m.MarkdownFile) 73 | if err != nil { 74 | http.Error(w, "Error reading markdown file", http.StatusInternalServerError) 75 | return 76 | } 77 | defer file.Close() 78 | // read the file line by line 79 | scanner := bufio.NewScanner(file) 80 | headers := []stypes.MarkdownHeader{} 81 | metaTags := []stypes.MarkdownMetaTag{} 82 | backticksFound := 0 83 | for scanner.Scan() { 84 | line := scanner.Text() 85 | // skipping and backticks 86 | if strings.Contains(line, "```") { 87 | backticksFound++ 88 | continue 89 | } 90 | // if we are in a code block, skip the line 91 | if backticksFound%2 == 1 { 92 | continue 93 | } 94 | // if we find a meta tag, 95 | if strings.Contains(line, "") { 96 | metaTag := stypes.MarkdownMetaTag{ 97 | Tag: line, 98 | } 99 | metaTags = append(metaTags, metaTag) 100 | continue 101 | } 102 | if strings.Contains(line, "# ") { 103 | if strings.Count(line, "#") == 1 { 104 | continue 105 | } 106 | link := strings.ToLower(line) 107 | link = strings.TrimLeft(link, "# ") 108 | // when parsing headers using goldmark, the id of the header will have special chars stripped 109 | // for example a header of /docs will be converted to docs 110 | // also, spaces are replaced with dashes and text is converted to lowercase 111 | // for a header of Custom Components will be converted to custom-components 112 | // so when pointing to these headers, we need to ensure we are using the correct id 113 | // therefore, we need to also strip the hrefs of our pagenav links as well 114 | // here we are stripping those characters 115 | // if you find a case where a pagenav link is not working, this is most likely the issue 116 | // be sure to check their hrefs to see if they line up with the associated headers id 117 | unwantedLinkChars := []string{"!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+", "=", "{", "}", "[", "]", "|", "\\", ":", ";", "\"", "'", "<", ">", ",", ".", "/", "?"} 118 | for _, char := range unwantedLinkChars { 119 | link = strings.ReplaceAll(link, char, "") 120 | } 121 | link = strings.ReplaceAll(link, " ", "-") 122 | link = "#" + link 123 | depthClass := "" 124 | leftPadding := strings.Count(line, "#") - 2 // ensures we start at 0 125 | if leftPadding != 0 { 126 | depthClass = "pl-" + fmt.Sprintf("%d", leftPadding+2) 127 | } 128 | header := stypes.MarkdownHeader{ 129 | Line: strings.TrimLeft(line, "# "), 130 | DepthClass: depthClass, // ensures we start at a 0 index 131 | Link: link, 132 | } 133 | headers = append(headers, header) 134 | } 135 | } 136 | 137 | // removing meta tags from the markdown content 138 | mdContentString := mdBuf.String() 139 | for _, metaTag := range metaTags { 140 | mdContentString = strings.Replace(mdContentString, metaTag.Tag, "", 1) 141 | } 142 | 143 | // getting title from godocument.config.json 144 | title := config.GetTitle() 145 | if title == "" { 146 | title = "Godocument" 147 | } 148 | 149 | // Create a new instance of tdata.Base with the title and markdown content as HTML 150 | baseData := &stypes.BaseTemplate{ 151 | Title: title + " - " + m.BaseNodeData.Name, 152 | Content: mdContentString, 153 | Prev: m.Prev, 154 | Next: m.Next, 155 | MarkdownHeaders: headers, 156 | MetaTags: metaTags, 157 | } 158 | 159 | // Assuming you have already parsed your templates (including the base template) elsewhere 160 | tmpl := cc.Templates.Lookup("base.html") 161 | if tmpl == nil { 162 | http.Error(w, "Base template not found", http.StatusInternalServerError) 163 | return 164 | } 165 | 166 | // Execute the base template with the baseData instance 167 | var htmlBuf bytes.Buffer 168 | if err := tmpl.Execute(&htmlBuf, baseData); err != nil { 169 | fmt.Println(err) 170 | http.Error(w, "Error executing template", http.StatusInternalServerError) 171 | return 172 | } 173 | 174 | w.Header().Set("Content-Type", "text/html") 175 | w.Write(htmlBuf.Bytes()) 176 | } 177 | }) 178 | } 179 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Configuration 5 | 6 | ## godocument.config.json 7 | 8 | `godocument.config.json` is the configuration file for your application. It contains the necessary information to generate your website's routes. 9 | 10 | The order of items in `godocument.config.json` will determine the order of your pages in your website. 11 | 12 | Here is the base configuration needed to generate a site using Godocument: 13 | 14 | ```json 15 | { 16 | "docs": { 17 | "Introduction": "/introduction.md" 18 | } 19 | } 20 | ``` 21 | 22 | The `/docs` directory and the `/docs/introduction.md` file are required for Godocument. Also, the json object `"docs"` must be named `"docs"` and the first entry beneath `"docs"` must be `"Introduction": "/introduction.md"`. Failing to meet these requirements will result in a panic. 23 | 24 | ## Pages 25 | 26 | The entries in `godocument.config.json` can either be pages or sections. Let's start with pages. 27 | 28 | To denote a page, simply create a key-value pair with the key being the name of the page and the value being the file path to the `.md` file for the page. You can name pages whatever you would like. 29 | 30 | All file paths in `godocument.config.json` are relative to `/docs`. This means you do not have to the include `/docs` in your file paths as Godocument assumes all your markdown files are in `/docs`. 31 | 32 | Here is how you would add a new page to the base configuration: 33 | 34 | ```json 35 | { 36 | "docs": { 37 | "Introduction": "/introduction.md", 38 | "New Page": "/new-page.md" 39 | } 40 | } 41 | ``` 42 | 43 | After adding the page to `godocument.config.json` you will need to create the associated file. From the root of your application, run: 44 | 45 | ```bash 46 | touch /docs/new-page.md 47 | ``` 48 | 49 | Then, add the following lines to `/docs/new-page.md`: 50 | 51 | ```md 52 | # New Page 53 | 54 | I created a new page using Godocument! 55 | ``` 56 | 57 | From the root of your application, run `go run main.go` and view the results at `localhost:8080`. 58 | 59 | ## Sections 60 | 61 | Sections are named areas of your website which contain a series of pages. Sections can also contain sub-sections. In `godocument.config.json`, a section can be denoted by creating an object. For example: 62 | 63 | ```json 64 | { 65 | "docs": { 66 | "Introduction": "/introduction.md", 67 | "New Section": { 68 | 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | After creating a section, you can add pages within it: 75 | 76 | ```json 77 | { 78 | "docs": { 79 | "Introduction": "/introduction.md", 80 | "New Section": { 81 | "About": "/new-section/about.md" 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | You can also add sub-sections: 88 | 89 | ```json 90 | { 91 | "docs": { 92 | "Introduction": "/introduction.md", 93 | "New Section": { 94 | "About": "/new-section/about.md", 95 | "More Info": { 96 | "Origins": "/new-section/more-info/origins.md" 97 | } 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | Create the corresponding files and directories: 104 | 105 | ```bash 106 | mkdir /docs/new-section 107 | touch /docs/new-section/about.md 108 | mkdir /docs/new-section/more-info 109 | touch /docs/new-section/more-info/origins.md 110 | ``` 111 | 112 | Add the following content to `/docs/new-section/about.md` 113 | 114 | ```md 115 | # About 116 | 117 | I created a page within a section using Godocument! 118 | ``` 119 | 120 | Then, add the following lines to `/docs/new-section/more-info/origin.md`: 121 | 122 | ```md 123 | # Origins 124 | 125 | I created a page within a sub-section using Godocument! 126 | ``` 127 | 128 | To test the results, run `go run main.go` from the root of your application and visit `localhost:8080` 129 | 130 | ## /docs structure 131 | 132 | Godocument does not require you to structure your `/docs` directory in any particular way, **BUT** it is highly recommended to have your `/docs` directory mirror the structure of your `godocument.config.json` file. 133 | 134 | For example, here is a `godocument.config.json` file which does not follow the proper conventions. 135 | 136 | The example below does not follow the recommended conventions for `godocument.config.json`. 137 | 138 | ```json 139 | { 140 | "docs":{ 141 | "Introduction": "/introduction.md", 142 | "More Info": { 143 | "About": "/about.md" 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | It does not follow the conventions because `/about.md` should have a file path which mirrors the structure of `godocument.config.json`. 150 | 151 | To correct the above `godocument.config.json` make the changes below. 152 | 153 | ```json 154 | { 155 | "docs": { 156 | ... 157 | "More Info": { 158 | "About": "/more-info/about.md" 159 | } 160 | } 161 | } 162 | ``` 163 | 164 | Such a change will ensure that the /docs directory mirrors the structure of godocument.config.json, as recommended. 165 | 166 | ## Customizing Titles 167 | 168 | Godocument autogenerates titles based off the keys provided in the `"docs"` section of `godocument.config.json`. For example, a key value pair of `"Introduction": "/introduction.md"` would render a title of `Godocument - Introduction` when visiting the introduction page. 169 | 170 | All pags titles will be prefixed with "Godocument - " until you specify otherwise in the `"meta"` section of `godocument.config.json`. 171 | 172 | To modify title prefixes, set the following config: 173 | 174 | ```json 175 | { 176 | ... your "docs" section, 177 | "meta": { 178 | "title": "Custom Title Prefix" 179 | } 180 | } 181 | ``` 182 | 183 | Doing this will prefix all autogenerated titles with, `Custom Title Prefix - `. 184 | 185 | ## Hot Reloading 186 | 187 | [Air](https://github.com/cosmtrek/air) is a binary that provides hot-reloading functionality. It can be installed using: 188 | 189 | ```bash 190 | go install github.com/cosmtrek/air@latest 191 | ``` 192 | 193 | Just be sure your Go binaries are registered in your `$PATH`. 194 | 195 | All the configuration needed to implment Air properly in Godocument is included in `.air.toml`. 196 | 197 | When running Air, a `/tmp` directory will be generated. `/tmp` is already included in your `.gitignore` and you can disregard it during development. 198 | 199 | To enable hot-reloading, go to the root of your project and run: 200 | 201 | ```bash 202 | air 203 | ``` -------------------------------------------------------------------------------- /docs/theming.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Theming 5 | 6 | ## Code Blocks 7 | 8 | Code blocks are made possible by [Prism](https://prismjs.com/). 9 | 10 | Godocument includes support for all languages supported by Prism. This is to make it easy to get started with your website. However, when you plan to deploy, it is a good idea to go over to [Prism's download page](https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript) and select only the languages used in your application. 11 | 12 | When downloading the required languages for your site, you only need to download the `.js` file and replace `/static/js/prism.js` with the newly downloaded file. Be sure the file is named, `primsm.js`. 13 | 14 | ## CSS Usage 15 | 16 | Godocument uses both Vanilla CSS and Tailwind for styling. If you intend to make changes to the Tailwind classes in your markup, you will need to download the [Tailwind binary](https://tailwindcss.com/blog/standalone-cli) and run `tailwindcss -i './static/css/input.css' -o './static/css/output.css'` from the root of your project. Doing so will recompile your `/static/css/output.css` and adjust to any changes. 17 | 18 | The `tailwind.config.json` file provided in the Godocument repo will contain the proper configuration needed to target all the `.html` and `.go` files in your project. 19 | 20 | Vanilla CSS is used in Godocument for things like the page layout, scrollbar appearance, and a few other things. All Vanilla CSS can be found at `/static/css/index.css`. 21 | 22 | 23 | ## CSS Variables 24 | 25 | Godocument makes use of CSS variables to give users more control of their theme. Variables are either viewed as *utility* variables or *element-specific* variables. 26 | 27 | To adjust the theming for your site, edit the variables found at the top of `/static/css/index.css`. 28 | 29 | ## Utility Variables 30 | 31 | Utility variables are not directly used in markup. Rather, they are intended to be used in *element-specific* variables. Here are the color utility variables for this site: 32 | 33 | ```css 34 | --white: #fafafa; 35 | --black: #1f1f1f; 36 | --gray-800: #333333; 37 | --gray-700: #555555; 38 | --gray-600: #777777; 39 | --gray-500: #999999; 40 | --gray-400: #bbbbbb; 41 | --gray-300: #dddddd; 42 | --gray-200: #f0f0f0; 43 | --gray-100: #f5f5f5; 44 | --light-gray: #d0d0d0; 45 | --gray: #555555; 46 | --darkest-gray: #222222; 47 | --purple: #ba8ef7; 48 | --dark-purple: #712fec; 49 | --green: #3bec74; 50 | --dark-green: #057d2f; 51 | --pink: #b370b1; 52 | --yellow: #ffea6b; 53 | --dark-yellow: #7f7108; 54 | --orange: #ffa763; 55 | --dark-orange: #c64719; 56 | --blue: #2494da; 57 | --dark-blue: #1b6dbf; 58 | --red: #ff4d3f; 59 | --dark-red: #c82216; 60 | ``` 61 | 62 | ## Element-Specific Variables 63 | 64 | Element-specific variables make use of *utility variables*. Here are the element-specific variables that control the colors in the codeblocks found on this site: 65 | 66 | ```css 67 | /* light code blocks */ 68 | --code-bg-color: var(--gray-200); 69 | --code-token-property: var(--dark-purple); 70 | --code-string: var(--dark-green); 71 | --code-token-selector: var(--dark-orange); 72 | --code-function: var(--dark-yellow); 73 | --code-keyword: var(--dark-purple); 74 | --code-operator: var(--black); 75 | --code-punctuation: var(--gray-700); 76 | --code-important: var(--dark-orange); 77 | --code-comment: var(--gray-700); 78 | 79 | /* dark code blocks */ 80 | --dark-code-bg-color: var(--gray-800); 81 | --dark-code-token-property: var(--purple); 82 | --dark-code-string: var(--green); 83 | --dark-code-token-selector: var(--orange); 84 | --dark-code-function: var(--yellow); 85 | --dark-code-keyword: var(--purple); 86 | --dark-code-operator: var(--white); 87 | --dark-code-punctuation: var(--gray-300); 88 | --dark-code-comment: var(--gray-300) 89 | ``` 90 | 91 | ## Variables in Tailwind 92 | 93 | Godocument makes use of Tailwind's ability to use CSS variables within Tailwind classes. For example, here is the markup for the `
` at the top of this page: 94 | 95 | ```html 96 | 110 | ``` 111 | 112 | Take note of the classes on the `
` element itself. You'll see classes such as `bg-[var(--header-bg-color)]` or `dark:border-[var(--dark-b-color)]`. 113 | 114 | Although the syntax is *ugly*, it does come with its perks. 115 | 116 | You can adjust the colors of the elements on the page using variables instead of having to change the markup for each individual element. 117 | 118 | ## Styling Markdown Content 119 | 120 | Markdown content is styled using Vanilla CSS. This is done to minimize the amount of text found in `.md` files. To adjust the styling of markdown content, edit the `// markdown styles ------` section of `/static/css/index.css`. 121 | 122 | ## Logo 123 | 124 | To change the logo for your site, simply replace `/static/img/logo.svg` with your logo. There is only one caveat, logos with a large height may shift the navbar in unexpected ways. For this reason, it is recommended to use a logo which is wide, not tall. 125 | 126 | ## favicon.ico 127 | 128 | During development, the server searches for your favicon.ico at /favicon.ico. When you go to build your static assest, the favicon will be placed in `/out/favicon.ico`. To change your favicon.ico, simply replace the icon found at `/favicon.ico`. -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "godocument/internal/stypes" 10 | 11 | "github.com/iancoleman/orderedmap" 12 | ) 13 | 14 | const ( 15 | DocRoot = "Root" 16 | DocJSONKey = "docs" 17 | JSONConfigPath = "./godocument.config.json" 18 | IntroductionString = "Introduction" 19 | GeneratedNavPath = "./html/components/sitenav.html" 20 | DevStaticPrefix = "./static" 21 | StaticMarkdownPrefix = "./docs" 22 | StaticAssetsDir = "./out" 23 | ProdStaticPrefix = "./out/static" 24 | ) 25 | 26 | // taks the "docs" section of godocument.json.config and generates a workable data structure from it 27 | func GetDocConfig() stypes.DocConfig { 28 | 29 | // here is how we take the json found in ./godocument.config.json and generate the data in a structured format 30 | // each "line" in the json file is a DocNode (an interface that represents all lines in the "docs" section of the json file) 31 | // first, we get each line using orderedmap.OrderedMap in getUnstructuredDocs() 32 | // the order of the lines is important because it will dictate the arrangement of html components 33 | // then we generate a slice of each line in the json file using getLinearDocs() 34 | // we sequence each markdown node so it is easy for us to link them together 35 | // then, each markdown node is linked to eachother based on their sequence number 36 | // nodes not found at the root level have a parent node assigned to them 37 | // in assignMarkdownNodes, we assign each markdown node to their respective parent object node 38 | // in assignSubObjectNodes, we assign each object node to their respective parent object node 39 | // after doing this, we purge all markdown nodes that are not at the root level (because they now exist in objectnode.Children) 40 | // we also purge all object nodes that are not at the root level (because they will be assigned to another object node) 41 | 42 | u := getUnstructuredDocs() 43 | c := stypes.DocConfig{} 44 | for i := 0; i < len(u.Keys()); i++ { 45 | key := u.Keys()[i] 46 | if key == DocJSONKey { 47 | value, _ := u.Get(key) 48 | c = getLinearDocs(value, DocRoot, c, 0) 49 | } 50 | } 51 | sequenceMarkdownNodes(c) 52 | linkMarkdownNodes(c) 53 | assignMarkdownNodes(c) 54 | assignSubObjectNodes(c) 55 | c = purgeMarkdownNodes(c) 56 | c = purgeNonRootObjectNodes(c) 57 | ensureMarkdownsFileExists(c) 58 | ensureIntroductionFileExists(c) 59 | return c 60 | } 61 | 62 | // GetUnstructuredDocs reads the godocument.config.json file and returns the unstructured data 63 | func getUnstructuredDocs() *orderedmap.OrderedMap { 64 | file, err := os.ReadFile(JSONConfigPath) // Ensure the file path and extension are correct 65 | if err != nil { 66 | panic(err) 67 | } 68 | result := orderedmap.New() 69 | err = json.Unmarshal(file, &result) 70 | if err != nil { 71 | panic(err) 72 | } 73 | return result 74 | } 75 | 76 | // GetStructuredDocs recursively generates a structured representation of the unstructured doc config data 77 | func getLinearDocs(om interface{}, parent string, docConfig stypes.DocConfig, depth int) stypes.DocConfig { 78 | switch om := om.(type) { 79 | case orderedmap.OrderedMap: 80 | for _, key := range om.Keys() { 81 | value, _ := om.Get(key) 82 | switch value := value.(type) { 83 | case string: 84 | if depth == 0 { 85 | parent = DocRoot 86 | } 87 | routerPath := "" 88 | markdownFile := StaticMarkdownPrefix + value 89 | staticAssetPath := markdownFile 90 | if key == IntroductionString && depth == 0 { 91 | routerPath = "/" 92 | staticAssetPath = StaticAssetsDir + "/index.html" 93 | } else { 94 | routerPath = strings.TrimSuffix(value, ".md") 95 | staticAssetPath = strings.TrimPrefix(staticAssetPath, StaticMarkdownPrefix) 96 | staticAssetPath = StaticAssetsDir + staticAssetPath 97 | staticAssetPath = strings.Replace(staticAssetPath, ".md", ".html", 1) 98 | } 99 | docNode := &stypes.MarkdownNode{ 100 | BaseNodeData: &stypes.BaseNodeData{ 101 | Depth: depth, 102 | Parent: parent, 103 | Name: key, 104 | }, 105 | MarkdownFile: markdownFile, 106 | RouterPath: routerPath, 107 | StaticAssetPath: staticAssetPath, 108 | } 109 | docConfig = append(docConfig, docNode) 110 | case orderedmap.OrderedMap: 111 | docNode := &stypes.ObjectNode{ 112 | BaseNodeData: &stypes.BaseNodeData{ 113 | Depth: depth, 114 | Parent: parent, 115 | Name: key, 116 | }, 117 | Children: nil, 118 | } 119 | docConfig = append(docConfig, docNode) 120 | docConfig = getLinearDocs(value, key, docConfig, depth+1) 121 | } 122 | } 123 | case string: 124 | return docConfig 125 | case nil: 126 | return docConfig 127 | default: 128 | panic("Invalid type") 129 | } 130 | return docConfig 131 | } 132 | 133 | // sequenceMarkdownNodes assigns a sequence number to each MarkdownNode 134 | func sequenceMarkdownNodes(docConfig stypes.DocConfig) { 135 | sequence := 0 136 | for i := 0; i < len(docConfig); i++ { 137 | switch docConfig[i].(type) { 138 | case *stypes.ObjectNode: 139 | continue 140 | case *stypes.MarkdownNode: 141 | docConfig[i].(*stypes.MarkdownNode).Sequence = sequence 142 | sequence++ 143 | } 144 | } 145 | } 146 | 147 | // links each markdown node to the next markdown node based on their sequence number 148 | func linkMarkdownNodes(docConfig stypes.DocConfig) { 149 | for i := 0; i < len(docConfig); i++ { 150 | switch docConfig[i].(type) { 151 | case *stypes.ObjectNode: 152 | continue 153 | case *stypes.MarkdownNode: 154 | for j := 0; j < len(docConfig); j++ { 155 | switch docConfig[j].(type) { 156 | case *stypes.ObjectNode: 157 | continue 158 | case *stypes.MarkdownNode: 159 | if docConfig[j].(*stypes.MarkdownNode).Sequence == docConfig[i].(*stypes.MarkdownNode).Sequence+1 { 160 | docConfig[i].(*stypes.MarkdownNode).Next = docConfig[j].(*stypes.MarkdownNode) 161 | } 162 | if docConfig[j].(*stypes.MarkdownNode).Sequence == docConfig[i].(*stypes.MarkdownNode).Sequence-1 { 163 | docConfig[i].(*stypes.MarkdownNode).Prev = docConfig[j].(*stypes.MarkdownNode) 164 | } 165 | } 166 | } 167 | } 168 | 169 | } 170 | } 171 | 172 | // assignChildNodes assigns markdownNodes to each ObjectNode 173 | func assignMarkdownNodes(docConfig stypes.DocConfig) { 174 | for i := 0; i < len(docConfig); i++ { 175 | switch docConfig[i].(type) { 176 | case *stypes.ObjectNode: 177 | for j := 0; j < len(docConfig); j++ { 178 | switch docConfig[j].(type) { 179 | case *stypes.MarkdownNode: 180 | if docConfig[j].(*stypes.MarkdownNode).BaseNodeData.Parent == docConfig[i].(*stypes.ObjectNode).BaseNodeData.Name { 181 | docConfig[i].(*stypes.ObjectNode).Children = append(docConfig[i].(*stypes.ObjectNode).Children, docConfig[j]) 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | 189 | // assignSubObjectNodes assigns ObjectNodes to their respective parent ObjectNode 190 | func assignSubObjectNodes(docConfig stypes.DocConfig) { 191 | for i := 0; i < len(docConfig); i++ { 192 | switch docConfig[i].(type) { 193 | case *stypes.ObjectNode: 194 | for j := 0; j < len(docConfig); j++ { 195 | switch docConfig[j].(type) { 196 | case *stypes.ObjectNode: 197 | if docConfig[j].(*stypes.ObjectNode).BaseNodeData.Parent == docConfig[i].(*stypes.ObjectNode).BaseNodeData.Name { 198 | docConfig[i].(*stypes.ObjectNode).Children = append(docConfig[i].(*stypes.ObjectNode).Children, docConfig[j]) 199 | } 200 | } 201 | } 202 | } 203 | } 204 | 205 | } 206 | 207 | // purgeMarkdownNodes removes all MarkdownNodes (except Root-level markdown nodes) from the structured data and returns only ObjectNodes 208 | func purgeMarkdownNodes(docConfig stypes.DocConfig) stypes.DocConfig { 209 | objectNodes := stypes.DocConfig{} 210 | for i := 0; i < len(docConfig); i++ { 211 | switch docConfig[i].(type) { 212 | case *stypes.ObjectNode: 213 | objectNodes = append(objectNodes, docConfig[i]) 214 | case *stypes.MarkdownNode: 215 | if docConfig[i].(*stypes.MarkdownNode).BaseNodeData.Parent == DocRoot { 216 | objectNodes = append(objectNodes, docConfig[i]) 217 | } 218 | } 219 | } 220 | return objectNodes 221 | } 222 | 223 | // purgeNonRootObjectNodes removes all ObjectNodes that are not at the root level 224 | func purgeNonRootObjectNodes(docConfig stypes.DocConfig) stypes.DocConfig { 225 | rootObjectNodes := stypes.DocConfig{} 226 | for i := 0; i < len(docConfig); i++ { 227 | switch docConfig[i].(type) { 228 | case *stypes.ObjectNode: 229 | if docConfig[i].(*stypes.ObjectNode).BaseNodeData.Parent == DocRoot { 230 | rootObjectNodes = append(rootObjectNodes, docConfig[i]) 231 | } 232 | case *stypes.MarkdownNode: 233 | rootObjectNodes = append(rootObjectNodes, docConfig[i]) 234 | } 235 | } 236 | return rootObjectNodes 237 | 238 | } 239 | 240 | // WorkOnMarkdownNodes applies the action function to each MarkdownNode in the structured data 241 | func WorkOnMarkdownNodes(docConfig stypes.DocConfig, action func(*stypes.MarkdownNode)) { 242 | for i := 0; i < len(docConfig); i++ { 243 | switch docConfig[i].(type) { 244 | case *stypes.ObjectNode: 245 | WorkOnMarkdownNodes(docConfig[i].(*stypes.ObjectNode).Children, action) 246 | case *stypes.MarkdownNode: 247 | action(docConfig[i].(*stypes.MarkdownNode)) 248 | } 249 | } 250 | } 251 | 252 | // ensures all markdown files in godocument.config.json exist 253 | func ensureMarkdownsFileExists(docConfig stypes.DocConfig) { 254 | WorkOnMarkdownNodes(docConfig, func(m *stypes.MarkdownNode) { 255 | if _, err := os.Stat(m.MarkdownFile); os.IsNotExist(err) { 256 | panic(fmt.Sprintf("Markdown file %s does not exist", m.MarkdownFile)) 257 | } 258 | }) 259 | } 260 | 261 | // ensures the first markdown file in godocument.config.json is the introduction file 262 | func ensureIntroductionFileExists(docConfig stypes.DocConfig) { 263 | name := docConfig[0].(*stypes.MarkdownNode).BaseNodeData.Name 264 | file := docConfig[0].(*stypes.MarkdownNode).MarkdownFile 265 | parent := docConfig[0].(*stypes.MarkdownNode).BaseNodeData.Parent 266 | if name != IntroductionString || parent != DocRoot || file != StaticMarkdownPrefix+"/introduction.md" { 267 | panic("First entry of \"docs\" section of godocument.config.json must be:\n{\n \"docs\": {\n \"Introduction\": \"/introduction.md\"\n }\n}") 268 | } 269 | } 270 | 271 | // GetTitle reads the godocument.config.json file and returns the title 272 | func GetTitle() string { 273 | file, err := os.ReadFile(JSONConfigPath) 274 | if err != nil { 275 | panic(err) 276 | } 277 | var result map[string]interface{} 278 | err = json.Unmarshal(file, &result) 279 | if err != nil { 280 | panic(err) 281 | } 282 | metaData := result["meta"].(map[string]interface{}) 283 | title := "" 284 | for key, value := range metaData { 285 | if key == "title" { 286 | title = value.(string) 287 | } 288 | } 289 | return title 290 | } 291 | -------------------------------------------------------------------------------- /static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 11 | 13 | 15 | 18 | 19 | 21 | 24 | 25 | 27 | 30 | 31 | 33 | 36 | 37 | 39 | 42 | 43 | 49 | 50 | 55 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 108 | Godocument 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /internal/filewriter/filewriter.go: -------------------------------------------------------------------------------- 1 | package filewriter 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "godocument/internal/config" 7 | "godocument/internal/stypes" 8 | "io" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | 15 | "github.com/PuerkitoBio/goquery" 16 | "github.com/tdewolff/minify/v2" 17 | "github.com/tdewolff/minify/v2/css" 18 | "github.com/tdewolff/minify/v2/html" 19 | "github.com/tdewolff/minify/v2/js" 20 | "github.com/tdewolff/minify/v2/json" 21 | "github.com/tdewolff/minify/v2/svg" 22 | "github.com/tdewolff/minify/v2/xml" 23 | ) 24 | 25 | // GenerateDynamicNavbar generates the dynamic navbar based on ./godocument.config.json 26 | func GenerateDynamicNavbar(cnf stypes.DocConfig) { 27 | removeGeneratedNavbar() 28 | html := ` 29 | " 51 | writeNavbarHTML(html) 52 | } 53 | 54 | // workOnNavbar is a recursive function that generates the navbar html 55 | func workOnNavbar(node stypes.DocNode, html string) string { 56 | switch n := node.(type) { 57 | case *stypes.ObjectNode: 58 | // to ensure tailwind classes are caught during build, we need to include this string 59 | // tailwind will find this sting and include all these classes in our output.css 60 | // this is needed because we are using integer values for padding in the navbar 61 | // and tailwind does not include these classes by default 62 | // for example, in the fmt.Sprintf() below we have included pl-%d which will be replaced by the integer value 63 | _ = "pl-0 pl-1 pl-2 pl-3 pl-4 pl-5 pl-6 pl-7" 64 | innerHTML := "" 65 | for i := 0; i < len(n.Children); i++ { 66 | innerHTML = workOnNavbar(n.Children[i], innerHTML) 67 | } 68 | leftPadding := 0 69 | if n.BaseNodeData.Depth > 0 { 70 | leftPadding = n.BaseNodeData.Depth + 2 71 | } 72 | html += fmt.Sprintf(` 73 | 86 | `, leftPadding, n.BaseNodeData.Name, innerHTML) 87 | case *stypes.MarkdownNode: 88 | leftPadding := 0 89 | if n.BaseNodeData.Depth > 0 { 90 | leftPadding = n.BaseNodeData.Depth + 2 91 | } 92 | html += fmt.Sprintf(` 93 |
  • 94 | %s 95 |
  • 96 | `, leftPadding, n.RouterPath, n.BaseNodeData.Name) 97 | } 98 | return html 99 | } 100 | 101 | // writeNavbarHTML writes the generated navbar html to ./template/generated-nav.html 102 | func writeNavbarHTML(html string) { 103 | f, err := os.Create(config.GeneratedNavPath) 104 | if err != nil { 105 | panic(err) 106 | } 107 | defer f.Close() 108 | _, err = f.WriteString("\n") 109 | if err != nil { 110 | panic(err) 111 | } 112 | _, err = f.WriteString(html) 113 | if err != nil { 114 | panic(err) 115 | } 116 | } 117 | 118 | // removes the generated navbar between builds 119 | func removeGeneratedNavbar() { 120 | if _, err := os.Stat(config.GeneratedNavPath); err == nil { 121 | if err := os.Remove(config.GeneratedNavPath); err != nil { 122 | panic(err) 123 | } 124 | } else if os.IsNotExist(err) { 125 | } else { 126 | panic(err) 127 | } 128 | } 129 | 130 | // WARNING: This function will reset the project to its initial state. 131 | // This will delete all progress and cannot be undone. 132 | // Use with caution. 133 | func ResetProject() { 134 | resetDocsDir() 135 | resetOutDir() 136 | resetGodocumentConfig() 137 | removeGeneratedNavbar() 138 | } 139 | 140 | // GenerateStaticAssets generates all static assets for ./out using godocument.config.json 141 | func GenerateStaticAssets(cnf stypes.DocConfig, absolutePath string, port string) { 142 | m := prepareMinify() 143 | resetOutDir() 144 | copyDir(config.DevStaticPrefix, config.ProdStaticPrefix) 145 | copyOverFavicon() 146 | bundleCSSFiles() 147 | generateStaticHTML(cnf, absolutePath, port) 148 | minifyStaticFiles(m, config.StaticAssetsDir) 149 | } 150 | 151 | func removeUnusedCSSLinks(doc *goquery.Document) { 152 | doc.Find("link").Each(func(i int, s *goquery.Selection) { 153 | href, exists := s.Attr("href") 154 | if exists { 155 | if strings.Contains(href, "output.css") { 156 | s.Remove() 157 | } 158 | } 159 | }) 160 | } 161 | 162 | // bundleCSSFiles bundles all css files in ./out/css into a single file 163 | // also removes all css files except the bundled file 164 | func bundleCSSFiles() { 165 | cssFiles := []string{ 166 | config.ProdStaticPrefix + "/css/index.css", 167 | config.ProdStaticPrefix + "/css/output.css", 168 | } 169 | bundle := "" 170 | for _, file := range cssFiles { 171 | f, err := os.Open(file) 172 | if err != nil { 173 | panic(err) 174 | } 175 | defer f.Close() 176 | fileBytes, err := io.ReadAll(f) 177 | if err != nil { 178 | panic(err) 179 | } 180 | bundle += string(fileBytes) 181 | os.Remove(file) 182 | } 183 | f, err := os.Create(config.ProdStaticPrefix + "/css/index.css") 184 | if err != nil { 185 | panic(err) 186 | } 187 | defer f.Close() 188 | _, err = f.WriteString(bundle) 189 | if err != nil { 190 | panic(err) 191 | } 192 | os.Remove(config.ProdStaticPrefix + "/css/input.css") 193 | } 194 | 195 | // takes the favicon from ./ and moves it to ./out 196 | func copyOverFavicon() { 197 | src := "./favicon.ico" 198 | dst := config.StaticAssetsDir + "/favicon.ico" 199 | err := copyFile(src, dst) 200 | if err != nil { 201 | panic("error moving favicon from " + src + " to " + dst) 202 | } 203 | } 204 | 205 | // prepareMinify prepares the minify.M object with all the minification functions 206 | func prepareMinify() *minify.M { 207 | m := minify.New() 208 | m.AddFunc("text/css", css.Minify) 209 | m.AddFunc("text/html", html.Minify) 210 | m.AddFunc("image/svg+xml", svg.Minify) 211 | m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify) 212 | m.AddFuncRegexp(regexp.MustCompile("[/+]json$"), json.Minify) 213 | m.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify) 214 | return m 215 | } 216 | 217 | // ResetDocsDir resets the ./docs directory to its initial state 218 | func resetDocsDir() { 219 | err := os.RemoveAll(config.StaticMarkdownPrefix) 220 | if err != nil { 221 | fmt.Printf("Error removing directory: %s\n", err) 222 | return 223 | } 224 | if _, err := os.Stat(config.StaticMarkdownPrefix); os.IsNotExist(err) { 225 | err := os.Mkdir(config.StaticMarkdownPrefix, 0755) 226 | if err != nil { 227 | fmt.Printf("Error creating directory: %s\n", err) 228 | return 229 | } 230 | } 231 | filePath := fmt.Sprintf("%s/introduction.md", config.StaticMarkdownPrefix) 232 | content := "# Introduction\n\n## Hello, World\nGenerated using `go run main.go --reset`. Edit `./docs/introduction.md` and see the changes here!" 233 | err = os.WriteFile(filePath, []byte(content), 0644) 234 | if err != nil { 235 | fmt.Printf("Error writing to file: %s\n", err) 236 | } 237 | } 238 | 239 | // ResetOutDir resets the ./out directory to its initial state 240 | func resetOutDir() { 241 | err := os.RemoveAll(config.StaticAssetsDir) 242 | if err != nil { 243 | fmt.Printf("Error removing directory: %s\n", err) 244 | return 245 | } 246 | if _, err := os.Stat(config.StaticAssetsDir); os.IsNotExist(err) { 247 | err := os.Mkdir(config.StaticAssetsDir, 0755) 248 | if err != nil { 249 | fmt.Printf("Error creating directory: %s\n", err) 250 | return 251 | } 252 | } 253 | } 254 | 255 | // ResetGodocumentConfig resets the ./godocument.config.json file to its initial state 256 | func resetGodocumentConfig() { 257 | path := config.JSONConfigPath 258 | jsonData := "{\n\t\"docs\": {\n\t\t\"Introduction\": \"/introduction.md\"\n\t},\n\t\"meta\": {\n\t\t\"title\": \"Godocument\"\n\t}\n}" 259 | 260 | // Write the JSON data to the file, creating it if it doesn't exist 261 | err := os.WriteFile(path, []byte(jsonData), 0644) 262 | if err != nil { 263 | panic(err) 264 | } 265 | } 266 | 267 | func makeRequiredStaticDirs(staticAssetPath string) { 268 | dirPath := filepath.Dir(staticAssetPath) 269 | if err := os.MkdirAll(dirPath, 0755); err != nil { 270 | fmt.Printf("Failed to create directories: %v", err) 271 | return 272 | } 273 | } 274 | 275 | func createStaticAssetFile(staticAssetPath string) *os.File { 276 | f, err := os.Create(staticAssetPath) 277 | if err != nil { 278 | fmt.Printf("Error creating file: %s\n", err) 279 | return nil 280 | } 281 | return f 282 | } 283 | 284 | // takes our godocument.config.json and generates static html files from it 285 | func generateStaticHTML(cnf stypes.DocConfig, absolutePath string, port string) { 286 | config.WorkOnMarkdownNodes(cnf, func(n *stypes.MarkdownNode) { 287 | client := &http.Client{} 288 | res, err := client.Get("http://localhost:" + port + n.RouterPath) 289 | if err != nil { 290 | panic(err) 291 | } 292 | defer res.Body.Close() 293 | body, err := io.ReadAll(res.Body) 294 | if err != nil { 295 | fmt.Printf("Error reading body: %s\n", err) 296 | return 297 | } 298 | makeRequiredStaticDirs(n.StaticAssetPath) 299 | f := createStaticAssetFile(n.StaticAssetPath) 300 | if f == nil { 301 | return 302 | } 303 | defer f.Close() 304 | doc, err := getQueryDoc(body) 305 | if err != nil { 306 | fmt.Printf("Error *goquery.Document from res.Body(): %s\n", err) 307 | return 308 | } 309 | modifyAnchorTagsForStatic(doc, absolutePath) 310 | setOtherAbsolutePaths(doc, absolutePath) 311 | removeUnusedCSSLinks(doc) 312 | htmlString, err := doc.Html() 313 | if err != nil { 314 | fmt.Printf("Error converting doc to html: %s\n", err) 315 | return 316 | } 317 | body = []byte(htmlString) 318 | _, err = f.Write(body) 319 | if err != nil { 320 | fmt.Printf("Error writing body to file: %s\n", err) 321 | return 322 | } 323 | }) 324 | } 325 | 326 | // getQueryDoc returns a *goquery.Document to parse and modify html 327 | func getQueryDoc(body []byte) (*goquery.Document, error) { 328 | doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body)) 329 | if err != nil { 330 | return nil, err 331 | } 332 | return doc, nil 333 | } 334 | 335 | // prepares all anchor tags in the static html files to point to the correct .html file 336 | // also converts relative paths to absolute paths 337 | func modifyAnchorTagsForStatic(doc *goquery.Document, absolutePath string) { 338 | doc.Find("a").Each(func(i int, s *goquery.Selection) { 339 | href, exists := s.Attr("href") 340 | if exists { 341 | if len(href) > 3 && href[0:4] == "http" { 342 | return 343 | } 344 | if href[0] == '#' { 345 | return 346 | } 347 | if href == "/" { 348 | s.SetAttr("href", absolutePath+"/") 349 | return 350 | } 351 | 352 | s.SetAttr("href", absolutePath+href+".html") 353 | } 354 | }) 355 | } 356 | 357 | // finds all local paths to static assets (other than anchor links) and converts them to absolute paths 358 | func setOtherAbsolutePaths(doc *goquery.Document, absolutePath string) { 359 | doc.Find("link").Each(func(i int, s *goquery.Selection) { 360 | href, exists := s.Attr("href") 361 | if exists { 362 | if len(href) > 3 && href[0:4] == "http" { 363 | return 364 | } 365 | s.SetAttr("href", absolutePath+href) 366 | } 367 | }) 368 | doc.Find("script").Each(func(i int, s *goquery.Selection) { 369 | src, exists := s.Attr("src") 370 | if exists { 371 | if len(src) > 3 && src[0:4] == "http" { 372 | return 373 | } 374 | s.SetAttr("src", absolutePath+src) 375 | } 376 | }) 377 | doc.Find("img").Each(func(i int, s *goquery.Selection) { 378 | src, exists := s.Attr("src") 379 | if exists { 380 | if len(src) > 3 && src[0:4] == "http" { 381 | return 382 | } 383 | s.SetAttr("src", absolutePath+src) 384 | } 385 | }) 386 | } 387 | 388 | // minifyStaticFiles minifies all static files in the ./out directory 389 | func minifyStaticFiles(m *minify.M, dirPath string) { 390 | err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { 391 | if err != nil { 392 | return err 393 | } 394 | if info.IsDir() { 395 | return nil 396 | } 397 | ext := filepath.Ext(path) 398 | var mimetype string 399 | switch ext { 400 | case ".css": 401 | mimetype = "text/css" 402 | case ".html": 403 | mimetype = "text/html" 404 | case ".js": 405 | mimetype = "application/javascript" 406 | case ".json": 407 | mimetype = "application/json" 408 | case ".svg": 409 | mimetype = "image/svg+xml" 410 | case ".xml": 411 | mimetype = "text/xml" 412 | default: 413 | return nil 414 | } 415 | f, err := os.Open(path) 416 | if err != nil { 417 | fmt.Printf("Error opening file: %s\n", err) 418 | return err 419 | } 420 | defer f.Close() 421 | fileBytes, err := io.ReadAll(f) 422 | if err != nil { 423 | fmt.Printf("Error reading file: %s\n", err) 424 | return err 425 | } 426 | minifiedBytes, err := m.Bytes(mimetype, fileBytes) 427 | if err != nil { 428 | fmt.Printf("Error minifying file: %s\n", err) 429 | return err 430 | } 431 | err = os.WriteFile(path, minifiedBytes, info.Mode()) // Preserving original file permissions 432 | if err != nil { 433 | fmt.Printf("Error writing minified file: %s\n", err) 434 | return err 435 | } 436 | return nil 437 | }) 438 | if err != nil { 439 | fmt.Printf("Error walking the directory: %s\n", err) 440 | } 441 | } 442 | 443 | // copyFile copies a single file from src to dst. 444 | func copyFile(src, dst string) error { 445 | srcFile, err := os.Open(src) 446 | if err != nil { 447 | return err 448 | } 449 | defer srcFile.Close() 450 | 451 | dstFile, err := os.Create(dst) 452 | if err != nil { 453 | return err 454 | } 455 | defer dstFile.Close() 456 | 457 | if _, err := io.Copy(dstFile, srcFile); err != nil { 458 | return err 459 | } 460 | 461 | srcInfo, err := srcFile.Stat() 462 | if err != nil { 463 | return err 464 | } 465 | 466 | return os.Chmod(dstFile.Name(), srcInfo.Mode()) 467 | } 468 | 469 | // copyDir recursively copies a directory tree, attempting to preserve permissions. 470 | // Source directory must exist, destination directory must *not* exist. 471 | func copyDir(src string, dst string) error { 472 | src = filepath.Clean(src) 473 | dst = filepath.Clean(dst) 474 | si, err := os.Stat(src) 475 | if err != nil { 476 | return err 477 | } 478 | if !si.IsDir() { 479 | return fmt.Errorf("source is not a directory") 480 | } 481 | _, err = os.Stat(dst) 482 | if err != nil && !os.IsNotExist(err) { 483 | return err 484 | } 485 | if err == nil { 486 | return fmt.Errorf("destination already exists") 487 | } 488 | err = os.MkdirAll(dst, si.Mode()) 489 | if err != nil { 490 | return err 491 | } 492 | entries, err := os.ReadDir(src) 493 | if err != nil { 494 | return err 495 | } 496 | for _, entry := range entries { 497 | srcPath := filepath.Join(src, entry.Name()) 498 | dstPath := filepath.Join(dst, entry.Name()) 499 | 500 | if entry.IsDir() { 501 | err = copyDir(srcPath, dstPath) 502 | if err != nil { 503 | return err 504 | } 505 | } else { 506 | err = copyFile(srcPath, dstPath) 507 | if err != nil { 508 | return err 509 | } 510 | } 511 | } 512 | return nil 513 | } 514 | -------------------------------------------------------------------------------- /static/css/index.css: -------------------------------------------------------------------------------- 1 | 2 | /* handling scroll ------------------------------------------------------------- */ 3 | 4 | html { 5 | scroll-padding-top: 75px; 6 | } 7 | 8 | /* variables ----------------------------------------------------------------- */ 9 | 10 | :root { 11 | 12 | /* utility ----------------------------------------------------------------- */ 13 | 14 | /* font sizes */ 15 | --text-base: 16px; 16 | --text-2xs: 0.75rem; 17 | --text-xs: 1rem; 18 | --text-sm: 1.25rem; 19 | --text-md: 1.5rem; 20 | --text-lg: 1.75rem; 21 | --text-xl: 2rem; 22 | --text-2xl: 3rem; 23 | 24 | /* border radius */ 25 | --rounded-sm: 0.25rem; 26 | --rounded-md: 0.5rem; 27 | 28 | /* colors */ 29 | --white: #fafafa; 30 | --black: #1f1f1f; 31 | --gray-800: #333333; 32 | --gray-700: #555555; 33 | --gray-600: #777777; 34 | --gray-500: #999999; 35 | --gray-400: #bbbbbb; 36 | --gray-300: #dddddd; 37 | --gray-200: #f0f0f0; 38 | --gray-100: #f5f5f5; 39 | --light-gray: #d0d0d0; 40 | --gray: #555555; 41 | --darkest-gray: #222222; 42 | --purple: #ba8ef7; 43 | --dark-purple: #712fec; 44 | --green: #3bec74; 45 | --dark-green: #057d2f; 46 | --pink: #b370b1; 47 | --yellow: #ffea6b; 48 | --dark-yellow: #7f7108; 49 | --orange: #ffa763; 50 | --dark-orange: #c64719; 51 | --blue: #2494da; 52 | --dark-blue: #1b6dbf; 53 | --red: #ff4d3f; 54 | --dark-red: #c82216; 55 | 56 | /* element specific variables ----------------------------------------------------------------- */ 57 | 58 | /* border colors */ 59 | --b-color: var(--gray-300); 60 | --dark-b-color: var(--gray-800); 61 | --b-hover-color: var(--gray-400); 62 | --dark-b-hover-color: var(--gray); 63 | 64 | /* text colors */ 65 | --default-text-color: var(--black); 66 | --dark-default-text-color: var(--white); 67 | --text-important: var(--dark-blue); 68 | --dark-text-important: var(--blue); 69 | 70 | /* background colors */ 71 | --default-bg-color: var(--white); 72 | --dark-default-bg-color: var(--black); 73 | --bg-hover-color: var(--gray-200); 74 | --dark-bg-hover-color: var(--gray-800); 75 | 76 | /* header */ 77 | --header-bg-color: var(--white); 78 | --dark-header-bg-color: var(--black); 79 | 80 | /* footer */ 81 | --footer-bg-color: var(--white); 82 | --dark-footer-bg-color: var(--black); 83 | 84 | /* overlay */ 85 | --overlay-bg-color: var(--black); 86 | 87 | /* sitenav */ 88 | --sitenav-bg-color: var(--white); 89 | --dark-sitenav-bg-color: var(--black); 90 | 91 | /* pagenav */ 92 | --pagenav-bg-color: var(--white); 93 | --dark-pagenav-bg-color: var(--black); 94 | 95 | /* scroll colors */ 96 | --scroll-thumb-light: var(--gray-400); 97 | --scroll-thumb-dark: var(--gray-800); 98 | 99 | /* markdown text sizes */ 100 | --md-h1-size: var(--text-2xl); 101 | --md-h2-size: var(--text-xl); 102 | --md-h3-size: var(--text-lg); 103 | --md-h4-size: var(--text-md); 104 | --md-h5-size: var(--text-sm); 105 | --md-h6-size: var(--text-xs); 106 | --md-p-size: var(--text-xs); 107 | 108 | /* light inline code */ 109 | --inline-code-b-radius: var(--rounded-sm); 110 | --inline-code-bg-color: var(--gray-200); 111 | --inline-code-web-component-bg-color: var(--gray-300); 112 | --dark-inline-code-web-component-bg-color: var(--gray-700); 113 | 114 | /* dark inline code */ 115 | --dark-inline-code-bg-color: var(--gray-800); 116 | 117 | /* all code blocks */ 118 | --code-b-radius: var(--rounded-sm); 119 | 120 | /* light code blocks */ 121 | --code-bg-color: var(--gray-200); 122 | --code-token-property: var(--dark-purple); 123 | --code-string: var(--dark-green); 124 | --code-token-selector: var(--dark-orange); 125 | --code-function: var(--dark-yellow); 126 | --code-keyword: var(--dark-purple); 127 | --code-operator: var(--black); 128 | --code-punctuation: var(--gray-700); 129 | --code-important: var(--dark-orange); 130 | --code-comment: var(--gray-700); 131 | 132 | /* dark code blocks */ 133 | --dark-code-bg-color: var(--gray-800); 134 | --dark-code-token-property: var(--purple); 135 | --dark-code-string: var(--green); 136 | --dark-code-token-selector: var(--orange); 137 | --dark-code-function: var(--yellow); 138 | --dark-code-keyword: var(--purple); 139 | --dark-code-operator: var(--white); 140 | --dark-code-punctuation: var(--gray-300); 141 | --dark-code-comment: var(--gray-500); 142 | 143 | /* custom md elements */ 144 | --md-bg-color: var(--gray-200); 145 | --dark-md-bg-color: var(--gray-800); 146 | 147 | /* */ 148 | --md-important-text-color: var(--dark-blue); 149 | --dark-md-important-text-color: var(--blue); 150 | --md-important-border-color: var(--dark-blue); 151 | --dark-md-important-border-color: var(--blue); 152 | 153 | /* */ 154 | --md-warning-text-color: var(--dark-red); 155 | --dark-md-warning-text-color: var(--red); 156 | --md-warning-border-color: var(--dark-red); 157 | --dark-md-warning-border-color: var(--red); 158 | 159 | /* */ 160 | --md-correct-text-color: var(--dark-green); 161 | --dark-md-correct-text-color: var(--green); 162 | --md-correct-border-color: var(--dark-green); 163 | --dark-md-correct-border-color: var(--green); 164 | 165 | } 166 | 167 | /* grids -------------------------------------------------------------------- */ 168 | 169 | #root { 170 | grid-template-columns: 1fr; 171 | grid-template-rows: 75px 1fr auto; 172 | grid-template-areas: 173 | "header" 174 | "content-wrapper" 175 | "footer" 176 | ; 177 | } 178 | 179 | #main { 180 | grid-template-columns: 100%; 181 | grid-template-areas: 182 | "article" 183 | ; 184 | overflow: hidden; 185 | } 186 | 187 | #content-wrapper { 188 | grid-template-columns: auto; 189 | grid-template-areas: 190 | "main" 191 | ; 192 | } 193 | 194 | 195 | @media (min-width: 1024px) { 196 | 197 | #content-wrapper { 198 | grid-template-columns: 300px 1fr; 199 | grid-template-areas: 200 | "sitenav main" 201 | ; 202 | } 203 | 204 | #main { 205 | grid-template-columns: 70% 40%; 206 | grid-template-rows: auto; 207 | grid-template-areas: 208 | "article pagenav" 209 | ; 210 | } 211 | 212 | 213 | } 214 | 215 | 216 | /* markdown styles ----------------------------------------------------------- */ 217 | 218 | article h1 { 219 | font-size: var(--md-h1-size); 220 | font-weight: bold; 221 | } 222 | 223 | article ol { 224 | list-style-type: decimal; 225 | list-style-position: inside; 226 | } 227 | 228 | article ol li, article ul li { 229 | line-height: 1.75rem; 230 | } 231 | 232 | article ul { 233 | list-style-type: disc; 234 | list-style-position: inside; 235 | } 236 | 237 | article h2 { 238 | font-size: var(--md-h2-size); 239 | padding-top: 2rem; 240 | font-weight: 500; 241 | } 242 | 243 | article h3 { 244 | font-size: var(--md-h3-size); 245 | padding-top: 2rem; 246 | font-weight: 500; 247 | } 248 | 249 | article h4 { 250 | font-size: var(--md-h4-size); 251 | padding-top: 2rem; 252 | font-weight: 500; 253 | } 254 | 255 | article h5 { 256 | font-size: var(--md-h5-size); 257 | padding-top: 2rem; 258 | font-weight: 500; 259 | } 260 | 261 | article p { 262 | font-size: var(--md-p-size); 263 | line-height: 1.75rem; 264 | font-weight: 400; 265 | } 266 | 267 | article p strong { 268 | font-weight: bold; 269 | } 270 | 271 | article pre { 272 | border-radius: var(--code-b-radius); 273 | overflow-x: auto; 274 | white-space: pre; 275 | } 276 | 277 | article code { 278 | border-radius: var(--code-b-radius); 279 | } 280 | 281 | article a { 282 | color: var(--text-important); 283 | text-decoration: underline; 284 | } 285 | 286 | html.dark article a { 287 | color: var(--dark-text-important); 288 | } 289 | 290 | article * code { 291 | background-color: var(--inline-code-bg-color); 292 | border-radius: var(--inline-code-b-radius); 293 | padding: 0.25rem; 294 | } 295 | 296 | article p.custom-inline-code code { 297 | background-color: var(--inline-code-web-component-bg-color); 298 | } 299 | 300 | html.dark p.custom-inline-code code { 301 | background-color: var(--dark-inline-code-web-component-bg-color); 302 | } 303 | 304 | 305 | html.dark article * code { 306 | background-color: var(--dark-inline-code-bg-color); 307 | } 308 | 309 | article * code { 310 | padding: 0.25rem; 311 | } 312 | 313 | article pre code { 314 | padding: 0; 315 | } 316 | 317 | 318 | 319 | /* custom scroll ------------------------------------------------------------- */ 320 | 321 | 322 | .custom-scroll::-webkit-scrollbar { 323 | width: 0px; /* Hides the scrollbar */ 324 | } 325 | 326 | .sm-custom-scoll::-webkit-scrollbar { 327 | width: 0px; 328 | } 329 | 330 | @media (min-width: 992px) { 331 | 332 | .custom-scroll::-webkit-scrollbar { 333 | width: 12px; 334 | } 335 | 336 | .sm-scroll::-webkit-scrollbar { 337 | width: 4px; 338 | } 339 | 340 | .custom-scroll::-webkit-scrollbar-track { 341 | background: #f1f1f1; 342 | } 343 | 344 | .sm-scroll::-webkit-scrollbar-track { 345 | background: var(--white); 346 | } 347 | 348 | .custom-scroll::-webkit-scrollbar-thumb { 349 | background-color: var(--scroll-thumb-light); 350 | border-radius: 10px; 351 | } 352 | 353 | .custom-scroll::-webkit-scrollbar-thumb:hover { 354 | background-color: var(--gray); 355 | cursor: pointer; 356 | } 357 | 358 | html.dark .custom-scroll::-webkit-scrollbar-thumb { 359 | background: var(--scroll-thumb-dark); 360 | } 361 | 362 | html.dark .custom-scroll::-webkit-scrollbar-thumb:hover { 363 | background-color: var(--gray); 364 | cursor: pointer; 365 | } 366 | 367 | html.dark .custom-scroll::-webkit-scrollbar-track { 368 | background: var(--darkest-gray); 369 | } 370 | 371 | .custom-scroll::-webkit-scrollbar-thumb { 372 | background-color: var(--scroll-thumb-light); 373 | border-radius: 10px; 374 | } 375 | 376 | .custom-scroll::-webkit-scrollbar-thumb:hover { 377 | background-color: var(--gray); 378 | cursor: pointer; 379 | } 380 | 381 | html.dark .custom-scroll::-webkit-scrollbar-thumb { 382 | background: var(--scroll-thumb-dark); 383 | } 384 | 385 | html.dark .custom-scroll::-webkit-scrollbar-thumb:hover { 386 | background-color: var(--gray); 387 | cursor: pointer; 388 | } 389 | 390 | html.dark .custom-scroll::-webkit-scrollbar-track { 391 | background: var(--darkest-gray); 392 | } 393 | 394 | html.dark .sm-scroll::-webkit-scrollbar-track { 395 | background: var(--black); 396 | } 397 | 398 | } 399 | 400 | /* light codeblocks ----------------------------------------------------------- */ 401 | 402 | html.light code[class*="language-"], 403 | html.light pre[class*="language-"] { 404 | color: black; 405 | background: none; 406 | text-shadow: 0 1px white; 407 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 408 | font-size: 1em; 409 | text-align: left; 410 | white-space: pre; 411 | word-spacing: normal; 412 | word-break: normal; 413 | word-wrap: normal; 414 | line-height: 1.5; 415 | 416 | -moz-tab-size: 4; 417 | -o-tab-size: 4; 418 | tab-size: 4; 419 | 420 | -webkit-hyphens: none; 421 | -moz-hyphens: none; 422 | -ms-hyphens: none; 423 | hyphens: none; 424 | } 425 | 426 | html.light pre[class*="language-"]::-moz-selection, html.light pre[class*="language-"] ::-moz-selection, 427 | html.light code[class*="language-"]::-moz-selection, html.light code[class*="language-"] ::-moz-selection { 428 | text-shadow: none; 429 | } 430 | 431 | html.light pre[class*="language-"]::selection, html.light pre[class*="language-"] ::selection, 432 | html.light code[class*="language-"]::selection, html.light code[class*="language-"] ::selection { 433 | text-shadow: none; 434 | } 435 | 436 | @media print { 437 | html.light code[class*="language-"], 438 | html.light pre[class*="language-"] { 439 | text-shadow: none; 440 | } 441 | } 442 | 443 | /* Code blocks */ 444 | html.light pre[class*="language-"] { 445 | padding: 1em; 446 | overflow: auto; 447 | } 448 | 449 | html.light :not(pre) > code[class*="language-"], 450 | html.light pre[class*="language-"] { 451 | background: var(--code-bg-color); 452 | } 453 | 454 | /* Inline code */ 455 | html.light :not(pre) > code[class*="language-"] { 456 | padding: .1em; 457 | border-radius: .3em; 458 | white-space: normal; 459 | } 460 | 461 | html.light .token.prolog, 462 | html.light .token.doctype, 463 | html.light .token.cdata { 464 | color: var(--code-punctuation); 465 | } 466 | 467 | html.light .token.comment { 468 | color: var(--code-comment); 469 | } 470 | 471 | 472 | html.light .token.punctuation { 473 | color: var(--code-punctuation); 474 | } 475 | 476 | html.light .token.namespace { 477 | opacity: .7; 478 | } 479 | 480 | html.light .token.property, 481 | html.light .token.tag, 482 | html.light .token.boolean, 483 | html.light .token.number, 484 | html.light .token.constant, 485 | html.light .token.symbol, 486 | html.light .token.deleted { 487 | color: var(--code-token-property); 488 | } 489 | 490 | html.light .token.attr-name, 491 | html.light .token.string, 492 | html.light .token.variable, 493 | html.light .token.char, 494 | html.light .token.inserted { 495 | color: var(--code-string); 496 | } 497 | 498 | html.light .token.selector { 499 | color: var(--code-token-selector); 500 | } 501 | 502 | html.light .token.operator, 503 | html.light .token.entity, 504 | html.light .token.url, 505 | html.light .language-css .token.string, 506 | html.light .style .token.string { 507 | color: var(--code-operator); 508 | background: var(--codeblock-light-background); 509 | } 510 | 511 | html.light .token.atrule, 512 | html.light .token.attr-value, 513 | html.light .token.builtin, 514 | html.light .token.class-name, 515 | html.light .token.keyword { 516 | color: var(--code-keyword); 517 | } 518 | 519 | html.light .token.function { 520 | color: var(--code-function); 521 | } 522 | 523 | html.light .token.regex, 524 | html.light .token.important { 525 | color: var(--code-important); 526 | } 527 | 528 | html.light .token.important, 529 | html.light .token.bold { 530 | font-weight: bold; 531 | } 532 | html.light .token.italic { 533 | font-style: italic; 534 | } 535 | 536 | html.light .token.entity { 537 | cursor: help; 538 | } 539 | 540 | 541 | /* dark codeblocks ----------------------------------------------------------- */ 542 | 543 | 544 | html.dark code[class*="language-"], 545 | html.dark pre[class*="language-"] { 546 | color: #ccc; 547 | background: none; 548 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 549 | font-size: 1em; 550 | text-align: left; 551 | white-space: pre; 552 | word-spacing: normal; 553 | word-break: normal; 554 | word-wrap: normal; 555 | line-height: 1.5; 556 | 557 | -moz-tab-size: 4; 558 | -o-tab-size: 4; 559 | tab-size: 4; 560 | 561 | -webkit-hyphens: none; 562 | -moz-hyphens: none; 563 | -ms-hyphens: none; 564 | hyphens: none; 565 | } 566 | 567 | /* Code blocks */ 568 | html.dark pre[class*="language-"] { 569 | padding: 1em; 570 | overflow: auto; 571 | } 572 | 573 | html.dark :not(pre) > code[class*="language-"], 574 | html.dark pre[class*="language-"] { 575 | background: var(--dark-code-bg-color); 576 | } 577 | 578 | /* Inline code */ 579 | html.dark :not(pre) > code[class*="language-"] { 580 | padding: .1em; 581 | border-radius: .3em; 582 | white-space: normal; 583 | } 584 | 585 | html.dark .token.prolog, 586 | html.dark .token.doctype, 587 | html.dark .token.cdata { 588 | color: var(--dark-code-punctuation); 589 | } 590 | 591 | html.dark .token.block-comment, 592 | html.dark .token.comment { 593 | color: var(--dark-code-comment); 594 | } 595 | 596 | html.dark .token.punctuation { 597 | color: var(--dark-code-punctuation); 598 | } 599 | 600 | html.dark .token.tag, 601 | html.dark .token.attr-name, 602 | html.dark .token.namespace, 603 | html.dark .token.deleted { 604 | color: #e2777a; 605 | } 606 | 607 | html.dark .token.function-name { 608 | color: #6196cc; 609 | } 610 | 611 | html.dark .token.boolean, 612 | html.dark .token.number, 613 | html.dark .token.function { 614 | color: var(--dark-code-function); 615 | } 616 | 617 | html.dark .token.property, 618 | html.dark .token.class-name, 619 | html.dark .token.constant, 620 | html.dark .token.symbol { 621 | color: var(--dark-code-token-property); 622 | } 623 | 624 | html.dark .token.selector, 625 | html.dark .token.important, 626 | html.dark .token.atrule { 627 | color: var(--dark-code-token-selector); 628 | } 629 | 630 | html.dark .token.keyword, 631 | html.dark .token.builtin { 632 | color: var(--dark-code-keyword); 633 | } 634 | 635 | html.dark .token.string, 636 | html.dark .token.char, 637 | html.dark .token.attr-value, 638 | html.dark .token.regex, 639 | html.dark .token.variable { 640 | color: var(--dark-code-string); 641 | } 642 | 643 | html.dark .token.operator, 644 | html.dark .token.entity, 645 | html.dark .token.url { 646 | color: var(--dark-code-operator); 647 | } 648 | 649 | html.dark .token.important, 650 | html.dark .token.bold { 651 | font-weight: bold; 652 | } 653 | html.dark .token.italic { 654 | font-style: italic; 655 | } 656 | 657 | html.dark .token.entity { 658 | cursor: help; 659 | } 660 | 661 | html.dark .token.inserted { 662 | color: green; 663 | } 664 | 665 | div.code-toolbar { 666 | position: relative; 667 | } 668 | 669 | div.code-toolbar > .toolbar { 670 | position: absolute; 671 | z-index: 10; 672 | top: -0.2em; 673 | right: .2em; 674 | transition: opacity 0.3s ease-in-out; 675 | opacity: 1; 676 | } 677 | 678 | div.code-toolbar:hover > .toolbar { 679 | opacity: 1; 680 | } 681 | 682 | /* Separate line b/c rules are thrown out if selector is invalid. 683 | IE11 and old Edge versions don't support :focus-within. */ 684 | div.code-toolbar:focus-within > .toolbar { 685 | opacity: 1; 686 | } 687 | 688 | div.code-toolbar > .toolbar > .toolbar-item { 689 | display: inline-block; 690 | } 691 | 692 | div.code-toolbar > .toolbar > .toolbar-item > a { 693 | cursor: pointer; 694 | } 695 | 696 | div.code-toolbar > .toolbar > .toolbar-item > button { 697 | background: none; 698 | border: 0; 699 | color: inherit; 700 | font: inherit; 701 | line-height: normal; 702 | overflow: visible; 703 | padding: 0; 704 | -webkit-user-select: none; /* for button */ 705 | -moz-user-select: none; 706 | -ms-user-select: none; 707 | } 708 | 709 | div.code-toolbar > .toolbar > .toolbar-item > a, 710 | div.code-toolbar > .toolbar > .toolbar-item > button, 711 | div.code-toolbar > .toolbar > .toolbar-item > span { 712 | color: #bbb; 713 | font-size: .8em; 714 | padding: 0 .5em; 715 | background: #f5f2f0; 716 | background: rgba(224, 224, 224, 0.2); 717 | box-shadow: 0 2px 0 0 rgba(0,0,0,0.2); 718 | border-radius: .5em; 719 | } 720 | 721 | div.code-toolbar > .toolbar > .toolbar-item > a:hover, 722 | div.code-toolbar > .toolbar > .toolbar-item > a:focus, 723 | div.code-toolbar > .toolbar > .toolbar-item > button:hover, 724 | div.code-toolbar > .toolbar > .toolbar-item > button:focus, 725 | div.code-toolbar > .toolbar > .toolbar-item > span:hover, 726 | div.code-toolbar > .toolbar > .toolbar-item > span:focus { 727 | color: inherit; 728 | text-decoration: none; 729 | } -------------------------------------------------------------------------------- /static/js/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | README 3 | Here is how this script works. 4 | We are using hx-boost (https://htmx.org/attributes/hx-boost/) to load pages without refreshing the entire page. 5 | This means the of our document is never reloaded. 6 | The is loaded on the initial page load, and then after navigating, the new HTML content is loaded into the of the document. 7 | At the bottom of this file, we are referencing the htmx event "htmx:afterOnLoad" to run the script after the new content is loaded. 8 | So, on the initial page load, we use the DOMContentLoaded event to run the script, and then after navigating, we use htmx:afterOnLoad to run the script. 9 | This changes the way we have to hook events 10 | Everytime you navigate, you have to detach and re-hook events to elements 11 | Failing to detach events will cause the event to be fired multiple times 12 | The utility function eReset(node, eventType, callback) is used to detach and re-hook events 13 | */ 14 | 15 | 16 | 17 | (() => { 18 | 19 | let isPopstate = false 20 | 21 | if (localStorage.getItem('popstate') == 'true') { 22 | isPopstate = true 23 | document.getElementsByTagName('html')[0].setAttribute('loaded', 'false') 24 | } 25 | 26 | if (document.getElementsByTagName('html')[0].getAttribute('loaded') == 'false') { 27 | 28 | function qs(root, selector) { 29 | if (!root) { 30 | console.error('Root is not defined in qs()') 31 | } 32 | return root.querySelector(selector) 33 | } 34 | 35 | function qsa(root, selector) { 36 | if (!root) { 37 | console.error('Root is not defined in qsa()') 38 | } 39 | return root.querySelectorAll(selector) 40 | } 41 | 42 | function climbTreeUntil(node, stopNode, callback) { 43 | if (node) { 44 | if (node == stopNode) { 45 | return 46 | } 47 | let exit = callback(node) 48 | if (exit == true) { 49 | return 50 | } 51 | climbTreeUntil(node.parentNode, stopNode, callback) 52 | } 53 | } 54 | 55 | function eReset(node, eventType, callback) { 56 | node.removeEventListener(eventType, callback) 57 | node.addEventListener(eventType, callback) 58 | } 59 | 60 | function rememberPopState() { 61 | localStorage.setItem('popstate', 'true') 62 | } 63 | 64 | function forgetPopState() { 65 | localStorage.setItem('popstate', 'false') 66 | } 67 | 68 | // ============================================================================== 69 | 70 | class Zez { 71 | getState(node, key) { 72 | return node.getAttribute("zez:" + key).split(" ") 73 | } 74 | applyState(node, stateKey) { 75 | let state = this.getState(node, stateKey) 76 | for (let i = 0; i < state.length; i++) { 77 | node.classList.add(state[i]) 78 | } 79 | } 80 | removeState(node, key) { 81 | let state = this.getState(node, key) 82 | let classListArray = Array.from(node.classList) 83 | for (let i = 0; i < state.length; i++) { 84 | let index = classListArray.indexOf(state[i]) 85 | if (index > -1) { 86 | classListArray.splice(index, 1) 87 | } 88 | 89 | } 90 | node.classList = classListArray.join(' ') 91 | } 92 | containsState(node, key) { 93 | let state = this.getState(node, key) 94 | let classListArray = Array.from(node.classList) 95 | for (let i = 0; i < state.length; i++) { 96 | if (classListArray.includes(state[i])) { 97 | return true 98 | } 99 | } 100 | return false 101 | } 102 | applyStateAll(nodes, key) { 103 | for (let i = 0; i < nodes.length; i++) { 104 | this.applyState(nodes[i], key) 105 | } 106 | } 107 | toggleState(node, key) { 108 | let containsState = this.containsState(node, key) 109 | if (containsState) { 110 | this.removeState(node, key) 111 | } else { 112 | this.applyState(node, key) 113 | } 114 | } 115 | swapStates(node, key1, key2) { 116 | if (this.containsState(node, key1)) { 117 | this.enforceState(node, key2, key1) 118 | } else { 119 | this.enforceState(node, key1, key2) 120 | } 121 | } 122 | toggleStateAll(nodes, key) { 123 | for (let i = 0; i < nodes.length; i++) { 124 | this.toggleState(nodes[i], key) 125 | } 126 | } 127 | enforceState(node, keyToApply, keyToRemove) { 128 | this.applyState(node, keyToApply) 129 | this.removeState(node, keyToRemove) 130 | } 131 | } 132 | 133 | let zez = new Zez() 134 | 135 | // ============================================================================== 136 | 137 | class SiteNav { 138 | constructor(sitenav, sitenavItems, sitenavDropdowns) { 139 | this.sitenav = sitenav 140 | this.sitenavItems = sitenavItems 141 | this.sitenavDropdowns = sitenavDropdowns 142 | this.hook() 143 | } 144 | hook() { 145 | this.setActiveNavItem() 146 | for (let i = 0; i < this.sitenavDropdowns.length; i++) { 147 | eReset(qs(this.sitenavDropdowns[i], 'button'), "click", this.toggleDropdown.bind(this)) 148 | } 149 | } 150 | toggleDropdown(e) { 151 | let dropdown = null 152 | climbTreeUntil(e.target, this.sitenav, (node) => { 153 | if (node.tagName == 'LI') { 154 | dropdown = node 155 | return true 156 | } 157 | }) 158 | let hiddenChildren = qs(dropdown, 'ul') 159 | let caret = qs(dropdown, 'div') 160 | zez.toggleStateAll([caret, hiddenChildren], 'active') 161 | } 162 | setActiveNavItem() { 163 | for (let i = 0; i < this.sitenavItems.length; i++) { 164 | let item = this.sitenavItems[i] 165 | let href = item.getAttribute('href') 166 | if (href == window.location.pathname || href == window.location.href) { 167 | zez.applyState(item, 'active') 168 | climbTreeUntil(item, this.sitenav, (node) => { 169 | if (node.classList.contains('dropdown')) { 170 | let hiddenChildren = qs(node, 'ul') 171 | let caret = qs(node, 'div') 172 | let summary = qs(node, 'summary') 173 | zez.applyStateAll([summary, caret, hiddenChildren], 'active') 174 | } 175 | }) 176 | } 177 | } 178 | } 179 | } 180 | 181 | // ============================================================================== 182 | 183 | class PageNav { 184 | constructor(pagenav, pagenavLinks, articleTitles) { 185 | this.pagenav = pagenav 186 | this.pagenavLinks = pagenavLinks 187 | this.articleTitles = articleTitles 188 | this.windowTimeout = null 189 | this.bufferZone = 200 190 | this.activeLink = null 191 | this.hook() 192 | } 193 | hook() { 194 | this.setActivePageNavItem() 195 | eReset(window, "scroll", this.handleWindowScroll.bind(this)) 196 | } 197 | setActivePageNavItem() { 198 | if (this.pagenavLinks.length == 0 || this.articleTitles.length == 0) { 199 | return 200 | } 201 | for (let i = 0; i < this.articleTitles.length; i++) { 202 | let link = this.pagenavLinks[i] 203 | let nextLink = this.pagenavLinks[i + 1] 204 | let title = this.articleTitles[i] 205 | let titlePos = title.getBoundingClientRect().top 206 | let nextTitle = this.articleTitles[i + 1] 207 | let nextTitlePos = nextTitle ? nextTitle.getBoundingClientRect().top : 0 208 | if (i == 0 && titlePos > 0) { 209 | this.activeLink = link 210 | break 211 | } 212 | if (i == this.articleTitles.length - 1 && titlePos < 0) { 213 | this.activeLink = link 214 | break 215 | } 216 | if (titlePos < 0 && nextTitlePos > 0) { 217 | if (nextTitlePos < this.bufferZone) { 218 | this.activeLink = nextLink 219 | continue 220 | } 221 | this.activeLink = link 222 | } 223 | } 224 | for (let i = 0; i < this.pagenavLinks.length; i++) { 225 | let link = this.pagenavLinks[i] 226 | if (link == this.activeLink) { 227 | zez.applyState(link, 'active') 228 | } else { 229 | zez.removeState(link, 'active') 230 | } 231 | } 232 | } 233 | handleWindowScroll() { 234 | clearTimeout(this.windowTimeout); 235 | this.windowTimeout = setTimeout(() => { 236 | this.setActivePageNavItem() 237 | }, 100); 238 | } 239 | } 240 | 241 | // ============================================================================== 242 | 243 | class Header { 244 | constructor(headerBars, overlay, sitenav) { 245 | this.headerBars = headerBars 246 | this.overlay = overlay 247 | this.sitenav = sitenav 248 | this.hook() 249 | } 250 | hook() { 251 | eReset(this.headerBars, "click", this.toggleMobileNav.bind(this)) 252 | eReset(this.overlay, "click", this.toggleMobileNav.bind(this)) 253 | this.closeMobileNav() 254 | } 255 | toggleMobileNav() { 256 | zez.toggleState(this.overlay, 'active') 257 | zez.toggleState(this.sitenav, 'active') 258 | } 259 | closeMobileNav() { 260 | zez.removeState(this.overlay, 'active') 261 | zez.removeState(this.sitenav, 'active') 262 | } 263 | } 264 | 265 | // ============================================================================== 266 | 267 | class Theme { 268 | constructor(sunIcons, moonIcons, htmlDocument) { 269 | this.sunIcons = sunIcons 270 | this.moonIcons = moonIcons 271 | this.htmlDocument = htmlDocument 272 | this.hook() 273 | } 274 | hook() { 275 | this.initTheme() 276 | for (let i = 0; i < this.sunIcons.length; i++) { 277 | eReset(this.sunIcons[i], "click", this.toggleTheme.bind(this)) 278 | } 279 | for (let i = 0; i < this.moonIcons.length; i++) { 280 | eReset(this.moonIcons[i], "click", this.toggleTheme.bind(this)) 281 | } 282 | } 283 | initTheme() { 284 | let theme = localStorage.getItem('theme') 285 | if (theme) { 286 | if (theme == 'dark') { 287 | zez.enforceState(this.htmlDocument, 'dark', 'light') 288 | return 289 | } 290 | zez.enforceState(this.htmlDocument, 'light', 'dark') 291 | return 292 | } 293 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 294 | zez.enforceState(this.htmlDocument, 'dark', 'light') 295 | } else { 296 | zez.enforceState(this.htmlDocument, 'light', 'dark') 297 | } 298 | } 299 | toggleTheme() { 300 | zez.swapStates(this.htmlDocument, 'dark', 'light') 301 | if (zez.containsState(this.htmlDocument, 'dark')) { 302 | localStorage.setItem('theme', 'dark') 303 | return 304 | } 305 | localStorage.setItem('theme', 'light') 306 | } 307 | } 308 | 309 | // ============================================================================== 310 | 311 | class CustomComponents { 312 | initComponent(node) { 313 | node.parentElement.replaceWith(node) 314 | let text = node.innerHTML 315 | text = this.replaceBackticksWithCodeTags(text) 316 | return text 317 | } 318 | replaceBackticksWithCodeTags(text) { 319 | for (let i = 0; i < text.length; i++) { 320 | if (text[i] == '`') { 321 | text = text.slice(0, i) + '' + text.slice(i + 1) 322 | i++ 323 | while (i < text.length && text[i] != '`') { 324 | i++ 325 | } 326 | text = text.slice(0, i) + '' + text.slice(i + 1) 327 | } 328 | } 329 | return text 330 | } 331 | registerComponent(className, htmlContent) { 332 | let elements = document.getElementsByClassName(className) 333 | for (let i = 0; i < elements.length; i++) { 334 | let node = elements[i] 335 | let text = this.initComponent(node) 336 | node.innerHTML = htmlContent.replace('{text}', text) 337 | } 338 | } 339 | } 340 | 341 | // ============================================================================== 342 | 343 | class MdImportant { 344 | constructor(customComponents) { 345 | customComponents.registerComponent("md-important", ` 346 |
    347 | 348 | 349 | 352 | 353 |

    Important

    354 |
    355 |

    {text}

    356 |
    357 | `) 358 | } 359 | } 360 | 361 | // ============================================================================== 362 | 363 | class MdWarning { 364 | constructor(customComponents) { 365 | customComponents.registerComponent("md-warning", ` 366 |
    367 | 368 | 369 | 373 | 374 | 375 |

    Warning

    376 |
    377 |

    {text}

    378 |
    379 | `) 380 | } 381 | } 382 | 383 | // ============================================================================== 384 | 385 | class MdCorrect { 386 | constructor(customComponents) { 387 | customComponents.registerComponent("md-correct", ` 388 |
    389 | 390 | 391 | 394 | 395 |

    Correct

    396 |
    397 |

    {text}

    398 |
    399 | `) 400 | } 401 | } 402 | 403 | // ============================================================================== 404 | 405 | function onLoad(e) { 406 | 407 | // handling backwards navigation 408 | if (e.type == 'popstate') { 409 | zez.removeState(document.getElementsByTagName('body')[0], 'loaded') 410 | rememberPopState() 411 | return 412 | } 413 | 414 | // elements 415 | const body = qs(document, 'body') 416 | const sitenav = qs(document, '#sitenav') 417 | const sitenavItems = qsa(sitenav, '.item') 418 | const sitenavDropdowns = qsa(sitenav, '.dropdown') 419 | const pagenav = qs(document, '#pagenav') 420 | const pagenavLinks = qsa(pagenav, 'a') 421 | const article = qs(document, '#article') 422 | const articleTitles = qsa(article, 'h2, h3, h4, h5, h6') 423 | const header = qs(document, '#header') 424 | const headerBars = qs(header, '#bars') 425 | const overlay = qs(document, '#overlay') 426 | const sunIcons = qsa(document, '.sun') 427 | const moonIcons = qsa(document, '.moon') 428 | const htmlDocument = qs(document, 'html') 429 | 430 | // hooking events and running initializations 431 | window.scrollTo(0, 0, { behavior: 'auto' }) 432 | new SiteNav(sitenav, sitenavItems, sitenavDropdowns, header, overlay) 433 | new PageNav(pagenav, pagenavLinks, articleTitles) 434 | new Header(headerBars, overlay, sitenav) 435 | new Theme(sunIcons, moonIcons, htmlDocument) 436 | 437 | // defining custom component 438 | if (!isPopstate) { 439 | let customComponents = new CustomComponents() 440 | new MdImportant(customComponents) 441 | new MdWarning(customComponents) 442 | new MdCorrect(customComponents) 443 | } 444 | 445 | // init 446 | Prism.highlightAll(); 447 | 448 | // reveal body 449 | zez.applyState(body, 'loaded') 450 | 451 | // set loaded attribute 452 | document.getElementsByTagName('html')[0].setAttribute('loaded', 'true') 453 | 454 | 455 | 456 | } 457 | 458 | if (localStorage.getItem('popstate') == 'true') { 459 | forgetPopState() 460 | onLoad({ type: 'DOMContentLoaded' }) 461 | } else { 462 | eReset(window, 'popstate', onLoad) 463 | eReset(window, 'DOMContentLoaded', onLoad) 464 | eReset(document.getElementsByTagName('body')[0], "htmx:afterOnLoad", onLoad) 465 | } 466 | 467 | } 468 | 469 | })(); 470 | -------------------------------------------------------------------------------- /static/css/output.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | 7. Disable tap highlights on iOS 36 | */ 37 | 38 | html, 39 | :host { 40 | line-height: 1.5; 41 | /* 1 */ 42 | -webkit-text-size-adjust: 100%; 43 | /* 2 */ 44 | -moz-tab-size: 4; 45 | /* 3 */ 46 | -o-tab-size: 4; 47 | tab-size: 4; 48 | /* 3 */ 49 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 50 | /* 4 */ 51 | font-feature-settings: normal; 52 | /* 5 */ 53 | font-variation-settings: normal; 54 | /* 6 */ 55 | -webkit-tap-highlight-color: transparent; 56 | /* 7 */ 57 | } 58 | 59 | /* 60 | 1. Remove the margin in all browsers. 61 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 62 | */ 63 | 64 | body { 65 | margin: 0; 66 | /* 1 */ 67 | line-height: inherit; 68 | /* 2 */ 69 | } 70 | 71 | /* 72 | 1. Add the correct height in Firefox. 73 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 74 | 3. Ensure horizontal rules are visible by default. 75 | */ 76 | 77 | hr { 78 | height: 0; 79 | /* 1 */ 80 | color: inherit; 81 | /* 2 */ 82 | border-top-width: 1px; 83 | /* 3 */ 84 | } 85 | 86 | /* 87 | Add the correct text decoration in Chrome, Edge, and Safari. 88 | */ 89 | 90 | abbr:where([title]) { 91 | -webkit-text-decoration: underline dotted; 92 | text-decoration: underline dotted; 93 | } 94 | 95 | /* 96 | Remove the default font size and weight for headings. 97 | */ 98 | 99 | h1, 100 | h2, 101 | h3, 102 | h4, 103 | h5, 104 | h6 { 105 | font-size: inherit; 106 | font-weight: inherit; 107 | } 108 | 109 | /* 110 | Reset links to optimize for opt-in styling instead of opt-out. 111 | */ 112 | 113 | a { 114 | color: inherit; 115 | text-decoration: inherit; 116 | } 117 | 118 | /* 119 | Add the correct font weight in Edge and Safari. 120 | */ 121 | 122 | b, 123 | strong { 124 | font-weight: bolder; 125 | } 126 | 127 | /* 128 | 1. Use the user's configured `mono` font-family by default. 129 | 2. Use the user's configured `mono` font-feature-settings by default. 130 | 3. Use the user's configured `mono` font-variation-settings by default. 131 | 4. Correct the odd `em` font sizing in all browsers. 132 | */ 133 | 134 | code, 135 | kbd, 136 | samp, 137 | pre { 138 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 139 | /* 1 */ 140 | font-feature-settings: normal; 141 | /* 2 */ 142 | font-variation-settings: normal; 143 | /* 3 */ 144 | font-size: 1em; 145 | /* 4 */ 146 | } 147 | 148 | /* 149 | Add the correct font size in all browsers. 150 | */ 151 | 152 | small { 153 | font-size: 80%; 154 | } 155 | 156 | /* 157 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 158 | */ 159 | 160 | sub, 161 | sup { 162 | font-size: 75%; 163 | line-height: 0; 164 | position: relative; 165 | vertical-align: baseline; 166 | } 167 | 168 | sub { 169 | bottom: -0.25em; 170 | } 171 | 172 | sup { 173 | top: -0.5em; 174 | } 175 | 176 | /* 177 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 178 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 179 | 3. Remove gaps between table borders by default. 180 | */ 181 | 182 | table { 183 | text-indent: 0; 184 | /* 1 */ 185 | border-color: inherit; 186 | /* 2 */ 187 | border-collapse: collapse; 188 | /* 3 */ 189 | } 190 | 191 | /* 192 | 1. Change the font styles in all browsers. 193 | 2. Remove the margin in Firefox and Safari. 194 | 3. Remove default padding in all browsers. 195 | */ 196 | 197 | button, 198 | input, 199 | optgroup, 200 | select, 201 | textarea { 202 | font-family: inherit; 203 | /* 1 */ 204 | font-feature-settings: inherit; 205 | /* 1 */ 206 | font-variation-settings: inherit; 207 | /* 1 */ 208 | font-size: 100%; 209 | /* 1 */ 210 | font-weight: inherit; 211 | /* 1 */ 212 | line-height: inherit; 213 | /* 1 */ 214 | letter-spacing: inherit; 215 | /* 1 */ 216 | color: inherit; 217 | /* 1 */ 218 | margin: 0; 219 | /* 2 */ 220 | padding: 0; 221 | /* 3 */ 222 | } 223 | 224 | /* 225 | Remove the inheritance of text transform in Edge and Firefox. 226 | */ 227 | 228 | button, 229 | select { 230 | text-transform: none; 231 | } 232 | 233 | /* 234 | 1. Correct the inability to style clickable types in iOS and Safari. 235 | 2. Remove default button styles. 236 | */ 237 | 238 | button, 239 | input:where([type='button']), 240 | input:where([type='reset']), 241 | input:where([type='submit']) { 242 | -webkit-appearance: button; 243 | /* 1 */ 244 | background-color: transparent; 245 | /* 2 */ 246 | background-image: none; 247 | /* 2 */ 248 | } 249 | 250 | /* 251 | Use the modern Firefox focus style for all focusable elements. 252 | */ 253 | 254 | :-moz-focusring { 255 | outline: auto; 256 | } 257 | 258 | /* 259 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 260 | */ 261 | 262 | :-moz-ui-invalid { 263 | box-shadow: none; 264 | } 265 | 266 | /* 267 | Add the correct vertical alignment in Chrome and Firefox. 268 | */ 269 | 270 | progress { 271 | vertical-align: baseline; 272 | } 273 | 274 | /* 275 | Correct the cursor style of increment and decrement buttons in Safari. 276 | */ 277 | 278 | ::-webkit-inner-spin-button, 279 | ::-webkit-outer-spin-button { 280 | height: auto; 281 | } 282 | 283 | /* 284 | 1. Correct the odd appearance in Chrome and Safari. 285 | 2. Correct the outline style in Safari. 286 | */ 287 | 288 | [type='search'] { 289 | -webkit-appearance: textfield; 290 | /* 1 */ 291 | outline-offset: -2px; 292 | /* 2 */ 293 | } 294 | 295 | /* 296 | Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | ::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /* 304 | 1. Correct the inability to style clickable types in iOS and Safari. 305 | 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; 310 | /* 1 */ 311 | font: inherit; 312 | /* 2 */ 313 | } 314 | 315 | /* 316 | Add the correct display in Chrome and Safari. 317 | */ 318 | 319 | summary { 320 | display: list-item; 321 | } 322 | 323 | /* 324 | Removes the default spacing and border for appropriate elements. 325 | */ 326 | 327 | blockquote, 328 | dl, 329 | dd, 330 | h1, 331 | h2, 332 | h3, 333 | h4, 334 | h5, 335 | h6, 336 | hr, 337 | figure, 338 | p, 339 | pre { 340 | margin: 0; 341 | } 342 | 343 | fieldset { 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | legend { 349 | padding: 0; 350 | } 351 | 352 | ol, 353 | ul, 354 | menu { 355 | list-style: none; 356 | margin: 0; 357 | padding: 0; 358 | } 359 | 360 | /* 361 | Reset default styling for dialogs. 362 | */ 363 | 364 | dialog { 365 | padding: 0; 366 | } 367 | 368 | /* 369 | Prevent resizing textareas horizontally by default. 370 | */ 371 | 372 | textarea { 373 | resize: vertical; 374 | } 375 | 376 | /* 377 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 378 | 2. Set the default placeholder color to the user's configured gray 400 color. 379 | */ 380 | 381 | input::-moz-placeholder, textarea::-moz-placeholder { 382 | opacity: 1; 383 | /* 1 */ 384 | color: #9ca3af; 385 | /* 2 */ 386 | } 387 | 388 | input::placeholder, 389 | textarea::placeholder { 390 | opacity: 1; 391 | /* 1 */ 392 | color: #9ca3af; 393 | /* 2 */ 394 | } 395 | 396 | /* 397 | Set the default cursor for buttons. 398 | */ 399 | 400 | button, 401 | [role="button"] { 402 | cursor: pointer; 403 | } 404 | 405 | /* 406 | Make sure disabled buttons don't get the pointer cursor. 407 | */ 408 | 409 | :disabled { 410 | cursor: default; 411 | } 412 | 413 | /* 414 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 415 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 416 | This can trigger a poorly considered lint error in some tools but is included by design. 417 | */ 418 | 419 | img, 420 | svg, 421 | video, 422 | canvas, 423 | audio, 424 | iframe, 425 | embed, 426 | object { 427 | display: block; 428 | /* 1 */ 429 | vertical-align: middle; 430 | /* 2 */ 431 | } 432 | 433 | /* 434 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 435 | */ 436 | 437 | img, 438 | video { 439 | max-width: 100%; 440 | height: auto; 441 | } 442 | 443 | /* Make elements with the HTML hidden attribute stay hidden by default */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | 449 | *, ::before, ::after { 450 | --tw-border-spacing-x: 0; 451 | --tw-border-spacing-y: 0; 452 | --tw-translate-x: 0; 453 | --tw-translate-y: 0; 454 | --tw-rotate: 0; 455 | --tw-skew-x: 0; 456 | --tw-skew-y: 0; 457 | --tw-scale-x: 1; 458 | --tw-scale-y: 1; 459 | --tw-pan-x: ; 460 | --tw-pan-y: ; 461 | --tw-pinch-zoom: ; 462 | --tw-scroll-snap-strictness: proximity; 463 | --tw-gradient-from-position: ; 464 | --tw-gradient-via-position: ; 465 | --tw-gradient-to-position: ; 466 | --tw-ordinal: ; 467 | --tw-slashed-zero: ; 468 | --tw-numeric-figure: ; 469 | --tw-numeric-spacing: ; 470 | --tw-numeric-fraction: ; 471 | --tw-ring-inset: ; 472 | --tw-ring-offset-width: 0px; 473 | --tw-ring-offset-color: #fff; 474 | --tw-ring-color: rgb(59 130 246 / 0.5); 475 | --tw-ring-offset-shadow: 0 0 #0000; 476 | --tw-ring-shadow: 0 0 #0000; 477 | --tw-shadow: 0 0 #0000; 478 | --tw-shadow-colored: 0 0 #0000; 479 | --tw-blur: ; 480 | --tw-brightness: ; 481 | --tw-contrast: ; 482 | --tw-grayscale: ; 483 | --tw-hue-rotate: ; 484 | --tw-invert: ; 485 | --tw-saturate: ; 486 | --tw-sepia: ; 487 | --tw-drop-shadow: ; 488 | --tw-backdrop-blur: ; 489 | --tw-backdrop-brightness: ; 490 | --tw-backdrop-contrast: ; 491 | --tw-backdrop-grayscale: ; 492 | --tw-backdrop-hue-rotate: ; 493 | --tw-backdrop-invert: ; 494 | --tw-backdrop-opacity: ; 495 | --tw-backdrop-saturate: ; 496 | --tw-backdrop-sepia: ; 497 | --tw-contain-size: ; 498 | --tw-contain-layout: ; 499 | --tw-contain-paint: ; 500 | --tw-contain-style: ; 501 | } 502 | 503 | ::backdrop { 504 | --tw-border-spacing-x: 0; 505 | --tw-border-spacing-y: 0; 506 | --tw-translate-x: 0; 507 | --tw-translate-y: 0; 508 | --tw-rotate: 0; 509 | --tw-skew-x: 0; 510 | --tw-skew-y: 0; 511 | --tw-scale-x: 1; 512 | --tw-scale-y: 1; 513 | --tw-pan-x: ; 514 | --tw-pan-y: ; 515 | --tw-pinch-zoom: ; 516 | --tw-scroll-snap-strictness: proximity; 517 | --tw-gradient-from-position: ; 518 | --tw-gradient-via-position: ; 519 | --tw-gradient-to-position: ; 520 | --tw-ordinal: ; 521 | --tw-slashed-zero: ; 522 | --tw-numeric-figure: ; 523 | --tw-numeric-spacing: ; 524 | --tw-numeric-fraction: ; 525 | --tw-ring-inset: ; 526 | --tw-ring-offset-width: 0px; 527 | --tw-ring-offset-color: #fff; 528 | --tw-ring-color: rgb(59 130 246 / 0.5); 529 | --tw-ring-offset-shadow: 0 0 #0000; 530 | --tw-ring-shadow: 0 0 #0000; 531 | --tw-shadow: 0 0 #0000; 532 | --tw-shadow-colored: 0 0 #0000; 533 | --tw-blur: ; 534 | --tw-brightness: ; 535 | --tw-contrast: ; 536 | --tw-grayscale: ; 537 | --tw-hue-rotate: ; 538 | --tw-invert: ; 539 | --tw-saturate: ; 540 | --tw-sepia: ; 541 | --tw-drop-shadow: ; 542 | --tw-backdrop-blur: ; 543 | --tw-backdrop-brightness: ; 544 | --tw-backdrop-contrast: ; 545 | --tw-backdrop-grayscale: ; 546 | --tw-backdrop-hue-rotate: ; 547 | --tw-backdrop-invert: ; 548 | --tw-backdrop-opacity: ; 549 | --tw-backdrop-saturate: ; 550 | --tw-backdrop-sepia: ; 551 | --tw-contain-size: ; 552 | --tw-contain-layout: ; 553 | --tw-contain-paint: ; 554 | --tw-contain-style: ; 555 | } 556 | 557 | .container { 558 | width: 100%; 559 | } 560 | 561 | @media (min-width: 480px) { 562 | .container { 563 | max-width: 480px; 564 | } 565 | } 566 | 567 | @media (min-width: 640px) { 568 | .container { 569 | max-width: 640px; 570 | } 571 | } 572 | 573 | @media (min-width: 768px) { 574 | .container { 575 | max-width: 768px; 576 | } 577 | } 578 | 579 | @media (min-width: 1024px) { 580 | .container { 581 | max-width: 1024px; 582 | } 583 | } 584 | 585 | @media (min-width: 1280px) { 586 | .container { 587 | max-width: 1280px; 588 | } 589 | } 590 | 591 | @media (min-width: 1536px) { 592 | .container { 593 | max-width: 1536px; 594 | } 595 | } 596 | 597 | .\!visible { 598 | visibility: visible !important; 599 | } 600 | 601 | .visible { 602 | visibility: visible; 603 | } 604 | 605 | .invisible { 606 | visibility: hidden; 607 | } 608 | 609 | .static { 610 | position: static; 611 | } 612 | 613 | .fixed { 614 | position: fixed; 615 | } 616 | 617 | .absolute { 618 | position: absolute; 619 | } 620 | 621 | .relative { 622 | position: relative; 623 | } 624 | 625 | .sticky { 626 | position: sticky; 627 | } 628 | 629 | .top-0 { 630 | top: 0px; 631 | } 632 | 633 | .z-30 { 634 | z-index: 30; 635 | } 636 | 637 | .z-40 { 638 | z-index: 40; 639 | } 640 | 641 | .z-50 { 642 | z-index: 50; 643 | } 644 | 645 | .my-12 { 646 | margin-top: 3rem; 647 | margin-bottom: 3rem; 648 | } 649 | 650 | .\!block { 651 | display: block !important; 652 | } 653 | 654 | .block { 655 | display: block; 656 | } 657 | 658 | .inline { 659 | display: inline; 660 | } 661 | 662 | .\!flex { 663 | display: flex !important; 664 | } 665 | 666 | .flex { 667 | display: flex; 668 | } 669 | 670 | .table { 671 | display: table; 672 | } 673 | 674 | .grid { 675 | display: grid; 676 | } 677 | 678 | .hidden { 679 | display: none; 680 | } 681 | 682 | .h-6 { 683 | height: 1.5rem; 684 | } 685 | 686 | .h-\[75px\] { 687 | height: 75px; 688 | } 689 | 690 | .h-full { 691 | height: 100%; 692 | } 693 | 694 | .h-screen { 695 | height: 100vh; 696 | } 697 | 698 | .w-6 { 699 | width: 1.5rem; 700 | } 701 | 702 | .w-\[250px\] { 703 | width: 250px; 704 | } 705 | 706 | .w-\[80\%\] { 707 | width: 80%; 708 | } 709 | 710 | .w-full { 711 | width: 100%; 712 | } 713 | 714 | .w-screen { 715 | width: 100vw; 716 | } 717 | 718 | .shrink-0 { 719 | flex-shrink: 0; 720 | } 721 | 722 | .rotate-90 { 723 | --tw-rotate: 90deg; 724 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 725 | } 726 | 727 | .transform { 728 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 729 | } 730 | 731 | .cursor-pointer { 732 | cursor: pointer; 733 | } 734 | 735 | .resize { 736 | resize: both; 737 | } 738 | 739 | .list-none { 740 | list-style-type: none; 741 | } 742 | 743 | .flex-row { 744 | flex-direction: row; 745 | } 746 | 747 | .flex-col { 748 | flex-direction: column; 749 | } 750 | 751 | .items-end { 752 | align-items: flex-end; 753 | } 754 | 755 | .items-center { 756 | align-items: center; 757 | } 758 | 759 | .justify-center { 760 | justify-content: center; 761 | } 762 | 763 | .justify-between { 764 | justify-content: space-between; 765 | } 766 | 767 | .gap-1 { 768 | gap: 0.25rem; 769 | } 770 | 771 | .gap-2 { 772 | gap: 0.5rem; 773 | } 774 | 775 | .gap-6 { 776 | gap: 1.5rem; 777 | } 778 | 779 | .gap-8 { 780 | gap: 2rem; 781 | } 782 | 783 | .overflow-y-scroll { 784 | overflow-y: scroll; 785 | } 786 | 787 | .truncate { 788 | overflow: hidden; 789 | text-overflow: ellipsis; 790 | white-space: nowrap; 791 | } 792 | 793 | .break-words { 794 | overflow-wrap: break-word; 795 | } 796 | 797 | .rounded-md { 798 | border-radius: 0.375rem; 799 | } 800 | 801 | .border { 802 | border-width: 1px; 803 | } 804 | 805 | .border-b { 806 | border-bottom-width: 1px; 807 | } 808 | 809 | .border-l { 810 | border-left-width: 1px; 811 | } 812 | 813 | .border-l-4 { 814 | border-left-width: 4px; 815 | } 816 | 817 | .border-r { 818 | border-right-width: 1px; 819 | } 820 | 821 | .border-t { 822 | border-top-width: 1px; 823 | } 824 | 825 | .border-\[var\(--b-color\)\] { 826 | border-color: var(--b-color); 827 | } 828 | 829 | .border-\[var\(--md-correct-border-color\)\] { 830 | border-color: var(--md-correct-border-color); 831 | } 832 | 833 | .border-\[var\(--md-important-border-color\)\] { 834 | border-color: var(--md-important-border-color); 835 | } 836 | 837 | .border-\[var\(--md-warning-border-color\)\] { 838 | border-color: var(--md-warning-border-color); 839 | } 840 | 841 | .bg-\[var\(--bg-hover-color\)\] { 842 | background-color: var(--bg-hover-color); 843 | } 844 | 845 | .bg-\[var\(--default-bg-color\)\] { 846 | background-color: var(--default-bg-color); 847 | } 848 | 849 | .bg-\[var\(--footer-bg-color\)\] { 850 | background-color: var(--footer-bg-color); 851 | } 852 | 853 | .bg-\[var\(--header-bg-color\)\] { 854 | background-color: var(--header-bg-color); 855 | } 856 | 857 | .bg-\[var\(--md-bg-color\)\] { 858 | background-color: var(--md-bg-color); 859 | } 860 | 861 | .bg-\[var\(--overlay-bg-color\)\] { 862 | background-color: var(--overlay-bg-color); 863 | } 864 | 865 | .bg-\[var\(--pagenav-bg-color\)\] { 866 | background-color: var(--pagenav-bg-color); 867 | } 868 | 869 | .bg-\[var\(--sitenav-bg-color\)\] { 870 | background-color: var(--sitenav-bg-color); 871 | } 872 | 873 | .p-2 { 874 | padding: 0.5rem; 875 | } 876 | 877 | .p-4 { 878 | padding: 1rem; 879 | } 880 | 881 | .p-6 { 882 | padding: 1.5rem; 883 | } 884 | 885 | .pl-0 { 886 | padding-left: 0px; 887 | } 888 | 889 | .pl-1 { 890 | padding-left: 0.25rem; 891 | } 892 | 893 | .pl-2 { 894 | padding-left: 0.5rem; 895 | } 896 | 897 | .pl-3 { 898 | padding-left: 0.75rem; 899 | } 900 | 901 | .pl-4 { 902 | padding-left: 1rem; 903 | } 904 | 905 | .pl-5 { 906 | padding-left: 1.25rem; 907 | } 908 | 909 | .pl-6 { 910 | padding-left: 1.5rem; 911 | } 912 | 913 | .pl-7 { 914 | padding-left: 1.75rem; 915 | } 916 | 917 | .pt-1 { 918 | padding-top: 0.25rem; 919 | } 920 | 921 | .text-sm { 922 | font-size: 0.875rem; 923 | line-height: 1.25rem; 924 | } 925 | 926 | .text-xs { 927 | font-size: 0.75rem; 928 | line-height: 1rem; 929 | } 930 | 931 | .font-bold { 932 | font-weight: 700; 933 | } 934 | 935 | .lowercase { 936 | text-transform: lowercase; 937 | } 938 | 939 | .capitalize { 940 | text-transform: capitalize; 941 | } 942 | 943 | .italic { 944 | font-style: italic; 945 | } 946 | 947 | .ordinal { 948 | --tw-ordinal: ordinal; 949 | font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction); 950 | } 951 | 952 | .text-\[var\(--default-text-color\)\] { 953 | color: var(--default-text-color); 954 | } 955 | 956 | .text-\[var\(--md-correct-text-color\)\] { 957 | color: var(--md-correct-text-color); 958 | } 959 | 960 | .text-\[var\(--md-important-text-color\)\] { 961 | color: var(--md-important-text-color); 962 | } 963 | 964 | .text-\[var\(--md-warning-text-color\)\] { 965 | color: var(--md-warning-text-color); 966 | } 967 | 968 | .text-\[var\(--text-important\)\] { 969 | color: var(--text-important); 970 | } 971 | 972 | .underline { 973 | text-decoration-line: underline; 974 | } 975 | 976 | .no-underline { 977 | text-decoration-line: none; 978 | } 979 | 980 | .opacity-70 { 981 | opacity: 0.7; 982 | } 983 | 984 | .outline { 985 | outline-style: solid; 986 | } 987 | 988 | .ring { 989 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 990 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); 991 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 992 | } 993 | 994 | .invert { 995 | --tw-invert: invert(100%); 996 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 997 | } 998 | 999 | .filter { 1000 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 1001 | } 1002 | 1003 | .\[-\:\=\] { 1004 | -: =; 1005 | } 1006 | 1007 | .hover\:border-\[var\(--b-hover-color\)\]:hover { 1008 | border-color: var(--b-hover-color); 1009 | } 1010 | 1011 | .hover\:bg-\[var\(--bg-hover-color\)\]:hover { 1012 | background-color: var(--bg-hover-color); 1013 | } 1014 | 1015 | @media (min-width: 1024px) { 1016 | .lg\:invisible { 1017 | visibility: hidden; 1018 | } 1019 | 1020 | .lg\:sticky { 1021 | position: sticky; 1022 | } 1023 | 1024 | .lg\:top-\[75px\] { 1025 | top: 75px; 1026 | } 1027 | 1028 | .lg\:z-0 { 1029 | z-index: 0; 1030 | } 1031 | 1032 | .lg\:block { 1033 | display: block; 1034 | } 1035 | 1036 | .lg\:hidden { 1037 | display: none; 1038 | } 1039 | 1040 | .lg\:w-auto { 1041 | width: auto; 1042 | } 1043 | 1044 | .lg\:pl-8 { 1045 | padding-left: 2rem; 1046 | } 1047 | } 1048 | 1049 | @media (min-width: 1280px) { 1050 | .xl\:pr-8 { 1051 | padding-right: 2rem; 1052 | } 1053 | } 1054 | 1055 | @media (min-width: 1536px) { 1056 | .\32xl\:py-4 { 1057 | padding-top: 1rem; 1058 | padding-bottom: 1rem; 1059 | } 1060 | 1061 | .\32xl\:pl-40 { 1062 | padding-left: 10rem; 1063 | } 1064 | } 1065 | 1066 | .dark\:block:where(.dark, .dark *) { 1067 | display: block; 1068 | } 1069 | 1070 | .dark\:hidden:where(.dark, .dark *) { 1071 | display: none; 1072 | } 1073 | 1074 | .dark\:border-\[var\(--dark-b-color\)\]:where(.dark, .dark *) { 1075 | border-color: var(--dark-b-color); 1076 | } 1077 | 1078 | .dark\:border-\[var\(--dark-md-correct-border-color\)\]:where(.dark, .dark *) { 1079 | border-color: var(--dark-md-correct-border-color); 1080 | } 1081 | 1082 | .dark\:border-\[var\(--dark-md-important-border-color\)\]:where(.dark, .dark *) { 1083 | border-color: var(--dark-md-important-border-color); 1084 | } 1085 | 1086 | .dark\:border-\[var\(--dark-md-warning-border-color\)\]:where(.dark, .dark *) { 1087 | border-color: var(--dark-md-warning-border-color); 1088 | } 1089 | 1090 | .dark\:bg-\[var\(--dark-bg-hover-color\)\]:where(.dark, .dark *) { 1091 | background-color: var(--dark-bg-hover-color); 1092 | } 1093 | 1094 | .dark\:bg-\[var\(--dark-default-bg-color\)\]:where(.dark, .dark *) { 1095 | background-color: var(--dark-default-bg-color); 1096 | } 1097 | 1098 | .dark\:bg-\[var\(--dark-footer-bg-color\)\]:where(.dark, .dark *) { 1099 | background-color: var(--dark-footer-bg-color); 1100 | } 1101 | 1102 | .dark\:bg-\[var\(--dark-header-bg-color\)\]:where(.dark, .dark *) { 1103 | background-color: var(--dark-header-bg-color); 1104 | } 1105 | 1106 | .dark\:bg-\[var\(--dark-md-bg-color\)\]:where(.dark, .dark *) { 1107 | background-color: var(--dark-md-bg-color); 1108 | } 1109 | 1110 | .dark\:bg-\[var\(--dark-pagenav-bg-color\)\]:where(.dark, .dark *) { 1111 | background-color: var(--dark-pagenav-bg-color); 1112 | } 1113 | 1114 | .dark\:bg-\[var\(--dark-sitenav-bg-color\)\]:where(.dark, .dark *) { 1115 | background-color: var(--dark-sitenav-bg-color); 1116 | } 1117 | 1118 | .dark\:text-\[var\(--dark-default-text-color\)\]:where(.dark, .dark *) { 1119 | color: var(--dark-default-text-color); 1120 | } 1121 | 1122 | .dark\:text-\[var\(--dark-md-correct-text-color\)\]:where(.dark, .dark *) { 1123 | color: var(--dark-md-correct-text-color); 1124 | } 1125 | 1126 | .dark\:text-\[var\(--dark-md-important-text-color\)\]:where(.dark, .dark *) { 1127 | color: var(--dark-md-important-text-color); 1128 | } 1129 | 1130 | .dark\:text-\[var\(--dark-md-warning-text-color\)\]:where(.dark, .dark *) { 1131 | color: var(--dark-md-warning-text-color); 1132 | } 1133 | 1134 | .dark\:text-\[var\(--dark-text-important\)\]:where(.dark, .dark *) { 1135 | color: var(--dark-text-important); 1136 | } 1137 | 1138 | .dark\:text-\[var\(--text-important\)\]:where(.dark, .dark *) { 1139 | color: var(--text-important); 1140 | } 1141 | 1142 | .dark\:hover\:border-\[var\(--dark-b-hover-color\)\]:hover:where(.dark, .dark *) { 1143 | border-color: var(--dark-b-hover-color); 1144 | } 1145 | 1146 | .hover\:dark\:bg-\[var\(--dark-bg-hover-color\)\]:where(.dark, .dark *):hover { 1147 | background-color: var(--dark-bg-hover-color); 1148 | } 1149 | 1150 | @media (min-width: 1024px) { 1151 | .dark\:lg\:block:where(.dark, .dark *) { 1152 | display: block; 1153 | } 1154 | 1155 | .dark\:lg\:hidden:where(.dark, .dark *) { 1156 | display: none; 1157 | } 1158 | } --------------------------------------------------------------------------------