Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions lib/utopia/content/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,7 @@ def write(string)
def text(content)
return unless content

if content.respond_to?(:build_markup)
content.build_markup(self)
else
XRB::Markup.append(@output, content)
end
content.build_markup(self)
end

# Write a complete tag to the output.
Expand Down
8 changes: 5 additions & 3 deletions lib/utopia/content/links.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2015-2025, by Samuel Williams.
# Copyright, 2015-2026, by Samuel Williams.

require_relative "link"

Expand Down Expand Up @@ -204,8 +204,10 @@ def indices
def each(locale)
return to_enum(:each, locale) unless block_given?

ordered.each do |links|
yield links.find{|link| link.locale == locale}
@named.each_key do |name|
if link = lookup(name, locale)
yield link
end
end
end

Expand Down
6 changes: 3 additions & 3 deletions lib/utopia/content/markup.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2009-2025, by Samuel Williams.
# Copyright, 2009-2026, by Samuel Williams.

require "xrb/parsers"
require "xrb/entities"
Expand Down Expand Up @@ -108,9 +108,9 @@ def end_location
# @returns [String] The resulting string.
def to_s
if @closing_tag
"#{start_location}: #{@opening_tag} was not closed!"
else
"#{start_location}: #{@opening_tag} was closed by #{@closing_tag}!"
else
"#{start_location}: #{@opening_tag} was not closed!"
end
end
end
Expand Down
20 changes: 15 additions & 5 deletions lib/utopia/content/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,7 @@ def call(request)
private

def lookup_content(name, parent_path)
if String === name && name.index("/")
name = Path.create(name)
end

if Path === name
name = parent_path + name
name_path = name.components.dup
name_path[-1] += XNODE_EXTENSION
else
Expand Down Expand Up @@ -195,6 +190,21 @@ def lookup_content(name, parent_path)
end

def content_tag(name, node, parent_path: node.parent_path)
# Preserve nested names while searching the physical content hierarchy:
if String === name
if name.index("/")
name = Path.create(name)
end
end

if Path === name
cache_key = parent_path + name

return @node_cache.fetch_or_store(cache_key) do
lookup_content(name, parent_path)
end
end

full_path = parent_path + name

name = full_path.pop
Expand Down
6 changes: 6 additions & 0 deletions test/utopia/content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@
expect(last_response.status).to be == 307
expect(last_response.headers["location"]).to be == "foo"
end

it "passes missing content to the downstream middleware" do
client.get "/missing"

expect(last_response.status).to be == 404
end
end

describe Utopia::Content do
Expand Down
32 changes: 32 additions & 0 deletions test/utopia/content/builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "utopia/content/builder"

describe Utopia::Content::Builder do
it "parses captured content for non-callable nodes" do
builder = subject.new(nil, nil, Object.new, {})
markup = nil
document = Object.new
document.define_singleton_method(:parse_markup) do |content|
markup = content
end

builder.write("<p>Hello</p>")
builder.call(document)

expect(markup).to be == "<p>Hello</p>"
end

it "escapes plain text" do
builder = subject.new(nil, nil, Object.new, {})
content = Object.new
content.define_singleton_method(:to_s){"Cats & Dogs"}

builder.text(content)

expect(builder.to_s).to be == "Cats &amp; Dogs"
end
end
31 changes: 30 additions & 1 deletion test/utopia/content/document.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2017-2025, by Samuel Williams.
# Copyright, 2017-2026, by Samuel Williams.

require "utopia/content/document"
require "utopia/request"
Expand All @@ -16,6 +16,17 @@
expect(document.request.delegate).to be == request.delegate
end

it "exposes document attributes and request context" do
variables = Object.new
localization = Object.new
request.variables = variables
document = subject.new(request, {title: "Hello"}, localization: localization)

expect(document[:title]).to be == "Hello"
expect(document.controller).to be_equal(variables)
expect(document.localization).to be_equal(localization)
end

it "uses the original request path" do
request.path = "/rewritten"

Expand Down Expand Up @@ -69,6 +80,24 @@
expect(document.base_uri(relative_to)).to be == Utopia::Path[""]
end

it "generates a base uri from the current node" do
node = Struct.new(:uri_path) do
def call(document, state)
document.text(document.base_uri.to_s)
end
end.new(Utopia::Path["/page"])

expect(document.render_node(node)).to be == ""
end

it "exposes captured content while rendering" do
node = proc do |document, state|
document.text(document.content)
end

expect(document.render_node(node)).to be == ""
end

with "nested request path" do
let(:path) {"/nested/index"}

Expand Down
60 changes: 59 additions & 1 deletion test/utopia/content/links.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2015-2025, by Samuel Williams.
# Copyright, 2015-2026, by Samuel Williams.

require "utopia/content/links"

describe Utopia::Content::Links do
let(:root) {File.expand_path("links", __dir__)}
let(:links) {subject.new(root)}

it "provides uncached convenience methods" do
mock(subject) do |mock|
mock.replace(:warn){}
end

