From 4d47343264dbc118dbd7a6f8cf0b171202b2f0ac Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 15 Aug 2026 12:15:04 +1200 Subject: [PATCH] Support explicit relative URL paths --- lib/protocol/url/absolute.rb | 3 ++- lib/protocol/url/path.rb | 11 ++++++++--- lib/protocol/url/relative.rb | 5 +++-- releases.md | 4 ++++ test/protocol/url/absolute.rb | 1 + test/protocol/url/path.rb | 8 ++++++++ test/protocol/url/relative.rb | 14 ++++++++++++++ 7 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/protocol/url/absolute.rb b/lib/protocol/url/absolute.rb index 0b11155..7c0b23e 100644 --- a/lib/protocol/url/absolute.rb +++ b/lib/protocol/url/absolute.rb @@ -147,8 +147,9 @@ def with(scheme: @scheme, authority: @authority, path: nil, query: @query, fragm # Absolute URLs cannot be made relative without comparing their origins. # @parameter base [Object] The ignored base URL or path. + # @parameter explicit [Boolean] Ignored for absolute URLs. # @returns [self] This absolute URL. - def relative_to(base) + def relative_to(base, explicit: false) return self end diff --git a/lib/protocol/url/path.rb b/lib/protocol/url/path.rb index 384188b..03baa9b 100644 --- a/lib/protocol/url/path.rb +++ b/lib/protocol/url/path.rb @@ -67,6 +67,7 @@ def self.for(components, encoding: Encoding) # # @parameter target [String] The destination path (where you want to go). # @parameter from [String] The source path (where you are starting from). + # @parameter explicit [Boolean] Whether same-directory paths should start with `./`. # @returns [String] The relative path from `from` to `target`. # # @example Calculate relative path between pages. @@ -76,8 +77,8 @@ def self.for(components, encoding: Encoding) # @example Calculate relative path in same directory. # Path.relative("/docs/guide.html", "/docs/index.html") # # => "guide.html" - def self.relative(target, from) - return Path[target].relative(from).to_s + def self.relative(target, from, explicit: false) + return Path[target].relative(from, explicit: explicit).to_s end # Initialize a path from either its complete encoded representation or encoded segments. @@ -357,8 +358,9 @@ def join(other, pop: true, simplify: true) # Calculate this path relative to another path. # # @parameter from [String | Array(String) | Path] The source path. + # @parameter explicit [Boolean] Whether same-directory paths should start with `./`. # @returns [Path] The relative path from `from` to this path. - def relative(from) + def relative(from, explicit: false) target_segments = self.segments from_segments = Path[from].segments @@ -386,6 +388,9 @@ def relative(from) # An empty reference identifies the current document, so identify the current directory explicitly: if relative_segments == [""] relative_segments = [".", ""] + elsif explicit && relative_segments.first != ".." + # Identify same-directory references explicitly: + relative_segments.unshift(".") elsif relative_segments.first&.include?(":") # A colon in the first segment would be interpreted as a URI scheme: relative_segments.unshift(".") diff --git a/lib/protocol/url/relative.rb b/lib/protocol/url/relative.rb index 633e0e6..625f21f 100644 --- a/lib/protocol/url/relative.rb +++ b/lib/protocol/url/relative.rb @@ -138,15 +138,16 @@ def with(path: nil, query: @query, fragment: @fragment, pop: true) # are preserved when converting a root-relative path. # # @parameter base [Relative | Path | String] The base URL or path. + # @parameter explicit [Boolean] Whether same-directory paths should start with `./`. # @returns [Relative] The relative URL. - def relative_to(base) + def relative_to(base, explicit: false) return self unless @path.absolute? if base.is_a?(Relative) base = base.path end - return self.class.new(@path.relative(base), @query, @fragment) + return self.class.new(@path.relative(base, explicit: explicit), @query, @fragment) end # Normalize the encoded path and simplify its structure. diff --git a/releases.md b/releases.md index 5952efa..8a4aaed 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Add optional explicit `./` prefixes when generating same-directory relative URLs. + ## v0.16.0 - Preserve directory and file semantics when generating relative URL paths. diff --git a/test/protocol/url/absolute.rb b/test/protocol/url/absolute.rb index b1d9095..54183ed 100644 --- a/test/protocol/url/absolute.rb +++ b/test/protocol/url/absolute.rb @@ -204,6 +204,7 @@ url = Protocol::URL::Absolute.new("https", "example.com", "/docs/guide", "q=ruby", "examples") expect(url.relative_to("/docs/index")).to be_equal(url) + expect(url.relative_to("/docs/index", explicit: true)).to be_equal(url) end end diff --git a/test/protocol/url/path.rb b/test/protocol/url/path.rb index 7507eb7..ab7b6ec 100644 --- a/test/protocol/url/path.rb +++ b/test/protocol/url/path.rb @@ -501,6 +501,14 @@ expect(Protocol::URL::Path.relative("/docs/guide.html", "/docs/index.html")).to be == "guide.html" end + it "can identify a same-directory path explicitly" do + expect(Protocol::URL::Path.relative("/docs/guide.html", "/docs/index.html", explicit: true)).to be == "./guide.html" + end + + it "does not prefix a parent-directory path" do + expect(Protocol::URL::Path.relative("/assets/app.js", "/docs/index.html", explicit: true)).to be == "../assets/app.js" + end + it "calculates relative path from root to subdirectory" do expect(Protocol::URL::Path.relative("/foo/bar/", "/")).to be == "foo/bar/" end diff --git a/test/protocol/url/relative.rb b/test/protocol/url/relative.rb index c3a97ee..f0b63e1 100644 --- a/test/protocol/url/relative.rb +++ b/test/protocol/url/relative.rb @@ -205,6 +205,20 @@ expect(result.fragment).to be == "examples" end + it "can identify same-directory URLs explicitly" do + url = Protocol::URL::Relative.new("/docs/guide", "q=ruby", "examples") + result = url.relative_to("/docs/index", explicit: true) + + expect(result.to_s).to be == "./guide?q=ruby#examples" + end + + it "does not prefix parent-directory URLs" do + url = Protocol::URL::Relative.new("/assets/app.js") + result = url.relative_to("/docs/index", explicit: true) + + expect(result.to_s).to be == "../assets/app.js" + end + it "accepts a URL as the base" do url = Protocol::URL::Relative.new("/docs/guide") base = Protocol::URL::Relative.new("/docs/index")