ngage 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/exe/ngage +55 -0
  4. data/lib/ngage.rb +3 -0
  5. data/lib/ngage/jekyll.rb +204 -0
  6. data/lib/ngage/jekyll/cleaner.rb +111 -0
  7. data/lib/ngage/jekyll/collection.rb +235 -0
  8. data/lib/ngage/jekyll/command.rb +103 -0
  9. data/lib/ngage/jekyll/commands/build.rb +93 -0
  10. data/lib/ngage/jekyll/commands/clean.rb +45 -0
  11. data/lib/ngage/jekyll/commands/doctor.rb +173 -0
  12. data/lib/ngage/jekyll/commands/help.rb +34 -0
  13. data/lib/ngage/jekyll/commands/new.rb +157 -0
  14. data/lib/ngage/jekyll/commands/new_theme.rb +42 -0
  15. data/lib/ngage/jekyll/commands/serve.rb +354 -0
  16. data/lib/ngage/jekyll/commands/serve/live_reload_reactor.rb +122 -0
  17. data/lib/ngage/jekyll/commands/serve/livereload_assets/livereload.js +1183 -0
  18. data/lib/ngage/jekyll/commands/serve/servlet.rb +203 -0
  19. data/lib/ngage/jekyll/commands/serve/websockets.rb +81 -0
  20. data/lib/ngage/jekyll/configuration.rb +391 -0
  21. data/lib/ngage/jekyll/converter.rb +54 -0
  22. data/lib/ngage/jekyll/converters/identity.rb +41 -0
  23. data/lib/ngage/jekyll/converters/markdown.rb +116 -0
  24. data/lib/ngage/jekyll/converters/markdown/kramdown_parser.rb +122 -0
  25. data/lib/ngage/jekyll/converters/smartypants.rb +70 -0
  26. data/lib/ngage/jekyll/convertible.rb +253 -0
  27. data/lib/ngage/jekyll/deprecator.rb +50 -0
  28. data/lib/ngage/jekyll/document.rb +503 -0
  29. data/lib/ngage/jekyll/drops/collection_drop.rb +20 -0
  30. data/lib/ngage/jekyll/drops/document_drop.rb +69 -0
  31. data/lib/ngage/jekyll/drops/drop.rb +209 -0
  32. data/lib/ngage/jekyll/drops/excerpt_drop.rb +15 -0
  33. data/lib/ngage/jekyll/drops/jekyll_drop.rb +32 -0
  34. data/lib/ngage/jekyll/drops/site_drop.rb +56 -0
  35. data/lib/ngage/jekyll/drops/static_file_drop.rb +14 -0
  36. data/lib/ngage/jekyll/drops/unified_payload_drop.rb +26 -0
  37. data/lib/ngage/jekyll/drops/url_drop.rb +89 -0
  38. data/lib/ngage/jekyll/entry_filter.rb +127 -0
  39. data/lib/ngage/jekyll/errors.rb +20 -0
  40. data/lib/ngage/jekyll/excerpt.rb +180 -0
  41. data/lib/ngage/jekyll/external.rb +76 -0
  42. data/lib/ngage/jekyll/filters.rb +390 -0
  43. data/lib/ngage/jekyll/filters/date_filters.rb +110 -0
  44. data/lib/ngage/jekyll/filters/grouping_filters.rb +64 -0
  45. data/lib/ngage/jekyll/filters/url_filters.rb +68 -0
  46. data/lib/ngage/jekyll/frontmatter_defaults.rb +233 -0
  47. data/lib/ngage/jekyll/generator.rb +5 -0
  48. data/lib/ngage/jekyll/hooks.rb +106 -0
  49. data/lib/ngage/jekyll/layout.rb +62 -0
  50. data/lib/ngage/jekyll/liquid_extensions.rb +22 -0
  51. data/lib/ngage/jekyll/liquid_renderer.rb +63 -0
  52. data/lib/ngage/jekyll/liquid_renderer/file.rb +56 -0
  53. data/lib/ngage/jekyll/liquid_renderer/table.rb +98 -0
  54. data/lib/ngage/jekyll/log_adapter.rb +151 -0
  55. data/lib/ngage/jekyll/mime.types +825 -0
  56. data/lib/ngage/jekyll/page.rb +185 -0
  57. data/lib/ngage/jekyll/page_without_a_file.rb +14 -0
  58. data/lib/ngage/jekyll/plugin.rb +92 -0
  59. data/lib/ngage/jekyll/plugin_manager.rb +115 -0
  60. data/lib/ngage/jekyll/publisher.rb +23 -0
  61. data/lib/ngage/jekyll/reader.rb +154 -0
  62. data/lib/ngage/jekyll/readers/collection_reader.rb +22 -0
  63. data/lib/ngage/jekyll/readers/data_reader.rb +75 -0
  64. data/lib/ngage/jekyll/readers/layout_reader.rb +70 -0
  65. data/lib/ngage/jekyll/readers/page_reader.rb +25 -0
  66. data/lib/ngage/jekyll/readers/post_reader.rb +72 -0
  67. data/lib/ngage/jekyll/readers/static_file_reader.rb +25 -0
  68. data/lib/ngage/jekyll/readers/theme_assets_reader.rb +51 -0
  69. data/lib/ngage/jekyll/regenerator.rb +195 -0
  70. data/lib/ngage/jekyll/related_posts.rb +52 -0
  71. data/lib/ngage/jekyll/renderer.rb +266 -0
  72. data/lib/ngage/jekyll/site.rb +476 -0
  73. data/lib/ngage/jekyll/static_file.rb +169 -0
  74. data/lib/ngage/jekyll/stevenson.rb +60 -0
  75. data/lib/ngage/jekyll/tags/highlight.rb +108 -0
  76. data/lib/ngage/jekyll/tags/include.rb +226 -0
  77. data/lib/ngage/jekyll/tags/link.rb +40 -0
  78. data/lib/ngage/jekyll/tags/post_url.rb +104 -0
  79. data/lib/ngage/jekyll/theme.rb +73 -0
  80. data/lib/ngage/jekyll/theme_builder.rb +121 -0
  81. data/lib/ngage/jekyll/url.rb +160 -0
  82. data/lib/ngage/jekyll/utils.rb +370 -0
  83. data/lib/ngage/jekyll/utils/ansi.rb +57 -0
  84. data/lib/ngage/jekyll/utils/exec.rb +26 -0
  85. data/lib/ngage/jekyll/utils/internet.rb +37 -0
  86. data/lib/ngage/jekyll/utils/platforms.rb +82 -0
  87. data/lib/ngage/jekyll/utils/thread_event.rb +31 -0
  88. data/lib/ngage/jekyll/utils/win_tz.rb +75 -0
  89. data/lib/ngage/site_template/.gitignore +5 -0
  90. data/lib/ngage/site_template/404.html +25 -0
  91. data/lib/ngage/site_template/_config.yml +47 -0
  92. data/lib/ngage/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +29 -0
  93. data/lib/ngage/site_template/about.markdown +18 -0
  94. data/lib/ngage/site_template/index.markdown +6 -0
  95. data/lib/ngage/theme_template/CODE_OF_CONDUCT.md.erb +74 -0
  96. data/lib/ngage/theme_template/Gemfile +4 -0
  97. data/lib/ngage/theme_template/LICENSE.txt.erb +21 -0
  98. data/lib/ngage/theme_template/README.md.erb +52 -0
  99. data/lib/ngage/theme_template/_layouts/default.html +1 -0
  100. data/lib/ngage/theme_template/_layouts/page.html +5 -0
  101. data/lib/ngage/theme_template/_layouts/post.html +5 -0
  102. data/lib/ngage/theme_template/example/_config.yml.erb +1 -0
  103. data/lib/ngage/theme_template/example/_post.md +12 -0
  104. data/lib/ngage/theme_template/example/index.html +14 -0
  105. data/lib/ngage/theme_template/example/style.scss +7 -0
  106. data/lib/ngage/theme_template/gitignore.erb +6 -0
  107. data/lib/ngage/theme_template/theme.gemspec.erb +19 -0
  108. data/lib/ngage/version.rb +5 -0
  109. metadata +328 -0
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Tags
5
+ class Link < Liquid::Tag
6
+ class << self
7
+ def tag_name
8
+ name.split("::").last.downcase
9
+ end
10
+ end
11
+
12
+ def initialize(tag_name, relative_path, tokens)
13
+ super
14
+
15
+ @relative_path = relative_path.strip
16
+ end
17
+
18
+ def render(context)
19
+ site = context.registers[:site]
20
+
21
+ liquid = site.liquid_renderer.file("(jekyll:link)")
22
+ relative_path = liquid.parse(@relative_path).render(context)
23
+
24
+ site.each_site_file do |item|
25
+ return item.url if item.relative_path == relative_path
26
+ # This takes care of the case for static files that have a leading /
27
+ return item.url if item.relative_path == "/#{relative_path}"
28
+ end
29
+
30
+ raise ArgumentError, <<~MSG
31
+ Could not find document '#{relative_path}' in tag '#{self.class.tag_name}'.
32
+
33
+ Make sure the document exists and the path is correct.
34
+ MSG
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ Liquid::Template.register_tag(Jekyll::Tags::Link.tag_name, Jekyll::Tags::Link)
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Tags
5
+ class PostComparer
6
+ MATCHER = %r!^(.+/)*(\d+-\d+-\d+)-(.*)$!
7
+
8
+ attr_reader :path, :date, :slug, :name
9
+
10
+ def initialize(name)
11
+ @name = name
12
+
13
+ all, @path, @date, @slug = *name.sub(%r!^/!, "").match(MATCHER)
14
+ unless all
15
+ raise Jekyll::Errors::InvalidPostNameError,
16
+ "'#{name}' does not contain valid date and/or title."
17
+ end
18
+
19
+ escaped_slug = Regexp.escape(slug)
20
+ @name_regex = %r!^_posts/#{path}#{date}-#{escaped_slug}\.[^.]+|
21
+ ^#{path}_posts/?#{date}-#{escaped_slug}\.[^.]+!x
22
+ end
23
+
24
+ def post_date
25
+ @post_date ||= Utils.parse_date(
26
+ date,
27
+ "'#{date}' does not contain valid date and/or title."
28
+ )
29
+ end
30
+
31
+ def ==(other)
32
+ other.relative_path.match(@name_regex)
33
+ end
34
+
35
+ def deprecated_equality(other)
36
+ slug == post_slug(other) &&
37
+ post_date.year == other.date.year &&
38
+ post_date.month == other.date.month &&
39
+ post_date.day == other.date.day
40
+ end
41
+
42
+ private
43
+
44
+ # Construct the directory-aware post slug for a Jekyll::Post
45
+ #
46
+ # other - the Jekyll::Post
47
+ #
48
+ # Returns the post slug with the subdirectory (relative to _posts)
49
+ def post_slug(other)
50
+ path = other.basename.split("/")[0...-1].join("/")
51
+ if path.nil? || path == ""
52
+ other.data["slug"]
53
+ else
54
+ path + "/" + other.data["slug"]
55
+ end
56
+ end
57
+ end
58
+
59
+ class PostUrl < Liquid::Tag
60
+ def initialize(tag_name, post, tokens)
61
+ super
62
+ @orig_post = post.strip
63
+ begin
64
+ @post = PostComparer.new(@orig_post)
65
+ rescue StandardError => e
66
+ raise Jekyll::Errors::PostURLError, <<~MSG
67
+ Could not parse name of post "#{@orig_post}" in tag 'post_url'.
68
+ Make sure the post exists and the name is correct.
69
+ #{e.class}: #{e.message}
70
+ MSG
71
+ end
72
+ end
73
+
74
+ def render(context)
75
+ site = context.registers[:site]
76
+
77
+ site.posts.docs.each do |p|
78
+ return p.url if @post == p
79
+ end
80
+
81
+ # New matching method did not match, fall back to old method
82
+ # with deprecation warning if this matches
83
+
84
+ site.posts.docs.each do |p|
85
+ next unless @post.deprecated_equality p
86
+
87
+ Jekyll::Deprecator.deprecation_message "A call to "\
88
+ "'{% post_url #{@post.name} %}' did not match " \
89
+ "a post using the new matching method of checking name " \
90
+ "(path-date-slug) equality. Please make sure that you " \
91
+ "change this tag to match the post's name exactly."
92
+ return p.url
93
+ end
94
+
95
+ raise Jekyll::Errors::PostURLError, <<~MSG
96
+ Could not find post "#{@orig_post}" in tag 'post_url'.
97
+ Make sure the post exists and the name is correct.
98
+ MSG
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ Liquid::Template.register_tag("post_url", Jekyll::Tags::PostUrl)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ class Theme
5
+ extend Forwardable
6
+ attr_reader :name
7
+ def_delegator :gemspec, :version, :version
8
+
9
+ def initialize(name)
10
+ @name = name.downcase.strip
11
+ Jekyll.logger.debug "Theme:", name
12
+ Jekyll.logger.debug "Theme source:", root
13
+ configure_sass
14
+ end
15
+
16
+ def root
17
+ # Must use File.realpath to resolve symlinks created by rbenv
18
+ # Otherwise, Jekyll.sanitized path with prepend the unresolved root
19
+ @root ||= File.realpath(gemspec.full_gem_path)
20
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
21
+ raise "Path #{gemspec.full_gem_path} does not exist, is not accessible "\
22
+ "or includes a symbolic link loop"
23
+ end
24
+
25
+ def includes_path
26
+ @includes_path ||= path_for "_includes"
27
+ end
28
+
29
+ def layouts_path
30
+ @layouts_path ||= path_for "_layouts"
31
+ end
32
+
33
+ def sass_path
34
+ @sass_path ||= path_for "_sass"
35
+ end
36
+
37
+ def assets_path
38
+ @assets_path ||= path_for "assets"
39
+ end
40
+
41
+ def configure_sass
42
+ return unless sass_path
43
+
44
+ External.require_with_graceful_fail("sass") unless defined?(Sass)
45
+ Sass.load_paths << sass_path
46
+ end
47
+
48
+ def runtime_dependencies
49
+ gemspec.runtime_dependencies
50
+ end
51
+
52
+ private
53
+
54
+ def path_for(folder)
55
+ path = realpath_for(folder)
56
+ path if path && File.directory?(path)
57
+ end
58
+
59
+ def realpath_for(folder)
60
+ File.realpath(Jekyll.sanitized_path(root, folder.to_s))
61
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
62
+ Jekyll.logger.warn "Invalid theme folder:", folder
63
+ nil
64
+ end
65
+
66
+ def gemspec
67
+ @gemspec ||= Gem::Specification.find_by_name(name)
68
+ rescue Gem::LoadError
69
+ raise Jekyll::Errors::MissingDependencyException,
70
+ "The #{name} theme could not be found."
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ class ThemeBuilder
5
+ SCAFFOLD_DIRECTORIES = %w(
6
+ assets _layouts _includes _sass
7
+ ).freeze
8
+
9
+ attr_reader :name, :path, :code_of_conduct
10
+
11
+ def initialize(theme_name, opts)
12
+ @name = theme_name.to_s.tr(" ", "_").squeeze("_")
13
+ @path = Pathname.new(File.expand_path(name, Dir.pwd))
14
+ @code_of_conduct = !!opts["code_of_conduct"]
15
+ end
16
+
17
+ def create!
18
+ create_directories
19
+ create_starter_files
20
+ create_gemspec
21
+ create_accessories
22
+ initialize_git_repo
23
+ end
24
+
25
+ def user_name
26
+ @user_name ||= `git config user.name`.chomp
27
+ end
28
+
29
+ def user_email
30
+ @user_email ||= `git config user.email`.chomp
31
+ end
32
+
33
+ private
34
+
35
+ def root
36
+ @root ||= Pathname.new(File.expand_path("../", __dir__))
37
+ end
38
+
39
+ def template_file(filename)
40
+ [
41
+ root.join("theme_template", "#{filename}.erb"),
42
+ root.join("theme_template", filename.to_s),
43
+ ].find(&:exist?)
44
+ end
45
+
46
+ def template(filename)
47
+ erb.render(template_file(filename).read)
48
+ end
49
+
50
+ def erb
51
+ @erb ||= ERBRenderer.new(self)
52
+ end
53
+
54
+ def mkdir_p(directories)
55
+ Array(directories).each do |directory|
56
+ full_path = path.join(directory)
57
+ Jekyll.logger.info "create", full_path.to_s
58
+ FileUtils.mkdir_p(full_path)
59
+ end
60
+ end
61
+
62
+ def write_file(filename, contents)
63
+ full_path = path.join(filename)
64
+ Jekyll.logger.info "create", full_path.to_s
65
+ File.write(full_path, contents)
66
+ end
67
+
68
+ def create_directories
69
+ mkdir_p(SCAFFOLD_DIRECTORIES)
70
+ end
71
+
72
+ def create_starter_files
73
+ %w(page post default).each do |layout|
74
+ write_file("_layouts/#{layout}.html", template("_layouts/#{layout}.html"))
75
+ end
76
+ end
77
+
78
+ def create_gemspec
79
+ write_file("Gemfile", template("Gemfile"))
80
+ write_file("#{name}.gemspec", template("theme.gemspec"))
81
+ end
82
+
83
+ def create_accessories
84
+ accessories = %w(README.md LICENSE.txt)
85
+ accessories << "CODE_OF_CONDUCT.md" if code_of_conduct
86
+ accessories.each do |filename|
87
+ write_file(filename, template(filename))
88
+ end
89
+ end
90
+
91
+ def initialize_git_repo
92
+ Jekyll.logger.info "initialize", path.join(".git").to_s
93
+ Dir.chdir(path.to_s) { `git init` }
94
+ write_file(".gitignore", template("gitignore"))
95
+ end
96
+
97
+ class ERBRenderer
98
+ extend Forwardable
99
+
100
+ def_delegator :@theme_builder, :name, :theme_name
101
+ def_delegator :@theme_builder, :user_name, :user_name
102
+ def_delegator :@theme_builder, :user_email, :user_email
103
+
104
+ def initialize(theme_builder)
105
+ @theme_builder = theme_builder
106
+ end
107
+
108
+ def jekyll_version_with_minor
109
+ Jekyll::VERSION.split(".").take(2).join(".")
110
+ end
111
+
112
+ def theme_directories
113
+ SCAFFOLD_DIRECTORIES
114
+ end
115
+
116
+ def render(contents)
117
+ ERB.new(contents).result binding
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Public: Methods that generate a URL for a resource such as a Post or a Page.
4
+ #
5
+ # Examples
6
+ #
7
+ # URL.new({
8
+ # :template => /:categories/:title.html",
9
+ # :placeholders => {:categories => "ruby", :title => "something"}
10
+ # }).to_s
11
+ #
12
+ module Jekyll
13
+ class URL
14
+ # options - One of :permalink or :template must be supplied.
15
+ # :template - The String used as template for URL generation,
16
+ # for example "/:path/:basename:output_ext", where
17
+ # a placeholder is prefixed with a colon.
18
+ # :placeholders - A hash containing the placeholders which will be
19
+ # replaced when used inside the template. E.g.
20
+ # { "year" => Time.now.strftime("%Y") } would replace
21
+ # the placeholder ":year" with the current year.
22
+ # :permalink - If supplied, no URL will be generated from the
23
+ # template. Instead, the given permalink will be
24
+ # used as URL.
25
+ def initialize(options)
26
+ @template = options[:template]
27
+ @placeholders = options[:placeholders] || {}
28
+ @permalink = options[:permalink]
29
+
30
+ if (@template || @permalink).nil?
31
+ raise ArgumentError, "One of :template or :permalink must be supplied."
32
+ end
33
+ end
34
+
35
+ # The generated relative URL of the resource
36
+ #
37
+ # Returns the String URL
38
+ def to_s
39
+ sanitize_url(generated_permalink || generated_url)
40
+ end
41
+
42
+ # Generates a URL from the permalink
43
+ #
44
+ # Returns the _unsanitized String URL
45
+ def generated_permalink
46
+ (@generated_permalink ||= generate_url(@permalink)) if @permalink
47
+ end
48
+
49
+ # Generates a URL from the template
50
+ #
51
+ # Returns the unsanitized String URL
52
+ def generated_url
53
+ @generated_url ||= generate_url(@template)
54
+ end
55
+
56
+ # Internal: Generate the URL by replacing all placeholders with their
57
+ # respective values in the given template
58
+ #
59
+ # Returns the unsanitized String URL
60
+ def generate_url(template)
61
+ if @placeholders.is_a? Drops::UrlDrop
62
+ generate_url_from_drop(template)
63
+ else
64
+ generate_url_from_hash(template)
65
+ end
66
+ end
67
+
68
+ def generate_url_from_hash(template)
69
+ @placeholders.inject(template) do |result, token|
70
+ break result if result.index(":").nil?
71
+
72
+ if token.last.nil?
73
+ # Remove leading "/" to avoid generating urls with `//`
74
+ result.gsub("/:#{token.first}", "")
75
+ else
76
+ result.gsub(":#{token.first}", self.class.escape_path(token.last))
77
+ end
78
+ end
79
+ end
80
+
81
+ # We include underscores in keys to allow for 'i_month' and so forth.
82
+ # This poses a problem for keys which are followed by an underscore
83
+ # but the underscore is not part of the key, e.g. '/:month_:day'.
84
+ # That should be :month and :day, but our key extraction regexp isn't
85
+ # smart enough to know that so we have to make it an explicit
86
+ # possibility.
87
+ def possible_keys(key)
88
+ if key.end_with?("_")
89
+ [key, key.chomp("_")]
90
+ else
91
+ [key]
92
+ end
93
+ end
94
+
95
+ def generate_url_from_drop(template)
96
+ template.gsub(%r!:([a-z_]+)!) do |match|
97
+ pool = possible_keys(match.sub(":", ""))
98
+
99
+ winner = pool.find { |key| @placeholders.key?(key) }
100
+ if winner.nil?
101
+ raise NoMethodError,
102
+ "The URL template doesn't have #{pool.join(" or ")} keys. "\
103
+ "Check your permalink template!"
104
+ end
105
+
106
+ value = @placeholders[winner]
107
+ value = "" if value.nil?
108
+ replacement = self.class.escape_path(value)
109
+
110
+ match.sub(":#{winner}", replacement)
111
+ end.squeeze("/")
112
+ end
113
+
114
+ # Returns a sanitized String URL, stripping "../../" and multiples of "/",
115
+ # as well as the beginning "/" so we can enforce and ensure it.
116
+
117
+ def sanitize_url(str)
118
+ "/#{str}".gsub("..", "/").gsub("./", "").squeeze("/")
119
+ end
120
+
121
+ # Escapes a path to be a valid URL path segment
122
+ #
123
+ # path - The path to be escaped.
124
+ #
125
+ # Examples:
126
+ #
127
+ # URL.escape_path("/a b")
128
+ # # => "/a%20b"
129
+ #
130
+ # Returns the escaped path.
131
+ def self.escape_path(path)
132
+ # Because URI.escape doesn't escape "?", "[" and "]" by default,
133
+ # specify unsafe string (except unreserved, sub-delims, ":", "@" and "/").
134
+ #
135
+ # URI path segment is defined in RFC 3986 as follows:
136
+ # segment = *pchar
137
+ # pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
138
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
139
+ # pct-encoded = "%" HEXDIG HEXDIG
140
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
141
+ # / "*" / "+" / "," / ";" / "="
142
+ path = Addressable::URI.encode(path)
143
+ path.encode("utf-8").sub("#", "%23")
144
+ end
145
+
146
+ # Unescapes a URL path segment
147
+ #
148
+ # path - The path to be unescaped.
149
+ #
150
+ # Examples:
151
+ #
152
+ # URL.unescape_path("/a%20b")
153
+ # # => "/a b"
154
+ #
155
+ # Returns the unescaped path.
156
+ def self.unescape_path(path)
157
+ Addressable::URI.unencode(path.encode("utf-8"))
158
+ end
159
+ end
160
+ end