// script/website/main.go
package main import ( md2html ) func init() { if := godotenv.Load(); != nil { log.Printf("Warning: Error loading .env file: %v", ) } } var apiUrl = os.Getenv("AM_DEPLOY_API_URL") var amMainMenu = sitemap.MainMenu const infoIcon = `<svg class=align-bottom xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6" style="width: 25px;display: inline;"> <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"></path> </svg>` func main() { := filepath.Join("docs", "website") // 1. Create the output directory if it doesn't exist if := os.MkdirAll(, 0755); != nil { panic(fmt.Errorf("failed to create output directory: %w", )) } fmt.Printf("Rendering README.md files...\n") os.MkdirAll(, 0755) for , := range amMainMenu { if .Path == "" { continue } if := renderFile(, ); != nil { fmt.Printf("Error rendering %s: %v\n", .Path, ) } else { // fmt.Printf("Rendered: %s\n", e.Path) } } fmt.Println("Done.") } func renderFile( sitemap.Entry, string) error { // A. Read the Markdown file := .Path , := os.ReadFile() if != nil { return } // B. Normalize newlines = markdown.NormalizeNewlines() // C. Configure Parser // Enable common GitHub-like extensions (tables, strikethrough, autolinks) := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock := parser.NewWithExtensions() .Opts.ParserHook = ParserHook // Parse content into AST := .Parse() // TODO optimize with classes html.New(html.WithClasses(true)) // D. Configure Renderer // We purposely do NOT add a full page skeleton. // Default behavior renders an HTML fragment. := md2html.CommonFlags | md2html.HrefTargetBlank := md2html.RendererOptions{ Flags: , RenderNodeHook: RenderHook, } := md2html.NewRenderer() // Render AST to HTML := markdown.Render(, ) // E. Generate Flat Filename := .Url + ".html" if == "README.md" { = "index.html" } := filepath.Join(, ) // fix ULs := strings.ReplaceAll(string(), "<ul>", `<ul class="ps-5 list-disc list-outside">`) // fix <code> color = strings.ReplaceAll(, "<code>", `<code class="dark:bg-slate-700 p-1 rounded-sm">`) // wrap in layout , := os.ReadFile("docs/website/templates/layout.html") if != nil { return } = strings.ReplaceAll(string(), "{{ CONTENT }}", ) = strings.ReplaceAll(, "{{ NAVIGATION }}", renderMainMenu()) , = processHtml(, ) if != nil { return } // F. Write to file return os.WriteFile(, []byte(), 0644) } func processHtml( sitemap.Entry, string) (string, error) { := .Path // 2. Load the HTML into goquery , := goquery.NewDocumentFromReader(strings.NewReader()) if != nil { log.Fatal() } // 3. Configure Chroma // We select the "go" lexer, "monokai" style, and HTML formatter := lexers.Get("go") if == nil { = lexers.Fallback } = chroma.Coalesce() := lexers.Get("bash") if == nil { = lexers.Fallback } = chroma.Coalesce() := styles.Get("monokai") if == nil { = styles.Fallback } // WithClasses(false) puts CSS styles inline (style="color:...") // If you want to use a separate CSS file, set this to true. := html.New(html.WithClasses(false), html.TabWidth(4)) // highlight code .Find("pre > code.language-go").Each(func( int, *goquery.Selection) { highlightCode(, , , ) }) .Find("pre > code.language-bash").Each(func( int, *goquery.Selection) { highlightCode(, , , ) }) .Find("pre > code").Parent().AddClass("p-2 rounded-lg overflow-x-auto") // fix ULs and H1 .Find("#page-content > ul").RemoveClass("ps-5") .Find("#page-content > h1").Remove() // fix readme prefix if !strings.Contains(, "/") { // main readme .Find("#page-content > h1").PrevAll().Remove().End().Remove() := "/" + .Path if == "README.md" { = "/" } .Find("h1").SetText() } else { // nested readmes .Find("#page-content > blockquote").PrevAll().Remove().End().Remove() .Find("h1").SetText("/" + .Path) } // fix readme suffix .Find("#monorepo").NextAll().Remove().End().Remove() // TODO rewrite links // - .md files to slugs (find missing slugs) // - dir links with README.md to slugs // - other links to code.am.dev .Find("#page-content a[href]").Each(func( int, *goquery.Selection) { := .AttrOr("href", "") // skip external and local if strings.HasPrefix(, "http") || strings.HasPrefix(, "#") { return } for , := range amMainMenu { if .Path == "" { continue } // to github if strings.HasSuffix(, "_test.go") && strings.HasPrefix(, "/") { .SetAttr("href", fmt.Sprintf("https://github.com/pancsta/asyncmachine-go/blob/main%s", )) // fmt.Printf("github link %s\n", href) return } // to code if (strings.HasSuffix(, ".go") || strings.HasSuffix(, ".json")) && strings.HasPrefix(, "/") { .SetAttr("href", fmt.Sprintf("%s/src/github.com/pancsta/asyncmachine-go%s.html", apiUrl, )) // fmt.Printf("code link %s\n", href) return } // to slugs := strings.TrimPrefix(, "/") := strings.TrimSuffix(.Path, "README.md") if strings.HasPrefix(.Path, ) || (strings.HasPrefix(, ) && != "") { := "/" + .Url if strings.Contains(, "#") { += [strings.Index(, "#"):] } .SetAttr("href", ) // fmt.Printf("link %s -> %s\n", href, newHref) return } } // some dirs to code if !strings.Contains(, ".") && (strings.HasPrefix(, "/tools") || strings.HasPrefix(, "/pkg")) { .SetAttr("href", fmt.Sprintf("%s/pkg/github.com/pancsta/asyncmachine-go%s.html", apiUrl, )) // fmt.Printf("code dir link %s\n", href) return } // log err, fallback to github fmt.Printf("unhandled link %s\n", ) .SetAttr("href", "https://github.com/pancsta/asyncmachine-go/tree/main"+) }) .Find("#footer-packages a").Each(func( int, *goquery.Selection) { .SetAttr("href", fmt.Sprintf( "%s/pkg/github.com/pancsta/asyncmachine-go/pkg/%s.html", apiUrl, .Text(), )) }) .Find("#footer-tools a").Each(func( int, *goquery.Selection) { .SetAttr("href", fmt.Sprintf( "%s/pkg/github.com/pancsta/asyncmachine-go/tools/%s.html", apiUrl, .Text(), )) }) .Find("header a:contains(APIs)").SetAttr("href", apiUrl) // parse github alerts .Find(`blockquote:contains("[!NOTE]")`).Each(func( int, *goquery.Selection) { .Prev().AddClass("mb-1") := strings.ReplaceAll(strings.TrimSpace(.Text()), "[!NOTE]\n", "") := ` <blockquote class="border-l-3 border-blue-500 pl-3 pb-2"> <p class="text-blue-500 mb-1 py-1"> ` + infoIcon + ` Note </p> <p>` + + `</p> </blockquote>` .ReplaceWithHtml() }) // 6. output the modified HTML // We use Find("html") to get the whole document string including <html> tags , := .Find("html").Html() if != nil { log.Fatal() } return , nil } func highlightCode( *goquery.Selection, chroma.Lexer, *html.Formatter, *chroma.Style) { // Get the raw source code text inside the <code> block := .Text() // Highlight the code using Chroma , := .Tokenise(nil, ) if != nil { log.Printf("Tokenization error: %v", ) return } var bytes.Buffer = .Format(&, , ) if != nil { log.Printf("Formatting error: %v", ) return } := .String() // 5. Replace the original block. // Since Chroma generates its own <pre> wrapper, we usually want to // replace the parent <pre> of our <code> selection to avoid <pre><pre>...</pre></pre> .Parent().ReplaceWithHtml() } func renderMainMenu( string) string { := "flex-shrink-0 px-4 py-2 text-sm font-semibold text-white bg-blue-600 dark:text-black dark:bg-sunlit-clay-400 rounded-full shadow-md" := "flex-shrink-0 px-4 py-2 text-sm font-medium text-gray-400 hover:text-white hover:bg-gray-700/80 dark:hover:bg-sunlit-clay-700/80 rounded-full transition-all duration-200" := "" for , := range amMainMenu { if .SkipMenu { continue } // separator if .Path == "" { += `<span class="p-1">•</span>` += "\n" continue } := if .Path == { += fmt.Sprintf(` <script> const pageSlug = "%s"; </script>`, .Url) = } := .Url if .Url == "" { = "/" } += fmt.Sprintf(`<a href="/%s" class="%s" id=nav-%s>%s</a>`, .Url, , .Url, ) += "\n" } return mainMenuPre + + mainMenuPost } const mainMenuPre = ` <div class="w-full flex justify-center pt-6 px-4 pointer-events-none"> <nav class="pointer-events-auto bg-gray-800/90 backdrop-blur-md border border-gray-700/50 p-1.5 rounded-full shadow-2xl overflow-x-auto max-w-full no-scrollbar"> <div class="flex space-x-1"> ` const mainMenuPost = ` </div> </nav> </div> ` // <details> fix // TODO https://github.com/gomarkdown/markdown/issues/276 func ( []byte) (ast.Node, []byte, int) { if , , := parseDetails(); != nil { return , , } return nil, nil, 0 } type Details struct { ast.Container } const ( detailsBegin = "<details>" detailsEnd = "</details>" ) func parseDetails( []byte) (ast.Node, []byte, int) { if !bytes.HasPrefix(, []byte(detailsBegin)) { return nil, nil, 0 } := len(detailsBegin) := bytes.Index([:], []byte(detailsEnd)) + if < 0 { return nil, nil, 0 } return &Details{}, [:], + len(detailsEnd) } func ( io.Writer, ast.Node, bool) (ast.WalkStatus, bool) { switch n := .(type) { case *Details: renderDetails(, , ) return ast.GoToNext, true } return ast.GoToNext, false } func renderDetails( io.Writer, *Details, bool) { if { io.WriteString(, detailsBegin) } else { io.WriteString(, detailsEnd) } }