diff --git a/lib/volumen/markdown.rb b/lib/volumen/markdown.rb index a61c885..b339f38 100644 --- a/lib/volumen/markdown.rb +++ b/lib/volumen/markdown.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "kramdown" +require "rexml/document" module Volumen # Renders Markdown to HTML using kramdown's GFM parser (tables, fenced code, @@ -8,6 +9,10 @@ module Volumen # # Posts are authored by the single trusted admin, so kramdown's default # behaviour of passing raw HTML through is intentional. + # + # Images with a title (`![alt](url "caption")`) are wrapped in a `
` + # element with a `
` so the frontend can style captions and + # overlay an info icon. module Markdown OPTIONS = { input: "GFM", @@ -17,7 +22,41 @@ module Volumen }.freeze def self.render(text) - ::Kramdown::Document.new(text.to_s, OPTIONS).to_html + html = ::Kramdown::Document.new(text.to_s, OPTIONS).to_html + wrap_figures(html) + end + + # Wrap every `` inside a `
` with `
`. + # Images without a title are left untouched. + def self.wrap_figures(html) + doc = REXML::Document.new("#{html}") + + REXML::XPath.each(doc, "//img[@title]") do |img| + caption = img.attributes["title"] + img.attributes.delete("title") + + figure = REXML::Element.new("figure") + figure.add_element(img.deep_clone) + + info = REXML::Element.new("div") + info.add_attribute("class", "fig-info") + info.add_attribute("aria-hidden", "true") + info.text = "i" + figure.add_element(info) + + figcaption = REXML::Element.new("figcaption") + figcaption.text = caption + figure.add_element(figcaption) + + img.parent.replace_child(img, figure) + end + + # Strip the wrapping we added. + result = +"" + doc.root.children.each { |child| result << child.to_s } + result + rescue REXML::ParseException + html end end end diff --git a/lib/volumen/post.rb b/lib/volumen/post.rb index 0833ea4..c75e372 100644 --- a/lib/volumen/post.rb +++ b/lib/volumen/post.rb @@ -58,6 +58,10 @@ module Volumen metadata["cover"] end + def cover_alt + metadata["cover_alt"] + end + def fediverse_creator value = metadata["fediverse_creator"] return nil if value.nil? @@ -128,6 +132,7 @@ module Volumen "author" => author, "fediverse_creator" => fediverse_creator, "cover" => cover, + "cover_alt" => cover_alt, "reading_time" => reading_time, "translations" => translations, "url" => "/api/volumen/posts/#{slug}" diff --git a/lib/volumen/server.rb b/lib/volumen/server.rb index 815f16b..1543fc9 100644 --- a/lib/volumen/server.rb +++ b/lib/volumen/server.rb @@ -376,7 +376,14 @@ module Volumen end def post_from_params(http_params, existing: nil) - metadata = { + metadata = base_metadata(http_params, existing: existing) + post = Post.new(metadata: clean_metadata(metadata), body: http_params["body"].to_s) + post.path = existing.path if existing + post + end + + def base_metadata(http_params, existing:) + { "title" => presence(http_params["title"]), "slug" => presence(http_params["slug"]) || existing&.slug, "lang" => presence(http_params["lang"]), @@ -387,12 +394,10 @@ module Volumen "tags" => parse_tags(http_params["tags"]), "excerpt" => presence(http_params["excerpt"]), "cover" => presence(http_params["cover"]), + "cover_alt" => presence(http_params["cover_alt"]), "draft" => http_params["draft"] == "on", "all_langs" => http_params["all_langs"] == "on" } - post = Post.new(metadata: clean_metadata(metadata), body: http_params["body"].to_s) - post.path = existing.path if existing - post end def fediverse_creator_error(post) diff --git a/lib/volumen/web/views/form.erb b/lib/volumen/web/views/form.erb index e1160df..b141b8a 100644 --- a/lib/volumen/web/views/form.erb +++ b/lib/volumen/web/views/form.erb @@ -46,6 +46,8 @@ + @@ -98,7 +100,7 @@ - + diff --git a/lib/volumen/web/views/layout.erb b/lib/volumen/web/views/layout.erb index c6e3524..d5a79cb 100644 --- a/lib/volumen/web/views/layout.erb +++ b/lib/volumen/web/views/layout.erb @@ -218,13 +218,13 @@ box-shadow: none; } .markdown th, .markdown td { border: 1px solid var(--border); padding: 0.45rem 0.7rem; text-align: left; } .markdown thead th { background: var(--accent-weak); color: var(--fg); } - .markdown tbody tr:hover td { background: transparent; } - .markdown hr { border: none; border-top: 1px solid var(--border); margin: 1.4em 0; } - .markdown img { max-width: 100%; border-radius: 8px; } + .markdown tbody tr:hover td { background: transparent; } + .markdown hr { border: none; border-top: 1px solid var(--border); margin: 1.4em 0; } + .markdown img { max-width: 100%; border-radius: 8px; } - @media (prefers-reduced-motion: reduce) { - * { transition: none !important; } - } + @media (prefers-reduced-motion: reduce) { + * { transition: none !important; } + }