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,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
class StaticFile
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
attr_reader :relative_path, :extname, :name, :data
|
|
8
|
+
|
|
9
|
+
def_delegator :to_liquid, :to_json, :to_json
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# The cache of last modification times [path] -> mtime.
|
|
13
|
+
def mtimes
|
|
14
|
+
@mtimes ||= {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reset_cache
|
|
18
|
+
@mtimes = nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Initialize a new StaticFile.
|
|
23
|
+
#
|
|
24
|
+
# site - The Site.
|
|
25
|
+
# base - The String path to the <source>.
|
|
26
|
+
# dir - The String path between <source> and the file.
|
|
27
|
+
# name - The String filename of the file.
|
|
28
|
+
# rubocop: disable ParameterLists
|
|
29
|
+
def initialize(site, base, dir, name, collection = nil)
|
|
30
|
+
@site = site
|
|
31
|
+
@base = base
|
|
32
|
+
@dir = dir
|
|
33
|
+
@name = name
|
|
34
|
+
@collection = collection
|
|
35
|
+
@relative_path = File.join(*[@dir, @name].compact)
|
|
36
|
+
@extname = File.extname(@name)
|
|
37
|
+
@data = @site.frontmatter_defaults.all(relative_path, type)
|
|
38
|
+
end
|
|
39
|
+
# rubocop: enable ParameterLists
|
|
40
|
+
|
|
41
|
+
# Returns source file path.
|
|
42
|
+
def path
|
|
43
|
+
# Static file is from a collection inside custom collections directory
|
|
44
|
+
if !@collection.nil? && !@site.config["collections_dir"].empty?
|
|
45
|
+
File.join(*[@base, @site.config["collections_dir"], @dir, @name].compact)
|
|
46
|
+
else
|
|
47
|
+
File.join(*[@base, @dir, @name].compact)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Obtain destination path.
|
|
52
|
+
#
|
|
53
|
+
# dest - The String path to the destination dir.
|
|
54
|
+
#
|
|
55
|
+
# Returns destination file path.
|
|
56
|
+
def destination(dest)
|
|
57
|
+
@site.in_dest_dir(*[dest, destination_rel_dir, @name].compact)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def destination_rel_dir
|
|
61
|
+
if @collection
|
|
62
|
+
File.dirname(url)
|
|
63
|
+
else
|
|
64
|
+
@dir
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def modified_time
|
|
69
|
+
@modified_time ||= File.stat(path).mtime
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns last modification time for this file.
|
|
73
|
+
def mtime
|
|
74
|
+
modified_time.to_i
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Is source path modified?
|
|
78
|
+
#
|
|
79
|
+
# Returns true if modified since last write.
|
|
80
|
+
def modified?
|
|
81
|
+
self.class.mtimes[path] != mtime
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Whether to write the file to the filesystem
|
|
85
|
+
#
|
|
86
|
+
# Returns true unless the defaults for the destination path from
|
|
87
|
+
# _config.yml contain `published: false`.
|
|
88
|
+
def write?
|
|
89
|
+
defaults.fetch("published", true)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Write the static file to the destination directory (if modified).
|
|
93
|
+
#
|
|
94
|
+
# dest - The String path to the destination dir.
|
|
95
|
+
#
|
|
96
|
+
# Returns false if the file was not modified since last time (no-op).
|
|
97
|
+
def write(dest)
|
|
98
|
+
dest_path = destination(dest)
|
|
99
|
+
|
|
100
|
+
return false if File.exist?(dest_path) && !modified?
|
|
101
|
+
|
|
102
|
+
self.class.mtimes[path] = mtime
|
|
103
|
+
|
|
104
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
105
|
+
FileUtils.rm(dest_path) if File.exist?(dest_path)
|
|
106
|
+
copy_file(dest_path)
|
|
107
|
+
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_liquid
|
|
112
|
+
@to_liquid ||= Drops::StaticFileDrop.new(self)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def basename
|
|
116
|
+
File.basename(name, extname)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def placeholders
|
|
120
|
+
{
|
|
121
|
+
:collection => @collection.label,
|
|
122
|
+
:path => relative_path[
|
|
123
|
+
@collection.relative_directory.size..relative_path.size],
|
|
124
|
+
:output_ext => "",
|
|
125
|
+
:name => "",
|
|
126
|
+
:title => "",
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Applies a similar URL-building technique as Jekyll::Document that takes
|
|
131
|
+
# the collection's URL template into account. The default URL template can
|
|
132
|
+
# be overriden in the collection's configuration in _config.yml.
|
|
133
|
+
def url
|
|
134
|
+
@url ||= if @collection.nil?
|
|
135
|
+
relative_path
|
|
136
|
+
else
|
|
137
|
+
::Jekyll::URL.new(
|
|
138
|
+
:template => @collection.url_template,
|
|
139
|
+
:placeholders => placeholders
|
|
140
|
+
)
|
|
141
|
+
end.to_s.chomp("/")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns the type of the collection if present, nil otherwise.
|
|
145
|
+
def type
|
|
146
|
+
@type ||= @collection.nil? ? nil : @collection.label.to_sym
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Returns the front matter defaults defined for the file's URL and/or type
|
|
150
|
+
# as defined in _config.yml.
|
|
151
|
+
def defaults
|
|
152
|
+
@defaults ||= @site.frontmatter_defaults.all url, type
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def copy_file(dest_path)
|
|
158
|
+
if @site.safe || Jekyll.env == "production"
|
|
159
|
+
FileUtils.cp(path, dest_path)
|
|
160
|
+
else
|
|
161
|
+
FileUtils.copy_entry(path, dest_path)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
unless File.symlink?(dest_path)
|
|
165
|
+
File.utime(self.class.mtimes[path], self.class.mtimes[path], dest_path)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
class Stevenson < ::Logger
|
|
5
|
+
def initialize
|
|
6
|
+
@progname = nil
|
|
7
|
+
@level = DEBUG
|
|
8
|
+
@default_formatter = Formatter.new
|
|
9
|
+
@logdev = $stdout
|
|
10
|
+
@formatter = proc do |_, _, _, msg|
|
|
11
|
+
msg.to_s
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add(severity, message = nil, progname = nil)
|
|
16
|
+
severity ||= UNKNOWN
|
|
17
|
+
@logdev = logdevice(severity)
|
|
18
|
+
|
|
19
|
+
return true if @logdev.nil? || severity < @level
|
|
20
|
+
|
|
21
|
+
progname ||= @progname
|
|
22
|
+
if message.nil?
|
|
23
|
+
if block_given?
|
|
24
|
+
message = yield
|
|
25
|
+
else
|
|
26
|
+
message = progname
|
|
27
|
+
progname = @progname
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
@logdev.puts(
|
|
31
|
+
format_message(format_severity(severity), Time.now, progname, message)
|
|
32
|
+
)
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Log a +WARN+ message
|
|
37
|
+
def warn(progname = nil, &block)
|
|
38
|
+
add(WARN, nil, progname.yellow, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Log an +ERROR+ message
|
|
42
|
+
def error(progname = nil, &block)
|
|
43
|
+
add(ERROR, nil, progname.red, &block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def close
|
|
47
|
+
# No LogDevice in use
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def logdevice(severity)
|
|
53
|
+
if severity > INFO
|
|
54
|
+
$stderr
|
|
55
|
+
else
|
|
56
|
+
$stdout
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module Tags
|
|
5
|
+
class HighlightBlock < Liquid::Block
|
|
6
|
+
include Liquid::StandardFilters
|
|
7
|
+
|
|
8
|
+
# The regular expression syntax checker. Start with the language specifier.
|
|
9
|
+
# Follow that by zero or more space separated options that take one of three
|
|
10
|
+
# forms: name, name=value, or name="<quoted list>"
|
|
11
|
+
#
|
|
12
|
+
# <quoted list> is a space-separated list of numbers
|
|
13
|
+
SYNTAX = %r!^([a-zA-Z0-9.+#_-]+)((\s+\w+(=(\w+|"([0-9]+\s)*[0-9]+"))?)*)$!
|
|
14
|
+
|
|
15
|
+
def initialize(tag_name, markup, tokens)
|
|
16
|
+
super
|
|
17
|
+
if markup.strip =~ SYNTAX
|
|
18
|
+
@lang = Regexp.last_match(1).downcase
|
|
19
|
+
@highlight_options = parse_options(Regexp.last_match(2))
|
|
20
|
+
else
|
|
21
|
+
raise SyntaxError, <<~MSG
|
|
22
|
+
Syntax Error in tag 'highlight' while parsing the following markup:
|
|
23
|
+
|
|
24
|
+
#{markup}
|
|
25
|
+
|
|
26
|
+
Valid syntax: highlight <lang> [linenos]
|
|
27
|
+
MSG
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render(context)
|
|
32
|
+
prefix = context["highlighter_prefix"] || ""
|
|
33
|
+
suffix = context["highlighter_suffix"] || ""
|
|
34
|
+
code = super.to_s.gsub(%r!\A(\n|\r)+|(\n|\r)+\z!, "")
|
|
35
|
+
|
|
36
|
+
output =
|
|
37
|
+
case context.registers[:site].highlighter
|
|
38
|
+
when "rouge"
|
|
39
|
+
render_rouge(code)
|
|
40
|
+
when "pygments"
|
|
41
|
+
render_pygments(code, context)
|
|
42
|
+
else
|
|
43
|
+
render_codehighlighter(code)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
rendered_output = add_code_tag(output)
|
|
47
|
+
prefix + rendered_output + suffix
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
OPTIONS_REGEX = %r!(?:\w="[^"]*"|\w=\w|\w)+!
|
|
53
|
+
|
|
54
|
+
def parse_options(input)
|
|
55
|
+
options = {}
|
|
56
|
+
return options if input.empty?
|
|
57
|
+
|
|
58
|
+
# Split along 3 possible forms -- key="<quoted list>", key=value, or key
|
|
59
|
+
input.scan(OPTIONS_REGEX) do |opt|
|
|
60
|
+
key, value = opt.split("=")
|
|
61
|
+
# If a quoted list, convert to array
|
|
62
|
+
if value&.include?('"')
|
|
63
|
+
value.delete!('"')
|
|
64
|
+
value = value.split
|
|
65
|
+
end
|
|
66
|
+
options[key.to_sym] = value || true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
options[:linenos] = "inline" if options[:linenos] == true
|
|
70
|
+
options
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def render_pygments(code, _context)
|
|
74
|
+
Jekyll.logger.warn "Warning:", "Highlight Tag no longer supports rendering with Pygments."
|
|
75
|
+
Jekyll.logger.warn "", "Using the default highlighter, Rouge, instead."
|
|
76
|
+
render_rouge(code)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_rouge(code)
|
|
80
|
+
require "rouge"
|
|
81
|
+
formatter = ::Rouge::Formatters::HTMLLegacy.new(
|
|
82
|
+
:line_numbers => @highlight_options[:linenos],
|
|
83
|
+
:wrap => false,
|
|
84
|
+
:css_class => "highlight",
|
|
85
|
+
:gutter_class => "gutter",
|
|
86
|
+
:code_class => "code"
|
|
87
|
+
)
|
|
88
|
+
lexer = ::Rouge::Lexer.find_fancy(@lang, code) || Rouge::Lexers::PlainText
|
|
89
|
+
formatter.format(lexer.lex(code))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_codehighlighter(code)
|
|
93
|
+
h(code).strip
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def add_code_tag(code)
|
|
97
|
+
code_attributes = [
|
|
98
|
+
"class=\"language-#{@lang.to_s.tr("+", "-")}\"",
|
|
99
|
+
"data-lang=\"#{@lang}\"",
|
|
100
|
+
].join(" ")
|
|
101
|
+
"<figure class=\"highlight\"><pre><code #{code_attributes}>"\
|
|
102
|
+
"#{code.chomp}</code></pre></figure>"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
Liquid::Template.register_tag("highlight", Jekyll::Tags::HighlightBlock)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module Tags
|
|
5
|
+
class IncludeTag < Liquid::Tag
|
|
6
|
+
VALID_SYNTAX = %r!
|
|
7
|
+
([\w-]+)\s*=\s*
|
|
8
|
+
(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+))
|
|
9
|
+
!x
|
|
10
|
+
VARIABLE_SYNTAX = %r!
|
|
11
|
+
(?<variable>[^{]*(\{\{\s*[\w\-\.]+\s*(\|.*)?\}\}[^\s{}]*)+)
|
|
12
|
+
(?<params>.*)
|
|
13
|
+
!mx
|
|
14
|
+
|
|
15
|
+
FULL_VALID_SYNTAX = %r!\A\s*(?:#{VALID_SYNTAX}(?=\s|\z)\s*)*\z!
|
|
16
|
+
VALID_FILENAME_CHARS = %r!^[\w/\.-]+$!
|
|
17
|
+
INVALID_SEQUENCES = %r![./]{2,}!
|
|
18
|
+
|
|
19
|
+
def initialize(tag_name, markup, tokens)
|
|
20
|
+
super
|
|
21
|
+
matched = markup.strip.match(VARIABLE_SYNTAX)
|
|
22
|
+
if matched
|
|
23
|
+
@file = matched["variable"].strip
|
|
24
|
+
@params = matched["params"].strip
|
|
25
|
+
else
|
|
26
|
+
@file, @params = markup.strip.split(%r!\s+!, 2)
|
|
27
|
+
end
|
|
28
|
+
validate_params if @params
|
|
29
|
+
@tag_name = tag_name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def syntax_example
|
|
33
|
+
"{% #{@tag_name} file.ext param='value' param2='value' %}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def parse_params(context)
|
|
37
|
+
params = {}
|
|
38
|
+
markup = @params
|
|
39
|
+
|
|
40
|
+
while (match = VALID_SYNTAX.match(markup))
|
|
41
|
+
markup = markup[match.end(0)..-1]
|
|
42
|
+
|
|
43
|
+
value = if match[2]
|
|
44
|
+
match[2].gsub('\\"', '"')
|
|
45
|
+
elsif match[3]
|
|
46
|
+
match[3].gsub("\\'", "'")
|
|
47
|
+
elsif match[4]
|
|
48
|
+
context[match[4]]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
params[match[1]] = value
|
|
52
|
+
end
|
|
53
|
+
params
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def validate_file_name(file)
|
|
57
|
+
if file =~ INVALID_SEQUENCES || file !~ VALID_FILENAME_CHARS
|
|
58
|
+
raise ArgumentError, <<~MSG
|
|
59
|
+
Invalid syntax for include tag. File contains invalid characters or sequences:
|
|
60
|
+
|
|
61
|
+
#{file}
|
|
62
|
+
|
|
63
|
+
Valid syntax:
|
|
64
|
+
|
|
65
|
+
#{syntax_example}
|
|
66
|
+
|
|
67
|
+
MSG
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_params
|
|
72
|
+
unless @params =~ FULL_VALID_SYNTAX
|
|
73
|
+
raise ArgumentError, <<~MSG
|
|
74
|
+
Invalid syntax for include tag:
|
|
75
|
+
|
|
76
|
+
#{@params}
|
|
77
|
+
|
|
78
|
+
Valid syntax:
|
|
79
|
+
|
|
80
|
+
#{syntax_example}
|
|
81
|
+
|
|
82
|
+
MSG
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Grab file read opts in the context
|
|
87
|
+
def file_read_opts(context)
|
|
88
|
+
context.registers[:site].file_read_opts
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Render the variable if required
|
|
92
|
+
def render_variable(context)
|
|
93
|
+
if @file =~ VARIABLE_SYNTAX
|
|
94
|
+
partial = context.registers[:site]
|
|
95
|
+
.liquid_renderer
|
|
96
|
+
.file("(variable)")
|
|
97
|
+
.parse(@file)
|
|
98
|
+
partial.render!(context)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def tag_includes_dirs(context)
|
|
103
|
+
context.registers[:site].includes_load_paths.freeze
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def locate_include_file(context, file, safe)
|
|
107
|
+
includes_dirs = tag_includes_dirs(context)
|
|
108
|
+
includes_dirs.each do |dir|
|
|
109
|
+
path = File.join(dir.to_s, file.to_s)
|
|
110
|
+
return path if valid_include_file?(path, dir.to_s, safe)
|
|
111
|
+
end
|
|
112
|
+
raise IOError, could_not_locate_message(file, includes_dirs, safe)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render(context)
|
|
116
|
+
site = context.registers[:site]
|
|
117
|
+
|
|
118
|
+
file = render_variable(context) || @file
|
|
119
|
+
validate_file_name(file)
|
|
120
|
+
|
|
121
|
+
path = locate_include_file(context, file, site.safe)
|
|
122
|
+
return unless path
|
|
123
|
+
|
|
124
|
+
add_include_to_dependency(site, path, context)
|
|
125
|
+
|
|
126
|
+
partial = load_cached_partial(path, context)
|
|
127
|
+
|
|
128
|
+
context.stack do
|
|
129
|
+
context["include"] = parse_params(context) if @params
|
|
130
|
+
begin
|
|
131
|
+
partial.render!(context)
|
|
132
|
+
rescue Liquid::Error => e
|
|
133
|
+
e.template_name = path
|
|
134
|
+
e.markup_context = "included " if e.markup_context.nil?
|
|
135
|
+
raise e
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def add_include_to_dependency(site, path, context)
|
|
141
|
+
if context.registers[:page]&.key?("path")
|
|
142
|
+
site.regenerator.add_dependency(
|
|
143
|
+
site.in_source_dir(context.registers[:page]["path"]),
|
|
144
|
+
path
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def load_cached_partial(path, context)
|
|
150
|
+
context.registers[:cached_partials] ||= {}
|
|
151
|
+
cached_partial = context.registers[:cached_partials]
|
|
152
|
+
|
|
153
|
+
if cached_partial.key?(path)
|
|
154
|
+
cached_partial[path]
|
|
155
|
+
else
|
|
156
|
+
unparsed_file = context.registers[:site]
|
|
157
|
+
.liquid_renderer
|
|
158
|
+
.file(path)
|
|
159
|
+
begin
|
|
160
|
+
cached_partial[path] = unparsed_file.parse(read_file(path, context))
|
|
161
|
+
rescue Liquid::Error => e
|
|
162
|
+
e.template_name = path
|
|
163
|
+
e.markup_context = "included " if e.markup_context.nil?
|
|
164
|
+
raise e
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def valid_include_file?(path, dir, safe)
|
|
170
|
+
!outside_site_source?(path, dir, safe) && File.file?(path)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def outside_site_source?(path, dir, safe)
|
|
174
|
+
safe && !realpath_prefixed_with?(path, dir)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def realpath_prefixed_with?(path, dir)
|
|
178
|
+
File.exist?(path) && File.realpath(path).start_with?(dir)
|
|
179
|
+
rescue StandardError
|
|
180
|
+
false
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# This method allows to modify the file content by inheriting from the class.
|
|
184
|
+
def read_file(file, context)
|
|
185
|
+
File.read(file, file_read_opts(context))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
def could_not_locate_message(file, includes_dirs, safe)
|
|
191
|
+
message = "Could not locate the included file '#{file}' in any of "\
|
|
192
|
+
"#{includes_dirs}. Ensure it exists in one of those directories and"
|
|
193
|
+
message + if safe
|
|
194
|
+
" is not a symlink as those are not allowed in safe mode."
|
|
195
|
+
else
|
|
196
|
+
", if it is a symlink, does not point outside your site source."
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
class IncludeRelativeTag < IncludeTag
|
|
202
|
+
def tag_includes_dirs(context)
|
|
203
|
+
Array(page_path(context)).freeze
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def page_path(context)
|
|
207
|
+
if context.registers[:page].nil?
|
|
208
|
+
context.registers[:site].source
|
|
209
|
+
else
|
|
210
|
+
site = context.registers[:site]
|
|
211
|
+
page_payload = context.registers[:page]
|
|
212
|
+
resource_path = \
|
|
213
|
+
if page_payload["collection"].nil?
|
|
214
|
+
page_payload["path"]
|
|
215
|
+
else
|
|
216
|
+
File.join(site.config["collections_dir"], page_payload["path"])
|
|
217
|
+
end
|
|
218
|
+
site.in_source_dir File.dirname(resource_path)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
Liquid::Template.register_tag("include", Jekyll::Tags::IncludeTag)
|
|
226
|
+
Liquid::Template.register_tag("include_relative", Jekyll::Tags::IncludeRelativeTag)
|