Diffstat (limited to 'internal')
20 files changed, 272 insertions, 672 deletions
diff --git a/internal/factgiver/factgiver.go b/internal/factgiver/factgiver.go deleted file mode 100644 index 6f77403..0000000 --- a/internal/factgiver/factgiver.go +++ /dev/null @@ -1,67 +0,0 @@ -package factgiver - -import ( - "fmt" - "math/rand" - "regexp" - "strings" - "time" - - "github.com/SayaAndy/saya-today-web/config" - "github.com/SayaAndy/saya-today-web/internal/b2" -) - -type FactGiver struct { - b2Client *b2.B2Client - cache map[string][]string - langs []string - factsFileName string - nlRe *regexp.Regexp - randGen *rand.Rand -} - -func NewFactGiver(cfg *config.FactGiverConfig, langs []string) (*FactGiver, error) { - b2Client, err := b2.NewB2Client(&cfg.Storage.Config) - if err != nil { - return nil, fmt.Errorf("fail to init b2 client for a new fact giver: %s", err.Error()) - } - - factGiver := &FactGiver{ - b2Client: b2Client, - cache: make(map[string][]string, len(langs)), - langs: langs, - factsFileName: cfg.FactsFileName, - nlRe: regexp.MustCompile(`\r?\n`), - randGen: rand.New(rand.NewSource(time.Now().UnixNano())), - } - if err = factGiver.initCache(); err != nil { - return nil, fmt.Errorf("fail to init cache for a new fact giver: %s", err.Error()) - } - - return factGiver, nil -} - -func (g *FactGiver) Give(lang string) [3]string { - factSlice := make([]string, len(g.cache[lang])) - copy(factSlice, g.cache[lang]) - g.randGen.Shuffle(len(factSlice), func(i, j int) { - factSlice[i], factSlice[j] = factSlice[j], factSlice[i] - }) - return [3]string{factSlice[0], factSlice[1], factSlice[2]} -} - -func (g *FactGiver) initCache() error { - for _, lang := range g.langs { - localFacts := strings.Replace(g.factsFileName, "*", lang, 1) - factsContentBytes, err := g.b2Client.ReadAll(localFacts) - if err != nil { - return fmt.Errorf("fail to read '%s' facts file: %s", lang, err.Error()) - } - factsContent := string(factsContentBytes) - g.cache[lang] = g.nlRe.Split(factsContent, -1) - if g.cache[lang][len(g.cache[lang])-1] == "" { - g.cache[lang] = g.cache[lang][:len(g.cache[lang])-1] - } - } - return nil -} diff --git a/internal/frontmatter/parser.go b/internal/frontmatter/parser.go index 83312dc..1a521c0 100644 --- a/internal/frontmatter/parser.go +++ b/internal/frontmatter/parser.go @@ -16,8 +16,6 @@ type Metadata struct { Thumbnail string `yaml:"thumbnail"` Tags []string `yaml:"tags"` Geolocation string `yaml:"geolocation"` - Medley string `yaml:"medley"` - MedleyPart int `yaml:"medleyPart"` } func ParseFrontmatter(content []byte) (metadata *Metadata, markdown []byte, err error) { diff --git a/internal/glightbox/block.go b/internal/glightbox/block.go deleted file mode 100644 index e53e67b..0000000 --- a/internal/glightbox/block.go +++ /dev/null @@ -1,32 +0,0 @@ -package glightbox - -import ( - "time" - - "github.com/yuin/goldmark/ast" -) - -// GLightboxBlock represents a light gallery block in the AST -type GLightboxBlock struct { - ast.BaseBlock - Images []GLightboxImage - Location *time.Location -} - -type GLightboxImage struct { - URL string - Tags []string - Caption []byte -} - -var KindGLightboxBlock = ast.NewNodeKind("GLightboxBlock") - -// Dump implements ast.Node.Dump -func (n *GLightboxBlock) Dump(source []byte, level int) { - ast.DumpHelper(n, source, level, nil, nil) -} - -// Kind implements ast.Node.Kind -func (n *GLightboxBlock) Kind() ast.NodeKind { - return KindGLightboxBlock -} diff --git a/internal/glightbox/html_renderer.go b/internal/glightbox/html_renderer.go deleted file mode 100644 index 44fbd4c..0000000 --- a/internal/glightbox/html_renderer.go +++ /dev/null @@ -1,188 +0,0 @@ -package glightbox - -import ( - "bytes" - "fmt" - "math/rand" - "regexp" - "strings" - "time" - - "github.com/SayaAndy/saya-today-web/internal/tailwind" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/renderer/html" - "github.com/yuin/goldmark/util" -) - -type GLightboxHTMLRenderer struct { - html.Config - md goldmark.Markdown - anchorMatchRe *regexp.Regexp -} - -func NewGLightboxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { - r := &GLightboxHTMLRenderer{ - Config: html.NewConfig(), - md: goldmark.New( - goldmark.WithParserOptions( - parser.WithAutoHeadingID(), - parser.WithAttribute(), - ), - goldmark.WithRenderer( - renderer.NewRenderer( - renderer.WithNodeRenderers( - util.Prioritized(tailwind.NewCustomLinkRenderer( - html.WithUnsafe(), html.WithHardWraps(), html.WithXHTML(), - ), 50), - util.Prioritized(html.NewRenderer( - html.WithUnsafe(), html.WithHardWraps(), html.WithXHTML(), - ), 100), - ), - ), - ), - ), - } - for _, opt := range opts { - opt.SetHTMLOption(&r.Config) - } - r.anchorMatchRe = regexp.MustCompile(`(?s)<\s*a(\s+[^<]*)(href\s*=\s*["'].*?["'])\s*([^<]*)>(.*?)<\s*\/\s*a\s*>`) - return r -} - -func (r *GLightboxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(KindGLightboxBlock, r.renderGLightbox) -} - -func (r *GLightboxHTMLRenderer) renderGLightbox(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { - if entering { - gallery := n.(*GLightboxBlock) - - if len(gallery.Images) == 0 { - return ast.WalkContinue, nil - } - - galleryID := generateDivId(8) - - var elements []string - for i, img := range gallery.Images { - imageUrlSegments := strings.Split(img.URL, ".") - imageUrlWithoutExt := strings.Join(imageUrlSegments[:len(imageUrlSegments)-1], ".") - imageUrlParts := strings.Split(imageUrlWithoutExt, "/") - imageNameParts := strings.Split(imageUrlParts[len(imageUrlParts)-1], "-") - - var captionBuf bytes.Buffer - captionHTML := img.Caption - if err := r.md.Convert(img.Caption, &captionBuf); err == nil { - captionHTML = captionBuf.Bytes() - captionHTML = bytes.TrimPrefix(captionHTML, []byte("<p>")) - captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>\n")) - captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>")) - } - - dayDate, _ := time.Parse("20060102 150405", imageNameParts[len(imageNameParts)-2]+" "+imageNameParts[len(imageNameParts)-1]) - dayDate = dayDate.In(gallery.Location) - - glightboxDescId := "" - if len(captionHTML) != 0 { - glightboxDescId = fmt.Sprintf("glightbox-desc-%s-%d", galleryID, i) - } - - dataDescriptionAttribute := "" - if glightboxDescId != "" { - dataDescriptionAttribute = fmt.Sprintf("data-description=\".%s\"", glightboxDescId) - } - - tagClassList := make([]string, 0, len(img.Tags)) - for _, tag := range img.Tags { - switch tag { - case "2x": - tagClassList = append(tagClassList, "grid-item-2x") - } - } - - if len(img.Caption) > 0 { - tagClassList = append(tagClassList, "grid-tooltip") - } - - anchorlessCaptionHTML := r.anchorMatchRe.ReplaceAll(captionHTML, []byte("<span class=\"linklike\" $1 $3>$4</span>")) - - elements = append(elements, fmt.Sprintf(` - <a href="https://f003.backblazeb2.com/file/sayana-photos/full/%s" class="glightbox-%s grid-item %s grid-item-%s p-1" - data-gallery="gallery-%s" data-title="%s" %s> - <picture> - <source media="(width < 640px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp" /> - <source media="(width < 1120px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-560p/%s.webp" /> - <source media="(width < 1600px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-800p/%s.webp" /> - <source media="(width < 2400px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-1200p/%s.webp" /> - <source media="(width >= 2400px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-1600p/%s.webp" /> - <img src="https://f003.backblazeb2.com/file/sayana-photos/webp-800p/%s.webp" /> - </picture> - <span class="grid-tooltip-text"><p>%s</p></span> - <span class="grid-item-index">%d</span> - </a>`, img.URL, galleryID, strings.Join(tagClassList, " "), galleryID, galleryID, dayDate.Format("2006-01-02 15:04:05 -07:00"), dataDescriptionAttribute, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, anchorlessCaptionHTML, i+1)) - - if glightboxDescId != "" { - elements = append(elements, fmt.Sprintf(` - <div class="glightbox-desc rounded-4 %s"> - <p>%s</p> - </div>`, glightboxDescId, captionHTML)) - } - } - - w.WriteString(fmt.Sprintf(` -<div class="justify-content-center display-block m-1"> - <hr class="border-t-3 border-dotted border-main-dark mt-1 mb-2 w-[80%%] ml-auto mr-auto"> - <div class="grid masonry-grid-%s mx-auto"> - <div class="grid-sizer grid-sizer-%s"></div> - %s - </div> - <hr class="border-t-3 border-dotted border-main-dark mt-1 mb-2 w-[80%%] ml-auto mr-auto"> -</div>`, galleryID, galleryID, strings.Join(elements, "\n"))) - - w.WriteString(fmt.Sprintf(` -<script> - var lightbox_%s = GLightbox({ - selector: '.glightbox-%s', - moreLength: 0 - }); - - var msnry_%s = new Masonry('.masonry-grid-%s', { - itemSelector: '.grid-item-%s', - columnWidth: '.grid-sizer-%s', - percentPosition: true, - horizontalOrder: true - }); - - var imgLoad_%s_timer; - var imgLoad_%s = imagesLoaded('.masonry-grid-%s'); - - function initMasonryLayout_%s() { - clearTimeout(imgLoad_%s_timer); - imgLoad_%s_timer = setTimeout(() => msnry_%s.layout(), 500); - } - - imgLoad_%s.on('progress', initMasonryLayout_%s); - - document.addEventListener('popout', (e) => { - imgLoad_%s.off('progress', initMasonryLayout_%s); - initMasonryLayout_%s = null; - imgLoad_%s = null; - }); -</script>`, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID)) - } - - return ast.WalkContinue, nil -} - -func generateDivId(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyz" - seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) // Seed with current time - b := make([]byte, length) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] - } - return string(b) -} diff --git a/internal/lightgallery/block.go b/internal/lightgallery/block.go new file mode 100644 index 0000000..8eb7c9e --- /dev/null +++ b/internal/lightgallery/block.go @@ -0,0 +1,31 @@ +package lightgallery + +import ( + "time" + + "github.com/yuin/goldmark/ast" +) + +// LightGalleryBlock represents a light gallery block in the AST +type LightGalleryBlock struct { + ast.BaseBlock + Images []LightGalleryImage + Location *time.Location +} + +type LightGalleryImage struct { + URL string + Caption []byte +} + +var KindLightGalleryBlock = ast.NewNodeKind("LightGalleryBlock") + +// Dump implements ast.Node.Dump +func (n *LightGalleryBlock) Dump(source []byte, level int) { + ast.DumpHelper(n, source, level, nil, nil) +} + +// Kind implements ast.Node.Kind +func (n *LightGalleryBlock) Kind() ast.NodeKind { + return KindLightGalleryBlock +} diff --git a/internal/glightbox/extension.go b/internal/lightgallery/extension.go index 7fd17b1..761dc05 100644 --- a/internal/glightbox/extension.go +++ b/internal/lightgallery/extension.go @@ -1,4 +1,4 @@ -package glightbox +package lightgallery import ( "github.com/yuin/goldmark" @@ -8,21 +8,21 @@ import ( ) // Extension that combines parser and renderer -type GLightboxExtension struct{} +type LightGalleryExtension struct{} -func NewGLightboxExtension() goldmark.Extender { - return &GLightboxExtension{} +func NewLightGalleryExtension() goldmark.Extender { + return &LightGalleryExtension{} } -func (e *GLightboxExtension) Extend(m goldmark.Markdown) { +func (e *LightGalleryExtension) Extend(m goldmark.Markdown) { m.Parser().AddOptions( parser.WithBlockParsers( - util.Prioritized(NewGLightboxParser(), 500), + util.Prioritized(NewLightGalleryParser(), 500), ), ) m.Renderer().AddOptions( renderer.WithNodeRenderers( - util.Prioritized(NewGLightboxHTMLRenderer(), 500), + util.Prioritized(NewLightGalleryHTMLRenderer(), 500), ), ) } diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go new file mode 100644 index 0000000..c8c1cf0 --- /dev/null +++ b/internal/lightgallery/html_renderer.go @@ -0,0 +1,144 @@ +package lightgallery + +import ( + "bytes" + "fmt" + "math/rand" + "strings" + "time" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" +) + +type LightGalleryHTMLRenderer struct { + html.Config + md goldmark.Markdown +} + +func NewLightGalleryHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &LightGalleryHTMLRenderer{ + Config: html.NewConfig(), + md: goldmark.New( + goldmark.WithRendererOptions( + html.WithHardWraps(), + html.WithXHTML(), + ), + ), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + return r +} + +func (r *LightGalleryHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindLightGalleryBlock, r.renderLightGallery) +} + +func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + gallery := n.(*LightGalleryBlock) + + if len(gallery.Images) == 0 { + return ast.WalkContinue, nil + } + + galleryID := generateDivId(8) + + w.WriteString(fmt.Sprintf(` +<div class="justify-content-center display-block m-2"> + <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%%] ml-auto mr-auto"> + <div id="lg-%s" class="inline-gallery-container relative ml-auto mr-auto"></div> + <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%%] ml-auto mr-auto"> +</div>`, galleryID)) + + var dynamicElements []string + for _, img := range gallery.Images { + imageUrlSegments := strings.Split(img.URL, ".") + imageUrlWithoutExt := strings.Join(imageUrlSegments[:len(imageUrlSegments)-1], ".") + imageNameParts := strings.Split(imageUrlWithoutExt, "-") + + var captionBuf bytes.Buffer + captionHTML := img.Caption + if err := r.md.Convert(img.Caption, &captionBuf); err == nil { + captionHTML = captionBuf.Bytes() + captionHTML = bytes.TrimPrefix(captionHTML, []byte("<p>")) + captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>\n")) + captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>")) + } + + dayDate, _ := time.Parse("20060102 150405", imageNameParts[len(imageNameParts)-2]+" "+imageNameParts[len(imageNameParts)-1]) + dayDate = dayDate.In(gallery.Location) + dynamicElements = append(dynamicElements, fmt.Sprintf(`{ + src: + "https://f003.backblazeb2.com/file/sayana-photos/full/%s", + downloadUrl: + "https://f003.backblazeb2.com/file/sayana-photos/full/%s", + alt: "%s", + responsive: "https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp 384, https://f003.backblazeb2.com/file/sayana-photos/webp-560p/%s.webp 672, https://f003.backblazeb2.com/file/sayana-photos/webp-800p/%s.webp 960, https://f003.backblazeb2.com/file/sayana-photos/webp-1200p/%s.webp 1440, https://f003.backblazeb2.com/file/sayana-photos/webp-1600p/%s.webp 1920", + thumb: + "https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp", + subHtml: `+"`"+`<div class="flex flex-row light-gallery-captions"> + <p class="grow !text-[1vmax]/[1] text-left font-spectral text-main-dark">%s</p> + <p class="!text-[1vmax]/[1] text-right font-spectral text-secondary">%s</p> + </div>`+"`"+` + }`, img.URL, img.URL, util.EscapeHTML(captionHTML), imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, captionHTML, dayDate.Format("2006-01-02 15:04:05 -07:00"))) + } + + w.WriteString(fmt.Sprintf(` +<script> +function createLightGallery%s() { + const $lgContainer = document.getElementById('lg-%s'); + const config = { + container: $lgContainer, + dynamic: true, + dynamicEl: [%s], + width: "100%%", + height: "50vmin", + hash: false, + closable: false, + showMaximizeIcon: true, + appendSubHtmlTo: ".lg-sub-html", + isMobile: () => false, + slideDelay: 0, + plugins: [lgZoom, lgThumbnail], + thumbWidth: calculateVmin(10), + thumbHeight: "10vmin", + thumbMargin: 4 + }; + const inlineGallery = lightGallery($lgContainer, config); + + setTimeout(() => { + inlineGallery.openGallery(); + }, 200); + + galleryMap.set(inlineGallery, createLightGallery%s); +} + +document.addEventListener('htmx:afterRequest', (e) => { + if (e.detail.xhr.status == 404 || e.detail.successful != true) { + return console.error(e); + } + if (e.detail.target.id == 'general-page-body') { + createLightGallery%s(); + } +}, {once: true}); +</script>`, galleryID, galleryID, strings.Join(dynamicElements, ","), galleryID, galleryID)) + } + + return ast.WalkContinue, nil +} + +func generateDivId(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyz" + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) // Seed with current time + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} diff --git a/internal/glightbox/parser.go b/internal/lightgallery/parser.go index 6780090..daec797 100644 --- a/internal/glightbox/parser.go +++ b/internal/lightgallery/parser.go @@ -1,10 +1,9 @@ -package glightbox +package lightgallery import ( "bytes" "log/slog" "regexp" - "strings" "time" "github.com/yuin/goldmark/ast" @@ -12,17 +11,17 @@ import ( "github.com/yuin/goldmark/text" ) -type GLightboxParser struct{} +type LightGalleryParser struct{} -func NewGLightboxParser() parser.BlockParser { - return &GLightboxParser{} +func NewLightGalleryParser() parser.BlockParser { + return &LightGalleryParser{} } -func (p *GLightboxParser) Trigger() []byte { +func (p *LightGalleryParser) Trigger() []byte { return []byte{'{'} } -func (p *GLightboxParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { +func (p *LightGalleryParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { line, _ := reader.PeekLine() if !bytes.HasPrefix(line, []byte("{Gallery:")) { @@ -44,10 +43,10 @@ func (p *GLightboxParser) Open(parent ast.Node, reader text.Reader, pc parser.Co return nil, parser.NoChildren } - return &GLightboxBlock{Location: loc}, parser.NoChildren + return &LightGalleryBlock{Location: loc}, parser.NoChildren } -func (p *GLightboxParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { +func (p *LightGalleryParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { line, segment := reader.PeekLine() if len(line) == 0 || segment.Len() == 0 { return parser.Continue | parser.NoChildren @@ -59,42 +58,31 @@ func (p *GLightboxParser) Continue(node ast.Node, reader text.Reader, pc parser. return parser.Close } - gallery := node.(*GLightboxBlock) + gallery := node.(*LightGalleryBlock) - parts := bytes.SplitN(trimmed, []byte{'|'}, 3) + parts := bytes.SplitN(trimmed, []byte{'|'}, 2) url := bytes.TrimSpace(parts[0]) caption := make([]byte, 0) - tagsRaw := "" - if len(parts) == 2 { + if len(parts) > 1 { caption = bytes.TrimSpace(parts[1]) } - if len(parts) >= 3 { - tagsRaw = string(parts[1]) - caption = bytes.TrimSpace(parts[2]) - } - - tags := strings.Split(tagsRaw, ",") - for i := range tags { - tags[i] = strings.TrimSpace(tags[i]) - } - gallery.Images = append(gallery.Images, GLightboxImage{ + gallery.Images = append(gallery.Images, LightGalleryImage{ URL: string(url), - Tags: tags, Caption: caption, }) return parser.Continue | parser.NoChildren } -func (p *GLightboxParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { +func (p *LightGalleryParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { } -func (p *GLightboxParser) CanInterruptParagraph() bool { +func (p *LightGalleryParser) CanInterruptParagraph() bool { return true } -func (p *GLightboxParser) CanAcceptIndentedLine() bool { +func (p *LightGalleryParser) CanAcceptIndentedLine() bool { return false } diff --git a/internal/router/api-v1-blog-search.go b/internal/router/api-v1-blog-search.go index 69be651..e22fabe 100644 --- a/internal/router/api-v1-blog-search.go +++ b/internal/router/api-v1-blog-search.go @@ -1,7 +1,6 @@ package router import ( - "encoding/json" "fmt" "log/slog" "net/url" @@ -36,18 +35,10 @@ func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Clie slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz)) } - cacheKey := "blog-search." + lang + ".pages-list" - var pages []*b2.BlogPage - if pagesBytes, ok := PCache.Get(cacheKey); pagesBytes != nil || ok { - json.Unmarshal(pagesBytes, &pages) - } else { - pages, err = b2Client.Scan(lang + "/") - if err != nil { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages for '%s' lang: %v", lang, err)) - } - pagesBytes, _ := json.Marshal(pages) - PCache.SetWithTTL(cacheKey, pagesBytes, int64(len(pagesBytes)), 5*time.Minute) + pages, err := b2Client.Scan(lang + "/") + if err != nil { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages for '%s' lang: %v", lang, err)) } encodedQuery := c.Request().URI().QueryString() @@ -79,7 +70,6 @@ func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Clie "Thumbnail": page.Metadata.Thumbnail, "Tags": page.Metadata.Tags, "LikeCount": CCache.GetLikeCount(page.FileName), - "ViewCount": CCache.GetViewCount(page.FileName), }) break } diff --git a/internal/router/api-v1-general-page-body.go b/internal/router/api-v1-general-page-body.go index c1a4a9f..4108a4f 100644 --- a/internal/router/api-v1-general-page-body.go +++ b/internal/router/api-v1-general-page-body.go @@ -4,22 +4,17 @@ import ( "fmt" "html/template" "log/slog" - "math/rand" "net/url" "regexp" "slices" "strings" - "time" "github.com/SayaAndy/saya-today-web/internal/b2" - "github.com/SayaAndy/saya-today-web/internal/factgiver" "github.com/SayaAndy/saya-today-web/locale" "github.com/gofiber/fiber/v2" "github.com/yuin/goldmark" ) -var FactGiver *factgiver.FactGiver - func init() { tm.Add("general-page-body", "views/partials/general-page-body.html") } @@ -38,15 +33,8 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []string, } path := urlStruct.EscapedPath() - - cacheKey := fmt.Sprintf("body.%s", path) - if val, ok := PCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) - } - pathParts := strings.Split(strings.Trim(path, "/"), "/") - if len(pathParts) == 0 { + if len(pathParts) < 2 { return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") } @@ -112,7 +100,6 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []string, values["Tags"] = tagsArray values["QuerySort"] = querySort values["QueryTags"] = strings.Join(queryTags, ",") - values["Title"] = l[lang].BlogSearch.Header additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") } else if len(pathParts) == 3 && pathParts[1] == "blog" { @@ -136,19 +123,8 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []string, values["MapLocationAreaMeters"] = areaError values["Title"] = metadata.Title values["ParsedMarkdown"] = template.HTML(parsedMarkdown) - values["PublishedDate"] = metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00") - values["ActionDate"] = metadata.ActionDate additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") - - go CCache.View(c.IP(), pathParts[2]) - } else if len(pathParts) == 1 { - values["Title"] = l[lang].HomePage.Header - values["FilledHeartCount"] = uint(40) - values["OutlineHeartCount"] = uint(40) - values["GifName"] = fmt.Sprintf("otter-%d.gif", rand.Int()%3+1) - values["FunFacts"] = FactGiver.Give(lang) - additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") } content, err := tm.Render("general-page-body", values, additionalTemplates...) @@ -157,7 +133,6 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []string, return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") } - go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) return c.Status(fiber.StatusOK).Type("html").Send(content) } diff --git a/internal/router/api-v1-general-page-bottom-embeds.go b/internal/router/api-v1-general-page-bottom-embeds.go index c4d9543..f0957f3 100644 --- a/internal/router/api-v1-general-page-bottom-embeds.go +++ b/internal/router/api-v1-general-page-bottom-embeds.go @@ -6,7 +6,6 @@ import ( "net/url" "slices" "strings" - "time" "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/locale" @@ -31,14 +30,8 @@ func Api_V1_GeneralPage_BottomEmbeds(l map[string]*locale.LocaleConfig, langs [] } path := urlStruct.EscapedPath() - cacheKey := fmt.Sprintf("bottom-embeds.%s", path) - if val, ok := PCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) - } - pathParts := strings.Split(strings.Trim(path, "/"), "/") - if len(pathParts) == 0 { + if len(pathParts) < 2 { return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") } @@ -58,8 +51,6 @@ func Api_V1_GeneralPage_BottomEmbeds(l map[string]*locale.LocaleConfig, langs [] additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") } else if len(pathParts) == 3 && pathParts[1] == "blog" { additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") - } else if len(pathParts) == 1 { - additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") } content, err := tm.Render("general-page-bottom-embeds", values, additionalTemplates...) @@ -68,7 +59,6 @@ func Api_V1_GeneralPage_BottomEmbeds(l map[string]*locale.LocaleConfig, langs [] return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") } - go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) return c.Status(fiber.StatusOK).Type("html").Send(content) } diff --git a/internal/router/api-v1-general-page-footer.go b/internal/router/api-v1-general-page-footer.go index 003f54c..39b6bea 100644 --- a/internal/router/api-v1-general-page-footer.go +++ b/internal/router/api-v1-general-page-footer.go @@ -6,7 +6,6 @@ import ( "net/url" "slices" "strings" - "time" "github.com/SayaAndy/saya-today-web/locale" "github.com/gofiber/fiber/v2" @@ -30,15 +29,8 @@ func Api_V1_GeneralPage_Footer(l map[string]*locale.LocaleConfig, langs []string } path := urlStruct.EscapedPath() - - cacheKey := fmt.Sprintf("footer.%s", path) - if val, ok := PCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) - } - pathParts := strings.Split(strings.Trim(path, "/"), "/") - if len(pathParts) == 0 { + if len(pathParts) < 2 { return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") } @@ -58,8 +50,6 @@ func Api_V1_GeneralPage_Footer(l map[string]*locale.LocaleConfig, langs []string additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") } else if len(pathParts) == 3 && pathParts[1] == "blog" { additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") - } else if len(pathParts) == 1 { - additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") } content, err := tm.Render("general-page-footer", values, additionalTemplates...) @@ -68,7 +58,6 @@ func Api_V1_GeneralPage_Footer(l map[string]*locale.LocaleConfig, langs []string return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") } - go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) return c.Status(fiber.StatusOK).Type("html").Send(content) } diff --git a/internal/router/api-v1-general-page-header.go b/internal/router/api-v1-general-page-header.go index 80228f7..0529ba6 100644 --- a/internal/router/api-v1-general-page-header.go +++ b/internal/router/api-v1-general-page-header.go @@ -6,7 +6,6 @@ import ( "net/url" "slices" "strings" - "time" "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/locale" @@ -31,15 +30,8 @@ func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string } path := urlStruct.EscapedPath() - - cacheKey := fmt.Sprintf("header.%s", path) - if val, ok := PCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) - } - pathParts := strings.Split(strings.Trim(path, "/"), "/") - if len(pathParts) == 0 { + if len(pathParts) < 2 { return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") } @@ -67,9 +59,6 @@ func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string values["PublishedDate"] = metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00") values["ActionDate"] = metadata.ActionDate additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") - } else if len(pathParts) == 1 { - values["Title"] = l[lang].HomePage.Header - additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") } content, err := tm.Render("general-page-header", values, additionalTemplates...) @@ -78,7 +67,6 @@ func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") } - go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) return c.Status(fiber.StatusOK).Type("html").Send(content) } diff --git a/internal/router/api-v1-general-page-top-embeds.go b/internal/router/api-v1-general-page-top-embeds.go index 78021b2..090107b 100644 --- a/internal/router/api-v1-general-page-top-embeds.go +++ b/internal/router/api-v1-general-page-top-embeds.go @@ -6,7 +6,6 @@ import ( "net/url" "slices" "strings" - "time" "github.com/SayaAndy/saya-today-web/locale" "github.com/gofiber/fiber/v2" @@ -30,15 +29,8 @@ func Api_V1_GeneralPage_TopEmbeds(l map[string]*locale.LocaleConfig, langs []str } path := urlStruct.EscapedPath() - - cacheKey := fmt.Sprintf("top-embeds.%s", path) - if val, ok := PCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) - } - pathParts := strings.Split(strings.Trim(path, "/"), "/") - if len(pathParts) == 0 { + if len(pathParts) < 2 { return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") } @@ -58,8 +50,6 @@ func Api_V1_GeneralPage_TopEmbeds(l map[string]*locale.LocaleConfig, langs []str additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") } else if len(pathParts) == 3 && pathParts[1] == "blog" { additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") - } else if len(pathParts) == 1 { - additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") } content, err := tm.Render("general-page-top-embeds", values, additionalTemplates...) @@ -68,7 +58,6 @@ func Api_V1_GeneralPage_TopEmbeds(l map[string]*locale.LocaleConfig, langs []str return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") } - go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) return c.Status(fiber.StatusOK).Type("html").Send(content) } diff --git a/internal/router/api-v1-general-page.go b/internal/router/api-v1-general-page.go deleted file mode 100644 index d0ea559..0000000 --- a/internal/router/api-v1-general-page.go +++ /dev/null @@ -1,52 +0,0 @@ -package router - -import ( - "fmt" - "log/slog" - "slices" - "strings" - - "github.com/SayaAndy/saya-today-web/locale" - "github.com/gofiber/fiber/v2" -) - -func init() { - tm.Add("general-page", "views/layouts/general-page.html") -} - -func Api_V1_GeneralPage(l map[string]*locale.LocaleConfig, langs []string) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - path := c.Path() - - pathParts := strings.Split(strings.Trim(path, "/"), "/") - if len(pathParts) == 0 { - return c.Status(fiber.ErrBadRequest.Code).SendString("url path is invalid: expect format '/{lang}/...'") - } - - lang := pathParts[0] - if !slices.Contains(langs, lang) { - return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) - } - - cacheKey := "general-page." + lang - if val, ok := PCache.Get(cacheKey); val != nil && ok { - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(val) - } - - content, err := tm.Render("general-page", fiber.Map{ - "L": l[lang], - "Lang": lang, - "QueryString": string(c.Request().URI().QueryString()), - }) - if err != nil { - slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") - } - - go PCache.Set(cacheKey, content, int64(len(content))) - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Type("html").Send(content) - } -} diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go index d607cdd..81d7641 100644 --- a/internal/router/client-cache.go +++ b/internal/router/client-cache.go @@ -11,12 +11,16 @@ import ( "golang.org/x/crypto/argon2" ) +type PageLike struct { + PageRef string + UserId string +} + type ClientCache struct { hashMap map[string]string hashMapMutex sync.RWMutex likePageMap map[string]map[string]struct{} - viewPageMap map[string]map[string]struct{} pageMutexMap map[string]*sync.RWMutex pageMutexMapMutex sync.Mutex @@ -39,7 +43,6 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { } likePageMap := make(map[string]map[string]struct{}) - viewPageMap := make(map[string]map[string]struct{}) pageMutexMap := make(map[string]*sync.RWMutex) for rows.Next() { @@ -52,32 +55,9 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { userIdString := base64.RawStdEncoding.EncodeToString(userId) if _, ok := likePageMap[pageRef]; !ok { likePageMap[pageRef] = make(map[string]struct{}) - viewPageMap[pageRef] = make(map[string]struct{}) pageMutexMap[pageRef] = &sync.RWMutex{} } likePageMap[pageRef][userIdString] = struct{}{} - viewPageMap[pageRef][userIdString] = struct{}{} - } - - rows, err = tx.Query("select * from blog_views;") - if err != nil { - tx.Rollback() - return nil, fmt.Errorf("fail to query db for blog_views to fill cache: %w", err) - } - - for rows.Next() { - var pageRef string - var userId []byte - if err = rows.Scan(&pageRef, &userId); err != nil { - tx.Rollback() - return nil, fmt.Errorf("fail scanning blog_views to fill cache: %w", err) - } - userIdString := base64.RawStdEncoding.EncodeToString(userId) - if _, ok := viewPageMap[pageRef]; !ok { - viewPageMap[pageRef] = make(map[string]struct{}) - pageMutexMap[pageRef] = &sync.RWMutex{} - } - viewPageMap[pageRef][userIdString] = struct{}{} } if err = tx.Commit(); err != nil { @@ -87,7 +67,6 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { return &ClientCache{ hashMap: make(map[string]string), likePageMap: likePageMap, - viewPageMap: viewPageMap, pageMutexMap: pageMutexMap, salt: salt, db: db, @@ -100,14 +79,51 @@ func (c *ClientCache) Close() error { return fmt.Errorf("fail to init transaction with db to dump cache: %w", err) } - if err = batchSave(tx, "blog_likes", c.likePageMap); err != nil { + if _, err = tx.Exec("delete from blog_likes;"); err != nil { tx.Rollback() - return fmt.Errorf("fail to save blog_likes: %s", err) + return fmt.Errorf("fail to truncate table blog_likes: %w", err) } - if err = batchSave(tx, "blog_views", c.viewPageMap); err != nil { - tx.Rollback() - return fmt.Errorf("fail to save blog_views: %s", err) + userIdBytes := make(map[string][]byte) + + sqlStatement := fmt.Sprintf(` + INSERT OR IGNORE INTO blog_likes (page_ref, user_id) + VALUES %s(?, ?); + `, strings.Repeat("(?, ?), ", 99)) + sqlStatementVars := make([]any, 0, 200) + + for pageRef, userSet := range c.likePageMap { + for userId := range userSet { + if _, ok := userIdBytes[userId]; !ok { + userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId) + if err != nil { + slog.Warn("couldn't parse one of user hashes into bytes back", slog.String("hash", userId), slog.String("error", err.Error())) + continue + } + } + + sqlStatementVars = append(sqlStatementVars, any(pageRef), any(userIdBytes[userId])) + if len(sqlStatementVars) < 200 { + continue + } + + if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { + slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) + } + + sqlStatementVars = make([]any, 0, 200) + } + } + + if len(sqlStatementVars) > 0 { + sqlStatement = fmt.Sprintf(` + INSERT OR IGNORE INTO blog_likes (page_ref, user_id) + VALUES %s(?, ?); + `, strings.Repeat("(?, ?), ", len(sqlStatementVars)/2-1)) + + if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { + slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) + } } return tx.Commit() @@ -211,94 +227,3 @@ func (c *ClientCache) LikeOff(id string, page string) (alreadyUnliked bool) { delete(c.likePageMap[page], hash) return false } - -func (c *ClientCache) GetViewStatus(id string, page string) bool { - page = strings.Clone(page) - - mutex := c.getPageMutex(page) - mutex.RLock() - defer mutex.RUnlock() - - if _, ok := c.viewPageMap[page]; !ok { - return false - } - _, ok := c.viewPageMap[page][c.GetHash(id)] - return ok -} - -func (c *ClientCache) GetViewCount(page string) int { - page = strings.Clone(page) - - mutex := c.getPageMutex(page) - mutex.RLock() - defer mutex.RUnlock() - - if userSet, ok := c.viewPageMap[page]; ok { - return len(userSet) - } - return 0 -} - -func (c *ClientCache) View(id string, page string) { - page = strings.Clone(page) - hash := c.GetHash(id) - - mutex := c.getPageMutex(page) - mutex.Lock() - defer mutex.Unlock() - - if _, ok := c.viewPageMap[page]; !ok { - c.viewPageMap[page] = make(map[string]struct{}) - } - c.viewPageMap[page][hash] = struct{}{} -} - -func batchSave(tx *sql.Tx, table string, pageMap map[string]map[string]struct{}) (err error) { - if _, err = tx.Exec(fmt.Sprintf("delete from %s;", table)); err != nil { - return fmt.Errorf("fail to truncate table %s: %w", table, err) - } - - userIdBytes := make(map[string][]byte) - - sqlStatement := fmt.Sprintf(` - INSERT OR IGNORE INTO %s (page_ref, user_id) - VALUES %s(?, ?); - `, table, strings.Repeat("(?, ?), ", 99)) - sqlStatementVars := make([]any, 0, 200) - - for pageRef, userSet := range pageMap { - for userId := range userSet { - if _, ok := userIdBytes[userId]; !ok { - userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId) - if err != nil { - slog.Warn("couldn't parse one of user hashes into bytes back", slog.String("hash", userId), slog.String("error", err.Error())) - continue - } - } - - sqlStatementVars = append(sqlStatementVars, any(pageRef), any(userIdBytes[userId])) - if len(sqlStatementVars) < 200 { - continue - } - - if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { - slog.Warn("couldn't insert blog stat pairs into db", slog.String("table", table), slog.String("error", err.Error())) - } - - sqlStatementVars = make([]any, 0, 200) - } - } - - if len(sqlStatementVars) > 0 { - sqlStatement = fmt.Sprintf(` - INSERT OR IGNORE INTO %s (page_ref, user_id) - VALUES %s(?, ?); - `, table, strings.Repeat("(?, ?), ", len(sqlStatementVars)/2-1)) - - if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { - slog.Warn("couldn't insert blog stat pairs into db", slog.String("table", table), slog.String("error", err.Error())) - } - } - - return nil -} diff --git a/internal/router/page-cache.go b/internal/router/page-cache.go deleted file mode 100644 index 01e4792..0000000 --- a/internal/router/page-cache.go +++ /dev/null @@ -1,5 +0,0 @@ -package router - -import "github.com/dgraph-io/ristretto/v2" - -var PCache *ristretto.Cache[string, []byte] diff --git a/internal/tailwind/link_renderer.go b/internal/tailwind/link_renderer.go deleted file mode 100644 index ff3d437..0000000 --- a/internal/tailwind/link_renderer.go +++ /dev/null @@ -1,49 +0,0 @@ -package tailwind - -import ( - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/renderer/html" - "github.com/yuin/goldmark/util" -) - -type CustomLinkRenderer struct { - html.Config -} - -func NewCustomLinkRenderer(opts ...html.Option) renderer.NodeRenderer { - r := &CustomLinkRenderer{ - Config: html.NewConfig(), - } - for _, opt := range opts { - opt.SetHTMLOption(&r.Config) - } - return r -} - -func (r *CustomLinkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - n := node.(*ast.Link) - if entering { - _, _ = w.WriteString("<a href=\"") - if r.Unsafe || !html.IsDangerousURL(n.Destination) { - _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true))) - } - _, _ = w.WriteString(`"`) - if n.Title != nil { - _, _ = w.WriteString(` title="`) - _, _ = w.Write(util.EscapeHTML(n.Title)) - _, _ = w.WriteString(`"`) - } - if n.Attributes() != nil { - html.RenderAttributes(w, n, nil) - } - _, _ = w.WriteString(">") - } else { - _, _ = w.WriteString("</a>") - } - return ast.WalkContinue, nil -} - -func (r *CustomLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(ast.KindLink, r.renderLink) -} diff --git a/internal/tailwind/transformer.go b/internal/tailwind/transformer.go index 23ba1a3..2f052b9 100644 --- a/internal/tailwind/transformer.go +++ b/internal/tailwind/transformer.go @@ -1,9 +1,6 @@ package tailwind import ( - "bytes" - "fmt" - "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" @@ -20,48 +17,44 @@ func (t *TailwindTransformer) Transform(node *ast.Document, reader text.Reader, switch node := n.(type) { case *ast.Heading: classes := map[int]string{ - 1: "font-patua text-4xl font-bold text-main-dark mb-3 tracking-[.0125rem]", - 2: "font-spectral text-3xl font-bold text-main-dark mb-1 tracking-[.0125rem]", - 3: "font-spectral text-2xl font-medium text-main-dark mb-0.8 tracking-[.0125rem]", - 4: "font-spectral text-xl font-medium text-main-medium mb-0.5 tracking-[.0125rem]", - 5: "font-spectral text-base font-medium text-main-medium mb-0.5 italic tracking-[.0125rem]", - 6: "font-spectral text-base font-medium text-secondary mb-0.5", + 1: "font-patua text-[2.5vmax] font-bold text-main-dark mb-[1.2vmin] tracking-[0.04vmin]", + 2: "font-spectral text-[2vmax] font-bold text-main-dark mb-[0.8vmin] tracking-[0.04vmin]", + 3: "font-spectral text-[1.5vmax] font-medium text-main-dark mb-[0.6vmin] tracking-[0.04vmin]", + 4: "font-spectral text-[1.2vmax] font-medium text-main-medium mb-[0.4vmin] tracking-[0.04vmin]", + 5: "font-spectral text-[1vmax] font-medium text-main-medium mb-[0.4vmin] italic tracking-[0.04vmin]", + 6: "font-spectral text-[1vmax] font-medium text-secondary mb-[0.4vmin]", } if class, ok := classes[node.Level]; ok { node.SetAttribute([]byte("class"), []byte(class)) } case *ast.Paragraph: - node.SetAttribute([]byte("class"), []byte("text-base/[2] font-spectral tracking-[.0125rem] -indent-8 ml-4 mb-8")) + node.SetAttribute([]byte("class"), []byte("text-[1vmax]/[2] font-spectral tracking-[0.04vmin] -indent-[2vmax] ml-[2vmax] mb-[1.6vmin]")) case *ast.List: if node.IsOrdered() { - node.SetAttribute([]byte("class"), []byte("list-decimal list-inside space-y-2 mb-4 pl-8")) + node.SetAttribute([]byte("class"), []byte("list-decimal list-inside space-y-[0.8vmin] mb-[1.6vmin] pl-[2vmax]")) } else { - node.SetAttribute([]byte("class"), []byte("list-disc list-inside space-y-2 mb-4 pl-8")) + node.SetAttribute([]byte("class"), []byte("list-disc list-inside space-y-[0.8vmin] mb-[1.6vmin] pl-[2vmax]")) } case *ast.ListItem: - node.SetAttribute([]byte("class"), []byte("text-base/[2] font-spectral tracking-[.0125rem]")) + node.SetAttribute([]byte("class"), []byte("text-[1vmax]/[2] font-spectral tracking-[0.04vmin]")) case *ast.Blockquote: - node.SetAttribute([]byte("class"), []byte("border-l-2 border-main-medium bg-background-dark p-1 mb-2 italic")) + node.SetAttribute([]byte("class"), []byte("border-l-[0.4vmin] border-main-medium bg-background-dark p-[0.8vmin] mb-[0.8vmin] italic")) case *ast.CodeSpan: node.SetAttribute([]byte("class"), []byte("bg-background-dark")) case *ast.CodeBlock, *ast.FencedCodeBlock: - node.SetAttribute([]byte("class"), []byte("bg-background-dark p-1 rounded-lg overflow-x-auto mb-4")) + node.SetAttribute([]byte("class"), []byte("bg-background-dark p-[0.8vmin] rounded-lg overflow-x-auto mb-[1.6vmin]")) case *ast.Link: - if bytes.HasPrefix(node.Destination, []byte{'.', '/'}) { - onclickAttr := fmt.Appendf(make([]byte, 0, 21+len(node.Destination)), "return changeUrl('%s');", node.Destination) - node.SetAttribute([]byte("onclick"), onclickAttr) - } node.SetAttribute([]byte("class"), []byte("text-secondary hover:text-main-dark underline")) case *ast.Image: - node.SetAttribute([]byte("class"), []byte("max-w-full h-auto rounded-lg shadow-lg mb-4")) + node.SetAttribute([]byte("class"), []byte("max-w-full h-auto rounded-lg shadow-lg mb-[1.6vmin]")) case *ast.Emphasis: switch node.Level { diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go index eebc297..d8adab6 100644 --- a/internal/templatemanager/templatemanager.go +++ b/internal/templatemanager/templatemanager.go @@ -22,22 +22,13 @@ type TemplateManagerTemplates struct { Files []string } -var templateFuncMap = template.FuncMap{ - "contains": strings.Contains, - "iterate": func(count uint) []uint { - items := make([]uint, count) - for i := range count { - items[i] = i - } - return items - }, -} - func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) { templateMap := make(map[string]templateManagerRender) for _, tmplStruct := range templates { - tmpl := template.New(tmplStruct.Name).Funcs(templateFuncMap) + tmpl := template.New("").Funcs(template.FuncMap{ + "contains": strings.Contains, + }) tmpl, err := tmpl.ParseFiles(tmplStruct.Files...) if err != nil { return nil, err @@ -84,7 +75,9 @@ func (tm *TemplateManager) Add(name string, files ...string) error { return fmt.Errorf("you can't add template without any files") } - tmpl := template.New(name).Funcs(templateFuncMap) + tmpl := template.New("").Funcs(template.FuncMap{ + "contains": strings.Contains, + }) tmpl, err := tmpl.ParseFiles(files...) if err != nil { |