asciisourcerer 0.4.0 → 0.6.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.
@@ -43,9 +43,13 @@ module Sourcerer
43
43
  # Loads AsciiDoc attributes from a document header as a Hash.
44
44
  #
45
45
  # @param path [String] The path to the AsciiDoc file.
46
+ # @param user_only [Boolean] When true, strips Asciidoctor built-in attributes
47
+ # and returns only user-defined attributes. Defaults to false.
46
48
  # @return [Hash] A hash of the document attributes.
47
- def self.load_attributes path
49
+ def self.load_attributes path, user_only: false
48
50
  doc = Asciidoctor.load_file(path, safe: :unsafe)
51
+ return AttributesFilter.user_attributes(doc) if user_only
52
+
49
53
  doc.attributes
50
54
  end
51
55
 
@@ -192,17 +196,20 @@ module Sourcerer
192
196
  #
193
197
  # @param source_path [String] Path to AsciiDoc source file.
194
198
  # @param markdown_output_path [String, nil] Optional markdown output path.
199
+ # @param markdown_converter [#call, nil] Callable that accepts `(html, markdown_options)`.
200
+ # Defaults to Sourcerer::MarkDownGrade.convert_html when nil.
195
201
  # @param html_output_path [String, nil] Optional HTML output path.
196
202
  # @param backend [String] HTML backend request (`html5` or `asciidoctor-html5s`).
197
203
  # @param header_footer [Boolean] Whether interim HTML should include document wrapper.
198
204
  # @param include_frontmatter [Boolean] Whether to prepend markdown YAML front matter.
199
205
  # @param markdown_options [Hash] Options passed to markdown converter.
200
- # @param markdown_converter [#call] Callable that accepts `(html, markdown_options)`.
201
206
  # @param convert_tables_to_markdown [Boolean] Convert all tables to markdown UNLESS they have .no-markdown class.
202
207
  # @return [Hash] Conversion result containing markdown, frontmatter, and backend info.
203
- def self.mark_down_grade source_path, markdown_output_path=nil, markdown_converter:, **options
208
+ def self.mark_down_grade source_path, markdown_output_path=nil, markdown_converter: nil, **options
204
209
  options = normalize_mark_down_grade_options(options)
205
210
 
211
+ markdown_converter ||= ->(html, opts) { Sourcerer::MarkDownGrade.convert_html(html, opts || {}) }
212
+
206
213
  source_text = File.read(source_path)
207
214
  conversion_source_text = strip_yaml_frontmatter(source_text)
208
215
  selected_backend = resolve_html_backend(options[:backend])
@@ -62,7 +62,7 @@ module Sourcerer
62
62
  attributes.each_with_object({}) do |entry, acc|
63
63
  source = entry[:source]
64
64
  name = entry[:name] || File.basename(source, '.adoc').to_sym
65
- acc[name.to_sym] = Sourcerer::AsciiDoc.load_attributes(source)
65
+ acc[name.to_sym] = Sourcerer::AsciiDoc.load_attributes(source, user_only: true)
66
66
  end
67
67
  end
68
68
 
@@ -37,7 +37,7 @@ module Sourcerer
37
37
  # @param includes_load_paths [Array<String>] Paths to load includes from.
38
38
  # @param plugin_dirs [Array<String>] Paths to load plugins from.
39
39
  # @return [Jekyll::Site] The initialized fake Jekyll site object.
40
- # rubocop:disable Lint/UnusedMethodArgument
40
+ # rubocop:disable-next Lint/UnusedMethodArgument
41
41
  def self.fake_site includes_load_paths: [], plugin_dirs: []
42
42
  # NOTE: plugin_dirs parameter is accepted but not yet implemented; reserved for future plugin loading
43
43
  ::Jekyll.logger.log_level = :error if ::Jekyll.logger.respond_to?(:log_level=)
@@ -72,7 +72,6 @@ module Sourcerer
72
72
 
73
73
  site
74
74
  end
75
- # rubocop:enable Lint/UnusedMethodArgument
76
75
  end
77
76
  end
