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
13 changes: 13 additions & 0 deletions lib/protocol/url/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -372,12 +372,25 @@ def relative(from)
common_length = i + 1
end

# Preserve the final segment when the target names the containing directory as a file:
if common_length > 0 && common_length == target_segments.size && target_segments.last != ""
common_length -= 1
end

# Calculate how many levels to go up
up_levels = from_segments.size - common_length

# Build the relative path segments
relative_segments = [".."] * up_levels + target_segments[common_length..-1]

# An empty reference identifies the current document, so identify the current directory explicitly:
if relative_segments == [""]
relative_segments = [".", ""]
elsif relative_segments.first&.include?(":")
# A colon in the first segment would be interpreted as a URI scheme:
relative_segments.unshift(".")
end

return Path.new(nil, relative_segments)
end

Expand Down
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Releases

## Unreleased

- Preserve directory and file semantics when generating relative URL paths.

## v0.15.0

- Add `Protocol::URL::Relative#relative_to` for expressing root-relative URLs relative to a base path.
Expand Down
16 changes: 16 additions & 0 deletions test/protocol/url/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,22 @@
expect(Protocol::URL::Path.relative("/docs/", "/docs/api/reference.html")).to be == "../"
end

it "identifies the current directory explicitly" do
expect(Protocol::URL::Path.relative("/docs/", "/docs/index.html")).to be == "./"
end

it "calculates relative path to the root directory" do
expect(Protocol::URL::Path.relative("/", "/index.html")).to be == "./"
end

it "distinguishes a file from its containing directory" do
expect(Protocol::URL::Path.relative("/docs", "/docs/index.html")).to be == "../docs"
end

it "disambiguates a colon in the first segment" do
expect(Protocol::URL::Path.relative("/docs/this:that", "/docs/index.html")).to be == "./this:that"
end

it "calculates relative path with multiple levels up" do
expect(Protocol::URL::Path.relative("/a/file.txt", "/x/y/z/")).to be == "../../../a/file.txt"
end
Expand Down
14 changes: 14 additions & 0 deletions test/protocol/url/relative.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@
expect(url.relative_to("/index").path).to be == Protocol::URL::Path["files/a%2Fb"]
end

it "identifies the root directory explicitly" do
url = Protocol::URL::Relative.new("/", "q=ruby", "examples")

expect(url.relative_to("/index").to_s).to be == "./?q=ruby#examples"
end

it "disambiguates a colon in the first segment" do
url = Protocol::URL::Relative.new("/docs/this:that")
relative_url = url.relative_to("/docs/index")

expect(relative_url.to_s).to be == "./this:that"
expect(Protocol::URL[relative_url.to_s]).to be_a(Protocol::URL::Relative)
end

it "returns already-relative URLs unchanged" do
url = Protocol::URL::Relative.new("../guide", "q=ruby", "examples")

Expand Down