Module: Protocol::URL::Path

Defined in:
lib/protocol/url/path.rb

Overview

Represents a relative URL, which does not include a scheme or authority.

Class Method Summary collapse

Class Method Details

.expand(base, relative, pop = true) ⇒ Object

Examples:

Expand a relative path against a base path.

Path.expand("/documents/reports/", "invoices/2024.pdf")
# => "/documents/reports/invoices/2024.pdf"

Navigate to parent directory.

Path.expand("/documents/reports/2024/", "../summary.pdf")
# => "/documents/reports/summary.pdf"


96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/protocol/url/path.rb', line 96

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

.join(components) ⇒ Object

Join the given path components into a single path.

Examples:

Join absolute path components.

Path.join(["", "documents", "report.pdf"])
# => "/documents/report.pdf"

Join relative path components.

Path.join(["images", "logo.png"])
# => "images/logo.png"


45
46
47
# File 'lib/protocol/url/path.rb', line 45

def self.join(components)
	return components.join("/")
end

.relative(target, from) ⇒ Object

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.

Examples:

Calculate relative path between pages.

Path.relative("/_components/app.js", "/foo/bar/")
# => "../../_components/app.js"

Calculate relative path in same directory.

Path.relative("/docs/guide.html", "/docs/index.html")
# => "guide.html"


138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/protocol/url/path.rb', line 138

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

.simplify(components) ⇒ Object

Simplify the given path components by resolving "." and "..".

Examples:

Resolve parent directory references.

Path.simplify(["documents", "reports", "..", "invoices", "2024.pdf"])
# => ["documents", "invoices", "2024.pdf"]

Remove current directory references.

Path.simplify(["documents", ".", "report.pdf"])
# => ["documents", "report.pdf"]


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/protocol/url/path.rb', line 61

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

.split(path) ⇒ Object

Split the given path into its components.

  • split("") => []
  • split("/") => ["", ""]
  • split("/a/b/c") => ["", "a", "b", "c"]
  • split("a/b/c/") => ["a", "b", "c", ""]

Examples:

Split an absolute path.

Path.split("/documents/report.pdf")
# => ["", "documents", "report.pdf"]

Split a relative path.

Path.split("images/logo.png")
# => ["images", "logo.png"]


29
30
31
# File 'lib/protocol/url/path.rb', line 29

def self.split(path)
	return path.split("/", -1)
end

.to_local_path(path) ⇒ Object

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 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.

Examples:

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"

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


182
183
184
185
186
187
188
189
190
191
# File 'lib/protocol/url/path.rb', line 182

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