78
77
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'base64'
4
+ require 'bigdecimal'
4
5
  require 'cgi'
6
+ require 'date'
5
7
  require 'kramdown-asciidoc'
6
8
 
7
9
  module Sourcerer
@@ -13,6 +15,22 @@ module Sourcerer
13
15
  # Core transformation logic lives in `Ops` so behavior is reusable and easier
14
16
  # to test/refactor without changing the Liquid surface.
15
17
  module Filters
18
+ # Load canonical CLI args templates from YAML once at module load time
19
+ def self.load_cli_args_parameters
20
+ require 'yaml'
21
+ # Path from filters.rb: lib/sourcerer/jekyll/liquid/filters.rb
22
+ # To specs/data/liquid-filters.yml: go up 4 levels to gem root, then to specs/data
23
+ yaml_path = File.join(__dir__, '../../../../specs/data/liquid-filters.yml')
24
+ yaml_path = File.expand_path(yaml_path)
25
+ data = YAML.load_file(yaml_path, permitted_classes: [Date, Time])
26
+ filters = data.is_a?(Array) ? data : data['filters'] || []
27
+ to_cli_args_filter = filters.find { |f| f['key'] == 'to_cli_args' }
28
+ to_cli_args_filter&.dig('parameters') || {}
29
+ end
30
+
31
+ # Canonical template definitions loaded from specs/data/liquid-filters.yml
32
+ CLI_ARGS_TEMPLATES = load_cli_args_parameters.freeze
33
+
16
34
  # Internal operations for filter behavior.
17
35
  module Ops
18
36
  module_function
@@ -42,6 +60,15 @@ module Sourcerer
42
60
  end
43
61
  end
44
62
 
