From 44fe2478478225450becb77ad5d273462c9b400a Mon Sep 17 00:00:00 2001
From: SayaAndy
Date: Thu, 16 Oct 2025 12:23:00 +0700
Subject: feat: implement new prototype gallery
---
internal/glightbox/block.go | 31 +++++++++
internal/glightbox/extension.go | 28 ++++++++
internal/glightbox/html_renderer.go | 134 ++++++++++++++++++++++++++++++++++++
internal/glightbox/parser.go | 88 +++++++++++++++++++++++
4 files changed, 281 insertions(+)
create mode 100644 internal/glightbox/block.go
create mode 100644 internal/glightbox/extension.go
create mode 100644 internal/glightbox/html_renderer.go
create mode 100644 internal/glightbox/parser.go
(limited to 'internal/glightbox')
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(""))
+ captionHTML = bytes.TrimSuffix(captionHTML, []byte("
\n"))
+ captionHTML = bytes.TrimSuffix(captionHTML, []byte("
"))
+ }
+
+ 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(`
+
+
+ `, img.URL, galleryID, galleryID, galleryID, dayDate.Format("2006-01-02 15:04:05 -07:00"), dataDescriptionAttribute, imageUrlWithoutExt))
+
+ if glightboxDescId != "" {
+ elements = append(elements, fmt.Sprintf(`
+ `, glightboxDescId, captionHTML))
+ }
+ }
+
+ w.WriteString(fmt.Sprintf(`
+`, galleryID, galleryID, strings.Join(elements, "\n")))
+
+ w.WriteString(fmt.Sprintf(`
+`, 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
+}
--
cgit v1.3.1+13
From db174993127a8b2faf863a670fd6ce8ca8d7c1af Mon Sep 17 00:00:00 2001
From: SayaAndy
Date: Fri, 17 Oct 2025 01:16:18 +0700
Subject: feat: add onclick links in blog post links feat: add href for any
onclick links/icons chore: rm any remains of old lightgallery fix: show
borders between post rows fix: map redefining
---
internal/glightbox/html_renderer.go | 2 +-
internal/tailwind/link_renderer.go | 49 +++++++++++++++++++++
internal/tailwind/transformer.go | 7 +++
internal/templatemanager/templatemanager.go | 4 +-
main.go | 14 ++++--
static/input.css | 31 ++++----------
views/layouts/general-page.html | 27 +++++++++---
views/pages/blog-page.html | 66 +++++------------------------
views/pages/global-map.html | 16 ++++---
views/partials/catalogue-blog-cards.html | 8 ++--
views/partials/general-page-header.html | 6 ++-
11 files changed, 131 insertions(+), 99 deletions(-)
create mode 100644 internal/tailwind/link_renderer.go
(limited to 'internal/glightbox')
diff --git a/internal/glightbox/html_renderer.go b/internal/glightbox/html_renderer.go
index 3b6a533..43d4790 100644
--- a/internal/glightbox/html_renderer.go
+++ b/internal/glightbox/html_renderer.go
@@ -81,7 +81,7 @@ func (r *GLightboxHTMLRenderer) renderGLightbox(w util.BufWriter, source []byte,
elements = append(elements, fmt.Sprintf(`
-
+
`, img.URL, galleryID, galleryID, galleryID, dayDate.Format("2006-01-02 15:04:05 -07:00"), dataDescriptionAttribute, imageUrlWithoutExt))
if glightboxDescId != "" {
diff --git a/internal/tailwind/link_renderer.go b/internal/tailwind/link_renderer.go
new file mode 100644
index 0000000..ff3d437
--- /dev/null
+++ b/internal/tailwind/link_renderer.go
@@ -0,0 +1,49 @@
+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("")
+ } else {
+ _, _ = w.WriteString("")
+ }
+ 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 f932533..23ba1a3 100644
--- a/internal/tailwind/transformer.go
+++ b/internal/tailwind/transformer.go
@@ -1,6 +1,9 @@
package tailwind
import (
+ "bytes"
+ "fmt"
+
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
@@ -51,6 +54,10 @@ func (t *TailwindTransformer) Transform(node *ast.Document, reader text.Reader,
node.SetAttribute([]byte("class"), []byte("bg-background-dark p-1 rounded-lg overflow-x-auto mb-4"))
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:
diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go
index 4bf9a49..eebc297 100644
--- a/internal/templatemanager/templatemanager.go
+++ b/internal/templatemanager/templatemanager.go
@@ -37,7 +37,7 @@ func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager
templateMap := make(map[string]templateManagerRender)
for _, tmplStruct := range templates {
- tmpl := template.New("").Funcs(templateFuncMap)
+ tmpl := template.New(tmplStruct.Name).Funcs(templateFuncMap)
tmpl, err := tmpl.ParseFiles(tmplStruct.Files...)
if err != nil {
return nil, err
@@ -84,7 +84,7 @@ func (tm *TemplateManager) Add(name string, files ...string) error {
return fmt.Errorf("you can't add template without any files")
}
- tmpl := template.New("").Funcs(templateFuncMap)
+ tmpl := template.New(name).Funcs(templateFuncMap)
tmpl, err := tmpl.ParseFiles(files...)
if err != nil {
diff --git a/main.go b/main.go
index 53dbb74..a6a30b0 100644
--- a/main.go
+++ b/main.go
@@ -23,7 +23,9 @@ import (
"github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
- gmhtml "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/util"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/mattn/go-sqlite3"
@@ -37,9 +39,15 @@ var (
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
+ parser.WithAttribute(),
),
- goldmark.WithRendererOptions(
- gmhtml.WithXHTML(),
+ goldmark.WithRenderer(
+ renderer.NewRenderer(
+ renderer.WithNodeRenderers(
+ util.Prioritized(tailwind.NewCustomLinkRenderer(html.WithUnsafe(), html.WithXHTML()), 50),
+ util.Prioritized(html.NewRenderer(html.WithXHTML()), 100),
+ ),
+ ),
),
)
b2Client *b2.B2Client
diff --git a/static/input.css b/static/input.css
index 36d17b2..1e7f9fc 100644
--- a/static/input.css
+++ b/static/input.css
@@ -236,37 +236,22 @@ a:hover {
border-style: outset;
}
-.desktop-sidebar-custom {
- width: 5vw;
- background-size: calc(var(--squares-and-triangles-background-size) * 1vw);
-}
-
.bg-sidebar {
background-image: var(--squares-and-triangles-background);
background-repeat: var(--squares-and-triangles-background-repeat);
- background-size: calc(var(--squares-and-triangles-background-width-coef) * 1vw) calc(var(--squares-and-triangles-background-height-coef) * 1vw);
+ background-size: calc(var(--squares-and-triangles-background-width-coef) * 1dvw) calc(var(--squares-and-triangles-background-height-coef) * 1dvw);
}
.logo-custom {
- width: 3.5vw;
- height: 20vw;
- font-size: 2.5vw;
- -webkit-text-stroke: 0.1vw var(--sidebar-stroke-color);
+ width: 3.5dvw;
+ height: 20dvw;
+ font-size: 2.5dvw;
+ -webkit-text-stroke: 0.1dvw var(--sidebar-stroke-color);
writing-mode: sideways-lr;
text-orientation: mixed;
display: block;
}
-.border-left-custom {
- width: 0.5vw;
- border-right: 0.3vh dashed var(--main-dark-color);
-}
-
-.border-right-custom {
- width: 0.5vw;
- border-left: 0.5vh dotted var(--main-dark-color);
-}
-
.inset-shadow {
box-shadow: inset 0vh 1vh 1vh -1vh rgba(75, 81, 58, 0.5),
inset 1vh 0vh 1vh -1vh rgba(75, 81, 58, 0.5),
@@ -278,8 +263,8 @@ a:hover {
-webkit-text-stroke-color: var(--sidebar-stroke-color);
}
-.text-stroke-\[0\.1vw\] {
- -webkit-text-stroke-width: 0.1vw;
+.text-stroke-\[0\.1dvw\] {
+ -webkit-text-stroke-width: 0.1dvw;
}
.theme-wheel {
@@ -411,7 +396,7 @@ body[data-theme~="ram"] .leaflet-tile {
@media (max-aspect-ratio: 0.8/1) {
.bg-sidebar {
- background-size: calc(var(--squares-and-triangles-background-width-coef) * 1.6vh) calc(var(--squares-and-triangles-background-height-coef) * 1.6vh);
+ background-size: calc(var(--squares-and-triangles-background-width-coef) * 1.6dvh) calc(var(--squares-and-triangles-background-height-coef) * 1.6dvh);
}
.ar-lt-0\.8\:bg-background-light {
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
index 88c5de5..b163387 100644
--- a/views/layouts/general-page.html
+++ b/views/layouts/general-page.html
@@ -14,13 +14,22 @@
+
+
@@ -165,12 +174,20 @@
-