slamdown 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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/changelog.md +7 -0
  3. data/lib/slamdown/conversion/configuration.rb +35 -0
  4. data/lib/slamdown/conversion/configuration_validator.rb +235 -0
  5. data/lib/slamdown/conversion/converter.rb +98 -0
  6. data/lib/slamdown/conversion/normalisation.rb +21 -0
  7. data/lib/slamdown/conversion/normalised_node.rb +40 -0
  8. data/lib/slamdown/conversion/normaliser/breaks.rb +104 -0
  9. data/lib/slamdown/conversion/normaliser/content.rb +174 -0
  10. data/lib/slamdown/conversion/normaliser/correction_diagnostics.rb +29 -0
  11. data/lib/slamdown/conversion/normaliser/element_actions.rb +423 -0
  12. data/lib/slamdown/conversion/normaliser/heading_levels.rb +45 -0
  13. data/lib/slamdown/conversion/normaliser/inline_semantics.rb +218 -0
  14. data/lib/slamdown/conversion/normaliser/node_factory.rb +112 -0
  15. data/lib/slamdown/conversion/normaliser/url_rewriter.rb +124 -0
  16. data/lib/slamdown/conversion/normaliser.rb +58 -0
  17. data/lib/slamdown/conversion/policy.rb +178 -0
  18. data/lib/slamdown/conversion/prepared_node.rb +23 -0
  19. data/lib/slamdown/conversion/preparer.rb +122 -0
  20. data/lib/slamdown/conversion/renderer.rb +432 -0
  21. data/lib/slamdown/conversion/rendering/entity_encoder.rb +56 -0
  22. data/lib/slamdown/conversion/rendering/escaper.rb +58 -0
  23. data/lib/slamdown/conversion/rendering/html.rb +183 -0
  24. data/lib/slamdown/conversion/rendering/result.rb +23 -0
  25. data/lib/slamdown/conversion/rendering/strategy.rb +157 -0
  26. data/lib/slamdown/conversion/semantic_extractor.rb +97 -0
  27. data/lib/slamdown/conversion/tables.rb +676 -0
  28. data/lib/slamdown/conversion.rb +34 -0
  29. data/lib/slamdown/diagnostic.rb +34 -0
  30. data/lib/slamdown/document.rb +107 -0
  31. data/lib/slamdown/errors.rb +25 -0
  32. data/lib/slamdown/immutable.rb +23 -0
  33. data/lib/slamdown/output.rb +30 -0
  34. data/lib/slamdown/parsing/html.rb +227 -0
  35. data/lib/slamdown/parsing/node.rb +50 -0
  36. data/lib/slamdown/parsing/parsed_node_adapter.rb +144 -0
  37. data/lib/slamdown/parsing.rb +17 -0
  38. data/lib/slamdown/version.rb +3 -0
  39. data/lib/slamdown.rb +10 -0
  40. data/readme.md +121 -0
  41. metadata +174 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slamdown
