asciisourcerer 0.3.1 → 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 +107 -26
- 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 +52 -10
- data/lib/sourcerer/builder.rb +1 -1
- data/lib/sourcerer/jekyll/bootstrapper.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 +321 -33
- data/lib/sourcerer/rendering.rb +10 -4
- 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 +22 -3
- data/specs/docs/frontmatter-reader_prd.adoc +0 -47
|
@@ -21,7 +21,9 @@ module Sourcerer
|
|
|
21
21
|
|
|
22
22
|
@config = {
|
|
23
23
|
preserve_heading_ids: true,
|
|
24
|
-
strip_internal_links: false
|
|
24
|
+
strip_internal_links: false,
|
|
25
|
+
convert_tables_to_markdown: false,
|
|
26
|
+
convert_dls_to_markdown: true
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
class << self
|
|
@@ -32,6 +34,8 @@ module Sourcerer
|
|
|
32
34
|
# Options:
|
|
33
35
|
# preserve_heading_ids: (default: true) Include <a id="..."> anchors before headings
|
|
34
36
|
# strip_internal_links: (default: false) Remove href from internal anchor links, keeping only text
|
|
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
|
|
35
39
|
def self.bootstrap! options={}
|
|
36
40
|
@config.merge!(options)
|
|
37
41
|
|
|
@@ -45,6 +49,7 @@ module Sourcerer
|
|
|
45
49
|
register_blockquote_converter
|
|
46
50
|
register_comment_converter
|
|
47
51
|
register_link_converter
|
|
52
|
+
register_list_converters
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
# Enhanced Pre converter to handle additional code block language patterns
|
|
@@ -136,33 +141,67 @@ module Sourcerer
|
|
|
136
141
|
end
|
|
137
142
|
end
|
|
138
143
|
|
|
139
|
-
# Definition list converter:
|
|
140
|
-
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
|
|
141
150
|
def convert node, state={}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
|
147
183
|
end
|
|
148
184
|
end
|
|
149
185
|
|
|
150
|
-
# Definition term converter:
|
|
186
|
+
# Definition term converter: formats term as italicized with colon.
|
|
151
187
|
class DtConverter < ReverseMarkdown::Converters::Base
|
|
152
188
|
def convert node, state={}
|
|
153
|
-
|
|
154
|
-
"
|
|
189
|
+
term_text = treat_children(node, state).strip
|
|
190
|
+
"**#{term_text}:**\n"
|
|
155
191
|
end
|
|
156
192
|
end
|
|
157
193
|
|
|
158
|
-
# 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.
|
|
159
197
|
class DdConverter < ReverseMarkdown::Converters::Base
|
|
160
198
|
def convert node, state={}
|
|
161
|
-
content = treat_children(node, state)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
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"
|
|
166
205
|
end
|
|
167
206
|
end
|
|
168
207
|
|
|
@@ -421,9 +460,97 @@ module Sourcerer
|
|
|
421
460
|
end
|
|
422
461
|
|
|
423
462
|
# Passthrough Tables: preserve HTML tables as-is (except admonition internals handled elsewhere).
|
|
463
|
+
# Tables with "to-markdown" class are converted via ReverseMarkdown instead.
|
|
464
|
+
# Supports both html5 (class on <table>) and html5s (class on parent <div class="table-block">).
|
|
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.
|
|
424
472
|
class TablePassthrough < ReverseMarkdown::Converters::Base
|
|
425
|
-
def
|
|
426
|
-
|
|
473
|
+
def initialize
|
|
474
|
+
super
|
|
475
|
+
@markdown_converter = ReverseMarkdown::Converters::Table.new
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def convert node, state={}
|
|
479
|
+
return convert_hdlist(node, state) if hdlist_table?(node)
|
|
480
|
+
|
|
481
|
+
global_mode = Thread.current[:sourcerer_table_conversion_mode] || false
|
|
482
|
+
|
|
483
|
+
# Check for per-table classes (on table or parent wrapper)
|
|
484
|
+
has_to_markdown = check_class_on_node(node, 'to-markdown')
|
|
485
|
+
has_no_markdown = check_class_on_node(node, 'no-markdown')
|
|
486
|
+
|
|
487
|
+
# Also check parent <div class="table-block"> wrapper (html5s backend)
|
|
488
|
+
parent_div = node.parent
|
|
489
|
+
if parent_div && parent_div.name == 'div'
|
|
490
|
+
has_to_markdown ||= check_class_on_node(parent_div, 'to-markdown')
|
|
491
|
+
has_no_markdown ||= check_class_on_node(parent_div, 'no-markdown')
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Determine whether to convert (per-table classes override global mode)
|
|
495
|
+
should_convert = if has_no_markdown
|
|
496
|
+
false # .no-markdown always prevents conversion
|
|
497
|
+
elsif has_to_markdown
|
|
498
|
+
true # .to-markdown always forces conversion
|
|
499
|
+
else
|
|
500
|
+
global_mode # Use global table conversion mode
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
if should_convert
|
|
504
|
+
@markdown_converter.convert(node, state)
|
|
505
|
+
else
|
|
506
|
+
"#{node.to_html}\n"
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
private
|
|
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
|
+
|
|
552
|
+
def check_class_on_node node, class_name
|
|
553
|
+
node['class'].to_s.split.include?(class_name)
|
|
427
554
|
end
|
|
428
555
|
end
|
|
429
556
|
|
|
@@ -467,7 +594,7 @@ module Sourcerer
|
|
|
467
594
|
is_checked = checkbox['checked'] || checkbox['data-item-complete'] == '1'
|
|
468
595
|
# Remove the checkbox from the DOM so it doesn't get rendered again
|
|
469
596
|
checkbox.remove
|
|
470
|
-
is_checked ? '<!--CHECKBOX_CHECKED--> ' : '<!--CHECKBOX_UNCHECKED--> '
|
|
597
|
+
is_checked ? '- <!--CHECKBOX_CHECKED--> ' : '- <!--CHECKBOX_UNCHECKED--> '
|
|
471
598
|
else
|
|
472
599
|
prefix_for(node)
|
|
473
600
|
end
|
|
@@ -484,9 +611,11 @@ module Sourcerer
|
|
|
484
611
|
result = "#{indentation}#{prefix}#{content}\n"
|
|
485
612
|
|
|
486
613
|
nested_lists.each do |nested_list|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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?
|
|
490
619
|
end
|
|
491
620
|
|
|
492
621
|
result
|
|
@@ -504,8 +633,11 @@ module Sourcerer
|
|
|
504
633
|
end
|
|
505
634
|
|
|
506
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.
|
|
507
639
|
length = state.fetch(:ol_count, 0)
|
|
508
|
-
'
|
|
640
|
+
' ' * [length - 1, 0].max
|
|
509
641
|
end
|
|
510
642
|
end # class LiWithNestedLists
|
|
511
643
|
|
|
@@ -528,8 +660,10 @@ module Sourcerer
|
|
|
528
660
|
end
|
|
529
661
|
|
|
530
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>.
|
|
531
665
|
def self.register_dl_converters
|
|
532
|
-
ReverseMarkdown::Converters.register :dl,
|
|
666
|
+
ReverseMarkdown::Converters.register :dl, DlPassthrough.new
|
|
533
667
|
ReverseMarkdown::Converters.register :dt, DtConverter.new
|
|
534
668
|
ReverseMarkdown::Converters.register :dd, DdConverter.new
|
|
535
669
|
end
|
|
@@ -576,6 +710,11 @@ module Sourcerer
|
|
|
576
710
|
ReverseMarkdown::Converters.register :a, LinkConverter.new
|
|
577
711
|
end
|
|
578
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
|
+
|
|
579
718
|
# Normalize block titles so escaped inline emphasis from html5s is converted
|
|
580
719
|
# to markdown emphasis consistently with html5 conversions.
|
|
581
720
|
def self.normalize_block_title text
|
|
@@ -606,18 +745,50 @@ module Sourcerer
|
|
|
606
745
|
end
|
|
607
746
|
|
|
608
747
|
# Convert HTML into Markdown with MarkDownGrade converters.
|
|
748
|
+
# Options include:
|
|
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)
|
|
609
751
|
def self.convert_html html, options={}
|
|
610
752
|
bootstrap! unless @setup_complete
|
|
611
753
|
@setup_complete = true
|
|
612
754
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
760
|
+
|
|
761
|
+
begin
|
|
762
|
+
normalized_html = normalize_html_for_markdown(html.to_s)
|
|
763
|
+
markdown = ReverseMarkdown.convert(normalized_html, options)
|
|
764
|
+
markdown = markdown.gsub(/(\*\*[^\n]+\*\* \n)\n+(?=\S)/, '\\1')
|
|
765
|
+
markdown = markdown.gsub(/<figcaption>\s+/, '<figcaption>')
|
|
766
|
+
markdown = markdown.gsub(%r{\s+</figcaption>}, '</figcaption>')
|
|
767
|
+
markdown = normalize_blank_lines(markdown)
|
|
768
|
+
|
|
769
|
+
# Replace checkbox markers: handle indented list items and maintain correct dash placement
|
|
770
|
+
replace_checkbox_markers(markdown)
|
|
771
|
+
ensure
|
|
772
|
+
Thread.current[:sourcerer_table_conversion_mode] = nil
|
|
773
|
+
Thread.current[:sourcerer_dl_conversion_mode] = nil
|
|
774
|
+
end
|
|
775
|
+
end
|
|
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
|
|
618
784
|
|
|
619
|
-
|
|
620
|
-
|
|
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[ ] ')
|
|
621
792
|
end
|
|
622
793
|
|
|
623
794
|
def self.convert html, options={}
|
|
@@ -629,6 +800,7 @@ module Sourcerer
|
|
|
629
800
|
fragment = Nokogiri::HTML::DocumentFragment.parse(html_body)
|
|
630
801
|
normalize_abstract_nodes!(fragment)
|
|
631
802
|
normalize_footnote_nodes!(fragment)
|
|
803
|
+
clean_html5s_tables!(fragment)
|
|
632
804
|
fragment.to_html
|
|
633
805
|
end
|
|
634
806
|
|
|
@@ -682,10 +854,126 @@ module Sourcerer
|
|
|
682
854
|
raw_id.empty? ? nil : raw_id
|
|
683
855
|
end
|
|
684
856
|
|
|
857
|
+
# Clean HTML5s table artifacts: remove colgroup elements and class/style attributes from cells.
|
|
858
|
+
# This normalizes tables generated by the asciidoctor-html5s backend for safer Markdown conversion.
|
|
859
|
+
#
|
|
860
|
+
# @param fragment [Nokogiri::HTML::DocumentFragment] The HTML fragment to clean.
|
|
861
|
+
# @return [void] Modifies fragment in place.
|
|
862
|
+
def self.clean_html5s_tables! fragment
|
|
863
|
+
# Remove all colgroup elements
|
|
864
|
+
fragment.css('colgroup').each(&:remove)
|
|
865
|
+
|
|
866
|
+
# Remove class and style attributes from table cells and rows
|
|
867
|
+
fragment.css('td, th, tr').each do |cell|
|
|
868
|
+
cell.delete('class')
|
|
869
|
+
cell.delete('style')
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# Determine the effective table conversion mode from options, frontmatter, or global config.
|
|
874
|
+
#
|
|
875
|
+
# Precedence:
|
|
876
|
+
# 1. Explicit convert_tables_to_markdown option in parameters
|
|
877
|
+
# 2. Document-level setting from frontmatter (YAML) or page attributes (AsciiDoc)
|
|
878
|
+
# 3. Global config setting
|
|
879
|
+
#
|
|
880
|
+
# @param html_body [String] The HTML document body.
|
|
881
|
+
# @param options [Hash] Optional override.
|
|
882
|
+
# @return [Boolean] Whether tables should be converted to markdown by default.
|
|
883
|
+
def self.determine_table_conversion_mode html_body, options
|
|
884
|
+
# Explicit option takes precedence
|
|
885
|
+
return options[:convert_tables_to_markdown] if options.key?(:convert_tables_to_markdown)
|
|
886
|
+
|
|
887
|
+
# Extract from document metadata
|
|
888
|
+
document_mode = extract_table_conversion_mode_from_html(html_body)
|
|
889
|
+
return document_mode unless document_mode.nil?
|
|
890
|
+
|
|
891
|
+
# Fall back to global config
|
|
892
|
+
@config[:convert_tables_to_markdown]
|
|
893
|
+
end
|
|
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
|
+
|
|
927
|
+
# Extract table conversion mode from document frontmatter or page attributes.
|
|
928
|
+
#
|
|
929
|
+
# Checks for:
|
|
930
|
+
# - YAML frontmatter key: tables-to-markdown
|
|
931
|
+
# - AsciiDoc page attribute: page-tables-to-markdown
|
|
932
|
+
#
|
|
933
|
+
# @param html_body [String] The HTML document body.
|
|
934
|
+
# @return [Boolean, nil] The setting if found, nil otherwise.
|
|
935
|
+
def self.extract_table_conversion_mode_from_html html_body
|
|
936
|
+
# Try to extract YAML frontmatter
|
|
937
|
+
frontmatter = Sourcerer::YamlFrontmatter.extract(html_body)
|
|
938
|
+
return string_to_boolean(frontmatter['tables-to-markdown']) if frontmatter.key?('tables-to-markdown')
|
|
939
|
+
|
|
940
|
+
# Try to find page-tables-to-markdown in HTML comments or metadata
|
|
941
|
+
# (This would be set by AsciiDoc's page attributes)
|
|
942
|
+
if html_body.include?('page-tables-to-markdown')
|
|
943
|
+
# Look for data attributes or comments that might encode this
|
|
944
|
+
if html_body.match?(/page-tables-to-markdown['"]?\s*[:=]\s*['"]*true/i)
|
|
945
|
+
return true
|
|
946
|
+
elsif html_body.match?(/page-tables-to-markdown['"]?\s*[:=]\s*['"]*false/i)
|
|
947
|
+
return false
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
nil
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
# Convert a string representation to a boolean value.
|
|
955
|
+
#
|
|
956
|
+
# @param value [String, Boolean, nil] The value to convert.
|
|
957
|
+
# @return [Boolean, nil] Boolean if value is truthy string, nil if unclear.
|
|
958
|
+
def self.string_to_boolean value
|
|
959
|
+
case value.to_s.downcase.strip
|
|
960
|
+
when 'true', '1', 'yes', 'on'
|
|
961
|
+
true
|
|
962
|
+
when 'false', '0', 'no', 'off', ''
|
|
963
|
+
false
|
|
964
|
+
end
|
|
965
|
+
end
|
|
966
|
+
|
|
685
967
|
private_class_method :normalize_html_for_markdown,
|
|
686
968
|
:normalize_abstract_nodes!,
|
|
687
969
|
:normalize_footnote_nodes!,
|
|
688
|
-
:canonical_footnote_anchor_id
|
|
970
|
+
:canonical_footnote_anchor_id,
|
|
971
|
+
:clean_html5s_tables!,
|
|
972
|
+
:determine_table_conversion_mode,
|
|
973
|
+
:extract_table_conversion_mode_from_html,
|
|
974
|
+
:determine_dl_conversion_mode,
|
|
975
|
+
:extract_dl_conversion_mode_from_html,
|
|
976
|
+
:string_to_boolean
|
|
689
977
|
end # module MarkDownGrade
|
|
690
978
|
end # module Sourcerer
|
|
691
979
|
|
data/lib/sourcerer/rendering.rb
CHANGED
|
@@ -36,7 +36,8 @@ 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] || {})
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -49,8 +50,11 @@ module Sourcerer
|
|
|
49
50
|
# @param includes_load_paths [Array<String>] Paths for Liquid includes.
|
|
50
51
|
# @param attrs_source [String] The path to an AsciiDoc file for attributes.
|
|
51
52
|
# @param engine [String] The template engine to use.
|
|
53
|
+
# @param vars [Hash] Arbitrary caller-supplied variables, exposed to the
|
|
54
|
+
# template as `vars` (e.g. to parameterize a shared template between
|
|
55
|
+
# multiple render entries in a manifest).
|
|
52
56
|
def self.render_template template_file, data_file, out_file, **options
|
|
53
|
-
supported_option_keys = %i[data_object includes_load_paths attrs_source engine]
|
|
57
|
+
supported_option_keys = %i[data_object includes_load_paths attrs_source engine vars]
|
|
54
58
|
unknown_option_keys = options.keys - supported_option_keys
|
|
55
59
|
raise ArgumentError, "unknown option(s): #{unknown_option_keys.join(', ')}" unless unknown_option_keys.empty?
|
|
56
60
|
|
|
@@ -58,6 +62,7 @@ module Sourcerer
|
|
|
58
62
|
includes_load_paths = options.fetch(:includes_load_paths, [])
|
|
59
63
|
attrs_source = options[:attrs_source]
|
|
60
64
|
engine = options.fetch(:engine, 'liquid')
|
|
65
|
+
vars = (options[:vars] || {}).transform_keys(&:to_s)
|
|
61
66
|
|
|
62
67
|
data = load_render_data(data_file, attrs_source)
|
|
63
68
|
out_file = File.expand_path(out_file)
|
|
@@ -68,7 +73,8 @@ module Sourcerer
|
|
|
68
73
|
|
|
69
74
|
context = {
|
|
70
75
|
data_object => data,
|
|
71
|
-
'include' => { data_object => data }
|
|
76
|
+
'include' => { data_object => data },
|
|
77
|
+
'vars' => vars
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
rendered = case engine.to_s
|
|
@@ -86,7 +92,7 @@ module Sourcerer
|
|
|
86
92
|
# @return [void]
|
|
87
93
|
def self.render_with_converter render_entry
|
|
88
94
|
data_file = render_entry[:data]
|
|
89
|
-
out_file
|
|
95
|
+
out_file = render_entry[:out]
|
|
90
96
|
raise ArgumentError, 'render entry missing :data' unless data_file
|
|
91
97
|
raise ArgumentError, 'render entry missing :out' unless out_file
|
|
92
98
|
|
|
@@ -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
|