diff --git a/lib/protocol/url/path.rb b/lib/protocol/url/path.rb index 79ec8f7..384188b 100644 --- a/lib/protocol/url/path.rb +++ b/lib/protocol/url/path.rb @@ -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 diff --git a/releases.md b/releases.md index 6145a2b..a64ed54 100644 --- a/releases.md +++ b/releases.md @@ -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. diff --git a/test/protocol/url/path.rb b/test/protocol/url/path.rb index c5cf35b..7507eb7 100644 --- a/test/protocol/url/path.rb +++ b/test/protocol/url/path.rb @@ -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 diff --git a/test/protocol/url/relative.rb b/test/protocol/url/relative.rb index c292f27..c3a97ee 100644 --- a/test/protocol/url/relative.rb +++ b/test/protocol/url/relative.rb @@ -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")