markbridge 0.1.1 → 0.1.2

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/lib/markbridge/all.rb +4 -7
  3. data/lib/markbridge/ast/document.rb +1 -1
  4. data/lib/markbridge/ast/element.rb +2 -2
  5. data/lib/markbridge/ast/list.rb +2 -2
  6. data/lib/markbridge/ast/table.rb +6 -12
  7. data/lib/markbridge/ast/text.rb +5 -1
  8. data/lib/markbridge/bbcode.rb +4 -0
  9. data/lib/markbridge/gem_loader.rb +2 -3
  10. data/lib/markbridge/html.rb +4 -0
  11. data/lib/markbridge/mediawiki.rb +4 -0
  12. data/lib/markbridge/parsers/bbcode/closing_strategies/base.rb +0 -10
  13. data/lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb +17 -4
  14. data/lib/markbridge/parsers/bbcode/closing_strategies/tag_reconciler.rb +64 -44
  15. data/lib/markbridge/parsers/bbcode/handler_registry.rb +21 -11
  16. data/lib/markbridge/parsers/bbcode/handlers/attachment_handler.rb +17 -12
  17. data/lib/markbridge/parsers/bbcode/handlers/base_handler.rb +0 -10
  18. data/lib/markbridge/parsers/bbcode/handlers/code_handler.rb +6 -10
  19. data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +9 -17
  20. data/lib/markbridge/parsers/bbcode/handlers/list_handler.rb +1 -5
  21. data/lib/markbridge/parsers/bbcode/handlers/list_item_handler.rb +1 -2
  22. data/lib/markbridge/parsers/bbcode/handlers/quote_handler.rb +6 -18
  23. data/lib/markbridge/parsers/bbcode/handlers/raw_handler.rb +2 -6
  24. data/lib/markbridge/parsers/bbcode/handlers/self_closing_handler.rb +4 -4
  25. data/lib/markbridge/parsers/bbcode/handlers/table_cell_handler.rb +1 -1
  26. data/lib/markbridge/parsers/bbcode/handlers/table_handler.rb +2 -2
  27. data/lib/markbridge/parsers/bbcode/handlers/table_row_handler.rb +3 -3
  28. data/lib/markbridge/parsers/bbcode/parser.rb +5 -8
  29. data/lib/markbridge/parsers/bbcode/parser_state.rb +12 -18
  30. data/lib/markbridge/parsers/bbcode/peekable_enumerator.rb +9 -59
  31. data/lib/markbridge/parsers/bbcode/raw_content_collector.rb +2 -2
  32. data/lib/markbridge/parsers/bbcode/scanner.rb +49 -63
  33. data/lib/markbridge/parsers/bbcode/tokens/tag_end_token.rb +1 -5
  34. data/lib/markbridge/parsers/bbcode/tokens/tag_start_token.rb +1 -6
  35. data/lib/markbridge/parsers/bbcode/tokens/text_token.rb +1 -7
  36. data/lib/markbridge/parsers/bbcode/tokens/token.rb +1 -1
  37. data/lib/markbridge/parsers/bbcode.rb +1 -0
  38. data/lib/markbridge/parsers/html/handler_registry.rb +32 -49
  39. data/lib/markbridge/parsers/html/handlers/base_handler.rb +0 -2
  40. data/lib/markbridge/parsers/html/handlers/image_handler.rb +1 -4
  41. data/lib/markbridge/parsers/html/parser.rb +3 -13
  42. data/lib/markbridge/parsers/media_wiki/inline_parser.rb +56 -67
  43. data/lib/markbridge/parsers/media_wiki/inline_tag_registry.rb +103 -0
  44. data/lib/markbridge/parsers/media_wiki/parser.rb +51 -76
  45. data/lib/markbridge/parsers/media_wiki.rb +1 -0
  46. data/lib/markbridge/parsers/text_formatter/handler_registry.rb +5 -37
  47. data/lib/markbridge/parsers/text_formatter/parser.rb +3 -8
  48. data/lib/markbridge/processors/discourse_markdown/code_block_tracker.rb +24 -17
  49. data/lib/markbridge/processors/discourse_markdown/detectors/base.rb +9 -15
  50. data/lib/markbridge/processors/discourse_markdown/detectors/event.rb +11 -10
  51. data/lib/markbridge/processors/discourse_markdown/detectors/poll.rb +11 -39
  52. data/lib/markbridge/processors/discourse_markdown/detectors/upload.rb +38 -63
  53. data/lib/markbridge/processors/discourse_markdown/scanner.rb +25 -33
  54. data/lib/markbridge/renderers/discourse/builders/list_item_builder.rb +6 -6
  55. data/lib/markbridge/renderers/discourse/html_escaper.rb +20 -0
  56. data/lib/markbridge/renderers/discourse/markdown_escaper.rb +49 -49
  57. data/lib/markbridge/renderers/discourse/render_context.rb +23 -11
  58. data/lib/markbridge/renderers/discourse/renderer.rb +54 -12
  59. data/lib/markbridge/renderers/discourse/rendering_interface.rb +12 -4
  60. data/lib/markbridge/renderers/discourse/tag.rb +14 -1
  61. data/lib/markbridge/renderers/discourse/tag_library.rb +30 -25
  62. data/lib/markbridge/renderers/discourse/tags/align_tag.rb +15 -7
  63. data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +2 -0
  64. data/lib/markbridge/renderers/discourse/tags/code_tag.rb +14 -9
  65. data/lib/markbridge/renderers/discourse/tags/email_tag.rb +5 -3
  66. data/lib/markbridge/renderers/discourse/tags/event_tag.rb +3 -1
  67. data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +6 -2
  68. data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +2 -2
  69. data/lib/markbridge/renderers/discourse/tags/image_tag.rb +13 -2
  70. data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +2 -0
  71. data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +2 -2
  72. data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +24 -47
  73. data/lib/markbridge/renderers/discourse/tags/list_tag.rb +10 -15
  74. data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +5 -1
  75. data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +10 -0
  76. data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +9 -2
  77. data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +2 -0
  78. data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +9 -0
  79. data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +2 -0
  80. data/lib/markbridge/renderers/discourse/tags/table_tag.rb +12 -8
  81. data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +10 -3
  82. data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +29 -2
  83. data/lib/markbridge/renderers/discourse/tags/url_tag.rb +5 -3
  84. data/lib/markbridge/renderers/discourse.rb +1 -0
  85. data/lib/markbridge/textformatter.rb +4 -0
  86. data/lib/markbridge/version.rb +1 -1
  87. data/lib/markbridge.rb +8 -8
  88. metadata +8 -2
