├── 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{{ .Prev.BaseNodeData.Name }}
11 | 12 | {{ end }} 13 |{{ .Next.BaseNodeData.Name }}
23 | 24 | {{ end }} 25 |{text}
46 |{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 `' + 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 | Important
354 | 355 |{text}
356 |Warning
376 | 377 |{text}
378 |Correct
396 | 397 |{text}
398 |