package main import ( "errors" "fmt" "html/template" "io" "io/fs" "log/slog" "net/http" "regexp" "slices" "sort" "strings" "capytal.cc/assets" "capytal.cc/internals/natsort" "capytal.cc/templates" "capytal.cc/tinyssert" "forge.capytal.company/loreddev/blogo" "forge.capytal.company/loreddev/blogo/plugin" "forge.capytal.company/loreddev/blogo/plugins" "forge.capytal.company/loreddev/blogo/plugins/gitea" "forge.capytal.company/loreddev/x/smalltrip" "forge.capytal.company/loreddev/x/smalltrip/exception" "forge.capytal.company/loreddev/x/smalltrip/middleware" links "github.com/fundipper/goldmark-links" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting/v2" meta "github.com/yuin/goldmark-meta" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/text" callout "gitlab.com/staticnoise/goldmark-callout" "go.abhg.dev/goldmark/anchor" ) var md = goldmark.New( goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), goldmark.WithExtensions( extension.Footnote, extension.GFM, extension.DefinitionList, extension.Typographer, highlighting.NewHighlighting( highlighting.WithStyle("catppuccin-mocha"), ), meta.New(meta.WithStoresInDocument()), &anchor.Extender{}, links.NewExtender( map[string]bool{ "capytal.cc": true, "capytal.company": true, "forge.capytal.company": true, "lored.dev": true, }, map[string]string{ "rel": "nofollow noopener noreferrer", "target": "_blank", }, ), callout.CalloutExtention, ), ) func NewApp(opts ...Option) (http.Handler, error) { app := &app{ assets: assets.Files(), templates: templates.Templates(), cache: true, log: slog.New(slog.DiscardHandler), assert: tinyssert.NewDisabledAssertions(), } for _, opt := range opts { opt(app) } app.setup() return app, nil } type Option func(a *app) func WithAssets(assets fs.FS) Option { return func(a *app) { a.assets = assets } } func WithTemplates(t templates.ITemplate) Option { return func(a *app) { a.templates = t } } func WithCacheDisabled() Option { return func(a *app) { a.cache = false } } func WithLogger(logger *slog.Logger) Option { return func(a *app) { a.log = logger } } func WithAssertions(assertions tinyssert.Assertions) Option { return func(a *app) { a.assert = assertions } } type app struct { router http.Handler assets fs.FS templates templates.ITemplate cache bool log *slog.Logger assert tinyssert.Assertions } func (app *app) setup() { app.assert.NotNil(app.log) router := smalltrip.NewRouter( smalltrip.WithAssertions(app.assert), smalltrip.WithLogger(app.log.WithGroup("smalltrip")), ) router.Use(middleware.Logger(app.log.WithGroup("requests"))) if app.cache { router.Use(middleware.Cache()) } else { router.Use(middleware.DisableCache()) } router.Handle("/assets/", http.StripPrefix("/assets/", http.FileServerFS(app.assets))) router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("lang") == "" { langRedirect(w, r) } err := app.templates.ExecuteTemplate(w, "homepage", map[string]any{ "Lang": r.URL.Query().Get("lang"), }) if err != nil { exception.InternalServerError(err).ServeHTTP(w, r) return } }) router.HandleFunc("/about/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("lang") == "" { langRedirect(w, r) } err := app.templates.ExecuteTemplate(w, "about", map[string]any{ "Lang": r.URL.Query().Get("lang"), }) if err != nil { exception.InternalServerError(err).ServeHTTP(w, r) return } }) router.HandleFunc("/privacy/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("lang") == "" { langRedirect(w, r) } lang := "" if l := r.URL.Query().Get("lang"); l != "" && !strings.Contains(l, "en") { lang = fmt.Sprintf("_%s", l) } res, err := http.Get(fmt.Sprintf("https://forge.capytal.company/api/v1/repos/capytal/privacy-policy/raw/PRIVACY_POLICY%s.md", lang)) if err != nil { exception.InternalServerError(err).ServeHTTP(w, r) return } c, err := io.ReadAll(res.Body) if err != nil { exception.InternalServerError(err).ServeHTTP(w, r) return } doc := md.Parser().Parse(text.NewReader(c)) meta := doc.OwnerDocument().Meta() title := "Privacy Policy" if t, ok := meta["title"]; ok { tt, ok := t.(string) if ok { title = tt } } f := new(strings.Builder) err = md.Renderer().Render(f, c, doc) if err != nil { exception.InternalServerError(err).ServeHTTP(w, r) return } err = app.templates.ExecuteTemplate(w, "privacy-policy", map[string]any{ "Title": title, "Lang": r.URL.Query().Get("lang"), "Content": template.HTML(f.String()), }) if err != nil { exception.InternalServerError(err).ServeHTTP(w, r) return } }) blogEN := app.blogEN() blogPT := app.blogPT() router.Handle("/blog/", http.StripPrefix("/blog/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("lang") == "" { langRedirect(w, r) } switch r.URL.Query().Get("lang") { case "pt-BR": blogPT.ServeHTTP(w, r) default: blogEN.ServeHTTP(w, r) } }))) app.router = router } func langRedirect(w http.ResponseWriter, r *http.Request) { acceptedLang := r.Header.Get("Accept-Language") if strings.Contains(acceptedLang, "pt") { http.Redirect(w, r, fmt.Sprintf("%s?lang=pt-BR", r.URL.Path), http.StatusSeeOther) } } func (app *app) blogEN() blogo.Blogo { blog := blogo.New(blogo.Opts{ Assertions: app.assert, Logger: app.log.WithGroup("blogo"), }) gitea := gitea.New("capytal", "capytal.cc-blog", "https://forge.capytal.company") blog.Use(gitea) blog.Use(&listRenderer{app.templates, "en-US"}) blog.Use(NewBlogPostRenderer(app.templates, "en-US")) blog.Use(plugins.NewPlainText()) return blog } func (app *app) blogPT() blogo.Blogo { blog := blogo.New(blogo.Opts{ Assertions: app.assert, Logger: app.log.WithGroup("blogo-pt"), }) gitea := gitea.New("capytal", "capytal.cc-blog", "https://forge.capytal.company", gitea.Opts{ Ref: "main-pt", }) blog.Use(gitea) blog.Use(&listRenderer{app.templates, "pt-BR"}) blog.Use(NewBlogPostRenderer(app.templates, "pt-BR")) blog.Use(plugins.NewPlainText()) return blog } func (app *app) ServeHTTP(w http.ResponseWriter, r *http.Request) { app.assert.NotNil(w) app.assert.NotNil(r) app.assert.NotNil(app.router) app.router.ServeHTTP(w, r) } type blogPostRenderer struct { templates templates.ITemplate lang string parser parser.Parser renderer renderer.Renderer } var _ plugin.Renderer = (*blogPostRenderer)(nil) func NewBlogPostRenderer(templates templates.ITemplate, lang string) *blogPostRenderer { return &blogPostRenderer{ templates: templates, lang: lang, parser: md.Parser(), renderer: md.Renderer(), } } func (r *blogPostRenderer) Name() string { return "capytal-blogpostrenderer-renderer" } var re = regexp.MustCompile(`

(.*?)

`) func (r *blogPostRenderer) Render(src fs.File, w io.Writer) error { c, err := io.ReadAll(src) if err != nil { return err } doc := r.parser.Parse(text.NewReader(c)) meta := doc.OwnerDocument().Meta() title := "Blog" if t, ok := meta["title"]; ok { tt, ok := t.(string) if ok { title = tt } } else { err := ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if n.Kind().String() != "Heading" { return ast.WalkContinue, nil } if h, ok := n.(*ast.Heading); !ok || h.Level > 1 { return ast.WalkContinue, nil } // TODO: This is deprecated title = string(n.Text(c)) return ast.WalkStop, nil }) if err != nil { return err } } f := new(strings.Builder) err = r.renderer.Render(f, c, doc) if err != nil { return err } return r.templates.ExecuteTemplate(w, "blog-post", map[string]any{ "Title": title, "Lang": r.lang, "Content": template.HTML(f.String()), }) } type listRenderer struct { templates templates.ITemplate lang string } var _ plugin.Renderer = (*listRenderer)(nil) func (r *listRenderer) Name() string { return "capytal-list-renderer" } func (r *listRenderer) Render(src fs.File, w io.Writer) error { d, ok := src.(fs.ReadDirFile) if !ok { return errors.New("renderer does not support single files") } entries, err := d.ReadDir(-1) if err != nil && !errors.Is(err, io.EOF) { return err } sort.Slice(entries, func(i, j int) bool { return natsort.Compare(entries[i].Name(), entries[j].Name()) }) links := map[string]string{} for _, e := range entries { n := e.Name() if strings.HasPrefix(n, ".") || e.IsDir() || slices.Contains([]string{ "LICENSE", "README.md", }, n) { continue } links[n] = r.lang } return r.templates.ExecuteTemplate(w, "blog", links) }