Diffstat (limited to 'internal/lightgallery')
| -rw-r--r-- | internal/lightgallery/block.go | 31 | ||||
| -rw-r--r-- | internal/lightgallery/extension.go | 28 | ||||
| -rw-r--r-- | internal/lightgallery/html_renderer.go | 137 | ||||
| -rw-r--r-- | internal/lightgallery/parser.go | 88 |
4 files changed, 284 insertions, 0 deletions
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/lightgallery/extension.go b/internal/lightgallery/extension.go new file mode 100644 index 0000000..761dc05 --- /dev/null +++ b/internal/lightgallery/extension.go @@ -0,0 +1,28 @@ +package lightgallery + +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 LightGalleryExtension struct{} + +func NewLightGalleryExtension() goldmark.Extender { + return &LightGalleryExtension{} +} + +func (e *LightGalleryExtension) Extend(m goldmark.Markdown) { + m.Parser().AddOptions( + parser.WithBlockParsers( + util.Prioritized(NewLightGalleryParser(), 500), + ), + ) + m.Renderer().AddOptions( + renderer.WithNodeRenderers( + util.Prioritized(NewLightGalleryHTMLRenderer(), 500), + ), + ) +} diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go new file mode 100644 index 0000000..17a6f2b --- /dev/null +++ b/internal/lightgallery/html_renderer.go @@ -0,0 +1,137 @@ +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('DOMContentLoaded', createLightGallery%s); +</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/lightgallery/parser.go b/internal/lightgallery/parser.go new file mode 100644 index 0000000..daec797 --- /dev/null +++ b/internal/lightgallery/parser.go @@ -0,0 +1,88 @@ +package lightgallery + +import ( + "bytes" + "log/slog" + "regexp" + "time" + + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" +) + +type LightGalleryParser struct{} + +func NewLightGalleryParser() parser.BlockParser { + return &LightGalleryParser{} +} + +func (p *LightGalleryParser) Trigger() []byte { + return []byte{'{'} +} + +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:")) { + 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 &LightGalleryBlock{Location: loc}, parser.NoChildren +} + +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 + } + + trimmed := bytes.TrimSpace(line) + if bytes.Equal(trimmed, []byte("{Gallery}")) || bytes.Equal(trimmed, []byte("{/Gallery}")) { + reader.AdvanceLine() + return parser.Close + } + + gallery := node.(*LightGalleryBlock) + + 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, LightGalleryImage{ + URL: string(url), + Caption: caption, + }) + + return parser.Continue | parser.NoChildren +} + +func (p *LightGalleryParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { +} + +func (p *LightGalleryParser) CanInterruptParagraph() bool { + return true +} + +func (p *LightGalleryParser) CanAcceptIndentedLine() bool { + return false +} |