@@ -27,55 +27,38 @@ module Markbridge
27
27
  # Create the default handler registry with common HTML tags
28
28
  # @return [HandlerRegistry]
29
29
  def self.default
30
- registry = new
31
-
32
- # Simple formatting handlers
33
- registry.register(%w[b strong], Handlers::SimpleHandler.new(AST::Bold))
34
- registry.register(%w[i em], Handlers::SimpleHandler.new(AST::Italic))
35
- registry.register(%w[s strike del], Handlers::SimpleHandler.new(AST::Strikethrough))
36
- registry.register("u", Handlers::SimpleHandler.new(AST::Underline))
37
- registry.register("sup", Handlers::SimpleHandler.new(AST::Superscript))
38
- registry.register("sub", Handlers::SimpleHandler.new(AST::Subscript))
39
-
40
- # Code handlers (raw content)
41
- registry.register(%w[code pre tt], Handlers::RawHandler.new(AST::Code))
42
-
43
- # Link and image handlers
44
- registry.register("a", Handlers::UrlHandler.new)
45
- registry.register("img", Handlers::ImageHandler.new)
46
-
47
- # Blockquote handler
48
- registry.register("blockquote", Handlers::QuoteHandler.new)
49
-
50
- # Void elements - use simple inline handlers
51
- registry.register(
52
- "br",
53
- lambda do |element:, parent:|
54
- parent << AST::LineBreak.new
55
- nil # Return nil - void element, no children
56
- end,
57
- )
58
- registry.register(
59
- "hr",
60
- lambda do |element:, parent:|
61
- parent << AST::HorizontalRule.new
62
- nil # Return nil - void element, no children
63
- end,
64
- )
65
-
66
- # List handlers
67
- registry.register(%w[ul ol], Handlers::ListHandler.new)
68
- registry.register("li", Handlers::ListItemHandler.new)
69
-
70
- # Table handlers (thead/tbody/tfoot are transparent - unregistered tags pass through)
71
- registry.register("table", Handlers::TableHandler.new)
72
- registry.register("tr", Handlers::TableRowHandler.new)
73
- registry.register(%w[td th], Handlers::TableCellHandler.new)
74
-
75
- # Paragraph handler (transparent - doesn't create AST node)
76
- registry.register("p", Handlers::ParagraphHandler.new)
77
-
78
- registry
30
+ new.tap do |registry|
31
+ registry.register(%w[b strong], Handlers::SimpleHandler.new(AST::Bold))
32
+ registry.register(%w[i em], Handlers::SimpleHandler.new(AST::Italic))
33
+ registry.register(%w[s strike del], Handlers::SimpleHandler.new(AST::Strikethrough))
34
+ registry.register("u", Handlers::SimpleHandler.new(AST::Underline))
35
+ registry.register("sup", Handlers::SimpleHandler.new(AST::Superscript))
36
+ registry.register("sub", Handlers::SimpleHandler.new(AST::Subscript))
37
+ registry.register(%w[code pre tt], Handlers::RawHandler.new(AST::Code))
38
+ registry.register("a", Handlers::UrlHandler.new)
39
+ registry.register("img", Handlers::ImageHandler.new)
40
+ registry.register("blockquote", Handlers::QuoteHandler.new)
41
+ registry.register(
42
+ "br",
43
+ lambda do |element:, parent:|
44
+ parent << AST::LineBreak.new
45
+ nil
46
+ end,
47
+ )
48
+ registry.register(
49
+ "hr",
50
+ lambda do |element:, parent:|
51
+ parent << AST::HorizontalRule.new
52
+ nil
53
+ end,
54
+ )
55
+ registry.register(%w[ul ol], Handlers::ListHandler.new)
56
+ registry.register("li", Handlers::ListItemHandler.new)
57
+ registry.register("table", Handlers::TableHandler.new)
58
+ registry.register("tr", Handlers::TableRowHandler.new)
59
+ registry.register(%w[td th], Handlers::TableCellHandler.new)
60
+ registry.register("p", Handlers::ParagraphHandler.new)
61
+ end
79
62
  end