expect(subject.for(root, Utopia::Path["/index"]).title).to be == "Home"
expect(subject.index(root, Utopia::Path["/"], name: "welcome").size).to be == 1
end

with "#index_filter" do
it "should match index" do
expect(links.index_filter).to be =~ "index.xnode"
Expand Down Expand Up @@ -81,6 +90,13 @@

expect(matched.collect(&:title)).to be == ["One", "Two", "Three", "四"]
end

it "merges default metadata into localized indexes" do
matched = links.index("/", locale: "en")
four = matched.find{|link| link.name == "four"}

expect(four.info[:category]).to be == "examples"
end
end

with "#index" do
Expand Down Expand Up @@ -213,6 +229,31 @@
link = links.for(Utopia::Path["/bar/index"])
expect(link.title).to be == "Bar"
end

it "falls back to an unlocalized link" do
link = links.for(Utopia::Path["/welcome"], "en")

expect(link.title).to be == "Welcome"
expect(links.for(Utopia::Path["/welcome"], "en", fallback: false)).to be_nil
end
end

it "handles missing content directories" do
resolver = links.links(Utopia::Path["/missing"])

expect(resolver.ordered).to be(:empty?)
expect(resolver.lookup("missing")).to be_nil
end

it "enumerates one localized link per name" do
resolver = links.links(Utopia::Path.root)
selected = resolver.each("en").to_a

expect(selected).not.to be(:empty?)
expect(selected).to be(:all?) do |link|
link.locale == "en" || link.locale.nil?
end
expect(selected.map(&:name)).to be(:include?, "welcome")
end

it "encodes inferred link paths" do
Expand Down Expand Up @@ -241,6 +282,23 @@
expect(link.href).to be == "/articles/document"
end

it "describes link metadata" do
link = Utopia::Content::Link.new(
:virtual,
"document",
nil,
Utopia::Path.new(["", "articles", "document"]),
{title: "Document"},
)
copy = Utopia::Content::Link.new(:virtual, "document", nil, link.path, link.info)

expect(link).to be(:virtual?)
expect(link).to be(:default_locale?)
expect(link.to_s).to be =~ /Document/
expect(link).to be(:eql?, copy)
expect(link).not.to be(:eql?, Object.new)
end

it "keeps explicit targets opaque until relativization" do
target = "some target"
link = Utopia::Content::Link.new(:virtual, "target", nil, nil, {href: target})
Expand Down
4 changes: 3 additions & 1 deletion test/utopia/content/localized/four/links.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
index:
category: examples
index.en:
title: "Four"
index.zh:
title: "四"
title: "四"
50 changes: 47 additions & 3 deletions test/utopia/content/markup.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2015-2025, by Samuel Williams.
# Copyright, 2015-2026, by Samuel Williams.

require "utopia/content/markup"

Expand All @@ -18,6 +18,24 @@ def method_missing(*arguments)
end

describe Utopia::Content::MarkupParser do
it "normalizes symbolic hash membership" do
attributes = Utopia::Content::SymbolicHash.new
attributes[:title] = "Hello"

expect(attributes.fetch("title")).to be == "Hello"
expect(attributes).to be(:include?, :title)
expect(attributes).to be(:include?, "title")
end

it "describes parsed tags" do
tag = subject::ParsedTag.new("section", 0)

expect(tag.to_s).to be == "<section>"

tag.tag.attributes[:class] = "content"
expect(tag.to_s).to be == "<section ...>"
end

it "should format open tags correctly" do
foo_tag = Utopia::Content::Tag.opened("foo", bar: true, baz: "bob")

Expand Down Expand Up @@ -65,11 +83,37 @@ def parse(string)
expect(delegate.events).to be == expected_events
end

it "should parse processing instructions" do
delegate = TestDelegate.new
parser = subject.new(XRB::Buffer.new(""), delegate)

parser.instruction("<?example?>")

expect(delegate.events).to be == [[:write, "<?example?>"]]
end

it "should fail with incorrect closing tag" do
expect{parse %Q{<p>Foobar</dl>}}.to raise_exception(Utopia::Content::MarkupParser::UnbalancedTagError)
error = begin
parse %Q{<p>Foobar</dl>}
rescue subject::UnbalancedTagError => error
error
end

expect(error).to be_a(subject::UnbalancedTagError)
expect(error.start_location.to_s).to be == "[1:1]"
expect(error.end_location.to_s).to be == "[1:11]"
expect(error.to_s).to be =~ /<p> was closed by <dl>/
end

it "should fail with unclosed tag" do
expect{parse %Q{<p>Foobar}}.to raise_exception(Utopia::Content::MarkupParser::UnbalancedTagError)
error = begin
parse %Q{<p>Foobar}
rescue subject::UnbalancedTagError => error
error
end

expect(error).to be_a(subject::UnbalancedTagError)
expect(error.end_location).to be_nil
expect(error.to_s).to be =~ /<p> was not closed/
end
end
Loading
Loading