markbridge 0.1.1 → 0.1.3
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/lib/markbridge/all.rb +4 -7
- data/lib/markbridge/ast/document.rb +1 -1
- data/lib/markbridge/ast/element.rb +2 -2
- data/lib/markbridge/ast/list.rb +2 -2
- data/lib/markbridge/ast/table.rb +6 -12
- data/lib/markbridge/ast/text.rb +5 -1
- data/lib/markbridge/bbcode.rb +4 -0
- data/lib/markbridge/gem_loader.rb +2 -3
- data/lib/markbridge/html.rb +4 -0
- data/lib/markbridge/mediawiki.rb +4 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/base.rb +0 -10
- data/lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb +17 -4
- data/lib/markbridge/parsers/bbcode/closing_strategies/tag_reconciler.rb +64 -44
- data/lib/markbridge/parsers/bbcode/handler_registry.rb +21 -11
- data/lib/markbridge/parsers/bbcode/handlers/attachment_handler.rb +17 -12
- data/lib/markbridge/parsers/bbcode/handlers/base_handler.rb +0 -10
- data/lib/markbridge/parsers/bbcode/handlers/code_handler.rb +6 -10
- data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +9 -17
- data/lib/markbridge/parsers/bbcode/handlers/list_handler.rb +1 -5
- data/lib/markbridge/parsers/bbcode/handlers/list_item_handler.rb +1 -2
- data/lib/markbridge/parsers/bbcode/handlers/quote_handler.rb +6 -18
- data/lib/markbridge/parsers/bbcode/handlers/raw_handler.rb +2 -6
- data/lib/markbridge/parsers/bbcode/handlers/self_closing_handler.rb +4 -4
- data/lib/markbridge/parsers/bbcode/handlers/table_cell_handler.rb +1 -1
- data/lib/markbridge/parsers/bbcode/handlers/table_handler.rb +2 -2
- data/lib/markbridge/parsers/bbcode/handlers/table_row_handler.rb +3 -3
- data/lib/markbridge/parsers/bbcode/parser.rb +5 -8
- data/lib/markbridge/parsers/bbcode/parser_state.rb +12 -18
- data/lib/markbridge/parsers/bbcode/peekable_enumerator.rb +9 -59
- data/lib/markbridge/parsers/bbcode/raw_content_collector.rb +2 -2
- data/lib/markbridge/parsers/bbcode/scanner.rb +49 -63
- data/lib/markbridge/parsers/bbcode/tokens/tag_end_token.rb +1 -5
- data/lib/markbridge/parsers/bbcode/tokens/tag_start_token.rb +1 -6
- data/lib/markbridge/parsers/bbcode/tokens/text_token.rb +1 -7
- data/lib/markbridge/parsers/bbcode/tokens/token.rb +1 -1
- data/lib/markbridge/parsers/bbcode.rb +1 -0
- data/lib/markbridge/parsers/html/handler_registry.rb +32 -49
- data/lib/markbridge/parsers/html/handlers/base_handler.rb +0 -2
- data/lib/markbridge/parsers/html/handlers/image_handler.rb +1 -4
- data/lib/markbridge/parsers/html/parser.rb +3 -13
- data/lib/markbridge/parsers/media_wiki/inline_parser.rb +56 -67
- data/lib/markbridge/parsers/media_wiki/inline_tag_registry.rb +103 -0
- data/lib/markbridge/parsers/media_wiki/parser.rb +51 -76
- data/lib/markbridge/parsers/media_wiki.rb +1 -0
- data/lib/markbridge/parsers/text_formatter/handler_registry.rb +5 -37
- data/lib/markbridge/parsers/text_formatter/parser.rb +3 -8
- data/lib/markbridge/processors/discourse_markdown/code_block_tracker.rb +24 -17
- data/lib/markbridge/processors/discourse_markdown/detectors/base.rb +9 -15
- data/lib/markbridge/processors/discourse_markdown/detectors/event.rb +11 -10
- data/lib/markbridge/processors/discourse_markdown/detectors/poll.rb +11 -39
- data/lib/markbridge/processors/discourse_markdown/detectors/upload.rb +38 -63
- data/lib/markbridge/processors/discourse_markdown/scanner.rb +25 -33
- data/lib/markbridge/renderers/discourse/builders/list_item_builder.rb +6 -6
- data/lib/markbridge/renderers/discourse/html_escaper.rb +20 -0
- data/lib/markbridge/renderers/discourse/markdown_escaper.rb +57 -50
- data/lib/markbridge/renderers/discourse/render_context.rb +23 -11
- data/lib/markbridge/renderers/discourse/renderer.rb +54 -12
- data/lib/markbridge/renderers/discourse/rendering_interface.rb +12 -4
- data/lib/markbridge/renderers/discourse/tag.rb +14 -1
- data/lib/markbridge/renderers/discourse/tag_library.rb +30 -25
- data/lib/markbridge/renderers/discourse/tags/align_tag.rb +15 -7
- data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +2 -0
- data/lib/markbridge/renderers/discourse/tags/code_tag.rb +14 -9
- data/lib/markbridge/renderers/discourse/tags/email_tag.rb +5 -3
- data/lib/markbridge/renderers/discourse/tags/event_tag.rb +3 -1
- data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +6 -2
- data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +2 -2
- data/lib/markbridge/renderers/discourse/tags/image_tag.rb +13 -2
- data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +2 -0
- data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +2 -2
- data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +24 -47
- data/lib/markbridge/renderers/discourse/tags/list_tag.rb +10 -15
- data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +5 -1
- data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +10 -0
- data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +9 -2
- data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +2 -0
- data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +9 -0
- data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +2 -0
- data/lib/markbridge/renderers/discourse/tags/table_tag.rb +12 -8
- data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +10 -3
- data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +29 -2
- data/lib/markbridge/renderers/discourse/tags/url_tag.rb +5 -3
- data/lib/markbridge/renderers/discourse.rb +1 -0
- data/lib/markbridge/textformatter.rb +4 -0
- data/lib/markbridge/version.rb +1 -1
- data/lib/markbridge.rb +8 -8
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd32ca1996ac298962ade2f5728f0e0b9999ae0e2934b44474e003ca74888e32
|
|
4
|
+
data.tar.gz: 43334aa515a7326ac7e514e1ebd252f7517866a6fe254c350f7c51085ca26ba4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2d9e6f8ed438bbe9425a33c4cf834e707bea7efc953bfd0d1c1aa53faea9b578ed54d7cb6a0fccb1a79ee433f43feb2a6858bf00f8fc19f8d55bef0237842f0
|
|
7
|
+
data.tar.gz: 90f49ebacb4bc8be03b27aa0b730aea8a4b6f6206a601e19cd7fdc6cfb56e23916dec63ae990ce05c525103afd29d2949b15e812d74662e53776dced970177f2
|
data/lib/markbridge/all.rb
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
4
|
-
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "parsers/html"
|
|
8
|
-
require_relative "parsers/text_formatter"
|
|
9
|
-
require_relative "renderers/discourse"
|
|
3
|
+
require_relative "bbcode"
|
|
4
|
+
require_relative "html"
|
|
5
|
+
require_relative "mediawiki"
|
|
6
|
+
require_relative "textformatter"
|
|
@@ -31,10 +31,10 @@ module Markbridge
|
|
|
31
31
|
def <<(child)
|
|
32
32
|
unless child.is_a?(Node)
|
|
33
33
|
actual = child.nil? ? "nil" : child.class
|
|
34
|
-
raise TypeError, "
|
|
34
|
+
raise TypeError, "<< on #{self.class} expected a #{Node}, got #{actual}"
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
if child.
|
|
37
|
+
if child.instance_of?(Text) && children.last.instance_of?(Text)
|
|
38
38
|
@children.last.merge(child)
|
|
39
39
|
else
|
|
40
40
|
@children << child
|
data/lib/markbridge/ast/list.rb
CHANGED
|
@@ -21,9 +21,9 @@ module Markbridge
|
|
|
21
21
|
# @return [List] self for chaining
|
|
22
22
|
# @raise [TypeError] if child is not a Node
|
|
23
23
|
def <<(child)
|
|
24
|
-
return self if child.
|
|
24
|
+
return self if child.instance_of?(Text) && !child.text.match?(/\S/)
|
|
25
25
|
|
|
26
|
-
if child.
|
|
26
|
+
if child.instance_of?(ListItem)
|
|
27
27
|
super
|
|
28
28
|
else
|
|
29
29
|
@children << ListItem.new if @children.empty?
|
data/lib/markbridge/ast/table.rb
CHANGED
|
@@ -8,13 +8,11 @@ module Markbridge
|
|
|
8
8
|
# table = AST::Table.new
|
|
9
9
|
# table << AST::TableRow.new
|
|
10
10
|
class Table < Element
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# @param child [Node] the node to add
|
|
15
|
-
# @return [Table] self for chaining
|
|
11
|
+
# HTML/BBCode parsers add a Text("\n") child for the whitespace
|
|
12
|
+
# between `<table>` and `<tr>` (and equivalent BBCode). Drop
|
|
13
|
+
# those so the AST contains only TableRow children.
|
|
16
14
|
def <<(child)
|
|
17
|
-
return self if child.
|
|
15
|
+
return self if child.instance_of?(Text) && child.text.strip.empty?
|
|
18
16
|
|
|
19
17
|
super
|
|
20
18
|
end
|
|
@@ -26,13 +24,9 @@ module Markbridge
|
|
|
26
24
|
# row = AST::TableRow.new
|
|
27
25
|
# row << AST::TableCell.new
|
|
28
26
|
class TableRow < Element
|
|
29
|
-
#
|
|
30
|
-
# Whitespace-only Text nodes are ignored.
|
|
31
|
-
#
|
|
32
|
-
# @param child [Node] the node to add
|
|
33
|
-
# @return [TableRow] self for chaining
|
|
27
|
+
# See Table#<< — same whitespace skip for `<tr>` / `<td>` gaps.
|
|
34
28
|
def <<(child)
|
|
35
|
-
return self if child.
|
|
29
|
+
return self if child.instance_of?(Text) && child.text.strip.empty?
|
|
36
30
|
|
|
37
31
|
super
|
|
38
32
|
end
|
data/lib/markbridge/ast/text.rb
CHANGED
|
@@ -19,9 +19,13 @@ module Markbridge
|
|
|
19
19
|
|
|
20
20
|
# Create a new text node with the given content.
|
|
21
21
|
#
|
|
22
|
+
# The text is copied into a fresh mutable buffer so that subsequent
|
|
23
|
+
# in-place mutations (e.g. via {#merge}) do not leak back to the caller's
|
|
24
|
+
# original string.
|
|
25
|
+
#
|
|
22
26
|
# @param text [String] the text content
|
|
23
27
|
def initialize(text)
|
|
24
|
-
@text =
|
|
28
|
+
@text = text.dup
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
# Merge another text node's content into this one.
|
|
@@ -12,10 +12,9 @@ module Markbridge
|
|
|
12
12
|
private
|
|
13
13
|
|
|
14
14
|
def missing_message(gem, feature)
|
|
15
|
-
gem_name = gem.to_s
|
|
16
15
|
[
|
|
17
|
-
"#{
|
|
18
|
-
"Add 'gem \"#{
|
|
16
|
+
"#{gem.capitalize} is required for #{feature}.",
|
|
17
|
+
"Add 'gem \"#{gem}\"' to your Gemfile or install it with 'gem install #{gem}'.",
|
|
19
18
|
].join(" ")
|
|
20
19
|
end
|
|
21
20
|
end
|
|
@@ -9,27 +9,17 @@ module Markbridge
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def handle_close(token:, context:, registry:, tokens: nil)
|
|
12
|
-
return unless context.current.is_a?(AST::Element)
|
|
13
|
-
|
|
14
12
|
current_handler = registry.handler_for_element(context.current)
|
|
15
13
|
closing_handler = registry[token.tag]
|
|
16
14
|
|
|
17
15
|
if current_handler == closing_handler
|
|
18
16
|
context.pop
|
|
19
|
-
elsif try_reorder(context:, tokens:, closing_handler:)
|
|
20
|
-
# Reordering handled
|
|
21
17
|
elsif @reconciler.try_auto_close(handler: closing_handler, context:)
|
|
22
18
|
# Auto-close succeeded
|
|
23
19
|
else
|
|
24
20
|
context.add_child(AST::Text.new(token.source))
|
|
25
21
|
end
|
|
26
22
|
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def try_reorder(context:, tokens:, closing_handler:)
|
|
31
|
-
false
|
|
32
|
-
end
|
|
33
23
|
end
|
|
34
24
|
end
|
|
35
25
|
end
|
|
@@ -4,11 +4,24 @@ module Markbridge
|
|
|
4
4
|
module BBCode
|
|
5
5
|
module ClosingStrategies
|
|
6
6
|
class Reordering < Base
|
|
7
|
-
|
|
7
|
+
def handle_close(token:, context:, registry:, tokens: nil)
|
|
8
|
+
closing_handler = registry[token.tag]
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
return
|
|
11
|
-
|
|
10
|
+
# Fast path: when the current element matches the closing tag,
|
|
11
|
+
# pop and return. try_reorder/try_reopen are reconciliation
|
|
12
|
+
# strategies that only make sense when the close is mismatched;
|
|
13
|
+
# running them eagerly here costs a full elements_from_current
|
|
14
|
+
# walk plus two sort_by/compare passes on every close token,
|
|
15
|
+
# which dominates runtime for well-formed input.
|
|
16
|
+
if registry.handler_for_element(context.current) == closing_handler
|
|
17
|
+
context.pop
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
return if tokens && @reconciler.try_reorder(handler: closing_handler, tokens:, context:)
|
|
22
|
+
return if @reconciler.try_reopen(handler: closing_handler, context:, tokens:)
|
|
23
|
+
|
|
24
|
+
super
|
|
12
25
|
end
|
|
13
26
|
end
|
|
14
27
|
end
|
|
@@ -18,15 +18,48 @@ module Markbridge
|
|
|
18
18
|
# @param context [ParserState]
|
|
19
19
|
# @return [Boolean] true if successful, false if auto-close not possible
|
|
20
20
|
def try_auto_close(handler:, context:)
|
|
21
|
-
|
|
21
|
+
count = auto_close_count(handler, context)
|
|
22
|
+
return false if count.nil?
|
|
23
|
+
|
|
24
|
+
count.times { context.pop }
|
|
25
|
+
context.auto_close!(count)
|
|
26
|
+
|
|
27
|
+
true
|
|
28
|
+
end
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
# Attempt to close the target tag and reopen any intervening
|
|
31
|
+
# auto-closeable tags so subsequent content continues in the same
|
|
32
|
+
# formatting context. Used when closing tags are not adjacent
|
|
33
|
+
# (e.g. "[b][i]x[/b] more[/i]").
|
|
34
|
+
#
|
|
35
|
+
# Reopening only makes sense when there is upcoming content that
|
|
36
|
+
# would benefit from the reopened context. If the next token is a
|
|
37
|
+
# closing tag (or nothing), plain auto-close is correct.
|
|
38
|
+
#
|
|
39
|
+
# @param handler [BaseHandler] the handler for the closing tag
|
|
40
|
+
# @param context [ParserState]
|
|
41
|
+
# @param tokens [Object, nil] the token stream (used to check that content follows)
|
|
42
|
+
# @return [Boolean] true if successful, false otherwise
|
|
43
|
+
def try_reopen(handler:, context:, tokens:)
|
|
44
|
+
case tokens&.peek
|
|
45
|
+
when TextToken, TagStartToken
|
|
46
|
+
nil # content follows -> reopening is justified
|
|
47
|
+
else
|
|
48
|
+
return false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
match_depth = find_matching_handler_depth(handler, context)
|
|
52
|
+
return false if match_depth.nil? || match_depth.zero?
|
|
24
53
|
return false unless all_auto_closeable?(context, match_depth)
|
|
25
54
|
|
|
55
|
+
intervening = context.elements_from_current(match_depth - 1).map(&:class)
|
|
56
|
+
|
|
26
57
|
count = match_depth + 1
|
|
27
58
|
count.times { context.pop }
|
|
28
59
|
context.auto_close!(count)
|
|
29
60
|
|
|
61
|
+
intervening.reverse_each { |klass| context.push(klass.new) }
|
|
62
|
+
|
|
30
63
|
true
|
|
31
64
|
end
|
|
32
65
|
|
|
@@ -38,24 +71,17 @@ module Markbridge
|
|
|
38
71
|
# @return [Boolean] true if successful, false otherwise
|
|
39
72
|
def try_reorder(handler:, tokens:, context:)
|
|
40
73
|
match_depth = find_matching_handler_depth(handler, context)
|
|
41
|
-
return false if match_depth.nil? || match_depth >= MAX_AUTO_CLOSE_DEPTH
|
|
42
|
-
|
|
43
74
|
opening_handlers = collect_auto_closeable_handlers(context, match_depth)
|
|
44
|
-
return false if opening_handlers.empty?
|
|
45
75
|
|
|
46
|
-
closing_handlers = [handler]
|
|
47
|
-
closing_handlers.concat(peek_closing_handlers(tokens, opening_handlers.size - 1))
|
|
48
|
-
return false if closing_handlers.size != opening_handlers.size
|
|
76
|
+
closing_handlers = [handler, *peek_closing_handlers(tokens, opening_handlers.size - 1)]
|
|
49
77
|
unless opening_handlers.sort_by(&:object_id) == closing_handlers.sort_by(&:object_id)
|
|
50
78
|
return false
|
|
51
79
|
end
|
|
52
80
|
|
|
53
|
-
# Consume the extra closing tags
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
tokens.next
|
|
58
|
-
end
|
|
81
|
+
# Consume the extra closing tags. We've already verified via
|
|
82
|
+
# peek_closing_handlers that the next opening_handlers.size - 1
|
|
83
|
+
# tokens are TagEndTokens with handlers we accept.
|
|
84
|
+
(opening_handlers.size - 1).times { tokens.next }
|
|
59
85
|
|
|
60
86
|
opening_handlers.each { context.pop }
|
|
61
87
|
context.auto_close!(opening_handlers.size)
|
|
@@ -65,15 +91,26 @@ module Markbridge
|
|
|
65
91
|
|
|
66
92
|
private
|
|
67
93
|
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
# Number of stack elements to pop in order to close `handler`, or nil
|
|
95
|
+
# when the handler is not on the stack within MAX_AUTO_CLOSE_DEPTH or
|
|
96
|
+
# any intervening element is not auto-closeable.
|
|
97
|
+
def auto_close_count(handler, context)
|
|
98
|
+
context
|
|
99
|
+
.elements_from_current(MAX_AUTO_CLOSE_DEPTH - 1)
|
|
100
|
+
.each_with_index do |element, depth|
|
|
101
|
+
return nil unless @registry.auto_closeable?(element.class)
|
|
102
|
+
return depth + 1 if @registry.handler_for_element(element) == handler
|
|
103
|
+
end
|
|
70
104
|
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
73
107
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
def find_matching_handler_depth(handler, context)
|
|
109
|
+
context
|
|
110
|
+
.elements_from_current(MAX_AUTO_CLOSE_DEPTH - 1)
|
|
111
|
+
.each_with_index do |element, depth|
|
|
112
|
+
return depth if @registry.handler_for_element(element) == handler
|
|
113
|
+
end
|
|
77
114
|
|
|
78
115
|
nil
|
|
79
116
|
end
|
|
@@ -84,35 +121,18 @@ module Markbridge
|
|
|
84
121
|
.all? { |element| @registry.auto_closeable?(element.class) }
|
|
85
122
|
end
|
|
86
123
|
|
|
124
|
+
# Caller must have verified all_auto_closeable?(context, target_depth) first.
|
|
87
125
|
def collect_auto_closeable_handlers(context, target_depth)
|
|
88
|
-
handlers = []
|
|
89
|
-
|
|
90
126
|
context
|
|
91
127
|
.elements_from_current(target_depth)
|
|
92
|
-
.
|
|
93
|
-
return [] unless @registry.auto_closeable?(element.class)
|
|
94
|
-
|
|
95
|
-
handler = @registry.handler_for_element(element)
|
|
96
|
-
handlers << handler if handler
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
handlers
|
|
128
|
+
.map { |element| @registry.handler_for_element(element) }
|
|
100
129
|
end
|
|
101
130
|
|
|
102
131
|
def peek_closing_handlers(tokens, max_count)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
break unless token.is_a?(TagEndToken)
|
|
108
|
-
|
|
109
|
-
handler = @registry[token.tag]
|
|
110
|
-
break unless handler
|
|
111
|
-
|
|
112
|
-
handlers << handler
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
handlers
|
|
132
|
+
tokens
|
|
133
|
+
.peek_ahead(max_count)
|
|
134
|
+
.take_while { |token| token.instance_of?(TagEndToken) }
|
|
135
|
+
.map { |token| @registry[token.tag] }
|
|
116
136
|
end
|
|
117
137
|
end
|
|
118
138
|
end
|
|
@@ -5,26 +5,33 @@ module Markbridge
|
|
|
5
5
|
module BBCode
|
|
6
6
|
# Registry of BBCode tag handlers
|
|
7
7
|
class HandlerRegistry
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
8
10
|
attr_writer :closing_strategy
|
|
9
11
|
|
|
10
12
|
def initialize(closing_strategy: nil)
|
|
11
13
|
@handlers = {}
|
|
12
|
-
@normalized_tag_cache = {}
|
|
13
14
|
@element_handlers = {}
|
|
14
15
|
@auto_closeable_elements = Set.new
|
|
15
16
|
@closing_strategy = closing_strategy
|
|
16
17
|
end
|
|
17
18
|
|
|
19
|
+
# Iterate over registered (tag_name, handler) pairs.
|
|
20
|
+
# Useful for debugging custom registries — e.g. confirming an override
|
|
21
|
+
# has stuck. Iteration order matches registration order.
|
|
22
|
+
# @yieldparam tag_name [String]
|
|
23
|
+
# @yieldparam handler [BaseHandler]
|
|
24
|
+
# @return [Enumerator] when no block is given
|
|
25
|
+
def each(&block)
|
|
26
|
+
@handlers.each(&block)
|
|
27
|
+
end
|
|
28
|
+
|
|
18
29
|
# Register a handler for one or more tag names and associate it with an element class
|
|
19
30
|
# @param tag_names [String, Array<String>] tag name(s) to register
|
|
20
31
|
# @param handler [BaseHandler] the handler instance
|
|
21
32
|
def register(tag_names, handler)
|
|
22
33
|
element_class = handler.element_class
|
|
23
|
-
Array(tag_names).each
|
|
24
|
-
normalized = tag_name.to_s.downcase
|
|
25
|
-
@handlers[normalized] = handler
|
|
26
|
-
@normalized_tag_cache[tag_name.to_s] = normalized
|
|
27
|
-
end
|
|
34
|
+
Array(tag_names).each { |tag_name| @handlers[tag_name.to_s.downcase] = handler }
|
|
28
35
|
@element_handlers[element_class] = handler
|
|
29
36
|
@auto_closeable_elements << element_class if handler.auto_closeable?
|
|
30
37
|
self
|
|
@@ -34,9 +41,7 @@ module Markbridge
|
|
|
34
41
|
# @param tag_name [String]
|
|
35
42
|
# @return [BaseHandler, nil]
|
|
36
43
|
def [](tag_name)
|
|
37
|
-
|
|
38
|
-
normalized = @normalized_tag_cache[tag_str] || tag_str.downcase
|
|
39
|
-
@handlers[normalized]
|
|
44
|
+
@handlers[tag_name.to_s.downcase]
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
# Get handler for an element instance
|
|
@@ -61,7 +66,12 @@ module Markbridge
|
|
|
61
66
|
@closing_strategy&.handle_close(token:, context:, registry: self, tokens:)
|
|
62
67
|
end
|
|
63
68
|
|
|
64
|
-
# Create the default handler registry with common BBCode tags
|
|
69
|
+
# Create the default handler registry with common BBCode tags.
|
|
70
|
+
#
|
|
71
|
+
# Each call returns a *fresh* instance — mutations made to one will
|
|
72
|
+
# not be visible to another. If you want a process-wide singleton,
|
|
73
|
+
# use {Markbridge.default_handlers} instead, which memoizes.
|
|
74
|
+
#
|
|
65
75
|
# @param closing_strategy [Object, nil] optional closing strategy to apply, defaults to Reordering strategy
|
|
66
76
|
# @return [HandlerRegistry]
|
|
67
77
|
def self.default(closing_strategy: nil)
|
|
@@ -95,7 +105,7 @@ module Markbridge
|
|
|
95
105
|
)
|
|
96
106
|
|
|
97
107
|
# Code handlers (raw content)
|
|
98
|
-
registry.register(%w[code pre tt], Handlers::
|
|
108
|
+
registry.register(%w[code pre tt], Handlers::CodeHandler.new)
|
|
99
109
|
|
|
100
110
|
# Image handler
|
|
101
111
|
registry.register("img", Handlers::ImageHandler.new)
|
|
@@ -38,21 +38,26 @@ module Markbridge
|
|
|
38
38
|
@collector.collect(token.tag, tokens).content
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
CLOSING_TAG_PEEK_DEPTH = 100
|
|
42
|
+
private_constant :CLOSING_TAG_PEEK_DEPTH
|
|
43
|
+
|
|
41
44
|
def closing_tag_ahead?(tag, tokens)
|
|
42
|
-
tokens
|
|
45
|
+
tokens
|
|
46
|
+
.peek_ahead(CLOSING_TAG_PEEK_DEPTH)
|
|
47
|
+
.any? { |token| token.instance_of?(TagEndToken) && token.tag == tag }
|
|
43
48
|
end
|
|
44
49
|
|
|
45
50
|
def build_attachment(token:, content:)
|
|
46
51
|
attrs = normalize_attrs(token.attrs)
|
|
47
52
|
option = attrs[:option]
|
|
48
|
-
body = presence(content
|
|
53
|
+
body = presence(content)
|
|
49
54
|
|
|
50
55
|
id = preferred_id(attrs)
|
|
51
56
|
index = preferred_index(attrs)
|
|
52
57
|
filename = attrs[:filename]
|
|
53
58
|
alt = attrs[:alt]
|
|
54
59
|
|
|
55
|
-
index ||= option if
|
|
60
|
+
index ||= option if numeric?(option)
|
|
56
61
|
id, filename = apply_body_content(body:, id:, index:, filename:)
|
|
57
62
|
|
|
58
63
|
AST::Attachment.new(id:, index:, filename:, alt:)
|
|
@@ -63,8 +68,6 @@ module Markbridge
|
|
|
63
68
|
end
|
|
64
69
|
|
|
65
70
|
def apply_body_content(body:, id:, index:, filename:)
|
|
66
|
-
return id, filename unless body
|
|
67
|
-
|
|
68
71
|
if id.nil?
|
|
69
72
|
return body, filename if index.nil?
|
|
70
73
|
return body, filename if numeric?(body)
|
|
@@ -75,27 +78,29 @@ module Markbridge
|
|
|
75
78
|
[id, filename]
|
|
76
79
|
end
|
|
77
80
|
|
|
81
|
+
# Caller must have applied normalize_attrs first (whitespace already stripped).
|
|
78
82
|
def preferred_id(attrs)
|
|
79
|
-
|
|
83
|
+
attrs[:msg] || attrs[:id]
|
|
80
84
|
end
|
|
81
85
|
|
|
86
|
+
# Caller must have applied normalize_attrs first.
|
|
82
87
|
def preferred_index(attrs)
|
|
83
|
-
|
|
84
|
-
smf_index = presence(attrs[:id]) if attrs[:msg]
|
|
85
|
-
|
|
86
|
-
explicit_index || smf_index
|
|
88
|
+
attrs[:index] || (attrs[:id] if attrs[:msg])
|
|
87
89
|
end
|
|
88
90
|
|
|
91
|
+
# Callers only ever pass String or nil (via #normalize_attrs
|
|
92
|
+
# over token.attrs, which the BBCode scanner populates with
|
|
93
|
+
# String values exclusively), so no defensive non-String
|
|
94
|
+
# passthrough is needed here or in #numeric?.
|
|
89
95
|
def presence(value)
|
|
90
96
|
return if value.nil?
|
|
91
|
-
return value unless value.is_a?(String)
|
|
92
97
|
|
|
93
98
|
stripped = value.strip
|
|
94
99
|
stripped unless stripped.empty?
|
|
95
100
|
end
|
|
96
101
|
|
|
97
102
|
def numeric?(value)
|
|
98
|
-
value
|
|
103
|
+
value&.match?(/\A\d+\z/)
|
|
99
104
|
end
|
|
100
105
|
end
|
|
101
106
|
end
|
|
@@ -5,36 +5,26 @@ module Markbridge
|
|
|
5
5
|
module BBCode
|
|
6
6
|
module Handlers
|
|
7
7
|
class BaseHandler
|
|
8
|
-
# Default opening behavior: create element and push to context
|
|
9
|
-
# Subclasses should override this method
|
|
10
8
|
# @param token [TagStartToken]
|
|
11
9
|
# @param context [ParserState]
|
|
12
10
|
# @param registry [HandlerRegistry]
|
|
13
11
|
# @param tokens [Enumerator, nil]
|
|
14
|
-
# @return [void]
|
|
15
12
|
def on_open(token:, context:, registry:, tokens: nil)
|
|
16
|
-
# Default: do nothing, subclasses override
|
|
17
13
|
end
|
|
18
14
|
|
|
19
|
-
# Default closing behavior: pop matching element from stack
|
|
20
|
-
# Subclasses can override or call super for custom behavior
|
|
21
15
|
# @param token [TagEndToken]
|
|
22
16
|
# @param context [ParserState]
|
|
23
17
|
# @param registry [HandlerRegistry]
|
|
24
18
|
# @param tokens [PeekableEnumerator, nil]
|
|
25
|
-
# @return [void]
|
|
26
19
|
def on_close(token:, context:, registry:, tokens: nil)
|
|
27
20
|
registry.close_element(token:, context:, tokens:)
|
|
28
21
|
end
|
|
29
22
|
|
|
30
|
-
# Whether elements created by this handler can be auto-closed
|
|
31
23
|
# @return [Boolean]
|
|
32
24
|
def auto_closeable?
|
|
33
25
|
false
|
|
34
26
|
end
|
|
35
27
|
|
|
36
|
-
# The element class created by this handler
|
|
37
|
-
# Subclasses must expose this via attr_reader :element_class
|
|
38
28
|
# @return [Class]
|
|
39
29
|
attr_reader :element_class
|
|
40
30
|
end
|
|
@@ -4,16 +4,12 @@ module Markbridge
|
|
|
4
4
|
module Parsers
|
|
5
5
|
module BBCode
|
|
6
6
|
module Handlers
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# [code=python]
|
|
14
|
-
# def hello_world():
|
|
15
|
-
# print("Hello, world!")
|
|
16
|
-
# [/code]
|
|
7
|
+
# @example
|
|
8
|
+
# # [code=python]
|
|
9
|
+
# # def hello_world
|
|
10
|
+
# # puts "hi"
|
|
11
|
+
# # end
|
|
12
|
+
# # [/code]
|
|
17
13
|
class CodeHandler < RawHandler
|
|
18
14
|
def initialize
|
|
19
15
|
super(AST::Code)
|
|
@@ -22,29 +22,21 @@ module Markbridge
|
|
|
22
22
|
AST::Image.new(src: content, width:, height:)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
height = sanitize_dimension(token.attrs[:height])
|
|
25
|
+
OPTION_DIMENSIONS_PATTERN = /\A(?<width>\d+)(?:x(?<height>\d+))?\z/i
|
|
26
|
+
private_constant :OPTION_DIMENSIONS_PATTERN
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
parts = option.split("x", 2)
|
|
32
|
-
width = sanitize_dimension(parts[0])
|
|
33
|
-
height = sanitize_dimension(parts[1])
|
|
34
|
-
elsif option&.match?(/^\d+$/)
|
|
35
|
-
width = sanitize_dimension(option)
|
|
36
|
-
end
|
|
28
|
+
def extract_dimensions(token)
|
|
29
|
+
option_match = OPTION_DIMENSIONS_PATTERN.match(token.attrs[:option])
|
|
37
30
|
|
|
38
|
-
[
|
|
31
|
+
[
|
|
32
|
+
sanitize_dimension(option_match&.[](:width) || token.attrs[:width]),
|
|
33
|
+
sanitize_dimension(option_match&.[](:height) || token.attrs[:height]),
|
|
34
|
+
]
|
|
39
35
|
end
|
|
40
36
|
|
|
41
|
-
# Convert dimension to positive integer or nil
|
|
42
|
-
# Handles string input from BBCode attributes
|
|
43
37
|
def sanitize_dimension(value)
|
|
44
|
-
return nil if value.nil?
|
|
45
|
-
|
|
46
38
|
dim = value.to_i
|
|
47
|
-
dim.positive?
|
|
39
|
+
dim if dim.positive?
|
|
48
40
|
end
|
|
49
41
|
end
|
|
50
42
|
end
|
|
@@ -11,7 +11,6 @@ module Markbridge
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def on_open(token:, context:, registry:, tokens: nil)
|
|
14
|
-
# Check if ordered: explicit ol/olist tag, or type=1, or option=1
|
|
15
14
|
ordered =
|
|
16
15
|
%w[ol olist].include?(token.tag) || token.attrs[:type] == "1" ||
|
|
17
16
|
token.attrs[:option] == "1"
|
|
@@ -21,10 +20,7 @@ module Markbridge
|
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
def on_close(token:, context:, registry:, tokens: nil)
|
|
24
|
-
|
|
25
|
-
context.pop if context.current.is_a?(AST::ListItem)
|
|
26
|
-
|
|
27
|
-
# Then use default closing behavior
|
|
23
|
+
context.pop if context.current.instance_of?(AST::ListItem)
|
|
28
24
|
super
|
|
29
25
|
end
|
|
30
26
|
|
|
@@ -11,8 +11,7 @@ module Markbridge
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def on_open(token:, context:, registry:, tokens: nil)
|
|
14
|
-
|
|
15
|
-
context.pop if context.current.is_a?(AST::ListItem)
|
|
14
|
+
context.pop if context.current.instance_of?(AST::ListItem)
|
|
16
15
|
|
|
17
16
|
element = AST::ListItem.new
|
|
18
17
|
context.push(element, token:)
|