80
63
 
81
64
  # Build a registry from the default configuration with optional customization
@@ -11,8 +11,6 @@ module Markbridge
11
11
  # @param parent [AST::Element] the parent AST node
12
12
  # @return [AST::Element, nil] the created element if children should be processed, nil otherwise
13
13
  def process(element:, parent:)
14
- # Default: do nothing, subclasses override
15
- nil
16
14
  end
17
15
 
18
16
  # The element class created by this handler
@@ -26,12 +26,9 @@ module Markbridge
26
26
 
27
27
  private
28
28
 
29
- # Convert dimension to positive integer or nil
30
29
  def sanitize_dimension(value)
31
- return nil if value.nil?
32
-
33
30
  dim = value.to_i
34
- dim.positive? ? dim : nil
31
+ dim if dim.positive?
35
32
  end
36
33
  end
37
34
  end
@@ -72,15 +72,14 @@ module Markbridge
72
72
  # @param node [Nokogiri::XML::Text]
73
73
  # @param parent [AST::Element]
74
74
  def process_text_node(node, parent)
75
- text = node.text
76
- parent << AST::Text.new(text) unless text.empty?
75
+ parent << AST::Text.new(node.text)
77
76
  end
78
77
 
79
78
  # Process an element node
80
79
  # @param node [Nokogiri::XML::Element]
81
80
  # @param parent [AST::Element]
82
81
  def process_element_node(node, parent)
83
- tag_name = node.name.downcase
82
+ tag_name = node.name
84
83
  return if IGNORED_TAGS.include?(tag_name)
85
84
 
86
85
  handler = @handlers[tag_name]
@@ -106,18 +105,9 @@ module Markbridge
106
105
  # @param node [Nokogiri::XML::Element]
107
106
  # @param parent [AST::Element]
108
107
  def handle_unknown_tag(node, parent)
