diff --git a/lib/pressa/config/loader.rb b/lib/pressa/config/loader.rb index 80ad86b..d999d67 100644 --- a/lib/pressa/config/loader.rb +++ b/lib/pressa/config/loader.rb @@ -214,7 +214,9 @@ module Pressa remote_links = build_output_links( format_config["remote_links"], context: "site.toml outputs.html.remote_links", - allow_icon: true + allow_icon: true, + allow_label_optional: false, + allow_string_entries: false ) HTMLOutputOptions.new( @@ -236,7 +238,9 @@ module Pressa home_links = build_output_links( format_config["home_links"], context: "site.toml outputs.gemini.home_links", - allow_icon: false + allow_icon: false, + allow_label_optional: true, + allow_string_entries: true ) recent_posts_limit = build_recent_posts_limit( format_config["recent_posts_limit"], @@ -339,11 +343,21 @@ module Pressa end end - def build_output_links(value, context:, allow_icon:) + def build_output_links(value, context:, allow_icon:, allow_label_optional:, allow_string_entries:) entries = array_or_empty(value, context) entries.map.with_index do |entry, index| + if allow_string_entries && entry.is_a?(String) + href = entry + unless !href.strip.empty? + raise ValidationError, "Expected #{context}[#{index}] to be a non-empty String" + end + validate_link_href!(href.strip, context: "#{context}[#{index}]") + + next OutputLink.new(label: nil, href: href.strip, icon: nil) + end + unless entry.is_a?(Hash) - raise ValidationError, "Expected #{context}[#{index}] to be a table" + raise ValidationError, "Expected #{context}[#{index}] to be a String or table" end allowed_keys = allow_icon ? %w[label href icon] : %w[label href] @@ -353,16 +367,23 @@ module Pressa context: "#{context}[#{index}]" ) - label = entry["label"] href = entry["href"] - unless label.is_a?(String) && !label.strip.empty? - raise ValidationError, "Expected #{context}[#{index}].label to be a non-empty String" - end unless href.is_a?(String) && !href.strip.empty? raise ValidationError, "Expected #{context}[#{index}].href to be a non-empty String" end validate_link_href!(href.strip, context: "#{context}[#{index}].href") + label = entry["label"] + if label.nil? + unless allow_label_optional + raise ValidationError, "Expected #{context}[#{index}].label to be a non-empty String" + end + else + unless label.is_a?(String) && !label.strip.empty? + raise ValidationError, "Expected #{context}[#{index}].label to be a non-empty String" + end + end + icon = entry["icon"] unless allow_icon if entry.key?("icon") @@ -376,7 +397,7 @@ module Pressa raise ValidationError, "Expected #{context}[#{index}].icon to be a non-empty String" end - OutputLink.new(label: label.strip, href: href.strip, icon: icon&.strip) + OutputLink.new(label: label&.strip, href: href.strip, icon: icon&.strip) end end diff --git a/lib/pressa/posts/gemini_writer.rb b/lib/pressa/posts/gemini_writer.rb index 213b579..76ddca2 100644 --- a/lib/pressa/posts/gemini_writer.rb +++ b/lib/pressa/posts/gemini_writer.rb @@ -20,7 +20,12 @@ module Pressa def write_recent_posts(target_path:, limit: RECENT_POSTS_LIMIT) rows = ["# #{@site.title}", ""] home_links.each do |link| - rows << "=> #{link.href}" + label = link.label&.strip + rows << if label.nil? || label.empty? + "=> #{link.href}" + else + "=> #{link.href} #{label}" + end end rows << "" unless home_links.empty? rows << "## Recent posts" diff --git a/lib/pressa/site.rb b/lib/pressa/site.rb index a8022eb..2e81332 100644 --- a/lib/pressa/site.rb +++ b/lib/pressa/site.rb @@ -6,7 +6,8 @@ module Pressa end class OutputLink < Dry::Struct - attribute :label, Types::String + # label is required for HTML remote links, but Gemini home_links may omit it. + attribute :label, Types::String.optional.default(nil) attribute :href, Types::String attribute :icon, Types::String.optional.default(nil) end diff --git a/lib/pressa/views/layout.rb b/lib/pressa/views/layout.rb index 99672b1..ef25eac 100644 --- a/lib/pressa/views/layout.rb +++ b/lib/pressa/views/layout.rb @@ -199,60 +199,64 @@ module Pressa } function setupGeminiFallback() { - var link = document.querySelector( - 'header.primary nav.remote li.gemini a[href^="gemini://"]' + var links = document.querySelectorAll( + 'header.primary nav.remote a[href^="gemini://"]' ); - if (!link) return; + if (!links || links.length === 0) return; - link.addEventListener("click", function (e) { - if (!isPlainLeftClick(e)) return; + for (var i = 0; i < links.length; i++) { + (function (link) { + link.addEventListener("click", function (e) { + if (!isPlainLeftClick(e)) return; - e.preventDefault(); + e.preventDefault(); - var geminiHref = link.getAttribute("href"); - var fallbackHref = "https://geminiprotocol.net"; + var geminiHref = link.getAttribute("href"); + var fallbackHref = "https://geminiprotocol.net"; - var done = false; - var fallbackTimer = null; + var done = false; + var fallbackTimer = null; - function cleanup() { - if (fallbackTimer) window.clearTimeout(fallbackTimer); - document.removeEventListener("visibilitychange", onVisibilityChange); - window.removeEventListener("pagehide", onPageHide); - window.removeEventListener("blur", onBlur); - } + function cleanup() { + if (fallbackTimer) window.clearTimeout(fallbackTimer); + document.removeEventListener("visibilitychange", onVisibilityChange); + window.removeEventListener("pagehide", onPageHide); + window.removeEventListener("blur", onBlur); + } - function markDone() { - done = true; - cleanup(); - } + function markDone() { + done = true; + cleanup(); + } - function onVisibilityChange() { - // If a handler opens and the browser backgrounded, consider it "successful". - if (document.visibilityState === "hidden") markDone(); - } + function onVisibilityChange() { + // If a handler opens and the browser backgrounded, consider it "successful". + if (document.visibilityState === "hidden") markDone(); + } - function onPageHide() { - markDone(); - } + function onPageHide() { + markDone(); + } - function onBlur() { - // Some browsers blur the page when a protocol handler is invoked. - markDone(); - } + function onBlur() { + // Some browsers blur the page when a protocol handler is invoked. + markDone(); + } - document.addEventListener("visibilitychange", onVisibilityChange); - window.addEventListener("pagehide", onPageHide, { once: true }); - window.addEventListener("blur", onBlur, { once: true }); + document.addEventListener("visibilitychange", onVisibilityChange); + window.addEventListener("pagehide", onPageHide, { once: true }); + window.addEventListener("blur", onBlur, { once: true }); - // If we're still here shortly after attempting navigation, assume it failed. - fallbackTimer = window.setTimeout(function () { - if (done) return; - window.location.href = fallbackHref; - }, 900); + // If we're still here shortly after attempting navigation, assume it failed. + fallbackTimer = window.setTimeout(function () { + if (done) return; + window.location.href = fallbackHref; + }, 900); - window.location.href = geminiHref; - }); + window.location.href = geminiHref; + }); + })(links[i]); + } } if (document.readyState === "loading") { diff --git a/site.toml b/site.toml index 8d34338..01ececa 100644 --- a/site.toml +++ b/site.toml @@ -29,14 +29,14 @@ remote_links = [ [outputs.gemini] recent_posts_limit = 20 home_links = [ - {"label": "About", "href": "/about"}, - {"label": "Posts", "href": "/posts"}, - {"label": "Projects", "href": "/projects"}, - {"label": "Mastodon", "href": "https://techhub.social/@sjs"}, - {"label": "GitHub", "href": "https://github.com/samsonjs"}, - {"label": "RSS (Gemini)", "href": "/posts/feed.gmi"}, - {"label": "RSS (Web)", "href": "https://samhuri.net/feed.xml"}, - {"label": "Email", "href": "mailto:sami@samhuri.net"} + "/about", + "/posts", + "/projects", + "https://techhub.social/@sjs", + "https://github.com/samsonjs", + "/posts/feed.gmi", + "https://samhuri.net/feed.xml", + "mailto:sami@samhuri.net" ] exclude_public = [ "tweets/**", diff --git a/test/config/loader_test.rb b/test/config/loader_test.rb index 7fb50df..86d6148 100644 --- a/test/config/loader_test.rb +++ b/test/config/loader_test.rb @@ -41,7 +41,7 @@ class Pressa::Config::LoaderTest < Minitest::Test assert_equal(["Pressa::Utils::GeminiMarkdownRenderer"], site.renderers.map(&:class).map(&:name)) assert_equal(["tweets/**"], site.public_excludes) assert_equal(20, site.gemini_output_options&.recent_posts_limit) - assert_equal(["About", "GitHub"], site.gemini_output_options&.home_links&.map(&:label)) + assert_equal(["/about/", "https://github.com/samsonjs"], site.gemini_output_options&.home_links&.map(&:href)) end end @@ -561,6 +561,35 @@ class Pressa::Config::LoaderTest < Minitest::Test end end + def test_build_site_allows_string_home_links_and_optional_labels_for_gemini + Dir.mktmpdir do |dir| + File.write(File.join(dir, "site.toml"), <<~TOML) + author = "Sami Samhuri" + email = "sami@samhuri.net" + title = "samhuri.net" + description = "blog" + url = "https://samhuri.net" + + [outputs.gemini] + home_links = [ + "/about/", + {"href": "/posts/"}, + {"label": "GitHub", "href": "https://github.com/samsonjs"} + ] + TOML + File.write(File.join(dir, "projects.toml"), "projects = []\n") + + loader = Pressa::Config::Loader.new(source_path: dir) + site = loader.build_site(output_format: "gemini") + + assert_equal("gemini", site.output_format) + assert_equal(3, site.gemini_output_options&.home_links&.length) + assert_nil(site.gemini_output_options&.home_links&.at(0)&.label) + assert_nil(site.gemini_output_options&.home_links&.at(1)&.label) + assert_equal("GitHub", site.gemini_output_options&.home_links&.at(2)&.label) + end + end + private def with_temp_config @@ -588,10 +617,7 @@ class Pressa::Config::LoaderTest < Minitest::Test [outputs.gemini] recent_posts_limit = 20 - home_links = [ - {"label": "About", "href": "/about/"}, - {"label": "GitHub", "href": "https://github.com/samsonjs"} - ] + home_links = ["/about/", "https://github.com/samsonjs"] exclude_public = ["tweets/**"] TOML