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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/exe/ngage +55 -0
- data/lib/ngage.rb +3 -0
- data/lib/ngage/jekyll.rb +204 -0
- data/lib/ngage/jekyll/cleaner.rb +111 -0
- data/lib/ngage/jekyll/collection.rb +235 -0
- data/lib/ngage/jekyll/command.rb +103 -0
- data/lib/ngage/jekyll/commands/build.rb +93 -0
- data/lib/ngage/jekyll/commands/clean.rb +45 -0
- data/lib/ngage/jekyll/commands/doctor.rb +173 -0
- data/lib/ngage/jekyll/commands/help.rb +34 -0
- data/lib/ngage/jekyll/commands/new.rb +157 -0
- data/lib/ngage/jekyll/commands/new_theme.rb +42 -0
- data/lib/ngage/jekyll/commands/serve.rb +354 -0
- data/lib/ngage/jekyll/commands/serve/live_reload_reactor.rb +122 -0
- data/lib/ngage/jekyll/commands/serve/livereload_assets/livereload.js +1183 -0
- data/lib/ngage/jekyll/commands/serve/servlet.rb +203 -0
- data/lib/ngage/jekyll/commands/serve/websockets.rb +81 -0
- data/lib/ngage/jekyll/configuration.rb +391 -0
- data/lib/ngage/jekyll/converter.rb +54 -0
- data/lib/ngage/jekyll/converters/identity.rb +41 -0
- data/lib/ngage/jekyll/converters/markdown.rb +116 -0
- data/lib/ngage/jekyll/converters/markdown/kramdown_parser.rb +122 -0
- data/lib/ngage/jekyll/converters/smartypants.rb +70 -0
- data/lib/ngage/jekyll/convertible.rb +253 -0
- data/lib/ngage/jekyll/deprecator.rb +50 -0
- data/lib/ngage/jekyll/document.rb +503 -0
- data/lib/ngage/jekyll/drops/collection_drop.rb +20 -0
- data/lib/ngage/jekyll/drops/document_drop.rb +69 -0
- data/lib/ngage/jekyll/drops/drop.rb +209 -0
- data/lib/ngage/jekyll/drops/excerpt_drop.rb +15 -0
- data/lib/ngage/jekyll/drops/jekyll_drop.rb +32 -0
- data/lib/ngage/jekyll/drops/site_drop.rb +56 -0
- data/lib/ngage/jekyll/drops/static_file_drop.rb +14 -0
- data/lib/ngage/jekyll/drops/unified_payload_drop.rb +26 -0
- data/lib/ngage/jekyll/drops/url_drop.rb +89 -0
- data/lib/ngage/jekyll/entry_filter.rb +127 -0
- data/lib/ngage/jekyll/errors.rb +20 -0
- data/lib/ngage/jekyll/excerpt.rb +180 -0
- data/lib/ngage/jekyll/external.rb +76 -0
- data/lib/ngage/jekyll/filters.rb +390 -0
- data/lib/ngage/jekyll/filters/date_filters.rb +110 -0
- data/lib/ngage/jekyll/filters/grouping_filters.rb +64 -0
- data/lib/ngage/jekyll/filters/url_filters.rb +68 -0
- data/lib/ngage/jekyll/frontmatter_defaults.rb +233 -0
- data/lib/ngage/jekyll/generator.rb +5 -0
- data/lib/ngage/jekyll/hooks.rb +106 -0
- data/lib/ngage/jekyll/layout.rb +62 -0
- data/lib/ngage/jekyll/liquid_extensions.rb +22 -0
- data/lib/ngage/jekyll/liquid_renderer.rb +63 -0
- data/lib/ngage/jekyll/liquid_renderer/file.rb +56 -0
- data/lib/ngage/jekyll/liquid_renderer/table.rb +98 -0
- data/lib/ngage/jekyll/log_adapter.rb +151 -0
- data/lib/ngage/jekyll/mime.types +825 -0
- data/lib/ngage/jekyll/page.rb +185 -0
- data/lib/ngage/jekyll/page_without_a_file.rb +14 -0
- data/lib/ngage/jekyll/plugin.rb +92 -0
- data/lib/ngage/jekyll/plugin_manager.rb +115 -0
- data/lib/ngage/jekyll/publisher.rb +23 -0
- data/lib/ngage/jekyll/reader.rb +154 -0
- data/lib/ngage/jekyll/readers/collection_reader.rb +22 -0
- data/lib/ngage/jekyll/readers/data_reader.rb +75 -0
- data/lib/ngage/jekyll/readers/layout_reader.rb +70 -0
- data/lib/ngage/jekyll/readers/page_reader.rb +25 -0
- data/lib/ngage/jekyll/readers/post_reader.rb +72 -0
- data/lib/ngage/jekyll/readers/static_file_reader.rb +25 -0
- data/lib/ngage/jekyll/readers/theme_assets_reader.rb +51 -0
- data/lib/ngage/jekyll/regenerator.rb +195 -0
- data/lib/ngage/jekyll/related_posts.rb +52 -0
- data/lib/ngage/jekyll/renderer.rb +266 -0
- data/lib/ngage/jekyll/site.rb +476 -0
- data/lib/ngage/jekyll/static_file.rb +169 -0
- data/lib/ngage/jekyll/stevenson.rb +60 -0
- data/lib/ngage/jekyll/tags/highlight.rb +108 -0
- data/lib/ngage/jekyll/tags/include.rb +226 -0
- data/lib/ngage/jekyll/tags/link.rb +40 -0
- data/lib/ngage/jekyll/tags/post_url.rb +104 -0
- data/lib/ngage/jekyll/theme.rb +73 -0
- data/lib/ngage/jekyll/theme_builder.rb +121 -0
- data/lib/ngage/jekyll/url.rb +160 -0
- data/lib/ngage/jekyll/utils.rb +370 -0
- data/lib/ngage/jekyll/utils/ansi.rb +57 -0
- data/lib/ngage/jekyll/utils/exec.rb +26 -0
- data/lib/ngage/jekyll/utils/internet.rb +37 -0
- data/lib/ngage/jekyll/utils/platforms.rb +82 -0
- data/lib/ngage/jekyll/utils/thread_event.rb +31 -0
- data/lib/ngage/jekyll/utils/win_tz.rb +75 -0
- data/lib/ngage/site_template/.gitignore +5 -0
- data/lib/ngage/site_template/404.html +25 -0
- data/lib/ngage/site_template/_config.yml +47 -0
- data/lib/ngage/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +29 -0
- data/lib/ngage/site_template/about.markdown +18 -0
- data/lib/ngage/site_template/index.markdown +6 -0
- data/lib/ngage/theme_template/CODE_OF_CONDUCT.md.erb +74 -0
- data/lib/ngage/theme_template/Gemfile +4 -0
- data/lib/ngage/theme_template/LICENSE.txt.erb +21 -0
- data/lib/ngage/theme_template/README.md.erb +52 -0
- data/lib/ngage/theme_template/_layouts/default.html +1 -0
- data/lib/ngage/theme_template/_layouts/page.html +5 -0
- data/lib/ngage/theme_template/_layouts/post.html +5 -0
- data/lib/ngage/theme_template/example/_config.yml.erb +1 -0
- data/lib/ngage/theme_template/example/_post.md +12 -0
- data/lib/ngage/theme_template/example/index.html +14 -0
- data/lib/ngage/theme_template/example/style.scss +7 -0
- data/lib/ngage/theme_template/gitignore.erb +6 -0
- data/lib/ngage/theme_template/theme.gemspec.erb +19 -0
- data/lib/ngage/version.rb +5 -0
- 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
|