diff --git a/lib/protocol/url/absolute.rb b/lib/protocol/url/absolute.rb index 7c0b23e..a3a6450 100644 --- a/lib/protocol/url/absolute.rb +++ b/lib/protocol/url/absolute.rb @@ -114,7 +114,8 @@ def +(other) end # Append the absolute URL to the given buffer. - def append(buffer = String.new) + # @parameter explicit [Boolean] Ignored because absolute URLs are already lexically identifiable. + def append(buffer = String.new, explicit: false) buffer << @scheme << ":" if @scheme buffer << "//" << @authority if @authority super(buffer) @@ -147,9 +148,8 @@ 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, explicit: false) + def relative_to(base) return self end @@ -170,9 +170,10 @@ def <=>(other) # Convert the URL to its string representation. # + # @parameter explicit [Boolean] Ignored because absolute URLs are already lexically identifiable. # @returns [String] The formatted absolute URL string. - def to_s - append + def to_s(explicit: false) + append(explicit: explicit) end end end diff --git a/lib/protocol/url/path.rb b/lib/protocol/url/path.rb index 03baa9b..384188b 100644 --- a/lib/protocol/url/path.rb +++ b/lib/protocol/url/path.rb @@ -67,7 +67,6 @@ 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. @@ -77,8 +76,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, explicit: false) - return Path[target].relative(from, explicit: explicit).to_s + def self.relative(target, from) + return Path[target].relative(from).to_s end # Initialize a path from either its complete encoded representation or encoded segments. @@ -358,9 +357,8 @@ 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, explicit: false) + def relative(from) target_segments = self.segments from_segments = Path[from].segments @@ -388,9 +386,6 @@ def relative(from, explicit: false) # 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/reference.rb b/lib/protocol/url/reference.rb index c36b70f..b8f94b4 100644 --- a/lib/protocol/url/reference.rb +++ b/lib/protocol/url/reference.rb @@ -162,8 +162,9 @@ def fragment? # Append the reference to the given buffer. # Encodes the fragment; the path already retains its encoded structure. # Query strings are passed through as-is (they contain = and & which are valid syntax). - def append(buffer = String.new) - buffer << @path.encoded + # @parameter explicit [Boolean] Whether the result should be lexically identifiable as a URL in a mixed grammar. + def append(buffer = String.new, explicit: false) + append_path(buffer, explicit: explicit) if @query and !@query.empty? buffer << "?" << @query diff --git a/lib/protocol/url/relative.rb b/lib/protocol/url/relative.rb index 625f21f..911f2a2 100644 --- a/lib/protocol/url/relative.rb +++ b/lib/protocol/url/relative.rb @@ -138,16 +138,15 @@ 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, explicit: false) + def relative_to(base) return self unless @path.absolute? if base.is_a?(Relative) base = base.path end - return self.class.new(@path.relative(base, explicit: explicit), @query, @fragment) + return self.class.new(@path.relative(base), @query, @fragment) end # Normalize the encoded path and simplify its structure. @@ -176,8 +175,9 @@ def normalize! # Append the relative URL to the given buffer. # The path, query, and fragment are expected to already be properly encoded. - def append(buffer = String.new) - buffer << @path.encoded + # @parameter explicit [Boolean] Whether the result should be lexically identifiable as a URL in a mixed grammar. + def append(buffer = String.new, explicit: false) + append_path(buffer, explicit: explicit) if @query and !@query.empty? buffer << "?" << @query @@ -237,10 +237,13 @@ def ===(other) end # Convert the URL to its string representation. + # When explicit, same-directory references start with `./` so they can be + # distinguished from non-URL values in a mixed grammar. # + # @parameter explicit [Boolean] Whether the result should be lexically identifiable as a URL in a mixed grammar. # @returns [String] The formatted URL string. - def to_s - append + def to_s(explicit: false) + append(explicit: explicit) end # Convert the URL to a JSON-compatible representation. @@ -264,6 +267,16 @@ def inspect "#<#{self.class} #{to_s}>" end + # Append the path, optionally identifying a relative URL explicitly. + private def append_path(buffer, explicit: false) + if explicit && @path.relative? + path = @path.encoded + buffer << "./" unless path.start_with?("./", "../") + end + + buffer << @path.encoded + end + end end end diff --git a/releases.md b/releases.md index 9130f21..3e9b4e8 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Support explicit URL serialization for mixed grammars, and remove explicit prefixes from relative path calculation. + ## v0.17.0 - Add optional explicit `./` prefixes when generating same-directory relative URLs. diff --git a/test/protocol/url/absolute.rb b/test/protocol/url/absolute.rb index 54183ed..f32b240 100644 --- a/test/protocol/url/absolute.rb +++ b/test/protocol/url/absolute.rb @@ -204,7 +204,14 @@ 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 + + with "#to_s" do + it "ignores explicit relative serialization" do + url = Protocol::URL::Absolute.new("https", "example.com", "/docs/guide") + + expect(url.to_s(explicit: true)).to be == "https://example.com/docs/guide" end end diff --git a/test/protocol/url/path.rb b/test/protocol/url/path.rb index ab7b6ec..7507eb7 100644 --- a/test/protocol/url/path.rb +++ b/test/protocol/url/path.rb @@ -501,14 +501,6 @@ 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/reference.rb b/test/protocol/url/reference.rb index 841f7ff..7ec71af 100644 --- a/test/protocol/url/reference.rb +++ b/test/protocol/url/reference.rb @@ -328,6 +328,14 @@ end end + with "#to_s" do + it "can serialize relative references explicitly" do + reference = subject.new("guide", nil, nil, {"page" => "2"}) + + expect(reference.to_s(explicit: true)).to be == "./guide?page=2" + end + end + describe Protocol::URL::Reference.parse("path%20with%20spaces/image.jpg") do it "preserves encoded whitespace" do expect(subject.to_s).to be == "path%20with%20spaces/image.jpg" diff --git a/test/protocol/url/relative.rb b/test/protocol/url/relative.rb index f0b63e1..3fa11e5 100644 --- a/test/protocol/url/relative.rb +++ b/test/protocol/url/relative.rb @@ -205,20 +205,6 @@ 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") @@ -313,6 +299,41 @@ end end + with "#to_s" do + it "preserves minimal relative URLs by default" do + url = Protocol::URL::Relative.new("guide", "q=ruby", "examples") + + expect(url.to_s).to be == "guide?q=ruby#examples" + end + + it "can serialize same-directory URLs explicitly" do + url = Protocol::URL::Relative.new("guide", "q=ruby", "examples") + serialized = url.to_s(explicit: true) + + expect(serialized).to be == "./guide?q=ruby#examples" + expect(Protocol::URL[serialized]).to be_a(Protocol::URL::Relative) + end + + it "can explicitly serialize a normalized combined URL" do + base = Protocol::URL::Relative.new("./_components/") + url = base + Protocol::URL::Relative.new("./app.js") + + expect(url.to_s).to be == "_components/app.js" + expect(url.to_s(explicit: true)).to be == "./_components/app.js" + end + + it "preserves parent- and root-relative URLs" do + expect(Protocol::URL::Relative.new("../guide").to_s(explicit: true)).to be == "../guide" + expect(Protocol::URL::Relative.new("/guide").to_s(explicit: true)).to be == "/guide" + end + + it "identifies an empty path explicitly" do + url = Protocol::URL::Relative.new("", "q=ruby", "examples") + + expect(url.to_s(explicit: true)).to be == "./?q=ruby#examples" + end + end + with "#as_json" do it "returns string representation" do url = Protocol::URL::Relative.new("/path", "q=test", "section")