diff options
| author | 2025-08-01 16:52:52 +0700 | |
|---|---|---|
| committer | 2025-08-01 16:52:52 +0700 | |
| commit | 0c0900cdea12364a25ebf9bb8205a795416e8d6d (patch) | |
| tree | 7052b25fb404afbfe51c27cdaceca5452c697cb0 /internal/lightgallery | |
| parent | a1f511368b898ba53ca685dd3f41f2901f295fd3 (diff) | |
| download | web-0c0900cdea12364a25ebf9bb8205a795416e8d6d.tar.gz web-0c0900cdea12364a25ebf9bb8205a795416e8d6d.zip | |
feat: support of multi-page blog via b2
feat: use markdown + frontmatter for page content
feat: add main page with dynamic page lookup
feat: add night theme
feat: extract timestamp of photo taken from picture name
Diffstat (limited to 'internal/lightgallery')
| -rw-r--r-- | internal/lightgallery/block.go | 26 | ||||
| -rw-r--r-- | internal/lightgallery/extension.go | 28 | ||||
| -rw-r--r-- | internal/lightgallery/html_renderer.go | 137 | ||||
| -rw-r--r-- | internal/lightgallery/parser.go | 77 |
4 files changed, 268 insertions, 0 deletions
diff --git a/internal/lightgallery/block.go b/internal/lightgallery/block.go new file mode 100644 index 0000000..dade2ea --- /dev/null +++ b/internal/lightgallery/block.go @@ -0,0 +1,26 @@ +package lightgallery + +import "github.com/yuin/goldmark/ast" + +// LightGalleryBlock represents a light gallery block in the AST +type LightGalleryBlock struct { + ast.BaseBlock + Images []LightGalleryImage +} + +type LightGalleryImage struct { + URL string + Caption string +} + +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..fb65b06 --- /dev/null +++ b/internal/lightgallery/html_renderer.go @@ -0,0 +1,137 @@ +package lightgallery + +import ( + "fmt" + "math/rand" + "strings" + "time" + + "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 +} + +func NewLightGalleryHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &LightGalleryHTMLRenderer{ + Config: html.NewConfig(), + } + 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(4) + + w.WriteString(fmt.Sprintf(` +<div class="justify-content-center display-block m-2"> + <hr class="border-t-4 border-dotted border-main-dark mt-vmin0-8 mb-vmin0-8 w-p80 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-vmin0-8 mb-vmin0-8 w-p80 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, "-") + + dayDate, _ := time.Parse("20060102 150405", imageNameParts[len(imageNameParts)-2]+" "+imageNameParts[len(imageNameParts)-1]) + 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", + sources: [{ + srcset: "https://f003.backblazeb2.com/file/sayana-photos/thumbnails/%s.webp", + media: "(max-width: 800px)" + }], + thumb: + "https://f003.backblazeb2.com/file/sayana-photos/thumbnails/%s.webp", + subHtml: `+"`"+`<div class="flex flex-row light-gallery-captions"> + <p class="grow text-vmax1 text-left lh-0-9 font-spectral text-main-dark">%s</p> + <p class="text-vmax1 text-right lh-0-9 font-spectral text-secondary">%s</p> + </div>`+"`"+` + }`, img.URL, img.URL, img.Caption, imageUrlWithoutExt, imageUrlWithoutExt, img.Caption, dayDate.Format("2006-01-02 15:04:05 -07:00"))) + } + + w.WriteString(fmt.Sprintf(` +<script> +function createLightLibrary%s() { + const $lgContainer = document.getElementById('lg-%s'); + const inlineGallery = lightGallery($lgContainer, { + container: $lgContainer, + dynamic: true, + dynamicEl: [%s], + width: "100%%", + height: "50vmin", + hash: false, + closable: false, + showMaximizeIcon: true, + appendSubHtmlTo: ".lg-sub-html", + isMobile: () => { return false; }, + slideDelay: 0, + plugins: [lgZoom, lgThumbnail], + thumbWidth: 160, + thumbHeight: "10vmin", + thumbMargin: 4 + }); + + setTimeout(() => { + inlineGallery.openGallery(); + }, 200); +} + +document.addEventListener('DOMContentLoaded', createLightLibrary%s); + +let resizeTimer%s; +window.addEventListener("resize", () => { + clearTimeout(resizeTimer%s); + resizeTimer%s = setTimeout(() => { + const $lgContainer = document.getElementById('lg-%s'); + $lgContainer.innerHTML = ""; + createLightLibrary%s(); + }, 1000); +}); +</script>`, galleryID, galleryID, strings.Join(dynamicElements, ","), galleryID, galleryID, galleryID, galleryID, galleryID, galleryID)) + } + + return ast.WalkContinue, nil +} + +func escapeHTML(s string) string { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + s = strings.ReplaceAll(s, "\"", """) + s = strings.ReplaceAll(s, "'", "'") + return s +} + +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..0ce209c --- /dev/null +++ b/internal/lightgallery/parser.go @@ -0,0 +1,77 @@ +package lightgallery + +import ( + "bytes" + "strings" + + "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 + } + + trimmed := bytes.TrimSpace(line) + if !bytes.Equal(trimmed, []byte("{Gallery}")) { + return nil, parser.NoChildren + } + + return &LightGalleryBlock{}, 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.Close + } + + trimmed := bytes.TrimSpace(line) + if bytes.Equal(trimmed, []byte("{Gallery}")) { + reader.AdvanceLine() + return parser.Close + } + + gallery := node.(*LightGalleryBlock) + lineStr := string(trimmed) + + parts := strings.SplitN(lineStr, "|", 2) + url := strings.TrimSpace(parts[0]) + caption := "" + + if len(parts) > 1 { + caption = strings.TrimSpace(parts[1]) + } + + gallery.Images = append(gallery.Images, LightGalleryImage{ + URL: 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 +} |