module Path
Represents a relative URL, which does not include a scheme or authority.
Definitions
def self.split(path)
Split the given path into its components.
split("")=>[]split("/")=>["", ""]split("/a/b/c")=>["", "a", "b", "c"]split("a/b/c/")=>["a", "b", "c", ""]
Example: Split an absolute path.
Path.split("/documents/report.pdf")
# => ["", "documents", "report.pdf"]
Example: Split a relative path.
Path.split("images/logo.png")
# => ["images", "logo.png"]
Signature
-
parameter
pathString The path to split.
-
returns
Array(String) The path components.
Implementation
def self.split(path)
return path.split("/", -1)
end
def self.join(components)
Join the given path components into a single path.
Example: Join absolute path components.
Path.join(["", "documents", "report.pdf"])
# => "/documents/report.pdf"
Example: Join relative path components.
Path.join(["images", "logo.png"])
# => "images/logo.png"
Signature
-
parameter
componentsArray(String) The path components to join.
-
returns
String The joined path.
Implementation
def self.join(components)
return components.join("/")
end
def self.simplify(components)
Simplify the given path components by resolving "." and "..".
Example: Resolve parent directory references.
Path.simplify(["documents", "reports", "..", "invoices", "2024.pdf"])
# => ["documents", "invoices", "2024.pdf"]
Example: Remove current directory references.
Path.simplify(["documents", ".", "report.pdf"])
# => ["documents", "report.pdf"]
Signature
-
parameter
componentsArray(String) The path components to simplify.
-
returns
Array(String) The simplified path components.
Implementation
def self.simplify(components)
output = []
components.each_with_index do |component, index|
if index == 0 && component == ""
# Preserve leading slash:
output << ""
elsif component == "."
# Handle current directory - trailing . means directory, preserve trailing slash:
output << "" if index == components.size - 1
elsif component == "" && index != components.size - 1
# Ignore empty segments (multiple slashes) except at end - no-op.
elsif component == ".." && output.last && output.last != ".."
# Handle parent directory: go up one level if not at root:
output.pop if output.last != ""
# Trailing .. means directory, preserve trailing slash:
output << "" if index == components.size - 1
else
# Regular path component:
output << component
end
end
return output
end
def self.expand(base, relative, pop = true)
Example: Expand a relative path against a base path.
Path.expand("/documents/reports/", "invoices/2024.pdf")
# => "/documents/reports/invoices/2024.pdf"
Example: Navigate to parent directory.
Path.expand("/documents/reports/2024/", "../summary.pdf")
# => "/documents/reports/summary.pdf"
Signature
-
parameter
popBoolean whether to remove the last path component of the base path, to conform to URI merging behaviour, as defined by RFC2396.
Implementation
def self.expand(base, relative, pop = true)
# Empty relative path means no change:
return base if relative.nil? || relative.empty?
components = split(base)
# RFC2396 Section 5.2:
# 6) a) All but the last segment of the base URI's path component is
# copied to the buffer. In other words, any characters after the
# last (right-most) slash character, if any, are excluded.
if pop and components.last != ".."
components.pop
elsif components.last == ""
components.pop
end
relative = relative.split("/", -1)
if relative.first == ""
components = relative
else
components.concat(relative)
end
return join(simplify(components))
end
def self.relative(target, from)
Calculate the relative path from one absolute path to another.
This is useful for generating relative URLs from one location to another, such as creating page-specific import maps or relative links.
Example: Calculate relative path between pages.
Path.relative("/_components/app.js", "/foo/bar/")
# => "../../_components/app.js"
Example: Calculate relative path in same directory.
Path.relative("/docs/guide.html", "/docs/index.html")
# => "guide.html"
Signature
-
parameter
targetString The destination path (where you want to go).
-
parameter
fromString The source path (where you are starting from).
-
returns
String The relative path from
fromtotarget.
Implementation
def self.relative(target, from)
target_components = split(target)
from_components = split(from)
# Remove the last component from 'from' to get the directory
from_components = from_components[0...-1] if from_components.size > 0
# Find the common prefix
common_length = 0
[target_components.size, from_components.size].min.times do |i|
break if target_components[i] != from_components[i]
common_length = i + 1
end
# Calculate how many levels to go up
up_levels = from_components.size - common_length
# Build the relative path components
relative_components = [".."] * up_levels + target_components[common_length..-1]
return join(relative_components)
end
def self.to_local_path(path)
Convert a URL path to a local file system path using the platform's file separator.
This method splits the URL path on / characters, unescapes each component using
Protocol::URL::Encoding.unescape_path (which preserves encoded separators), then joins the
components using File.join.
Percent-encoded path separators (%2F for / and %5C for \) are NOT decoded,
preventing them from being interpreted as directory boundaries. This ensures that
URL path components map directly to file system path components.
Example: Generating local paths.
Path.to_local_path("/documents/report.pdf") # => "/documents/report.pdf"
Path.to_local_path("/files/My%20Document.txt") # => "/files/My Document.txt"
Example: Preserves encoded separators.
Path.to_local_path("/folder/safe%2Fname/file.txt")
# => "/folder/safe%2Fname/file.txt"
# %2F is NOT decoded to prevent creating additional path components
Signature
-
parameter
pathString The URL path to convert (should be percent-encoded).
-
returns
String The local file system path.
Implementation
def self.to_local_path(path)
components = split(path)
# Unescape each component, preserving encoded path separators
components.map! do |component|
Encoding.unescape_path(component)
end
return File.join(*components)
end