109
- @unknown_tags[node.name.downcase] += 1
108
+ @unknown_tags[node.name] += 1
110
109
  process_children(node, parent)
111
110
  end
112
-
113
- # Check if an element is a void element (self-closing)
114
- # @param tag_name [String]
115
- # @return [Boolean]
116
- def void_element?(tag_name)
117
- %w[area base br col embed hr img input link meta param source track wbr].include?(
118
- tag_name.downcase,
119
- )
120
- end
121
111
  end
122
112
  end
123
113
  end
@@ -5,14 +5,19 @@ module Markbridge
5
5
  module MediaWiki
6
6
  # Parses inline MediaWiki markup within a line of text.
7
7
  # Handles bold ('''), italic (''), links ([[...]]), external links ([...]),
8
- # and HTML inline tags (<code>, <nowiki>, <s>, <del>, <u>, <ins>, <sup>, <sub>, <br>).
8
+ # and HTML inline tags via an InlineTagRegistry.
9
+ #
10
+ # @example With custom registry
11
+ # registry = InlineTagRegistry.build_from_default do |r|
12
+ # r.register("mark", :formatting, AST::Bold)
13
+ # end
14
+ # parser = InlineParser.new(inline_tag_registry: registry)
9
15
  class InlineParser
10
- def initialize
11
- @input = nil
12
- @pos = 0
13
- @length = 0
14
- @parent = nil
15
- @text_buffer = +""
16
+ MAX_INLINE_DEPTH = 20
17
+
18
+ def initialize(inline_tag_registry: nil, depth: 0)
19
+ @registry = inline_tag_registry || InlineTagRegistry.default
20
+ @depth = depth
16
21
  end
17
22
 
18
23
  # Parse inline markup and append resulting AST nodes to the parent element.
@@ -28,29 +33,18 @@ module Markbridge
28
33
 
29
34
  while @pos < @length
30
35
  char = @input[@pos]
31
- next_char = @pos + 1 < @length ? @input[@pos + 1] : nil
32
36
 
33
37
  case char
34
38
  when "'"
35
- if next_char == "'"
36
- parse_bold_italic
37
- else
38
- @text_buffer << char
39
- @pos += 1
40
- end
39
+ consecutive_apostrophes_at(@pos) >= 2 ? parse_bold_italic : append_literal(char)
41
40
  when "["
42
41
  flush_text
43
- if next_char == "["
44
- parse_internal_link
45
- else
46
- parse_external_link
47
- end
42
+ @input[@pos + 1] == "[" ? parse_internal_link : parse_external_link
48
43
  when "<"
49
44
  flush_text
50
45
  parse_html_tag
51
46
  else
52
- @text_buffer << char
53
- @pos += 1
47
+ append_literal(char)
54
48
  end
55
49
  end
56
50
 
@@ -59,22 +53,18 @@ module Markbridge
59
53
 
60
54
  private
61
55
 
62
- # Count consecutive apostrophes and dispatch to bold/italic parsing.
56
+ def append_literal(char)
57
+ @text_buffer << char
58
+ @pos += 1
59
+ end
60
+
61
+ # Precondition: caller has verified @input[@pos..@pos+1] is "''".
63
62
  def parse_bold_italic
64
63
  start = @pos
65
- count = 0
66
- count += 1 while @pos + count < @length && @input[@pos + count] == "'"
67
- # Clamp: 5 = bold+italic, 3 = bold, 2 = italic
68
- count = [count, 5].min
69
-
70
- if count < 2
71
- @text_buffer << @input[@pos]
72
- @pos += 1
73
- else
74
- flush_text
75
- @pos += count
76
- parse_apostrophe_formatting(count, start)
77
- end
64
+ count = [consecutive_apostrophes_at(@pos), 5].min
65
+ flush_text
66
+ @pos += count
67
+ parse_apostrophe_formatting(count, start)
78
68
  end
79
69
 
80
70
  # Parse apostrophe-delimited formatting (bold, italic, or bold+italic).
@@ -99,9 +89,7 @@ module Markbridge
99
89
  def build_formatting_element(apostrophe_count)
100
90
  case apostrophe_count
101
91
  when 5
102
- bold = AST::Bold.new
103
- bold << AST::Italic.new
104
- bold
92
+ AST::Bold.new << AST::Italic.new
105
93
  when 3
