asciisourcerer 0.4.0 → 0.5.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 +4 -4
- data/BILL_OF_MATERIALS.adoc +17 -0
- data/README.adoc +87 -7
- data/lib/sourcerer/_docs/partials/liquid-filters-by-kind.adoc +3227 -0
- data/lib/sourcerer/_docs/partials/liquid-filters-by-source.adoc +3155 -0
- data/lib/sourcerer/asciidoc.rb +10 -3
- data/lib/sourcerer/builder.rb +1 -1
- data/lib/sourcerer/jekyll/liquid/filters.rb +295 -0
- data/lib/sourcerer/jekyll.rb +7 -2
- data/lib/sourcerer/mark_down_grade.rb +181 -28
- data/lib/sourcerer/rendering.rb +9 -3
- data/lib/sourcerer/source_skim/config.rb +8 -2
- data/lib/sourcerer/source_skim/ruby_skimmer.rb +83 -0
- data/lib/sourcerer/source_skim/skimmer.rb +94 -1
- data/lib/sourcerer/source_skim.rb +11 -2
- data/lib/sourcerer/util/gem_uri.rb +34 -0
- data/lib/sourcerer/version.rb +1 -1
- data/lib/sourcerer.rb +0 -1
- data/specs/data/liquid-filters.yml +1314 -0
- metadata +26 -4
- data/specs/docs/frontmatter-reader_prd.adoc +0 -47
data/lib/sourcerer/asciidoc.rb
CHANGED
|
@@ -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
|
|
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])
|
data/lib/sourcerer/builder.rb
CHANGED
|
@@ -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
|
|
|
@@ -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
|
data/lib/sourcerer/jekyll.rb
CHANGED
|
@@ -18,12 +18,17 @@ module Sourcerer
|
|
|
18
18
|
Bootstrapper.load_plugins
|
|
19
19
|
Monkeypatches.patch_jekyll
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
|
|
21
|
+
# Registration order matters: Liquid's Strainer `include`s each module in
|
|
22
|
+
# turn, so a later registration wins over an earlier one for any
|
|
23
|
+
# same-named filter method. Sourcerer's filters register LAST so that,
|
|
24
|
+
# e.g., its `inspect` (which adds a `format` argument) overrides
|
|
25
|
+
# Jekyll's `inspect` rather than being shadowed by it.
|
|
23
26
|
# Ensure Jekyll filters are registered
|
|
24
27
|
::Liquid::Template.register_filter(::Jekyll::Filters)
|
|
25
28
|
# Ensure jekyll-asciidoc filters are registered
|
|
26
29
|
::Liquid::Template.register_filter(::Jekyll::AsciiDoc::Filters)
|
|
30
|
+
# Ensure Sourcerer filters are registered (last, so they can override)
|
|
31
|
+
::Liquid::Template.register_filter(::Sourcerer::Jekyll::Liquid::Filters)
|
|
27
32
|
# Ensure Sourcerer tags are registered
|
|
28
33
|
::Liquid::Template.register_tag('embed', ::Sourcerer::Jekyll::Liquid::Tags::EmbedTag)
|
|
29
34
|
end
|