4
+
5
+ # Describes one construction or conversion observation without exposing mutable internal state.
6
+ class Diagnostic
7
+ SEVERITIES = [:info, :warning, :error].freeze
8
+
9
+ attr_reader :code, :severity, :message, :node_path
10
+
11
+ def initialize(code:, severity:, message:, node_path: nil)
12
+ raise ArgumentError, "diagnostic code must be a Symbol" unless code.is_a?(Symbol)
13
+ raise ArgumentError, "invalid diagnostic severity: #{severity.inspect}" unless SEVERITIES.include?(severity)
14
+ raise ArgumentError, "diagnostic message must be a String" unless message.is_a?(String)
15
+ raise ArgumentError, "diagnostic node_path must contain only non-negative integers" unless valid_node_path?(node_path)
16
+
17
+ @code = code
18
+ @severity = severity
19
+ @message = message.dup.freeze
20
+ @node_path = node_path && node_path.dup.freeze
21
+ freeze
22
+ end
23
+
24
+ private
25
+
26
+ def valid_node_path?(node_path)
27
+ return true if node_path.nil?
28
+ return false unless node_path.is_a?(Array)
29
+
30
+ node_path.all? { |index| index.is_a?(Integer) && index >= 0 }
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slamdown
4
+
5
+ # Owns Slamdown's immutable, format-neutral representation of one HTML input.
6
+ class Document
7
+ ERROR_MODES = [:raise, :warn, :silent].freeze
8
+
9
+ attr_reader :tree, :diagnostics
10
+
11
+ def initialize(input, errors: :warn)
12
+ validate_error_mode(errors)
13
+ construct(input, errors)
14
+ end
15
+
16
+ class << self
17
+ # Converts a document or block-level parsed subtree through the selected output pipeline.
18
+ def convert(input, format:, options: {}, errors: :warn)
19
+ Conversion::Converter.convert(input, :format => format, :options => options, :errors => errors)
20
+ end
21
+ end
22
+
23
+ # Returns only the rendered Markdown string for the common convenience path.
24
+ def to_markdown(options = {}, controls = {})
25
+ convert_convenience(:markdown, options, controls)
26
+ end
27
+
28
+ # Returns only the rendered Kramdown string for the common convenience path.
29
+ def to_kramdown(options = {}, controls = {})
30
+ convert_convenience(:kramdown, options, controls)
31
+ end
32
+
33
+ private
34
+
35
+ def convert_convenience(format, options, controls)
36
+ if controls.empty? && options.is_a?(Hash) && options.keys == [:errors]
37
+ controls = options
38
+ options = {}
39
+ end
40
+
41
+ self.class.convert(self, :format => format, :options => options, :errors => conversion_errors(controls)).output
42
+ end
43
+
44
+ def conversion_errors(controls)
45
+ unless controls.is_a?(Hash) && (controls.keys - [:errors]).empty?
46
+ raise ArgumentError, "conversion controls accept only errors"
47
+ end
48
+
49
+ controls.fetch(:errors, :warn)
50
+ end
51
+
52
+ def construct(input, errors)
53
+ @tree, parser_warnings = Parsing.parse(input.to_s)
54
+ raise "the parser did not return a usable document tree" unless usable_tree?(@tree)
55
+
56
+ @diagnostics = parser_warnings.map { |message| parser_warning(message) }.freeze
57
+ freeze
58
+ rescue StandardError => error
59
+ handle_parse_error(error, errors)
60
+ end
61
+
62
+ def validate_error_mode(errors)
63
+ return if ERROR_MODES.include?(errors)
64
+
65
+ raise ArgumentError, "errors must be :raise, :warn or :silent"
66
+ end
67
+
68
+ def usable_tree?(tree)
69
+ tree.is_a?(Parsing::Node) && tree.kind == :document
70
+ end
71
+
72
+ def parser_warning(message)
73
+ Diagnostic.new(
74
+ :code => :parser_warning,
75
+ :severity => :warning,
76
+ :message => message.to_s,
77
+ :node_path => nil
78
+ )
79
+ end
80
+
81
+ def handle_parse_error(error, errors)
82
+ diagnostic = Diagnostic.new(
83
+ :code => :parse_error,
84
+ :severity => :error,
85
+ :message => "Unable to parse HTML: #{error.message}",
86
+ :node_path => nil
87
+ )
88
+ parse_error = ParseError.new(diagnostic)
89
+
90
+ raise parse_error if errors == :raise
91
+ Kernel.warn(parse_error.message) if errors == :warn
92
+
93
+ @tree = empty_tree
94
+ @diagnostics = [diagnostic].freeze
95
+ freeze
96
+ end
97
+
98
+ def empty_tree
99
+ Parsing::Node.new(
100
+ :kind => :document,
101
+ :children => [],
102
+ :properties => {:encoding => "UTF-8"}
103
+ )
104
+ end
105
+ end
106
+
107
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slamdown
4
+
5
+ # Reports a construction failure together with the diagnostic exposed by non-raising error modes.
6
+ class ParseError < StandardError
7
+ attr_reader :diagnostic
8
+
9
+ def initialize(diagnostic)
10
+ @diagnostic = diagnostic
11
+ super(diagnostic.message)
12
+ end
13
+ end
14
+
15
+ # Reports a conversion failure. Conversion stages populate this error as they are implemented.
16
+ class ConversionError < StandardError
17
+ attr_reader :diagnostic
18
+
19
+ def initialize(diagnostic)
20
+ @diagnostic = diagnostic
21
+ super(diagnostic.message)
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slamdown
4
+
5
+ # Copies nested public values into an immutable representation shared by Slamdown's boundary objects.
6
+ module Immutable
7
+ def self.copy(value)
8
+ case value
9
+ when String, Regexp
10
+ value.dup.freeze
11
+ when Array
12
+ value.map { |item| copy(item) }.freeze
13
+ when Hash
14
+ value.each_with_object({}) do |(name, item), result|
15
+ result[copy(name)] = copy(item)
16
+ end.freeze
17
+ else
18
+ value
19
+ end
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slamdown
4
+
5
+ # Contains the immutable public values returned by full conversions.
6
+ module Output
7
+ # Owns the canonical string and conversion-only diagnostics shared by every output format.
8
+ class Base
9
+ attr_reader :output, :diagnostics
10
+
11
+ def initialize(output, diagnostics)
12
+ raise ArgumentError, "output must be a String" unless output.is_a?(String)
13
+ raise ArgumentError, "diagnostics must contain only Slamdown diagnostics" unless diagnostics.is_a?(Array) && diagnostics.all? { |diagnostic| diagnostic.is_a?(Diagnostic) }
14
+
15
+ @output = output.dup.freeze
16
+ @diagnostics = diagnostics.dup.freeze
17
+ freeze
18
+ end
19
+ end
20
+
21
+ # Identifies output rendered through Slamdown's restricted Markdown profile.
22
+ class Markdown < Base
23
+ end
24
+
25
+ # Identifies output rendered through Slamdown's specified Kramdown extensions.
26
+ class Kramdown < Base
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kramdown"
4
+ require "kramdown/parser/html"
5
+
6
+ module Slamdown
7
+ module Parsing
8
+
9
+ # Parses HTML with Kramdown while retaining the explicit name of every source element that survives Kramdown's native conversion.
10
+ class Html < ::Kramdown::Parser::Html
11
+ # Extends only converter instances selected by Slamdown. Ordinary Kramdown parsing continues to instantiate its original converter class.
12
+ class SourceAwareElementConverter < ::Kramdown::Parser::Html::ElementConverter
13
+ CODE_SOURCE_NAMES = %w[code kbd pre samp tt var].freeze
14
+
15
+ def initialize(root)
16
+ @raw_elements = {}
17
+ @raw_list_children = {}
18
+ capture_raw_element(root)
19
+ super
20
+ end
21
+
22
+ # Captures the source name and contains a failure of Kramdown's optional native conversion to the affected element.
23
+ def process(element, do_conversion = true, preserve_text = false, parent = nil)
24
+ source_name = element.value if element.type == :html_element
25
+ raw_element = @raw_elements[element.object_id] if source_name
26
+ result = begin
27
+ super
28
+ rescue StandardError
29
+ raise unless source_name && do_conversion && @raw_elements.key?(element.object_id)
30
+
31
+ restore_raw_element(element)
32
+ process_html_element(element, false, preserve_text)
33
+ end
34
+ element.options[:source_name] = source_name if source_name
35
+ restore_list_text(element) if ["ol", "ul"].include?(source_name)
36
+ attach_code_source(element, raw_element) if CODE_SOURCE_NAMES.include?(source_name)
37
+ result
38
+ end
39
+
40
+ private
41
+
42
+ # Retains code text before Kramdown can flatten element boundaries or leave raw code text in child nodes.
43
+ def attach_code_source(element, raw_element)
44
+ state = {:value => String.new, :structural_boundary => false, :explicit_break => false}
45
+ raw_element.children.each { |child| append_code_source(child, state) }
46
+ element.options[:code_source] = decoded_code_source(raw_element, state[:value])
47
+ element.options[:code_source_has_break] = state[:explicit_break]
48
+ end
49
+
50
+ def append_code_source(element, state)
51
+ if element.type == :text
52
+ flush_structural_boundary(state)
53
+ state[:value] << element.value.to_s
54
+ elsif element.type == :html_element && element.value.to_s.downcase == "br"
55
+ state[:structural_boundary] = false
56
+ state[:value] << "\n"
57
+ state[:explicit_break] = true
58
+ elsif element.type == :html_element
59
+ block = HTML_BLOCK_ELEMENTS.include?(element.value.to_s.downcase)
60
+ state[:structural_boundary] = true if block
61
+ element.children.each { |child| append_code_source(child, state) }
62
+ state[:structural_boundary] = true if block
63
+ end
64
+ end
65
+
66
+ def flush_structural_boundary(state)
67
+ if state[:structural_boundary] && !state[:value].empty? && !state[:value].end_with?("\n")
68
+ state[:value] << "\n"
69
+ end
70
+ state[:structural_boundary] = false
71
+ end
72
+
73
+ # Reuses the active Kramdown version's entity handling instead of duplicating dependency-specific decoding rules.
74
+ def decoded_code_source(raw_element, value)
75
+ return "" if value.empty?
76
+
77
+ copy = duplicate_element_fields(raw_element)
78
+ copy.children = [::Kramdown::Element.new(:text, value)]
79
+ convert_code(copy)
80
+ copy.value.to_s
81
+ end
82
+
83
+ def capture_raw_element(element)
84
+ if element.type == :html_element && ["ol", "ul"].include?(element.value.to_s.downcase)
85
+ @raw_list_children[element.object_id] = element.children.map do |child|
86
+ child.type == :text ? [:text, child.value.to_s] : [:child, child.object_id]
87
+ end
88
+ end
89
+ copy = duplicate_element_fields(element)
90
+ @raw_elements[element.object_id] = copy
91
+ copy.children = element.children.map { |child| capture_raw_element(child) }
92
+ copy
93
+ end
94
+
95
+ # Kramdown discards direct text children of lists; retain non-blank runs as synthetic paragraphs in their source position.
96
+ def restore_list_text(element)
97
+ descriptors = @raw_list_children[element.object_id]
98
+ return unless descriptors
99
+
100
+ current = element.children.each_with_object({}) { |child, result| result[child.object_id] = child }
101
+ children = descriptors.each_with_object([]) do |(kind, value), result|
102
+ if kind == :child
103
+ child = current.delete(value)
104
+ result << child if child
105
+ elsif !value.to_s.strip.empty?
106
+ paragraph = ::Kramdown::Element.new(:p, nil, nil, :category => :block, :transparent => true)
107
+ paragraph.children << ::Kramdown::Element.new(:text, value)
108
+ result << paragraph
109
+ end
110
+ end
111
+ element.children = children + current.values
112
+ end
113
+
114
+ def restore_raw_element(element)
115
+ raw = duplicate_element(@raw_elements.fetch(element.object_id))
116
+ element.type = raw.type
117
+ element.value = raw.value
118
+ element.attr.replace(raw.attr)
119
+ element.options.replace(raw.options)
120
+ element.children = raw.children
121
+ end
122
+
123
+ def duplicate_element(element)
124
+ copy = duplicate_element_fields(element)
125
+ copy.children = element.children.map { |child| duplicate_element(child) }
126
+ copy
127
+ end
128
+
129
+ def duplicate_element_fields(element)
130
+ ::Kramdown::Element.new(
131
+ element.type,
132
+ duplicate_value(element.value),
133
+ duplicate_value(element.attr),
134
+ duplicate_value(element.options)
135
+ )
136
+ end
137
+
138
+ def duplicate_value(value)
139
+ case value
140
+ when String
141
+ value.dup
142
+ when Array
143
+ value.map { |item| duplicate_value(item) }
144
+ when Hash
145
+ result = value.dup
146
+ result.clear
147
+ value.each_with_object(result) do |(key, item), copy|
148
+ copy[duplicate_value(key)] = duplicate_value(item)
149
+ end
150
+ else
151
+ value
152
+ end
153
+ end
154
+ end
155
+
156
+ # Reproduces Kramdown::Parser::Html#parse's orchestration so the existing scanner can feed Slamdown's converter without global mutation.
157
+ def parse
158
+ @stack, @tree = [], @root
159
+ @src = ::Kramdown::Utils::StringScanner.new(adapt_source(repair_unclosed_table_rows(source)))
160
+
161
+ while true
162
+ if (result = @src.scan(/\s*#{HTML_INSTRUCTION_RE}/o))
163
+ @tree.children << ::Kramdown::Element.new(:xml_pi, result.strip, nil, :category => :block)
164
+ elsif @src.scan(/\s*#{HTML_DOCTYPE_RE}/o)
165
+ # Kramdown deliberately discards the document type.
166
+ elsif (result = @src.scan(/\s*#{HTML_COMMENT_RE}/o))
167
+ @tree.children << ::Kramdown::Element.new(:xml_comment, result.strip, nil, :category => :block)
168
+ else
169
+ break
170
+ end
171
+ end
172
+
173
+ tag_handler = lambda do |child, closed, handle_body|
174
+ parse_raw_html(child, &tag_handler) if !closed && handle_body
175
+ end
176
+ parse_raw_html(@tree, &tag_handler)
177
+
178
+ SourceAwareElementConverter.convert(@tree)
179
+ end
180
+
181
+ private
182
+
183
+ # Browsers implicitly close a row when another row starts; Kramdown instead nests it and can expose closing markup as text.
184
+ def repair_unclosed_table_rows(value)
185
+ return value unless value.valid_encoding?
186
+
187
+ table_rows = []
188
+ cursor = 0
189
+ result = String.new
190
+ value.to_enum(:scan, /<\/?\s*(?:table|tr)\b[^>]*>/i).each do
191
+ match = Regexp.last_match
192
+ result << value[cursor...match.begin(0)]
193
+ tag = match[0]
194
+ closing = /\A<\//.match(tag)
195
+ name = tag[/\A<\/?\s*([A-Za-z]+)/, 1].downcase
196
+
197
+ if name == "table"
198
+ if closing
199
+ result << "</tr>" if table_rows.last
200
+ table_rows.pop
201
+ else
202
+ table_rows << false
203
+ end
204
+ result << tag
205
+ elsif !table_rows.empty? && closing
206
+ unless table_rows.last
207
+ cursor = match.end(0)
208
+ next
209
+ end
210
+ table_rows[-1] = false
211
+ result << tag
212
+ elsif !table_rows.empty?
213
+ result << "</tr>" if table_rows.last
214
+ table_rows[-1] = true
215
+ result << tag
216
+ else
217
+ result << tag
218
+ end
219
+ cursor = match.end(0)
220
+ end
221
+ result << value[cursor..-1].to_s
222
+ result
223
+ end
224
+ end
225
+
226
+ end
227
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slamdown
4
+ module Parsing
5
+
6
+ # Immutable, format-neutral representation of one parsed construct. It contains parser facts only; element policy, structural correction and rendering belong to later stages.
7
+ class Node
8
+ ATTRIBUTES = [
9
+ :kind,
10
+ :source_name,
11
+ :semantic_kind,
12
+ :category,
13
+ :content_model,
14
+ :value,
15
+ :attributes,
16
+ :children,
17
+ :properties,
18
+ :source_form,
19
+ :synthetic
20
+ ].freeze
21
+
22
+ attr_reader(*ATTRIBUTES)
23
+
24
+ def initialize(kind:, source_name: nil, semantic_kind: nil, category: nil, content_model: nil, value: nil, attributes: [], children: [], properties: {}, source_form: nil, synthetic: false)
25
+ @kind = kind
26
+ @source_name = Immutable.copy(source_name)
27
+ @semantic_kind = semantic_kind
28
+ @category = category
29
+ @content_model = content_model
30
+ @value = Immutable.copy(value)
31
+ @attributes = Immutable.copy(attributes)
32
+ @children = children.dup.freeze
33
+ @properties = Immutable.copy(properties)
34
+ @source_form = Immutable.copy(source_form)
35
+ @synthetic = synthetic
36
+ freeze
37
+ end
38
+
39
+ # Provides a stable plain-Ruby projection for diagnostics and characterisation.
40
+ def to_h
41
+ ATTRIBUTES.each_with_object({}) do |attribute, result|
42
+ value = public_send(attribute)
43
+ result[attribute] = attribute == :children ? value.map(&:to_h) : value
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slamdown
4
+ module Parsing
5
+
6
+ # Projects Kramdown elements into immutable Slamdown nodes while removing only dependency-specific representation details from the parser boundary.
7
+ class ParsedNodeAdapter
8
+ SEMANTIC_KINDS = {
9
+ :p => :paragraph,
10
+ :header => :heading,
11
+ :blockquote => :blockquote,
12
+ :codeblock => :code_block,
13
+ :ul => :unordered_list,
14
+ :ol => :ordered_list,
15
+ :li => :list_item,
16
+ :dl => :definition_list,
17
+ :dt => :definition_term,
18
+ :dd => :definition_description,
19
+ :hr => :horizontal_rule,
20
+ :table => :table,
21
+ :thead => :table_head,
22
+ :tbody => :table_body,
23
+ :tfoot => :table_foot,
24
+ :tr => :table_row,
25
+ :td => :table_cell,
26
+ :a => :link,
27
+ :br => :line_break,
28
+ :img => :image,
29
+ :codespan => :code_span,
30
+ :em => :emphasis,
31
+ :strong => :strong,
32
+ :math => :math
33
+ }.freeze
34
+
35
+ NON_ELEMENT_KINDS = {
36
+ :root => :document,
37
+ :text => :text,
38
+ :raw => :raw,
39
+ :xml_comment => :comment,
40
+ :xml_pi => :processing_instruction,
41
+ :entity => :entity,
42
+ :typographic_sym => :typographic_symbol,
43
+ :smart_quote => :smart_quote
44
+ }.freeze
45
+
46
+ class << self
47
+ def adapt(root)
48
+ new.adapt_node(root)
49
+ end
50
+ end
51
+
52
+ # Recursively copies only fields that belong to Slamdown's parser contract.
53
+ def adapt_node(element, parent_type: nil, final_root_child: false)
54
+ source_name = explicit_source_name(element)
55
+ children = element.children.each_with_index.map do |child, index|
56
+ adapt_node(
57
+ child,
58
+ :parent_type => element.type,
59
+ :final_root_child => element.type == :root && index == element.children.length - 1
60
+ )
61
+ end.freeze
62
+
63
+ Node.new(
64
+ :kind => NON_ELEMENT_KINDS.fetch(element.type, :element),
65
+ :source_name => source_name,
66
+ :semantic_kind => SEMANTIC_KINDS[element.type],
67
+ :category => ::Kramdown::Element.category(element),
68
+ :content_model => element.options[:content_model],
69
+ :value => adapted_value(element, parent_type, final_root_child),
70
+ :attributes => adapted_attributes(element),
71
+ :children => children,
72
+ :properties => adapted_properties(element),
73
+ :source_form => adapted_source_form(element),
74
+ :synthetic => synthetic_element?(element, source_name)
75
+ )
76
+ end
77
+
78
+ private
79
+
80
+ def explicit_source_name(element)
81
+ name = element.options[:source_name]
82
+ name && name.downcase
83
+ end
84
+
85
+ def adapted_value(element, parent_type, final_root_child)
86
+ case element.type
87
+ when :raw, :xml_comment, :xml_pi, :html_element
88
+ nil
89
+ when :entity
90
+ {
91
+ :code_point => element.value.code_point,
92
+ :name => element.value.name,
93
+ :character => element.value.char
94
+ }
95
+ when :text
96
+ text = element.value
97
+ text = normalised_root_text(text, final_root_child) if parent_type == :root
98
+ text
99
+ else
100
+ element.value
101
+ end
102
+ end
103
+
104
+ # Kramdown 2.3.0 leaves root text adapted but unprocessed, whereas 2.5.2 collapses it and exposes the parser's terminal sentinel as a space.
105
+ def normalised_root_text(text, final_root_child)
106
+ result = text.gsub(/\s+/, " ")
107
+ result = result.sub(/\s+\z/, "") if final_root_child
108
+ result
109
+ end
110
+
111
+ def adapted_attributes(element)
112
+ element.attr.map do |name, value|
113
+ [name.downcase, value]
114
+ end
115
+ end
116
+
117
+ def adapted_properties(element)
118
+ properties = {}
119
+ properties[:encoding] = element.options[:encoding].name if element.options[:encoding]
120
+ properties[:level] = element.options[:level] if element.options.key?(:level)
121
+ properties[:alignment] = element.options[:alignment] if element.options.key?(:alignment)
122
+ properties[:code_source] = element.options[:code_source] if element.options.key?(:code_source)
123
+ properties[:code_source_has_break] = element.options[:code_source_has_break] if element.options.key?(:code_source_has_break)
124
+ properties
125
+ end
126
+
127
+ def adapted_source_form(element)
128
+ return unless element.type == :entity && element.options[:original]
129
+
130
+ element.options[:original]
131
+ end
132
+
133
+ def synthetic_element?(element, source_name)
134
+ return false unless NON_ELEMENT_KINDS.fetch(element.type, :element) == :element
135
+ return false if source_name
136
+ return true if element.type == :tbody || (element.type == :p && element.options[:transparent])
137
+
138
+ raise "Kramdown element #{element.type.inspect} lacks source provenance"
139
+ end
140
+
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parsing/html"
4
+ require_relative "parsing/node"
5
+ require_relative "parsing/parsed_node_adapter"
6
+
7
+ module Slamdown
8
+ module Parsing
9
+
10
+ # Parses HTML through Slamdown's isolated Kramdown extension and returns the format-neutral tree together with any parser warnings.
11
+ def self.parse(source, options = {})
12
+ root, warnings = Html.parse(source, options)
13
+ [ParsedNodeAdapter.adapt(root), warnings]
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Slamdown
2
+ VERSION = "0.5.0"
3
+ end
data/lib/slamdown.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slamdown/version"
4
+ require "slamdown/immutable"
5
+ require "slamdown/diagnostic"
6
+ require "slamdown/errors"
7
+ require "slamdown/parsing"
8
+ require "slamdown/output"
9
+ require "slamdown/document"
10
+ require "slamdown/conversion"