diff --git a/back_office/components/document_specification.go b/back_office/components/document_specification.go index 06178a1b24eb0e90fce7c0aeeb24211e4cb0686f..69dea8d44964ea3955046ed1776af0adeffb6189 100644 --- a/back_office/components/document_specification.go +++ b/back_office/components/document_specification.go @@ -56,7 +56,7 @@ func DocumentSpecification(doc models.Document) g.Node { h.P(g.Text("Status: "), h.Strong(g.Text(doc.Status.String()), h.Class("font-mono"))), ), h.Div( - h.Class("col-span-1 rounded shadow-sm bg-white border border-gray-200 p-2 px-4"), + h.Class("col-span-1 rounded shadow-sm bg-white border border-gray-200 p-2 px-4 truncate"), h.P(g.Text("Cache Hash: "), h.Strong(g.Text(doc.CacheHash), h.Class("font-mono"))), ), h.Div( diff --git a/back_office/documents_controller.go b/back_office/documents_controller.go index 26f0d702475902db0971e89fb4675c2cd5dd3411..ab6c23c63d9cc02f6ed9c54e0969bf8c053e0aa2 100644 --- a/back_office/documents_controller.go +++ b/back_office/documents_controller.go @@ -25,6 +25,7 @@ import ( type DocumentsController struct { Logger *zap.Logger DocumentRepo *repos.DocumentRepo + MediumRepo *repos.MediumRepo SessionManager *scs.SessionManager View *DocumentsView } @@ -219,7 +220,7 @@ func (c *DocumentsController) view(ctx echo.Context) error { } document.BakedContent = c.DocumentRepo.ReplaceMagicBlocks(document.BakedContent) - + document.BakedContent = c.MediumRepo.ReplaceMagicBlocks(document.BakedContent) return ctx.HTML(http.StatusOK, c.View.Show(ctx, DocumentsViewShowParams{Document: document})) } @@ -306,6 +307,7 @@ type NewDocumentsControllerParams struct { Logger *zap.Logger View *DocumentsView DocumentRepo *repos.DocumentRepo + MediumRepo *repos.MediumRepo SessionManager *scs.SessionManager } @@ -320,6 +322,7 @@ func NewDocumentsController(p NewDocumentsControllerParams) NewDocumentsControll Logger: p.Logger, View: p.View, DocumentRepo: p.DocumentRepo, + MediumRepo: p.MediumRepo, SessionManager: p.SessionManager, }, } diff --git a/back_office/documents_view.go b/back_office/documents_view.go index 124303203408cc9ac16b25a1a24e6d3907d53930..8ceb7c20986dab79f9ebca074e18cbdaa488d06d 100644 --- a/back_office/documents_view.go +++ b/back_office/documents_view.go @@ -157,7 +157,7 @@ func (v *DocumentsView) form(p DocumentsViewFormParams) g.Node { h.Class("my-2"), h.Summary(g.Text("Frontmatter/special blocks reference")), h.Pre( - g.Text("---\ntoc: true\nsubdocuments_disabled: false\nredirect: https://example.com\ndescription: |\n This is a description\n---\n\n{{latest:tag:N}} - will be replaced with an unordered list of latest N posts under `tag`\n\nFigure:\n\n!---\n[](http://host/!m/10/2137.jpg)\n!---\nFigure: caption for the figure."), + g.Text("---\ntoc: true\nsubdocuments_disabled: false\nredirect: https://example.com\ndescription: |\n This is a description\n---\n\n{{latest:tag:N}} - will be replaced with an unordered list of latest N posts under `tag`\n{{gallery:slug}} - will be replaced with a gallery of images from the collection `slug`\n\nFigure:\n\n!---\n[](http://host/!m/10/2137.jpg)\n!---\nFigure: caption for the figure."), h.Class("text-xs text-gray-500"), ), h.P(g.Text("For more fancy stuff, see "), h.A(g.Text("Mmark"), h.Href("https://mmark.miek.nl/post/syntax/")), g.Text(" for more information."), h.Class("text-xs text-gray-500 mt-3")), diff --git a/back_office/media_collection_create_form.go b/back_office/media_collection_create_form.go index 554fa2541da1d9547951326e0ec0747bbdf841ef..5b9ca525c060e19f6775b691aa3ceb8e8a08f059 100644 --- a/back_office/media_collection_create_form.go +++ b/back_office/media_collection_create_form.go @@ -8,7 +8,7 @@ package back_office import "kita.gawa.moe/paweljw/vellum/repos" type mediaCollectionCreateForm struct { - Name string `form:"name" validate:"required,min=3,max=255,lowercase,printascii"` + Name string `form:"name" validate:"required,min=3,max=255,lowercase,printascii,excludesall= !"` } func (f *mediaCollectionCreateForm) ToParams() repos.CreateMediaCollectionParams { diff --git a/commons/image.go b/commons/image.go index d32fb00d9ab9e901b2d83b422aa218aeabc418af..32479293b491652ff8fc50d5a3755a646b9d5034 100644 --- a/commons/image.go +++ b/commons/image.go @@ -90,7 +90,7 @@ func (o *ImageOptions) ToPathSuffix() string { } func (o *ImageOptions) isValid() bool { - return o.Width > 0 && o.Height > 0 + return (o.Width > 0 && o.Height > 0) || (o.Width == 0 && o.Height == 0) } func NewImageOptions(width, height, fit, fill, basePath string) (*ImageOptions, error) { diff --git a/commons/markdown.go b/commons/markdown.go index 6e4787e28293f2e40ceeb532c0cd25dc7759ab97..413e0ecd03cad7d71509b887a1eff28da51a40b7 100644 --- a/commons/markdown.go +++ b/commons/markdown.go @@ -142,8 +142,14 @@ func ExtractDescription(html string) string { html = parts[0] } + // Replace latest-tag and gallery tags with empty strings + re := regexp.MustCompile(`{{latest:[^}]+}}`) + html = re.ReplaceAllString(html, "") + re = regexp.MustCompile(`{{gallery:[^}]+}}`) + html = re.ReplaceAllString(html, "") + // Remove HTML tags using a regular expression - re := regexp.MustCompile("<[^>]*>") + re = regexp.MustCompile("<[^>]*>") html = re.ReplaceAllString(html, "") // Trim whitespace and limit to reasonable length diff --git a/frontend/archives_controller.go b/frontend/archives_controller.go index 6d208b8c6a5a4530120eab625ebea0beab330a64..c8015b6c4c2bb0ba7c5f1e6aa8a4cc3d1b9996c5 100644 --- a/frontend/archives_controller.go +++ b/frontend/archives_controller.go @@ -100,7 +100,7 @@ func buildGroupedByYear(docs []models.Document) map[uint]ArchivesYearGroup { if _, exists := yearGroups[year].Months[month]; !exists { yearGroups[year].Months[month] = ArchivesMonthGroup{ Month: month, - Docs: []models.Document{doc}, + Docs: []models.Document{}, } } diff --git a/frontend/documents_controller.go b/frontend/documents_controller.go index 9067a314654b3c070a3c24158abf070d5d8a1586..692e4f9c7f94e159073ed581d872c9cbb166a03f 100644 --- a/frontend/documents_controller.go +++ b/frontend/documents_controller.go @@ -25,6 +25,7 @@ type DocumentsController struct { View *DocumentView Logger *zap.Logger DocumentRepo *repos.DocumentRepo + MediumRepo *repos.MediumRepo SessionManager *scs.SessionManager } @@ -89,7 +90,7 @@ func (h *DocumentsController) view(ctx echo.Context) error { // Replace magic blocks in the format {{latest:tag:N}} with the latest N documents for the tag doc.BakedContent = h.DocumentRepo.ReplaceMagicBlocks(doc.BakedContent) - + doc.BakedContent = h.MediumRepo.ReplaceMagicBlocks(doc.BakedContent) // If we're still here, cache is stale, so we need to render the document, checking for raw or baked if ctx.QueryParam("raw") != "" { return ctx.String(200, doc.Content) @@ -126,6 +127,7 @@ type NewDocumentsControllerParams struct { View *DocumentView Logger *zap.Logger DocumentRepo *repos.DocumentRepo + MediumRepo *repos.MediumRepo SessionManager *scs.SessionManager } @@ -140,6 +142,7 @@ func NewDocumentsController(p NewDocumentsControllerParams) NewDocumentsControll View: p.View, Logger: p.Logger, DocumentRepo: p.DocumentRepo, + MediumRepo: p.MediumRepo, SessionManager: p.SessionManager, }, } diff --git a/frontend/media_controller.go b/frontend/media_controller.go index b2816d53aba24b801a247caeccca89a9776445c5..654f69f1037dab2210276efee040278b5686cfb5 100644 --- a/frontend/media_controller.go +++ b/frontend/media_controller.go @@ -6,6 +6,7 @@ package frontend import ( + "crypto/md5" "fmt" "net/http" "strings" @@ -43,6 +44,10 @@ func (c *MediaController) show(ctx echo.Context) error { fmt.Sprintf("%s/%s", c.Config.StoragePath, medium.StoragePath), ) + if err != nil { + return ctx.String(http.StatusBadRequest, "Invalid image options: "+err.Error()) + } + if imageOptions.VersionDoesntExist() { referer := ctx.Request().Referer() if referer == "" || !strings.HasPrefix(referer, c.Config.HttpHost) { @@ -62,6 +67,16 @@ func (c *MediaController) show(ctx echo.Context) error { if !medium.IsRenderable() || ctx.QueryParam("download") != "" { return ctx.Attachment(fullPath, medium.Filename) } + + etag := fmt.Sprintf("%x", md5.Sum([]byte(fullPath))) + ctx.Response().Header().Set("Etag", etag) + ctx.Response().Header().Set("Cache-Control", "public, max-age=0, must-revalidate") + if match := ctx.Request().Header.Get("If-None-Match"); match != "" { + if strings.Contains(match, etag) { + return ctx.NoContent(http.StatusNotModified) + } + } + return ctx.File(fullPath) } diff --git a/repos/medium_repo.go b/repos/medium_repo.go index 29f5574fd98262a84b1d0c7599e9aa3b29c26ca4..b73d247b135f84455bba62e1ffccfdae2d447dfd 100644 --- a/repos/medium_repo.go +++ b/repos/medium_repo.go @@ -9,6 +9,8 @@ import ( "errors" "fmt" "os" + "regexp" + "strings" "go.uber.org/fx" "kita.gawa.moe/paweljw/vellum/config" @@ -66,6 +68,33 @@ func (r *MediumRepo) Delete(collection *models.MediumCollection) error { return nil } +func (r *MediumRepo) ReplaceMagicBlocks(content string) string { + re := regexp.MustCompile(`{{gallery:([^}]+)}}`) + content = re.ReplaceAllStringFunc(content, func(match string) string { + parts := re.FindStringSubmatch(match) + collection, err := r.GetCollectionByName(parts[1]) + if err != nil { + return "<p class=\"latest-for-tag-error\">Failed to get latest documents for tag " + parts[1] + ". Sorry!</p>" + } + items := []string{} + for _, medium := range collection.Mediums { + fullPath := fmt.Sprintf("%s/%d/%s", config.MEDIA_CONTROLLER_ROOT, medium.ID, medium.Filename) + small := fmt.Sprintf("%s?width=682&height=384&fill=center", fullPath) + items = append( + items, + fmt.Sprintf( + "<div class=\"gallery-item\"><a href=\"%s\" target=\"_blank\"><img src=\"%s\" alt=\"%s\" class=\"gallery-image\"></a></div>", + fullPath, + small, + medium.Filename, + ), + ) + } + return "<div class=\"gallery\">" + strings.Join(items, "\n") + "</div>" + }) + return content +} + type CreateMediaCollectionParams struct { Name string } diff --git a/src/tailwind.css b/src/tailwind.css index 41a5f51e6c7b799bf1b8cf2c848a04bdbd27f970..ef2d28c0c30e6d2248573d6cb46aad2476f0e73e 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -241,4 +241,8 @@ body.mathjax-loaded:not(.no-js) .math.display { .rendered-markdown figure figcaption { @apply text-sm text-gray-500 mb-0 italic; +} + +.rendered-markdown .gallery { + @apply grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4; } \ No newline at end of file