Skip to content

Commit c37193f

Browse files
committed
Add Path.relative for relative path computations.
1 parent d1a3b88 commit c37193f

File tree

3 files changed

+77
-0
lines changed

3 files changed

+77
-0
lines changed

lib/protocol/url/path.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,45 @@ def self.expand(base, relative, pop = true)
119119
return join(simplify(components))
120120
end
121121

122+
# Calculate the relative path from one absolute path to another.
123+
#
124+
# This is useful for generating relative URLs from one location to another,
125+
# such as creating page-specific import maps or relative links.
126+
#
127+
# @parameter target [String] The destination path (where you want to go).
128+
# @parameter from [String] The source path (where you are starting from).
129+
# @returns [String] The relative path from `from` to `target`.
130+
#
131+
# @example Calculate relative path between pages.
132+
# Path.relative("/_components/app.js", "/foo/bar/")
133+
# # => "../../_components/app.js"
134+
#
135+
# @example Calculate relative path in same directory.
136+
# Path.relative("/docs/guide.html", "/docs/index.html")
137+
# # => "guide.html"
138+
def self.relative(target, from)
139+
target_components = split(target)
140+
from_components = split(from)
141+
142+
# Remove the last component from 'from' to get the directory
143+
from_components = from_components[0...-1] if from_components.size > 0
144+
145+
# Find the common prefix
146+
common_length = 0
147+
[target_components.size, from_components.size].min.times do |i|
148+
break if target_components[i] != from_components[i]
149+
common_length = i + 1
150+
end
151+
152+
# Calculate how many levels to go up
153+
up_levels = from_components.size - common_length
154+
155+
# Build the relative path components
156+
relative_components = [".."] * up_levels + target_components[common_length..-1]
157+
158+
return join(relative_components)
159+
end
160+
122161
# Convert a URL path to a local file system path using the platform's file separator.
123162
#
124163
# This method splits the URL path on `/` characters, unescapes each component using

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Add `relative(target, from)` for computing relative paths between URLs.
6+
37
## v0.2.0
48

59
- Move `Protocol::URL::PATTERN` to `protocol/url/pattern.rb` so it can be shared more easily.

test/protocol/url/path.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,40 @@
396396
end
397397
end
398398

399+
with ".relative" do
400+
it "calculates relative path between pages" do
401+
expect(Protocol::URL::Path.relative("/_components/app.js", "/foo/bar/")).to be == "../../_components/app.js"
402+
end
403+
404+
it "calculates relative path in same directory" do
405+
expect(Protocol::URL::Path.relative("/docs/guide.html", "/docs/index.html")).to be == "guide.html"
406+
end
407+
408+
it "calculates relative path from root to subdirectory" do
409+
expect(Protocol::URL::Path.relative("/foo/bar/", "/")).to be == "foo/bar/"
410+
end
411+
412+
it "calculates relative path to parent directory" do
413+
expect(Protocol::URL::Path.relative("/docs/", "/docs/api/reference.html")).to be == "../"
414+
end
415+
416+
it "calculates relative path with multiple levels up" do
417+
expect(Protocol::URL::Path.relative("/a/file.txt", "/x/y/z/")).to be == "../../../a/file.txt"
418+
end
419+
420+
it "calculates relative path with common prefix" do
421+
expect(Protocol::URL::Path.relative("/projects/alpha/file.txt", "/projects/beta/")).to be == "../alpha/file.txt"
422+
end
423+
424+
it "preserves trailing slashes in target" do
425+
expect(Protocol::URL::Path.relative("/foo/bar/", "/baz/")).to be == "../foo/bar/"
426+
end
427+
428+
it "handles target without trailing slash" do
429+
expect(Protocol::URL::Path.relative("/foo/bar", "/baz/")).to be == "../foo/bar"
430+
end
431+
end
432+
399433
with ".to_local_path" do
400434
it "converts simple absolute path" do
401435
result = Protocol::URL::Path.to_local_path("/documents/report.pdf")

0 commit comments

Comments
 (0)