106
94
  AST::Bold.new
107
95
  when 2
@@ -115,8 +103,17 @@ module Markbridge
115
103
  end
116
104
 
117
105
  # Parse inner content and append to a parent element.
106
+ # Respects MAX_INLINE_DEPTH to prevent stack overflow from deeply nested markup.
118
107
  def parse_inner_content(content, parent:)
119
- InlineParser.new.parse(content, parent:)
108
+ if @depth + 1 >= MAX_INLINE_DEPTH
109
+ parent << AST::Text.new(content)
110
+ return
111
+ end
112
+
113
+ InlineParser.new(inline_tag_registry: @registry, depth: @depth + 1).parse(
114
+ content,
115
+ parent:,
116
+ )
120
117
  end
121
118
 
122
119
  # Collect text until we find n consecutive apostrophes.
@@ -127,14 +124,13 @@ module Markbridge
127
124
  def collect_until_apostrophes(count)
128
125
  start = @pos
129
126
  while @pos < @length
130
- if @input[@pos] == "'" && consecutive_apostrophes_at(@pos) >= count
127
+ if consecutive_apostrophes_at(@pos) >= count
131
128
  content = @input[start...@pos]
132
129
  @pos += count
133
130
  return content
134
131
  end
135
132
  @pos += 1
136
133
  end
137
- nil
138
134
  end
139
135
 
140
136
  # Count consecutive apostrophes starting at position.
@@ -142,9 +138,7 @@ module Markbridge
142
138
  # @param pos [Integer]
143
139
  # @return [Integer]
144
140
  def consecutive_apostrophes_at(pos)
145
- count = 0
146
- count += 1 while pos + count < @length && @input[pos + count] == "'"
147
- count
141
+ @input[pos..].each_char.take_while { |c| c == "'" }.length
148
142
  end
149
143
 
150
144
  # Parse [[internal link]] or [[target|display text]].
@@ -196,7 +190,6 @@ module Markbridge
196
190
  @parent << url
197
191
  end
198
192
 
199
- # Parse an HTML tag (<code>, <nowiki>, <pre>, <br>, <s>, <del>, <u>, <ins>, <sup>, <sub>).
200
193
  def parse_html_tag
201
194
  tag_match = @input[@pos..].match(%r{\A<(/?)([a-z]+)(?: [^>]*)?\s*(/?)>}i)
202
195
  unless tag_match
@@ -211,34 +204,30 @@ module Markbridge
211
204
  tag_name = tag_match[2].downcase
212
205
 
213
206
  # Closing/self-closing tags and unknown tags are treated as literal text
214
- if closing || self_closing || !known_html_tag?(tag_name)
207
+ entry = @registry[tag_name]
208
+ if closing || self_closing || !entry
215
209
  advance_as_text(full_match)
216
210
  return
217
211
  end
218
212
 
219
- case tag_name
220
- when "nowiki"
221
- handle_nowiki_tag(full_match)
222
- when "code", "pre"
223
- handle_paired_raw_tag(tag_name, full_match, AST::Code)
224
- when "br"
225
- @pos += full_match.length
226
- @parent << AST::LineBreak.new
227
- when "s", "del"
228
- handle_paired_tag(tag_name, full_match, AST::Strikethrough)
229
- when "u", "ins"
230
- handle_paired_tag(tag_name, full_match, AST::Underline)
231
- when "sup"
232
- handle_paired_tag(tag_name, full_match, AST::Superscript)
233
- when "sub"
234
- handle_paired_tag(tag_name, full_match, AST::Subscript)
235
- end
213
+ dispatch_html_tag(entry, tag_name, full_match)
236
214
  end
237
215
 
238
- KNOWN_HTML_TAGS = %w[nowiki code pre br s del u ins sup sub].freeze
239
-
240
- def known_html_tag?(tag_name)
241
- KNOWN_HTML_TAGS.include?(tag_name)
216
+ # Dispatch an HTML-like tag based on its registry entry type.
217
+ def dispatch_html_tag(entry, tag_name, full_match)
218
+ case entry.type
219
+ when :raw
220
+ if entry.element_class.nil?
221
+ handle_nowiki_tag(full_match)
222
+ else
223
+ handle_paired_raw_tag(tag_name, full_match, entry.element_class)
224
+ end
225
+ when :formatting
226
+ handle_paired_tag(tag_name, full_match, entry.element_class)
227
+ when :self_closing
228
+ @pos += full_match.length
229
+ @parent << entry.element_class.new
230
+ end
242
231
  end
