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.
- checksums.yaml +4 -4
- data/BILL_OF_MATERIALS.adoc +17 -0
- data/README.adoc +111 -11
- 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/bootstrapper.rb +1 -2
- data/lib/sourcerer/jekyll/liquid/filters.rb +295 -0
- data/lib/sourcerer/jekyll/liquid/preserve_missing_variables.rb +148 -0
- data/lib/sourcerer/jekyll/liquid/tags.rb +31 -3
- data/lib/sourcerer/jekyll.rb +8 -2
- data/lib/sourcerer/mark_down_grade.rb +181 -28
- data/lib/sourcerer/rendering.rb +70 -41
- 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 +95 -3
- 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 +22 -2
- data/specs/docs/frontmatter-reader_prd.adoc +0 -47
|
@@ -22,7 +22,8 @@ module Sourcerer
|
|
|
22
22
|
@config = {
|
|
23
23
|
preserve_heading_ids: true,
|
|
24
24
|
strip_internal_links: false,
|
|
25
|
-
convert_tables_to_markdown: false
|
|
25
|
+
convert_tables_to_markdown: false,
|
|
26
|
+
convert_dls_to_markdown: true
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
class << self
|
|
@@ -34,6 +35,7 @@ module Sourcerer
|
|
|
34
35
|
# preserve_heading_ids: (default: true) Include <a id="..."> anchors before headings
|
|
35
36
|
# strip_internal_links: (default: false) Remove href from internal anchor links, keeping only text
|
|
36
37
|
# convert_tables_to_markdown: (default: false) Convert all tables to markdown UNLESS they have .no-markdown class
|
|
38
|
+
# convert_dls_to_markdown: (default: true) Convert all DLs to markdown UNLESS they have .no-markdown class
|
|
37
39
|
def self.bootstrap! options={}
|
|
38
40
|
@config.merge!(options)
|
|
39
41
|
|
|
@@ -47,6 +49,7 @@ module Sourcerer
|
|
|
47
49
|
register_blockquote_converter
|
|
48
50
|
register_comment_converter
|
|
49
51
|
register_link_converter
|
|
52
|
+
register_list_converters
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
# Enhanced Pre converter to handle additional code block language patterns
|
|
@@ -138,33 +141,67 @@ module Sourcerer
|
|
|
138
141
|
end
|
|
139
142
|
end
|
|
140
143
|
|
|
141
|
-
# Definition list converter:
|
|
142
|
-
class
|
|
144
|
+
# Definition list passthrough/converter: converts DL blocks to Markdown by default.
|
|
145
|
+
# DL blocks with .to-markdown class (or a parent div with that class) are converted to Markdown.
|
|
146
|
+
# Supports global conversion via convert_dls_to_markdown config or dls-to-markdown frontmatter key.
|
|
147
|
+
# Per-node .to-markdown and .no-markdown classes override the global mode.
|
|
148
|
+
# In AsciiDoc html5 output, [.to-markdown] on a dlist places the class on the outer <div>.
|
|
149
|
+
class DlPassthrough < ReverseMarkdown::Converters::Base
|
|
143
150
|
def convert node, state={}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
global_mode = Thread.current[:sourcerer_dl_conversion_mode] || false
|
|
152
|
+
|
|
153
|
+
has_to_markdown = check_class_on_node(node, 'to-markdown')
|
|
154
|
+
has_no_markdown = check_class_on_node(node, 'no-markdown')
|
|
155
|
+
|
|
156
|
+
# Also check parent <div> wrapper (html5 backend wraps dl in <div class="dlist ...">)
|
|
157
|
+
parent = node.parent
|
|
158
|
+
if parent && parent.name == 'div'
|
|
159
|
+
has_to_markdown ||= check_class_on_node(parent, 'to-markdown')
|
|
160
|
+
has_no_markdown ||= check_class_on_node(parent, 'no-markdown')
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
should_convert = if has_no_markdown
|
|
164
|
+
false
|
|
165
|
+
elsif has_to_markdown
|
|
166
|
+
true
|
|
167
|
+
else
|
|
168
|
+
global_mode
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if should_convert
|
|
172
|
+
body = node.children.map { |child| treat(child, state) }.join.strip
|
|
173
|
+
"#{body}\n"
|
|
174
|
+
else
|
|
175
|
+
"#{node.to_html}\n"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def check_class_on_node node, class_name
|
|
182
|
+
node['class'].to_s.split.include?(class_name)
|
|
149
183
|
end
|
|
150
184
|
end
|
|
151
185
|
|
|
152
|
-
# Definition term converter:
|
|
186
|
+
# Definition term converter: formats term as italicized with colon.
|
|
153
187
|
class DtConverter < ReverseMarkdown::Converters::Base
|
|
154
188
|
def convert node, state={}
|
|
155
|
-
|
|
156
|
-
"
|
|
189
|
+
term_text = treat_children(node, state).strip
|
|
190
|
+
"**#{term_text}:**\n"
|
|
157
191
|
end
|
|
158
192
|
end
|
|
159
193
|
|
|
160
|
-
# Definition description converter:
|
|
194
|
+
# Definition description converter: indents definition content by 3 spaces for Markdown.
|
|
195
|
+
# Block-level elements (lists, code blocks, etc.) within a definition are indented as-is,
|
|
196
|
+
# preserving their internal structure while still visually nesting under the term.
|
|
161
197
|
class DdConverter < ReverseMarkdown::Converters::Base
|
|
162
198
|
def convert node, state={}
|
|
163
|
-
content = treat_children(node, state)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
199
|
+
content = treat_children(node, state).strip
|
|
200
|
+
# Indent non-blank lines of the definition by 3 spaces (works for both inline and
|
|
201
|
+
# block content); blank lines are left empty so they stay collapsible as paragraph
|
|
202
|
+
# breaks instead of becoming whitespace-only lines that pile up under nesting.
|
|
203
|
+
indented = content.split("\n").map { |line| line.strip.empty? ? '' : " #{line}" }.join("\n")
|
|
204
|
+
"#{indented}\n\n"
|
|
168
205
|
end
|
|
169
206
|
end
|
|
170
207
|
|
|
@@ -426,6 +463,12 @@ module Sourcerer
|
|
|
426
463
|
# Tables with "to-markdown" class are converted via ReverseMarkdown instead.
|
|
427
464
|
# Supports both html5 (class on <table>) and html5s (class on parent <div class="table-block">).
|
|
428
465
|
# Per-table classes (.to-markdown, .no-markdown) override the global conversion mode.
|
|
466
|
+
#
|
|
467
|
+
# Horizontal dlists ([horizontal]) render as a classless <table> inside a
|
|
468
|
+
# <div class="hdlist">, not as a <dl>, so they're intercepted here rather than
|
|
469
|
+
# by DlPassthrough. They follow the DL conversion mode/overrides (not the table
|
|
470
|
+
# mode), converting each hdlist1/hdlist2 row to "**Term:** Description" instead
|
|
471
|
+
# of a Markdown table.
|
|
429
472
|
class TablePassthrough < ReverseMarkdown::Converters::Base
|
|
430
473
|
def initialize
|
|
431
474
|
super
|
|
@@ -433,6 +476,8 @@ module Sourcerer
|
|
|
433
476
|
end
|
|
434
477
|
|
|
435
478
|
def convert node, state={}
|
|
479
|
+
return convert_hdlist(node, state) if hdlist_table?(node)
|
|
480
|
+
|
|
436
481
|
global_mode = Thread.current[:sourcerer_table_conversion_mode] || false
|
|
437
482
|
|
|
438
483
|
# Check for per-table classes (on table or parent wrapper)
|
|
@@ -464,6 +509,46 @@ module Sourcerer
|
|
|
464
509
|
|
|
465
510
|
private
|
|
466
511
|
|
|
512
|
+
def hdlist_table? node
|
|
513
|
+
parent = node.parent
|
|
514
|
+
parent && parent.name == 'div' && check_class_on_node(parent, 'hdlist')
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def convert_hdlist node, state={}
|
|
518
|
+
global_mode = Thread.current[:sourcerer_dl_conversion_mode] || false
|
|
519
|
+
|
|
520
|
+
has_to_markdown = check_class_on_node(node, 'to-markdown')
|
|
521
|
+
has_no_markdown = check_class_on_node(node, 'no-markdown')
|
|
522
|
+
|
|
523
|
+
parent = node.parent
|
|
524
|
+
if parent && parent.name == 'div'
|
|
525
|
+
has_to_markdown ||= check_class_on_node(parent, 'to-markdown')
|
|
526
|
+
has_no_markdown ||= check_class_on_node(parent, 'no-markdown')
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
should_convert = if has_no_markdown
|
|
530
|
+
false
|
|
531
|
+
elsif has_to_markdown
|
|
532
|
+
true
|
|
533
|
+
else
|
|
534
|
+
global_mode
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
return "#{node.to_html}\n" unless should_convert
|
|
538
|
+
|
|
539
|
+
body = node.css('> tr').map { |row| convert_hdlist_row(row, state) }.join
|
|
540
|
+
"#{body}\n"
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def convert_hdlist_row row, state={}
|
|
544
|
+
# Match by column position, not the hdlist1/hdlist2 classes: normalize_html_for_markdown
|
|
545
|
+
# (clean_html5s_tables!) strips class attributes from td/tr before this converter runs.
|
|
546
|
+
term_node, desc_node = row.css('> td')
|
|
547
|
+
term = term_node ? treat_children(term_node, state).strip : ''
|
|
548
|
+
desc = desc_node ? treat_children(desc_node, state).strip : ''
|
|
549
|
+
"**#{term}:** #{desc}\n"
|
|
550
|
+
end
|
|
551
|
+
|
|
467
552
|
def check_class_on_node node, class_name
|
|
468
553
|
node['class'].to_s.split.include?(class_name)
|
|
469
554
|
end
|
|
@@ -509,7 +594,7 @@ module Sourcerer
|
|
|
509
594
|
is_checked = checkbox['checked'] || checkbox['data-item-complete'] == '1'
|
|
510
595
|
# Remove the checkbox from the DOM so it doesn't get rendered again
|
|
511
596
|
checkbox.remove
|
|
512
|
-
is_checked ? '<!--CHECKBOX_CHECKED--> ' : '<!--CHECKBOX_UNCHECKED--> '
|
|
597
|
+
is_checked ? '- <!--CHECKBOX_CHECKED--> ' : '- <!--CHECKBOX_UNCHECKED--> '
|
|
513
598
|
else
|
|
514
599
|
prefix_for(node)
|
|
515
600
|
end
|
|
@@ -526,9 +611,11 @@ module Sourcerer
|
|
|
526
611
|
result = "#{indentation}#{prefix}#{content}\n"
|
|
527
612
|
|
|
528
613
|
nested_lists.each do |nested_list|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
614
|
+
# Pass the same state; the Ul/Ol converter will handle ol_count incrementing.
|
|
615
|
+
# Use sub to strip only the leading newline emitted by the Ul/Ol converter,
|
|
616
|
+
# preserving per-line indentation built by indentation_from.
|
|
617
|
+
nested_md = treat(nested_list, state).sub(/\A\n+/, '').rstrip
|
|
618
|
+
result << "#{nested_md}\n" unless nested_md.strip.empty?
|
|
532
619
|
end
|
|
533
620
|
|
|
534
621
|
result
|
|
@@ -546,8 +633,11 @@ module Sourcerer
|
|
|
546
633
|
end
|
|
547
634
|
|
|
548
635
|
def indentation_from state
|
|
636
|
+
# Mirror ReverseMarkdown's built-in Li behaviour: ol_count is incremented by the
|
|
637
|
+
# Ul/Ol converter before reaching LI, so subtract 1 to get the nesting depth.
|
|
638
|
+
# ol_count 1 = top level (no indent), 2 = nested once (2 spaces), etc.
|
|
549
639
|
length = state.fetch(:ol_count, 0)
|
|
550
|
-
'
|
|
640
|
+
' ' * [length - 1, 0].max
|
|
551
641
|
end
|
|
552
642
|
end # class LiWithNestedLists
|
|
553
643
|
|
|
@@ -570,8 +660,10 @@ module Sourcerer
|
|
|
570
660
|
end
|
|
571
661
|
|
|
572
662
|
# Register all definition list converters.
|
|
663
|
+
# DlPassthrough handles opt-in conversion; DtConverter and DdConverter are
|
|
664
|
+
# called only when DlPassthrough decides to convert a given <dl>.
|
|
573
665
|
def self.register_dl_converters
|
|
574
|
-
ReverseMarkdown::Converters.register :dl,
|
|
666
|
+
ReverseMarkdown::Converters.register :dl, DlPassthrough.new
|
|
575
667
|
ReverseMarkdown::Converters.register :dt, DtConverter.new
|
|
576
668
|
ReverseMarkdown::Converters.register :dd, DdConverter.new
|
|
577
669
|
end
|
|
@@ -618,6 +710,11 @@ module Sourcerer
|
|
|
618
710
|
ReverseMarkdown::Converters.register :a, LinkConverter.new
|
|
619
711
|
end
|
|
620
712
|
|
|
713
|
+
# Register list item converter to handle nested lists and checkboxes.
|
|
714
|
+
def self.register_list_converters
|
|
715
|
+
ReverseMarkdown::Converters.register :li, LiWithNestedLists.new
|
|
716
|
+
end
|
|
717
|
+
|
|
621
718
|
# Normalize block titles so escaped inline emphasis from html5s is converted
|
|
622
719
|
# to markdown emphasis consistently with html5 conversions.
|
|
623
720
|
def self.normalize_block_title text
|
|
@@ -650,13 +747,16 @@ module Sourcerer
|
|
|
650
747
|
# Convert HTML into Markdown with MarkDownGrade converters.
|
|
651
748
|
# Options include:
|
|
652
749
|
# convert_tables_to_markdown: Override global config for table conversion (true/false)
|
|
750
|
+
# convert_dls_to_markdown: Override global config for DL conversion (true/false)
|
|
653
751
|
def self.convert_html html, options={}
|
|
654
752
|
bootstrap! unless @setup_complete
|
|
655
753
|
@setup_complete = true
|
|
656
754
|
|
|
657
|
-
# Determine effective table conversion
|
|
658
|
-
|
|
659
|
-
|
|
755
|
+
# Determine effective table and DL conversion modes
|
|
756
|
+
effective_table_mode = determine_table_conversion_mode(html.to_s, options)
|
|
757
|
+
effective_dl_mode = determine_dl_conversion_mode(html.to_s, options)
|
|
758
|
+
Thread.current[:sourcerer_table_conversion_mode] = effective_table_mode
|
|
759
|
+
Thread.current[:sourcerer_dl_conversion_mode] = effective_dl_mode
|
|
660
760
|
|
|
661
761
|
begin
|
|
662
762
|
normalized_html = normalize_html_for_markdown(html.to_s)
|
|
@@ -664,14 +764,33 @@ module Sourcerer
|
|
|
664
764
|
markdown = markdown.gsub(/(\*\*[^\n]+\*\* \n)\n+(?=\S)/, '\\1')
|
|
665
765
|
markdown = markdown.gsub(/<figcaption>\s+/, '<figcaption>')
|
|
666
766
|
markdown = markdown.gsub(%r{\s+</figcaption>}, '</figcaption>')
|
|
767
|
+
markdown = normalize_blank_lines(markdown)
|
|
667
768
|
|
|
668
|
-
|
|
669
|
-
|
|
769
|
+
# Replace checkbox markers: handle indented list items and maintain correct dash placement
|
|
770
|
+
replace_checkbox_markers(markdown)
|
|
670
771
|
ensure
|
|
671
772
|
Thread.current[:sourcerer_table_conversion_mode] = nil
|
|
773
|
+
Thread.current[:sourcerer_dl_conversion_mode] = nil
|
|
672
774
|
end
|
|
673
775
|
end
|
|
674
776
|
|
|
777
|
+
# Collapse whitespace-only lines to truly blank ones, then collapse runs of
|
|
778
|
+
# 3+ consecutive newlines down to a single blank line (one \n\n). Nested
|
|
779
|
+
# definition lists and example blocks otherwise stack indentation and blank
|
|
780
|
+
# lines from each conversion level, producing long runs of whitespace-only lines.
|
|
781
|
+
def self.normalize_blank_lines markdown
|
|
782
|
+
markdown.gsub(/^[ \t]+$/, '').gsub(/\n{3,}/, "\n\n")
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Replace checkbox placeholder markers with proper Markdown checkbox syntax.
|
|
786
|
+
# Handles various indentation levels and ensures correct list item formatting.
|
|
787
|
+
def self.replace_checkbox_markers markdown
|
|
788
|
+
# Replace checkbox markers that appear after list item dashes
|
|
789
|
+
# Pattern: optional indentation + dash + space + marker + space
|
|
790
|
+
markdown.gsub(/^(\s*- )<!--CHECKBOX_CHECKED-->\s/, '\1[x] ')
|
|
791
|
+
.gsub(/^(\s*- )<!--CHECKBOX_UNCHECKED-->\s/, '\1[ ] ')
|
|
792
|
+
end
|
|
793
|
+
|
|
675
794
|
def self.convert html, options={}
|
|
676
795
|
convert_html(html, options)
|
|
677
796
|
end
|
|
@@ -773,6 +892,38 @@ module Sourcerer
|
|
|
773
892
|
@config[:convert_tables_to_markdown]
|
|
774
893
|
end
|
|
775
894
|
|
|
895
|
+
# Determine the effective DL conversion mode from options, frontmatter, or global config.
|
|
896
|
+
#
|
|
897
|
+
# Precedence:
|
|
898
|
+
# 1. Explicit convert_dls_to_markdown option in parameters
|
|
899
|
+
# 2. Document-level setting from frontmatter key: dls-to-markdown
|
|
900
|
+
# 3. Global config setting
|
|
901
|
+
#
|
|
902
|
+
# @param html_body [String] The HTML document body.
|
|
903
|
+
# @param options [Hash] Optional override.
|
|
904
|
+
# @return [Boolean] Whether DLs should be converted to markdown by default.
|
|
905
|
+
def self.determine_dl_conversion_mode html_body, options
|
|
906
|
+
return options[:convert_dls_to_markdown] if options.key?(:convert_dls_to_markdown)
|
|
907
|
+
|
|
908
|
+
document_mode = extract_dl_conversion_mode_from_html(html_body)
|
|
909
|
+
return document_mode unless document_mode.nil?
|
|
910
|
+
|
|
911
|
+
@config[:convert_dls_to_markdown]
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
# Extract DL conversion mode from document frontmatter.
|
|
915
|
+
#
|
|
916
|
+
# Checks for YAML frontmatter key: dls-to-markdown
|
|
917
|
+
#
|
|
918
|
+
# @param html_body [String] The HTML document body.
|
|
919
|
+
# @return [Boolean, nil] The setting if found, nil otherwise.
|
|
920
|
+
def self.extract_dl_conversion_mode_from_html html_body
|
|
921
|
+
frontmatter = Sourcerer::YamlFrontmatter.extract(html_body)
|
|
922
|
+
return string_to_boolean(frontmatter['dls-to-markdown']) if frontmatter.key?('dls-to-markdown')
|
|
923
|
+
|
|
924
|
+
nil
|
|
925
|
+
end
|
|
926
|
+
|
|
776
927
|
# Extract table conversion mode from document frontmatter or page attributes.
|
|
777
928
|
#
|
|
778
929
|
# Checks for:
|
|
@@ -820,6 +971,8 @@ module Sourcerer
|
|
|
820
971
|
:clean_html5s_tables!,
|
|
821
972
|
:determine_table_conversion_mode,
|
|
822
973
|
:extract_table_conversion_mode_from_html,
|
|
974
|
+
:determine_dl_conversion_mode,
|
|
975
|
+
:extract_dl_conversion_mode_from_html,
|
|
823
976
|
:string_to_boolean
|
|
824
977
|
end # module MarkDownGrade
|
|
825
978
|
end # module Sourcerer
|
data/lib/sourcerer/rendering.rb
CHANGED
|
@@ -36,7 +36,10 @@ module Sourcerer
|
|
|
36
36
|
render_entry[:out],
|
|
37
37
|
data_object: data_obj,
|
|
38
38
|
attrs_source: attrs_source,
|
|
39
|
-
engine: engine
|
|
39
|
+
engine: engine,
|
|
40
|
+
vars: render_entry[:vars] || {},
|
|
41
|
+
preserve_missing: render_entry[:preserve_missing] || false,
|
|
42
|
+
preserve_empty: render_entry[:preserve_empty] || false)
|
|
40
43
|
end
|
|
41
44
|
end
|
|
42
45
|
|
|
@@ -49,37 +52,56 @@ module Sourcerer
|
|
|
49
52
|
# @param includes_load_paths [Array<String>] Paths for Liquid includes.
|
|
50
53
|
# @param attrs_source [String] The path to an AsciiDoc file for attributes.
|
|
51
54
|
# @param engine [String] The template engine to use.
|
|
55
|
+
# @param vars [Hash] Arbitrary caller-supplied variables, exposed to the
|
|
56
|
+
# template as `vars` (e.g. to parameterize a shared template between
|
|
57
|
+
# multiple render entries in a manifest).
|
|
58
|
+
# @param preserve_missing [Boolean] Liquid-only, independent of
|
|
59
|
+
# `preserve_empty`. When true, a `{{ ... }}` expression that evaluates
|
|
60
|
+
# to nil renders as its own original source text instead of an empty
|
|
61
|
+
# string, so an unfilled field stays visible for manual completion
|
|
62
|
+
# rather than silently disappearing. Has no effect on a defined
|
|
63
|
+
# empty value (`""`, `[]`, `{}`).
|
|
64
|
+
# @param preserve_empty [Boolean] Liquid-only, independent of
|
|
65
|
+
# `preserve_missing`. When true, a `{{ ... }}` expression that
|
|
66
|
+
# evaluates to a defined-but-empty String/Array/Hash (`""`, `[]`,
|
|
67
|
+
# `{}`) is preserved as its own original source text the same way.
|
|
68
|
+
# Has no effect on nil. `0` and `false` are never affected by either
|
|
69
|
+
# option.
|
|
52
70
|
def self.render_template template_file, data_file, out_file, **options
|
|
53
|
-
supported_option_keys = %i[data_object includes_load_paths attrs_source engine
|
|
71
|
+
supported_option_keys = %i[data_object includes_load_paths attrs_source engine vars
|
|
72
|
+
preserve_missing preserve_empty]
|
|
54
73
|
unknown_option_keys = options.keys - supported_option_keys
|
|
55
74
|
raise ArgumentError, "unknown option(s): #{unknown_option_keys.join(', ')}" unless unknown_option_keys.empty?
|
|
56
75
|
|
|
57
76
|
data_object = options.fetch(:data_object, 'data')
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
data = load_render_data(data_file, options[:attrs_source])
|
|
78
|
+
vars = (options[:vars] || {}).transform_keys(&:to_s)
|
|
79
|
+
context = { data_object => data, 'include' => { data_object => data }, 'vars' => vars }
|
|
80
|
+
|
|
81
|
+
preserve = { missing: options.fetch(:preserve_missing, false), empty: options.fetch(:preserve_empty, false) }
|
|
82
|
+
rendered = render_in_engine(
|
|
83
|
+
options.fetch(:engine, 'liquid'), template_file, context, options.fetch(:includes_load_paths, []), preserve)
|
|
61
84
|
|
|
62
|
-
data = load_render_data(data_file, attrs_source)
|
|
63
85
|
out_file = File.expand_path(out_file)
|
|
64
86
|
FileUtils.mkdir_p(File.dirname(out_file))
|
|
65
|
-
|
|
66
|
-
template_path = File.expand_path(template_file)
|
|
67
|
-
template_content = File.read(template_path)
|
|
68
|
-
|
|
69
|
-
context = {
|
|
70
|
-
data_object => data,
|
|
71
|
-
'include' => { data_object => data }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
rendered = case engine.to_s
|
|
75
|
-
when 'erb' then render_erb(template_content, context)
|
|
76
|
-
when 'liquid' then render_liquid(template_file, template_content, context, includes_load_paths)
|
|
77
|
-
else raise ArgumentError, "Unsupported template engine: #{engine}"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
87
|
File.write(out_file, rendered)
|
|
81
88
|
end
|
|
82
89
|
|
|
90
|
+
# @api private
|
|
91
|
+
# Dispatches rendering to the requested template engine.
|
|
92
|
+
#
|
|
93
|
+
# @param preserve [Hash] `{missing:, empty:}` -- see {.render_template}'s
|
|
94
|
+
# `preserve_missing`/`preserve_empty` options.
|
|
95
|
+
# @return [String]
|
|
96
|
+
def self.render_in_engine engine, template_file, context, includes_load_paths, preserve
|
|
97
|
+
template_content = File.read(File.expand_path(template_file))
|
|
98
|
+
case engine.to_s
|
|
99
|
+
when 'erb' then render_erb(template_content, context)
|
|
100
|
+
when 'liquid' then render_liquid(template_file, template_content, context, includes_load_paths, preserve)
|
|
101
|
+
else raise ArgumentError, "Unsupported template engine: #{engine}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
83
105
|
# Renders output using a converter callable or converter constant name.
|
|
84
106
|
#
|
|
85
107
|
# @param render_entry [Hash] Render entry containing converter config.
|
|
@@ -145,38 +167,43 @@ module Sourcerer
|
|
|
145
167
|
# @param template_content [String]
|
|
146
168
|
# @param context [Hash]
|
|
147
169
|
# @param includes_load_paths [Array<String>]
|
|
170
|
+
# @param preserve [Hash] `{missing:, empty:}` -- see {.render_template}'s
|
|
171
|
+
# `preserve_missing`/`preserve_empty` options.
|
|
148
172
|
# @return [String]
|
|
149
|
-
def self.render_liquid template_file, template_content, context, includes_load_paths
|
|
173
|
+
def self.render_liquid template_file, template_content, context, includes_load_paths, preserve
|
|
150
174
|
require_relative 'jekyll'
|
|
151
175
|
require_relative 'jekyll/liquid/filters'
|
|
152
176
|
require_relative 'jekyll/liquid/tags'
|
|
177
|
+
require_relative 'jekyll/liquid/preserve_missing_variables'
|
|
153
178
|
require 'liquid' unless defined?(Liquid::Template)
|
|
154
179
|
Sourcerer::Jekyll.initialize_liquid_runtime
|
|
155
180
|
|
|
181
|
+
registers = liquid_registers(template_file, includes_load_paths)
|
|
182
|
+
template = Liquid::Template.parse(template_content)
|
|
183
|
+
render = -> { template.render(context, registers: registers) }
|
|
184
|
+
|
|
185
|
+
return render.call unless preserve[:missing] || preserve[:empty]
|
|
186
|
+
|
|
187
|
+
Sourcerer::Jekyll::Liquid::PreserveMissingVariables.with_active(
|
|
188
|
+
preserve_missing: preserve[:missing], preserve_empty: preserve[:empty], &render)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @api private
|
|
192
|
+
# Builds the Liquid registers hash (fake site + file system) a template
|
|
193
|
+
# render needs to resolve `{% embed %}`/include partials.
|
|
194
|
+
#
|
|
195
|
+
# @return [Hash]
|
|
196
|
+
def self.liquid_registers template_file, includes_load_paths
|
|
156
197
|
fallback_templates_dir = File.expand_path('.', Dir.pwd)
|
|
157
198
|
template_dir = File.dirname(File.expand_path(template_file))
|
|
158
199
|
template_parent_dir = File.dirname(template_dir)
|
|
159
200
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
[template_parent_dir, template_dir, fallback_templates_dir]
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
site = Sourcerer::Jekyll::Bootstrapper.fake_site(
|
|
167
|
-
includes_load_paths: paths,
|
|
168
|
-
plugin_dirs: [])
|
|
169
|
-
|
|
201
|
+
default_paths = [template_parent_dir, template_dir, fallback_templates_dir]
|
|
202
|
+
paths = includes_load_paths.any? ? includes_load_paths : default_paths
|
|
203
|
+
site = Sourcerer::Jekyll::Bootstrapper.fake_site(includes_load_paths: paths, plugin_dirs: [])
|
|
170
204
|
file_system = Sourcerer::Jekyll::Liquid::FileSystem.new(paths)
|
|
171
205
|
|
|
172
|
-
|
|
173
|
-
options = {
|
|
174
|
-
registers: {
|
|
175
|
-
site: site,
|
|
176
|
-
file_system: file_system
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
template.render(context, options)
|
|
206
|
+
{ site: site, file_system: file_system, includes_load_paths: paths }
|
|
180
207
|
end
|
|
181
208
|
|
|
182
209
|
# Render a Liquid template string directly with a data hash.
|
|
@@ -210,6 +237,8 @@ module Sourcerer
|
|
|
210
237
|
private_class_method :load_render_data,
|
|
211
238
|
:resolve_converter,
|
|
212
239
|
:render_erb,
|
|
213
|
-
:render_liquid
|
|
240
|
+
:render_liquid,
|
|
241
|
+
:render_in_engine,
|
|
242
|
+
:liquid_registers
|
|
214
243
|
end
|
|
215
244
|
end
|
|
@@ -16,6 +16,7 @@ module Sourcerer
|
|
|
16
16
|
admonitions
|
|
17
17
|
quotes
|
|
18
18
|
images
|
|
19
|
+
includes
|
|
19
20
|
].freeze
|
|
20
21
|
|
|
21
22
|
# Categories included when a caller passes +categories: nil+ (the default).
|
|
@@ -30,11 +31,12 @@ module Sourcerer
|
|
|
30
31
|
# arguments on {Sourcerer::SourceSkim.skim_file} and friends.
|
|
31
32
|
# @api private
|
|
32
33
|
class Config
|
|
33
|
-
attr_reader :forms, :categories
|
|
34
|
+
attr_reader :forms, :categories, :descriptions
|
|
34
35
|
|
|
35
|
-
def initialize forms: [:tree], categories: nil
|
|
36
|
+
def initialize forms: [:tree], categories: nil, descriptions: false
|
|
36
37
|
@forms = Array(forms).map(&:to_sym)
|
|
37
38
|
@categories = categories ? Array(categories).map(&:to_sym) : DEFAULT_CATEGORIES.dup
|
|
39
|
+
@descriptions = descriptions
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
def include? category
|
|
@@ -48,6 +50,10 @@ module Sourcerer
|
|
|
48
50
|
def flat?
|
|
49
51
|
@forms.include?(:flat)
|
|
50
52
|
end
|
|
53
|
+
|
|
54
|
+
def descriptions?
|
|
55
|
+
@descriptions
|
|
56
|
+
end
|
|
51
57
|
end
|
|
52
58
|
end
|
|
53
59
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yard'
|
|
4
|
+
|
|
5
|
+
module Sourcerer
|
|
6
|
+
module SourceSkim
|
|
7
|
+
# Parses Ruby source code and produces a JSON-ready skim hash.
|
|
8
|
+
#
|
|
9
|
+
# A new instance should be created per-document call. External callers should
|
|
10
|
+
# use {Sourcerer::SourceSkim.skim_file} or {Sourcerer::SourceSkim.skim_string}
|
|
11
|
+
# with a Ruby file or +format: :ruby+ rather than instantiating this class
|
|
12
|
+
# directly.
|
|
13
|
+
# @api private
|
|
14
|
+
class RubySkimmer
|
|
15
|
+
# @param content [String] raw Ruby source code
|
|
16
|
+
# @param config [Config]
|
|
17
|
+
# @return [Hash] JSON-ready skim
|
|
18
|
+
def process content, config: Config.new(forms: [:flat], descriptions: true)
|
|
19
|
+
require 'tempfile'
|
|
20
|
+
|
|
21
|
+
@config = config
|
|
22
|
+
|
|
23
|
+
# Set logging level to ERROR to suppress YARD warnings about missing files or other issues. This ensures that the skimming process is not interrupted by non-critical warnings.
|
|
24
|
+
YARD::Logger.instance.level = 3
|
|
25
|
+
|
|
26
|
+
# YARD's registry is a process-wide global. Without clearing it first,
|
|
27
|
+
# a class/module/method path already registered from a previous
|
|
28
|
+
# #process call keeps pointing at that call's (now-deleted) tempfile,
|
|
29
|
+
# so it would be silently excluded from every subsequent skim.
|
|
30
|
+
YARD::Registry.clear
|
|
31
|
+
|
|
32
|
+
# YARD's parser is designed for documentation generation, so it expects
|
|
33
|
+
# a file path to determine the source type.
|
|
34
|
+
Tempfile.create(['sourcerer', '.rb']) do |tempfile|
|
|
35
|
+
tempfile.write(content)
|
|
36
|
+
tempfile.flush
|
|
37
|
+
|
|
38
|
+
# Parse ONLY the modules, methods, and classes defined in the current file.
|
|
39
|
+
YARD::Parser::SourceParser.parse(tempfile.path)
|
|
40
|
+
objects = YARD::Registry.all(:class, :module, :method).select { |obj| obj.file == tempfile.path }
|
|
41
|
+
|
|
42
|
+
result = {}
|
|
43
|
+
result[:classes] = build_classes(objects.select { |obj| obj.type == :class })
|
|
44
|
+
result[:modules] = build_modules(objects.select { |obj| obj.type == :module })
|
|
45
|
+
result[:methods] = build_methods(objects.select { |obj| obj.type == :method })
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def build_classes classes
|
|
53
|
+
classes.map do |cls|
|
|
54
|
+
{
|
|
55
|
+
name: cls.path,
|
|
56
|
+
line: cls.line
|
|
57
|
+
}.merge(
|
|
58
|
+
@config.descriptions? ? { desc: cls.docstring.to_s } : {})
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_modules modules
|
|
63
|
+
modules.map do |mod|
|
|
64
|
+
{
|
|
65
|
+
name: mod.path,
|
|
66
|
+
line: mod.line
|
|
67
|
+
}.merge(
|
|
68
|
+
@config.descriptions? ? { desc: mod.docstring.to_s } : {})
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_methods methods
|
|
73
|
+
methods.map do |meth|
|
|
74
|
+
{
|
|
75
|
+
name: meth.path,
|
|
76
|
+
line: meth.line
|
|
77
|
+
}.merge(
|
|
78
|
+
@config.descriptions? ? { desc: meth.docstring.to_s } : {})
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|