63
+ # Convert a string into a lowercase URL slug, joining words with the
64
+ # given separator (default `-`).
65
+ def slugify input, *args
66
+ separator = args[0] || '-'
67
+ input.downcase
68
+ .gsub(/[^a-z0-9]+/, separator)
69
+ .gsub(/\A#{Regexp.escape(separator)}+|#{Regexp.escape(separator)}+\z/, '')
70
+ end
71
+
45
72
  def plusify input
46
73
  input.gsub(/\n\n+/, "\n+\n")
47
74
  end
@@ -113,6 +140,210 @@ module Sourcerer
113
140
  def html_unescape input
114
141
  CGI.unescapeHTML(input.to_s)
115
142
  end
143
+
144
+ def wrap input, width=80
145
+ return input unless input.is_a?(String)
146
+
147
+ width = width.to_i
148
+ return input if width <= 0
149
+
150
+ input.split("\n").map do |line|
151
+ line.gsub(/(.{1,#{width}})(\s+|$)/, "\\1\n").rstrip
152
+ end.join("\n")
153
+ end
154
+
155
+ def commentwrap input, width=80, prefix=nil
156
+ return input unless input.is_a?(String)
157
+
158
+ wrapped = wrap(input, width)
159
+ return wrapped unless prefix
160
+
161
+ case prefix
162
+ when 'xml'
163
+ "<!-- #{wrapped} -->"
164
+ when /\|/
165
+ # Format like "/*|*/" becomes /* ... */
166
+ parts = prefix.split('|')
167
+ open_tag = parts[0]
168
+ close_tag = parts[1] || ''
169
+ "#{open_tag} #{wrapped}\n#{close_tag}"
170
+ else
171
+ # Regular comment prefix
172
+ wrapped.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
173
+ end
174
+ end
175
+
176
+ def to_yaml input, *args
177
+ require 'yaml'
178
+
179
+ # Parse args: can be "flow" and/or "quotes"
180
+ flow = args.include?('flow')
181
+ quotes = args.include?('quotes')
182
+
183
+ if flow
184
+ # Flow format (inline): {key: val}
185
+ if input.is_a?(Array)
186
+ if quotes
187
+ "[#{input.map { |item| "\"#{item}\"" }.join(', ')}]"
188
+ else
189
+ "[#{input.map(&:inspect).join(', ')}]"
190
+ end
191
+ elsif input.is_a?(Hash)
192
+ "{#{input.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}}"
193
+ else
194
+ input.to_s
195
+ end
196
+ else
197
+ # Block format (YAML): key: val
198
+ output = YAML.dump(input)
199
+ output.chomp
200
+ end
201
+ end
202
+
203
+ def to_json input
204
+ require 'json'
205
+ input.to_json
206
+ end
207
+
208
+ def replace_regex input, pattern, replacement=''
209
+ return input unless input.is_a?(String)
210
+
211
+ input.gsub(/#{pattern}/, replacement)
212
+ end
213
+
214
+ def match input, pattern
215
+ return false unless input.is_a?(String)
216
+
217
+ !!(input =~ /#{pattern}/)
218
+ end
219
+
220
+ def holds_liquid input
221
+ return false unless input.is_a?(String)
222
+
223
+ # Check for Liquid tags: {{ }}, {% %}, {%- -%}, {{- -}}
224
+ !!(input =~ /\{\{.*?\}\}|\{%-?.*?-?%\}/)
225
+ end
226
+
227
+ def to_cli_args input, template=nil, delimiter=' '
228
+ return input unless input.is_a?(Hash)
229
+
230
+ # Use templates loaded from canonical YAML source (Filters::CLI_ARGS_TEMPLATES)
231
+ selected_template = Filters::CLI_ARGS_TEMPLATES[template] || template || '--<option> <argument>'
232
+
233
+ input.map do |key, value|
234
+ selected_template
235
+ .gsub('<option>', key.to_s)
236
+ .gsub('<o>', key.to_s[0])
237
+ .gsub('<argument>', value.to_s)
238
+ .gsub('<VARIABLE>', key.to_s.upcase)
239
+ .gsub('<key>', key.to_s)
240
+ .gsub('<value>', value.to_s)
241
+ end.join(delimiter)
242
+ end
243
+
244
+ def store_list_concat input, key_name
245
+ return input unless input.is_a?(Array)
246
+
247
+ input.each_with_object([]) do |item, acc|
248
+ values = item[key_name.to_s]
249
+ acc.concat(values) if values.is_a?(Array)
250
+ end.uniq
251
+ end
252
+
253
+ def store_list_dupes input, key_name
254
+ return input unless input.is_a?(Array)
255
+
256
+ # Collect all arrays from specified key
257
+ arrays = input.map { |item| item[key_name.to_s] }.compact.grep(Array)
258
+
259
+ # Find items that appear in multiple arrays
260
+ all_items = arrays.flatten
261
+ all_items.select { |item| all_items.count(item) > 1 }.uniq
262
+ end
263
+
264
+ # Overrides Jekyll's `inspect` filter, adding a `format` argument.
265
+ # `html` (the default) reproduces Jekyll's own behavior exactly:
266
+ # an HTML-escaped `Object#inspect` string.
267
+ def inspect input, format='html'
268
+ case format
269
+ when 'yaml'
270
+ require 'yaml'
271
+ YAML.dump(input)
272
+ when 'json'
273
+ require 'json'
274
+ input.to_json
275
+ else
276
+ html_escape(input.inspect)
277
+ end
278
+ end
279
+
280
+ # -- Ports of Liquid 5 StandardFilters --
281
+ #
282
+ # Jekyll pins to Liquid 4, which lacks these four filters (added in
283
+ # Liquid 5). Ported here so downstream templates can use them
284
+ # regardless of the Liquid version Jekyll pulls in.
285
+ # See BILL_OF_MATERIALS.adoc for provenance/license details.
286
+
287
+ # Removes the last instance of a substring from a string.
288
+ def remove_last input, string
289
+ replace_last(input, string, '')
290
+ end
291
+
292
+ # Replaces the last instance of a substring in a string with a replacement.
293
+ def replace_last input, string, replacement=''
294
+ return input unless input.is_a?(String)
295
+
296
+ target = string.to_s
297
+ start_index = input.rindex(target)
298
+ return input unless start_index
299
+
300
+ output = input.dup
301
+ output[start_index, target.length] = replacement.to_s
302
+ output
303
+ end
304
+
305
+ # Strips leading/trailing whitespace and collapses interior runs of
306
+ # whitespace to a single space.
307
+ def squish input
308
+ return input unless input.is_a?(String)
309
+
310
+ input.strip.gsub(/\s+/, ' ')
311
+ end
312
+
313
+ # Sums a numeric array, or an array of Hashes at the given property.
314
+ def sum input, property=nil
315
+ return 0 unless input.is_a?(Array)
316
+
317
+ values = input.map do |item|
318
+ if property.nil?
319
+ item
320
+ elsif item.respond_to?(:[])
321
+ item[property]
322
+ else
323
+ 0
324
+ end
325
+ end
326
+
327
+ result = values.sum { |value| to_number(value) }
328
+ result.is_a?(BigDecimal) ? result.to_f : result
329
+ end
330
+
331
+ # @api private
332
+ # Coerces a Liquid value to a number, matching Liquid 5's
333
+ # `Utils.to_number` (Float -> BigDecimal for precision, numeric
334
+ # strings parsed, everything else 0).
335
+ def to_number obj
336
+ case obj
337
+ when Float
338
+ BigDecimal(obj.to_s)
339
+ when Numeric
340
+ obj
341
+ when String
342
+ /\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
343
+ else
344
+ 0
345
+ end
346
+ end
116
347
  end
117
348
  private_constant :Ops
118
349
 
@@ -124,6 +355,10 @@ module Sourcerer
124
355
  Ops.sluggerize(input, format)
125
356
  end
126
357
 
358
+ def slugify(input, *)
359
+ Ops.slugify(input, *)
360
+ end
361
+
127
362
  def plusify input
128
363
  Ops.plusify(input)
129
364
  end
@@ -167,6 +402,66 @@ module Sourcerer
167
402
  def html_unescape input
168
403
  Ops.html_unescape(input)
169
404
  end
405
+
406
+ def wrap input, width=80
407
+ Ops.wrap(input, width)
408
+ end
409
+
410
+ def commentwrap input, width=80, prefix=nil
411
+ Ops.commentwrap(input, width, prefix)
412
+ end
413
+
414
+ def to_yaml(input, *)
415
+ Ops.to_yaml(input, *)
416
+ end
417
+
418
+ def to_json input
419
+ Ops.to_json(input)
420
+ end
421
+
422
+ def replace_regex input, pattern, replacement=''
423
+ Ops.replace_regex(input, pattern, replacement)
424
+ end
425
+
426
+ def match input, pattern
427
+ Ops.match(input, pattern)
428
+ end
429
+
430
+ def holds_liquid input
431
+ Ops.holds_liquid(input)
432
+ end
433
+
434
+ def to_cli_args input, template=nil, delimiter=' '
435
+ Ops.to_cli_args(input, template, delimiter)
436
+ end
437
+
438
+ def store_list_concat input, key_name
439
+ Ops.store_list_concat(input, key_name)
440
+ end
441
+
442
+ def store_list_dupes input, key_name
443
+ Ops.store_list_dupes(input, key_name)
444
+ end
445
+
446
+ def inspect input, format='html'
447
+ Ops.inspect(input, format)
448
+ end
449
+
450
+ def remove_last input, string
451
+ Ops.remove_last(input, string)
452
+ end
453
+
454
+ def replace_last input, string, replacement=''
455
+ Ops.replace_last(input, string, replacement)
456
+ end
457
+
458
+ def squish input
459
+ Ops.squish(input)
460
+ end
461
+
462
+ def sum input, property=nil
463
+ Ops.sum(input, property)
464
+ end
170
465
  end
171
466
  end
172
467
  end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'liquid'
4
+
5
+ module Sourcerer
6
+ module Jekyll
7
+ module Liquid
8
+ # Opt-in alternative to Liquid's default "render nil/empty as empty
9
+ # string" behavior: when active, a `{{ ... }}` expression that
10
+ # evaluates to a missing or empty value renders as its own original
11
+ # source text (e.g. `{{ data.foo | capitalize }}`) instead of
12
+ # vanishing. Useful for document templates where an unfilled field
13
+ # should stay visible for the preparer to fill in by hand, rather
14
+ # than silently disappearing.
15
+ #
16
+ # Two independent toggles, each off by default:
17
+ #
18
+ # - `preserve_missing`: an outright `nil` result is preserved.
19
+ # - `preserve_empty`: a defined-but-empty String/Array/Hash result
20
+ # (`""`, `[]`, `{}`) is preserved.
21
+ #
22
+ # Neither affects the other's target, and neither affects `0` or
23
+ # `false`. Both checks run against the fully-filtered result (i.e.
24
+ # any `| filters` in the tag have already run), matching what Liquid
25
+ # itself would otherwise render -- we never re-run the filters, we
26
+ # just restore the tag's own pre-render source text once a render
27
+ # has been judged missing/empty. `raw` is used verbatim (no
28
+ # whitespace trimming), so the reconstructed tag matches the
29
+ # original byte-for-byte -- except for `{{-`/`-}}` whitespace
30
+ # control markers, which Liquid's tokenizer strips before the
31
+ # markup ever reaches a `Variable` instance and so cannot be
32
+ # recovered here.
33
+ #
34
+ # Substitution only ever applies to a *top-level output* `{{ }}`
35
+ # tag -- never to a `Liquid::Variable` some other tag builds
36
+ # privately to parse its own syntax. `Assign`, for example, does
37
+ # exactly that (`lib/liquid/tags/assign.rb`: `@from = Variable.new(
38
+ # $2, options)`, then `@from.render(context)` directly), and that
39
+ # Variable's `#render` is the same patched method, with the same
40
+ # global toggle active. Without this distinction,
41
+ # `{% assign name = data.missing %}` under `preserve_missing: true`
42
+ # would assign the *reconstructed source text of the assign's own
43
+ # right-hand side* (a String) to `name`, rather than the real `nil`
44
+ # -- corrupting every later `{{ name }}` reference instead of
45
+ # leaving it to render its own clean placeholder. `BlockBodyPatch`
46
+ # below marks the one call site (`BlockBody#render_node_to_output`,
47
+ # when the node being rendered is itself a `Variable`) that
48
+ # corresponds to a literal `{{ }}` sitting directly in a template
49
+ # body's nodelist -- the only place a Variable's rendered text
50
+ # actually becomes document output. Any other tag's internal use of
51
+ # a Variable to compute a value (Assign's right-hand side, or
52
+ # anything similar) renders outside that marker and is left with
53
+ # Liquid's normal nil/empty behavior, exactly as if this feature
54
+ # were off.
55
+ #
56
+ # Liquid 4 has no built-in hook for this -- there is no Environment or
57
+ # parser-injection point (introduced only in Liquid 5) to substitute a
58
+ # custom Variable class per template -- so this prepends onto
59
+ # `::Liquid::Variable` globally and gates the actual behavior change
60
+ # with toggles scoped to one render call. See
61
+ # `Sourcerer::Rendering.render_template`'s `preserve_missing:`/
62
+ # `preserve_empty:` options, which flip these toggles for the
63
+ # duration of that one render.
64
+ module PreserveMissingVariables
65
+ def self.preserve_missing?
66
+ Thread.current[:sourcerer_preserve_missing] == true
67
+ end
68
+
69
+ def self.preserve_empty?
70
+ Thread.current[:sourcerer_preserve_empty] == true
71
+ end
72
+
73
+ def self.top_level_output?
74
+ Thread.current[:sourcerer_rendering_output_variable] == true
75
+ end
76
+
77
+ # Activates preserve-missing/-empty rendering for the duration of
78
+ # the block. Restores the prior values afterward so nested/
79
+ # sequential renders that don't ask for it are unaffected.
80
+ #
81
+ # @param preserve_missing [Boolean] See {PreserveMissingVariables}.
82
+ # @param preserve_empty [Boolean] See {PreserveMissingVariables}.
83
+ def self.with_active preserve_missing: false, preserve_empty: false
84
+ previous_missing = Thread.current[:sourcerer_preserve_missing]
85
+ previous_empty = Thread.current[:sourcerer_preserve_empty]
86
+ Thread.current[:sourcerer_preserve_missing] = preserve_missing
87
+ Thread.current[:sourcerer_preserve_empty] = preserve_empty
88
+ yield
89
+ ensure
90
+ Thread.current[:sourcerer_preserve_missing] = previous_missing
91
+ Thread.current[:sourcerer_preserve_empty] = previous_empty
92
+ end
93
+
94
+ # Prepended onto ::Liquid::Variable. Falls through to Liquid's own
95
+ # `render` untouched unless: this Variable is currently being
96
+ # rendered as a top-level output node (see `BlockBodyPatch`
97
+ # below), preserve-missing/-empty is active for this thread, and
98
+ # the fully-filtered result counts as missing (nil) or empty (a
99
+ # defined-but-empty String/Array/Hash).
100
+ module VariablePatch
101
+ def render context
102
+ result = super
103
+ return result unless PreserveMissingVariables.top_level_output?
104
+ return "{{#{raw}}}" if PreserveMissingVariables.preserve_missing? && result.nil?
105
+ return "{{#{raw}}}" if PreserveMissingVariables.preserve_empty? && empty_result?(result)
106
+
107
+ result
108
+ end
109
+
110
+ private
111
+
112
+ # Only the documented value types count as "empty" -- not any
113
+ # arbitrary object/Drop that happens to implement `empty?` with
114
+ # its own, possibly unrelated, meaning.
115
+ def empty_result? value
116
+ (value.is_a?(String) || value.is_a?(Array) || value.is_a?(Hash)) && value.empty?
117
+ end
118
+ end
119
+
120
+ # Prepended onto ::Liquid::BlockBody. `render_node_to_output` is
121
+ # the one call site where a node from a body's own nodelist --
122
+ # i.e. a literal `{{ }}` or `{% tag %}` written directly in
123
+ # template source -- gets rendered and appended to output. We
124
+ # only care about the `Variable` case: that's a genuine top-level
125
+ # `{{ }}` output tag, as opposed to a `Variable` some other tag
126
+ # (Assign, etc.) builds privately and renders itself, bypassing
127
+ # BlockBody entirely. See {PreserveMissingVariables} above for why
128
+ # this distinction matters.
129
+ module BlockBodyPatch
130
+ # rubocop:disable-next Style/OptionalBooleanParameter -- must match
131
+ # ::Liquid::BlockBody#render_node_to_output's own positional signature
132
+ def render_node_to_output node, output, context, skip_output = false
133
+ return super unless node.is_a?(::Liquid::Variable)
134
+
135
+ previous = Thread.current[:sourcerer_rendering_output_variable]
136
+ Thread.current[:sourcerer_rendering_output_variable] = true
137
+ super
138
+ ensure
139
+ Thread.current[:sourcerer_rendering_output_variable] = previous if node.is_a?(::Liquid::Variable)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ Liquid::Variable.prepend(Sourcerer::Jekyll::Liquid::PreserveMissingVariables::VariablePatch)
148
+ Liquid::BlockBody.prepend(Sourcerer::Jekyll::Liquid::PreserveMissingVariables::BlockBodyPatch)
@@ -8,12 +8,25 @@ module Sourcerer
8
8
  # A Liquid tag for embedding and rendering a file within a template.
9
9
  # It searches for the file in the configured include paths.
10
10
  class EmbedTag < ::Liquid::Tag
11
+ # Matches an optional single- or double-quoted string, capturing the
12
+ # inner content; falls back to the whole (trimmed) markup as a bareword.
13
+ # Requires at least one character inside the quotes: an empty quoted
14
+ # name (`""`/`''`) would otherwise expand to the includes directory
15
+ # itself, pass File.exist?, and raise Errno::EISDIR from File.read
16
+ # instead of the intended missing-file error. Left unmatched, it
17
+ # falls through to the bareword branch instead, where the literal
18
+ # (quoted) name is looked up and not found -- reported normally.
19
+ PARTIAL_NAME_PATTERN = /\A(?:"([^"]+)"|'([^']+)')\z/
20
+
11
21
  # @param tag_name [String] The name of the tag ('embed').
12
- # @param markup [String] The name of the partial to embed.
22
+ # @param markup [String] The name of the partial to embed, quoted
23
+ # (+"foo.liquid"+ / +'foo.liquid'+) or bareword (+foo.liquid+).
13
24
  # @param tokens [Array<String>] The list of tokens.
14
25
  def initialize tag_name, markup, tokens
15
26
  super
16
- @partial_name = markup.strip
27
+ trimmed = markup.strip
28
+ match = trimmed.match(PARTIAL_NAME_PATTERN)
29
+ @partial_name = match ? (match[1] || match[2]) : trimmed
17
30
  end
18
31
 
19
32
  # Renders the embedded file.
@@ -22,7 +35,9 @@ module Sourcerer
22
35
  # @return [String] The rendered content of the embedded file.
23
36
  # @raise [IOError] if the embed file is not found.
24
37
  def render context
25
- includes_paths = context.registers[:includes_load_paths] || []
38
+ includes_paths = context.registers[:includes_load_paths]
39
+ includes_paths = site_includes_load_paths(context) if includes_paths.nil? || includes_paths.empty?
40
+ includes_paths ||= []
26
41
 
27
42
  found_path = includes_paths.find do |base|
28
43
  candidate = File.expand_path(@partial_name, base)
@@ -37,6 +52,19 @@ module Sourcerer
37
52
  partial = ::Liquid::Template.parse(source)
38
53
  partial.render!(context)
39
54
  end
55
+
56
+ private
57
+
58
+ # Fallback for callers that register a fake/real Jekyll +:site+ but
59
+ # forget the separate +:includes_load_paths+ register (e.g. any
60
+ # future caller mirroring {Sourcerer::Rendering.render_liquid}).
61
+ # Reads +site.config['includes_load_paths']+ directly rather than
62
+ # +site.includes_load_paths+: the latter is Jekyll's own attribute,
63
+ # derived only from +config['includes_dir']+ (effectively just the
64
+ # first path), not the full list Sourcerer stores in config.
65
+ def site_includes_load_paths context
66
+ context.registers[:site]&.config&.[]('includes_load_paths')
67
+ end
40
68
  end
41
69
  end
42
70
  end
@@ -5,6 +5,7 @@ require_relative 'jekyll/monkeypatches'
5
5
  require_relative 'jekyll/liquid/file_system'
6
6
  require_relative 'jekyll/liquid/filters'
7
7
  require_relative 'jekyll/liquid/tags'
8
+ require_relative 'jekyll/liquid/preserve_missing_variables'
8
9
  require 'jekyll-asciidoc'
9
10
 
10
11
  module Sourcerer
@@ -18,12 +19,17 @@ module Sourcerer
18
19
  Bootstrapper.load_plugins
19
20
  Monkeypatches.patch_jekyll
20
21
 
21
- # Ensure Sourcerer filters are registered
22
- ::Liquid::Template.register_filter(::Sourcerer::Jekyll::Liquid::Filters)
22
+ # Registration order matters: Liquid's Strainer `include`s each module in
23
+ # turn, so a later registration wins over an earlier one for any
24
+ # same-named filter method. Sourcerer's filters register LAST so that,
25
+ # e.g., its `inspect` (which adds a `format` argument) overrides
26
+ # Jekyll's `inspect` rather than being shadowed by it.
23
27
  # Ensure Jekyll filters are registered
24
28
  ::Liquid::Template.register_filter(::Jekyll::Filters)
25
29
  # Ensure jekyll-asciidoc filters are registered
26
30
  ::Liquid::Template.register_filter(::Jekyll::AsciiDoc::Filters)
31
+ # Ensure Sourcerer filters are registered (last, so they can override)
32
+ ::Liquid::Template.register_filter(::Sourcerer::Jekyll::Liquid::Filters)
27
33
  # Ensure Sourcerer tags are registered
28
34
  ::Liquid::Template.register_tag('embed', ::Sourcerer::Jekyll::Liquid::Tags::EmbedTag)
29
35
  end