243
232
 
244
233
  # Advance position and buffer the match as literal text.
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Parsers
5
+ module MediaWiki
6
+ # Registry of inline HTML-like tag handlers for the MediaWiki parser.
7
+ #
8
+ # Supports three tag types:
9
+ # - :raw - content is preserved verbatim (e.g., <code>, <nowiki>)
10
+ # - :formatting - content is parsed for inline wiki markup (e.g., <s>, <u>)
11
+ # - :self_closing - no content, produces a leaf AST node (e.g., <br>)
12
+ #
13
+ # @example Default usage
14
+ # registry = InlineTagRegistry.default
15
+ # entry = registry["s"]
16
+ # entry.type # => :formatting
17
+ # entry.element_class # => AST::Strikethrough
18
+ #
19
+ # @example Custom registration
20
+ # registry = InlineTagRegistry.build_from_default do |r|
21
+ # r.register("mark", :formatting, AST::Bold)
22
+ # end
23
+ class InlineTagRegistry
24
+ Entry = Data.define(:type, :element_class)
25
+
26
+ def initialize
27
+ @entries = {}
28
+ end
29
+
30
+ # Register a handler for an inline HTML-like tag.
31
+ #
32
+ # @param tag_name [String] the tag name (case-insensitive)
33
+ # @param type [:raw, :formatting, :self_closing] how the tag content is handled
34
+ # @param element_class [Class] the AST node class to create
35
+ # @return [self]
36
+ def register(tag_name, type, element_class)
37
+ validate_type!(type)
38
+ @entries[tag_name.downcase] = Entry.new(type:, element_class:)
39
+ self
40
+ end
41
+
42
+ # Look up a tag entry by name.
43
+ #
44
+ # @param tag_name [String]
45
+ # @return [Entry, nil]
46
+ def [](tag_name)
47
+ @entries[tag_name.downcase]
48
+ end
49
+
50
+ # Check if a tag name is registered.
51
+ #
52
+ # @param tag_name [String]
53
+ # @return [Boolean]
54
+ def known?(tag_name)
55
+ @entries.key?(tag_name.downcase)
56
+ end
57
+
58
+ # Create the default registry with standard MediaWiki inline tags.
59
+ #
60
+ # @return [InlineTagRegistry]
61
+ def self.default
62
+ registry = new
63
+
64
+ # Raw tags -content preserved verbatim, not parsed for wiki markup
65
+ registry.register("nowiki", :raw, nil)
66
+ registry.register("code", :raw, AST::Code)
67
+ registry.register("pre", :raw, AST::Code)
68
+
69
+ # Formatting tags -content parsed for inline wiki markup
70
+ registry.register("s", :formatting, AST::Strikethrough)
71
+ registry.register("del", :formatting, AST::Strikethrough)
72
+ registry.register("u", :formatting, AST::Underline)
73
+ registry.register("ins", :formatting, AST::Underline)
74
+ registry.register("sup", :formatting, AST::Superscript)
75
+ registry.register("sub", :formatting, AST::Subscript)
76
+
77
+ # Self-closing tags -produce a leaf node, no content
78
+ registry.register("br", :self_closing, AST::LineBreak)
79
+ end
80
+
81
+ # Build a registry from the default with optional customization.
82
+ #
83
+ # @yield [InlineTagRegistry] the registry to customize
84
+ # @return [InlineTagRegistry]
85
+ def self.build_from_default
86
+ registry = default
87
+ yield(registry) if block_given?
88
+ registry
89
+ end
90
+
91
+ private
92
+
93
+ VALID_TYPES = %i[raw formatting self_closing].freeze
94
+
95
+ def validate_type!(type)
96
+ return if VALID_TYPES.include?(type)
97
+
98
+ raise ArgumentError, "type must be one of #{VALID_TYPES}, got #{type.inspect}"
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end