diff options
| author | 2025-10-16 12:23:00 +0700 | |
|---|---|---|
| committer | 2025-10-16 12:23:00 +0700 | |
| commit | 44fe2478478225450becb77ad5d273462c9b400a (patch) | |
| tree | 299d22a03f84c7eb0bdc36ea679fcc8687e848c4 /internal/glightbox | |
| parent | d3ffefbd3d9de2378b564e4c6f965c03a5a06113 (diff) | |
| download | web-44fe2478478225450becb77ad5d273462c9b400a.tar.gz web-44fe2478478225450becb77ad5d273462c9b400a.zip | |
feat: implement new prototype gallery
Diffstat (limited to 'internal/glightbox')
| -rw-r--r-- | internal/glightbox/block.go | 31 | ||||
| -rw-r--r-- | internal/glightbox/extension.go | 28 | ||||
| -rw-r--r-- | internal/glightbox/html_renderer.go | 134 | ||||
| -rw-r--r-- | internal/glightbox/parser.go | 88 |
4 files changed, 281 insertions, 0 deletions
diff --git a/internal/glightbox/block.go b/internal/glightbox/block.go new file mode 100644 index 0000000..696f328 --- /dev/null +++ b/internal/glightbox/block.go @@ -0,0 +1,31 @@ +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 + 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/extension.go b/internal/glightbox/extension.go new file mode 100644 index 0000000..7fd17b1 --- /dev/null +++ b/internal/glightbox/extension.go @@ -0,0 +1,28 @@ +package glightbox + +import ( + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/util" +) + +// Extension that combines parser and renderer +type GLightboxExtension struct{} + +func NewGLightboxExtension() goldmark.Extender { + return &GLightboxExtension{} +} + +func (e *GLightboxExtension) Extend(m goldmark.Markdown) { + m.Parser().AddOptions( + parser.WithBlockParsers( + util.Prioritized(NewGLightboxParser(), 500), + ), + ) + m.Renderer().AddOptions( + renderer.WithNodeRenderers( + util.Prioritized(NewGLightboxHTMLRenderer(), 500), + ), + ) +} diff --git a/internal/glightbox/html_renderer.go b/internal/glightbox/html_renderer.go new file mode 100644 index 0000000..3b6a533 --- /dev/null +++ b/internal/glightbox/html_renderer.go @@ -0,0 +1,134 @@ +package glightbox + +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 GLightboxHTMLRenderer struct { + html.Config + md goldmark.Markdown +} + +func NewGLightboxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &GLightboxHTMLRenderer{ + Config: html.NewConfig(), + md: goldmark.New( + goldmark.WithRendererOptions( + html.WithHardWraps(), + html.WithXHTML(), + ), + ), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + 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) + } + + elements = append(elements, fmt.Sprintf(` + <a href="https://f003.backblazeb2.com/file/sayana-photos/full/%s" class="glightbox-%s grid-item grid-item-%s" + data-gallery="gallery-%s" data-title="%s" %s> + <img src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp" class="rounded-md p-0.5" alt="webp thumbnail" /> + </a>`, img.URL, galleryID, galleryID, galleryID, dayDate.Format("2006-01-02 15:04:05 -07:00"), dataDescriptionAttribute, imageUrlWithoutExt)) + + 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"> + <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' + }); + + var msnry_%s = new Masonry('.masonry-grid-%s', { + itemSelector: '.grid-item-%s', + columnWidth: '.grid-sizer-%s', + percentPosition: true + }); + + imagesLoaded('.masonry-grid-%s', function() { + msnry_%s.layout(); + }); +</script>`, 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/glightbox/parser.go b/internal/glightbox/parser.go new file mode 100644 index 0000000..ba3f22e --- /dev/null +++ b/internal/glightbox/parser.go @@ -0,0 +1,88 @@ +package glightbox + +import ( + "bytes" + "log/slog" + "regexp" + "time" + + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" +) + +type GLightboxParser struct{} + +func NewGLightboxParser() parser.BlockParser { + return &GLightboxParser{} +} + +func (p *GLightboxParser) Trigger() []byte { + return []byte{'{'} +} + +func (p *GLightboxParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { + line, _ := reader.PeekLine() + + if !bytes.HasPrefix(line, []byte("{Gallery:")) { + return nil, parser.NoChildren + } + + r := regexp.MustCompile(`^\{Gallery:([A-Za-z0-9\+\-/]+)\}$`) + + trimmed := bytes.TrimSpace(line) + parts := r.FindSubmatch(trimmed) + if len(parts) < 2 { + slog.Warn("invalid gallery header format", slog.String("line", string(trimmed)), slog.Int("submatch_count", len(parts))) + return nil, parser.NoChildren + } + + loc, err := time.LoadLocation(string(parts[1])) + if err != nil { + slog.Warn("invalid location specified for a gallery", slog.String("error", err.Error()), slog.String("line", string(trimmed)), slog.String("location", string(parts[1]))) + return nil, parser.NoChildren + } + + return &GLightboxBlock{Location: loc}, parser.NoChildren +} + +func (p *GLightboxParser) 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 + } + + trimmed := bytes.TrimSpace(line) + if bytes.Equal(trimmed, []byte("{Gallery}")) || bytes.Equal(trimmed, []byte("{/Gallery}")) { + reader.AdvanceLine() + return parser.Close + } + + gallery := node.(*GLightboxBlock) + + parts := bytes.SplitN(trimmed, []byte{'|'}, 2) + url := bytes.TrimSpace(parts[0]) + + caption := make([]byte, 0) + if len(parts) > 1 { + caption = bytes.TrimSpace(parts[1]) + } + + gallery.Images = append(gallery.Images, GLightboxImage{ + URL: string(url), + Caption: caption, + }) + + return parser.Continue | parser.NoChildren +} + +func (p *GLightboxParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { +} + +func (p *GLightboxParser) CanInterruptParagraph() bool { + return true +} + +func (p *GLightboxParser) CanAcceptIndentedLine() bool { + return false +} |