markbridge 0.1.0 → 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.
- 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 +61 -0
- data/lib/markbridge/ast/text.rb +5 -1
- data/lib/markbridge/ast.rb +1 -0
- 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 +26 -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 +13 -19
- 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 +30 -35
- 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 +26 -0
- data/lib/markbridge/parsers/bbcode/handlers/table_handler.rb +32 -0
- data/lib/markbridge/parsers/bbcode/handlers/table_row_handler.rb +35 -0
- 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 +4 -0
- data/lib/markbridge/parsers/html/handler_registry.rb +32 -44
- data/lib/markbridge/parsers/html/handlers/base_handler.rb +0 -3
- data/lib/markbridge/parsers/html/handlers/image_handler.rb +1 -4
- data/lib/markbridge/parsers/html/handlers/table_cell_handler.rb +24 -0
- data/lib/markbridge/parsers/html/handlers/table_handler.rb +24 -0
- data/lib/markbridge/parsers/html/handlers/table_row_handler.rb +24 -0
- data/lib/markbridge/parsers/html/parser.rb +16 -15
- data/lib/markbridge/parsers/html.rb +3 -0
- data/lib/markbridge/parsers/media_wiki/inline_parser.rb +115 -151
- data/lib/markbridge/parsers/media_wiki/inline_tag_registry.rb +103 -0
- data/lib/markbridge/parsers/media_wiki/parser.rb +174 -71
- data/lib/markbridge/parsers/media_wiki.rb +1 -0
- data/lib/markbridge/parsers/text_formatter/handler_registry.rb +10 -36
- data/lib/markbridge/parsers/text_formatter/handlers/table_cell_handler.rb +26 -0
- data/lib/markbridge/parsers/text_formatter/parser.rb +3 -8
- data/lib/markbridge/parsers/text_formatter.rb +1 -0
- data/lib/markbridge/processors/discourse_markdown/code_block_tracker.rb +111 -92
- data/lib/markbridge/processors/discourse_markdown/detectors/base.rb +13 -7
- data/lib/markbridge/processors/discourse_markdown/detectors/event.rb +11 -20
- data/lib/markbridge/processors/discourse_markdown/detectors/poll.rb +10 -48
- data/lib/markbridge/processors/discourse_markdown/detectors/upload.rb +38 -63
- data/lib/markbridge/processors/discourse_markdown/scanner.rb +36 -41
- 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 +262 -205
- data/lib/markbridge/renderers/discourse/render_context.rb +23 -11
- data/lib/markbridge/renderers/discourse/renderer.rb +54 -11
- 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/attachment_tag.rb +1 -1
- data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +2 -0
- data/lib/markbridge/renderers/discourse/tags/code_tag.rb +14 -8
- data/lib/markbridge/renderers/discourse/tags/email_tag.rb +5 -3
- data/lib/markbridge/renderers/discourse/tags/event_tag.rb +3 -3
- 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 +12 -1
- 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 +6 -2
- data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +10 -0
- data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +9 -4
- data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +17 -11
- 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_cell_tag.rb +18 -0
- data/lib/markbridge/renderers/discourse/tags/table_row_tag.rb +18 -0
- data/lib/markbridge/renderers/discourse/tags/table_tag.rb +128 -0
- data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +10 -3
- data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +28 -1
- data/lib/markbridge/renderers/discourse/tags/url_tag.rb +5 -3
- data/lib/markbridge/renderers/discourse.rb +4 -0
- data/lib/markbridge/textformatter.rb +4 -0
- data/lib/markbridge/version.rb +1 -1
- data/lib/markbridge.rb +27 -62
- metadata +19 -2
|
@@ -31,11 +31,12 @@ module Markbridge
|
|
|
31
31
|
|
|
32
32
|
def initialize
|
|
33
33
|
@in_fenced_block = false
|
|
34
|
-
@fence_char = nil
|
|
35
|
-
@fence_length = 0
|
|
36
34
|
@in_indented_block = false
|
|
37
35
|
@in_inline_code = false
|
|
38
|
-
@inline_delimiter
|
|
36
|
+
# @fence_char / @fence_length / @inline_delimiter are set by
|
|
37
|
+
# open_fence / open_inline before any helper reads them;
|
|
38
|
+
# they're only consulted when the corresponding in_X flag is
|
|
39
|
+
# true, which requires a prior open_* call.
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
# Check if currently inside any code context
|
|
@@ -52,55 +53,21 @@ module Markbridge
|
|
|
52
53
|
def check_fenced_boundary(input, pos, line_start:)
|
|
53
54
|
return nil unless line_start
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
scan_pos = pos
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
scan_pos += 1
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
return nil if scan_pos >= input.length
|
|
64
|
-
|
|
56
|
+
input_length = input.length
|
|
57
|
+
scan_pos = skip_leading_spaces(input, pos)
|
|
58
|
+
# No `scan_pos >= input_length` guard: `input[input_length]` is
|
|
59
|
+
# nil, and `nil == "`"` / `nil == "~"` are both false so the
|
|
60
|
+
# next check returns nil anyway.
|
|
65
61
|
fence_char = input[scan_pos]
|
|
66
62
|
return nil unless fence_char == "`" || fence_char == "~"
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
fence_start = scan_pos
|
|
70
|
-
fence_length = 0
|
|
71
|
-
while scan_pos < input.length && input[scan_pos] == fence_char
|
|
72
|
-
fence_length += 1
|
|
73
|
-
scan_pos += 1
|
|
74
|
-
end
|
|
75
|
-
|
|
64
|
+
fence_length, scan_pos = count_fence_chars(input, scan_pos, fence_char, input_length)
|
|
76
65
|
return nil if fence_length < 3
|
|
77
66
|
|
|
78
67
|
if @in_fenced_block
|
|
79
|
-
|
|
80
|
-
if fence_char == @fence_char && fence_length >= @fence_length
|
|
81
|
-
# Closing fence - must be followed by newline or end of input
|
|
82
|
-
# Skip any trailing whitespace
|
|
83
|
-
scan_pos += 1 while scan_pos < input.length && input[scan_pos] == " "
|
|
84
|
-
|
|
85
|
-
if scan_pos >= input.length || input[scan_pos] == "\n"
|
|
86
|
-
@in_fenced_block = false
|
|
87
|
-
@fence_char = nil
|
|
88
|
-
@fence_length = 0
|
|
89
|
-
# Return position after the newline if present
|
|
90
|
-
return scan_pos < input.length ? scan_pos + 1 : scan_pos
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
nil
|
|
68
|
+
try_close_fence(input, scan_pos, fence_char, fence_length, input_length)
|
|
94
69
|
else
|
|
95
|
-
|
|
96
|
-
scan_pos += 1 while scan_pos < input.length && input[scan_pos] != "\n"
|
|
97
|
-
|
|
98
|
-
@in_fenced_block = true
|
|
99
|
-
@fence_char = fence_char
|
|
100
|
-
@fence_length = fence_length
|
|
101
|
-
|
|
102
|
-
# Return position after the newline if present
|
|
103
|
-
scan_pos < input.length ? scan_pos + 1 : scan_pos
|
|
70
|
+
open_fence(input, scan_pos, fence_char, fence_length, input_length)
|
|
104
71
|
end
|
|
105
72
|
end
|
|
106
73
|
|
|
@@ -116,38 +83,23 @@ module Markbridge
|
|
|
116
83
|
return nil unless line_start
|
|
117
84
|
return nil if @in_fenced_block # Fenced blocks take precedence
|
|
118
85
|
|
|
119
|
-
|
|
120
|
-
line_end = input.index("\n", pos) ||
|
|
121
|
-
|
|
122
|
-
# Check if line is blank
|
|
86
|
+
input_length = input.length
|
|
87
|
+
line_end = input.index("\n", pos) || input_length
|
|
123
88
|
line_content = input[pos...line_end]
|
|
124
89
|
is_blank = line_content.match?(/\A\s*\z/)
|
|
125
|
-
|
|
126
|
-
# Check indentation (4+ spaces or tab)
|
|
127
90
|
has_code_indent = line_content.start_with?(" ") || line_content.start_with?("\t")
|
|
128
91
|
|
|
129
92
|
if @in_indented_block
|
|
130
|
-
if is_blank
|
|
131
|
-
|
|
132
|
-
# Return end of line (after newline if present)
|
|
133
|
-
return line_end < input.length ? line_end + 1 : line_end
|
|
134
|
-
elsif has_code_indent
|
|
135
|
-
# Still in indented code
|
|
136
|
-
return line_end < input.length ? line_end + 1 : line_end
|
|
93
|
+
if is_blank || has_code_indent
|
|
94
|
+
pos_after_line(line_end, input_length)
|
|
137
95
|
else
|
|
138
|
-
# Non-blank, non-indented line ends the block
|
|
139
96
|
@in_indented_block = false
|
|
140
|
-
|
|
141
|
-
end
|
|
142
|
-
else
|
|
143
|
-
if has_code_indent
|
|
144
|
-
# Start of indented code block
|
|
145
|
-
@in_indented_block = true
|
|
146
|
-
return line_end < input.length ? line_end + 1 : line_end
|
|
97
|
+
nil
|
|
147
98
|
end
|
|
99
|
+
elsif has_code_indent
|
|
100
|
+
@in_indented_block = true
|
|
101
|
+
pos_after_line(line_end, input_length)
|
|
148
102
|
end
|
|
149
|
-
|
|
150
|
-
nil
|
|
151
103
|
end
|
|
152
104
|
|
|
153
105
|
# Check for inline code boundary
|
|
@@ -156,42 +108,109 @@ module Markbridge
|
|
|
156
108
|
# @return [Integer, nil] end position after inline code, or nil if not at boundary
|
|
157
109
|
def check_inline_boundary(input, pos)
|
|
158
110
|
return nil if @in_fenced_block || @in_indented_block
|
|
159
|
-
|
|
111
|
+
# No `pos >= input_length` guard: `input[input_length]` is nil,
|
|
112
|
+
# and `nil != "`"` is true so the next check returns nil anyway.
|
|
113
|
+
return nil if input[pos] != "`"
|
|
160
114
|
|
|
115
|
+
input_length = input.length
|
|
161
116
|
if @in_inline_code
|
|
162
|
-
|
|
163
|
-
delimiter_length = @inline_delimiter.length
|
|
164
|
-
if input[pos, delimiter_length] == @inline_delimiter
|
|
165
|
-
# Check what follows - should not be another backtick
|
|
166
|
-
next_pos = pos + delimiter_length
|
|
167
|
-
if next_pos >= input.length || input[next_pos] != "`"
|
|
168
|
-
@in_inline_code = false
|
|
169
|
-
@inline_delimiter = nil
|
|
170
|
-
return next_pos
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
nil
|
|
117
|
+
try_close_inline(input, pos, input_length)
|
|
174
118
|
else
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
119
|
+
open_inline(input, pos, input_length)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
178
122
|
|
|
179
|
-
|
|
180
|
-
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# Skip up to 3 leading spaces of indentation.
|
|
126
|
+
def skip_leading_spaces(input, pos)
|
|
127
|
+
scan_pos = pos
|
|
128
|
+
spaces = 0
|
|
129
|
+
while spaces < 3 && input[scan_pos] == " "
|
|
130
|
+
spaces += 1
|
|
131
|
+
scan_pos += 1
|
|
132
|
+
end
|
|
133
|
+
scan_pos
|
|
134
|
+
end
|
|
181
135
|
|
|
182
|
-
|
|
183
|
-
|
|
136
|
+
# Count consecutive fence characters and return [count, new_position].
|
|
137
|
+
def count_fence_chars(input, scan_pos, fence_char, input_length)
|
|
138
|
+
fence_length = 0
|
|
139
|
+
while scan_pos < input_length && input[scan_pos] == fence_char
|
|
140
|
+
fence_length += 1
|
|
141
|
+
scan_pos += 1
|
|
184
142
|
end
|
|
143
|
+
[fence_length, scan_pos]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Try to close an open fenced code block. Returns position after fence or nil.
|
|
147
|
+
def try_close_fence(input, scan_pos, fence_char, fence_length, input_length)
|
|
148
|
+
return nil unless fence_char == @fence_char && fence_length >= @fence_length
|
|
149
|
+
|
|
150
|
+
# Closing fence must be followed only by spaces then newline/EOF
|
|
151
|
+
scan_pos += 1 while scan_pos < input_length && input[scan_pos] == " "
|
|
152
|
+
return nil unless scan_pos >= input_length || input[scan_pos] == "\n"
|
|
153
|
+
|
|
154
|
+
# @fence_char / @fence_length are not reset here: they are only
|
|
155
|
+
# consulted while @in_fenced_block is true, and open_fence
|
|
156
|
+
# overwrites them on the next opening.
|
|
157
|
+
@in_fenced_block = false
|
|
158
|
+
pos_after_line(scan_pos, input_length)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Open a new fenced code block. Returns position after the opening line.
|
|
162
|
+
def open_fence(input, scan_pos, fence_char, fence_length, input_length)
|
|
163
|
+
# Skip to end of line (info string)
|
|
164
|
+
scan_pos += 1 while scan_pos < input_length && input[scan_pos] != "\n"
|
|
165
|
+
|
|
166
|
+
@in_fenced_block = true
|
|
167
|
+
@fence_char = fence_char
|
|
168
|
+
@fence_length = fence_length
|
|
169
|
+
pos_after_line(scan_pos, input_length)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Try to close inline code. Returns position after delimiter or nil.
|
|
173
|
+
def try_close_inline(input, pos, input_length)
|
|
174
|
+
delimiter_length = @inline_delimiter.length
|
|
175
|
+
return nil unless input[pos, delimiter_length] == @inline_delimiter
|
|
176
|
+
|
|
177
|
+
# Should not be followed by another backtick
|
|
178
|
+
next_pos = pos + delimiter_length
|
|
179
|
+
return nil if next_pos < input_length && input[next_pos] == "`"
|
|
180
|
+
|
|
181
|
+
# @inline_delimiter is not reset here: it is only consulted
|
|
182
|
+
# while @in_inline_code is true, and open_inline overwrites it
|
|
183
|
+
# on the next opening.
|
|
184
|
+
@in_inline_code = false
|
|
185
|
+
next_pos
|
|
185
186
|
end
|
|
186
187
|
|
|
187
|
-
#
|
|
188
|
+
# Open inline code. Returns position after opening delimiter.
|
|
189
|
+
def open_inline(input, pos, input_length)
|
|
190
|
+
delimiter_start = pos
|
|
191
|
+
pos += 1 while pos < input_length && input[pos] == "`"
|
|
192
|
+
|
|
193
|
+
@inline_delimiter = input[delimiter_start...pos]
|
|
194
|
+
@in_inline_code = true
|
|
195
|
+
pos
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Return position after a line (after newline if present, otherwise at end).
|
|
199
|
+
def pos_after_line(line_end, input_length)
|
|
200
|
+
line_end < input_length ? line_end + 1 : line_end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
public
|
|
204
|
+
|
|
205
|
+
# Reset the tracker state. The @fence_char / @fence_length /
|
|
206
|
+
# @inline_delimiter companions are not cleared: they're only
|
|
207
|
+
# consulted while the corresponding in_* flag is true, and
|
|
208
|
+
# open_fence / open_inline overwrites them on the next
|
|
209
|
+
# opening (same pattern as try_close_fence / try_close_inline).
|
|
188
210
|
def reset!
|
|
189
211
|
@in_fenced_block = false
|
|
190
|
-
@fence_char = nil
|
|
191
|
-
@fence_length = 0
|
|
192
212
|
@in_indented_block = false
|
|
193
213
|
@in_inline_code = false
|
|
194
|
-
@inline_delimiter = nil
|
|
195
214
|
end
|
|
196
215
|
end
|
|
197
216
|
end
|
|
@@ -38,17 +38,23 @@ module Markbridge
|
|
|
38
38
|
!prev_char.match?(/\w/)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
WORD_PATTERN = /\A[\w\-]*/
|
|
42
|
+
private_constant :WORD_PATTERN
|
|
43
|
+
|
|
44
|
+
# Helper to extract a word starting at position. Caller must ensure
|
|
45
|
+
# pos is within input bounds (`pos <= input.length`).
|
|
42
46
|
# @param input [String] the input string
|
|
43
47
|
# @param pos [Integer] starting position
|
|
44
48
|
# @return [String] the word (may be empty)
|
|
45
49
|
def extract_word(input, pos)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
input[pos..].match(WORD_PATTERN)[0]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Parse key="value" or key='value' attribute pairs from a string.
|
|
54
|
+
# @param attr_string [String] the attribute string to parse
|
|
55
|
+
# @return [Hash<String, String>] parsed attributes with downcased keys
|
|
56
|
+
def parse_attributes(attr_string)
|
|
57
|
+
attr_string.scan(/(\w+)=["']([^"']*)["']/).to_h.transform_keys(&:downcase)
|
|
52
58
|
end
|
|
53
59
|
end
|
|
54
60
|
end
|
|
@@ -12,7 +12,7 @@ module Markbridge
|
|
|
12
12
|
# match = detector.detect(input, 0)
|
|
13
13
|
# match.node.name # => "Meeting"
|
|
14
14
|
class Event < Base
|
|
15
|
-
OPEN_TAG_PATTERN = /\[event([^\]]*)\]/i
|
|
15
|
+
OPEN_TAG_PATTERN = /\[event(?<attrs>[^\]]*)\]/i
|
|
16
16
|
CLOSE_TAG_PATTERN = %r{\[/event\]}i
|
|
17
17
|
|
|
18
18
|
# Attempt to detect an event at the given position.
|
|
@@ -21,15 +21,14 @@ module Markbridge
|
|
|
21
21
|
# @param pos [Integer] current position to check
|
|
22
22
|
# @return [Match, nil] match result or nil if no match
|
|
23
23
|
def detect(input, pos)
|
|
24
|
-
return nil unless input[pos] == "["
|
|
25
|
-
|
|
26
|
-
# Check for opening tag
|
|
27
24
|
remaining = input[pos..]
|
|
28
25
|
open_match = OPEN_TAG_PATTERN.match(remaining)
|
|
29
26
|
return nil unless open_match&.begin(0)&.zero?
|
|
30
27
|
|
|
31
|
-
# Find closing tag
|
|
32
|
-
|
|
28
|
+
# Find closing tag. The opening tag pattern forbids `]` between
|
|
29
|
+
# `[event` and its closing `]`, so `[/event]` cannot appear inside
|
|
30
|
+
# the opening tag - no need to skip past it.
|
|
31
|
+
close_match = CLOSE_TAG_PATTERN.match(remaining)
|
|
33
32
|
return nil unless close_match
|
|
34
33
|
|
|
35
34
|
# Extract raw content
|
|
@@ -37,15 +36,17 @@ module Markbridge
|
|
|
37
36
|
raw = input[pos...end_pos]
|
|
38
37
|
|
|
39
38
|
# Parse attributes from opening tag
|
|
40
|
-
attrs = parse_attributes(open_match[
|
|
39
|
+
attrs = parse_attributes(open_match[:attrs])
|
|
41
40
|
|
|
42
41
|
# Validate required attributes
|
|
43
|
-
|
|
42
|
+
name = attrs["name"]
|
|
43
|
+
starts_at = attrs["start"]
|
|
44
|
+
return nil if name.nil? || starts_at.nil?
|
|
44
45
|
|
|
45
46
|
node =
|
|
46
47
|
AST::Event.new(
|
|
47
|
-
name
|
|
48
|
-
starts_at
|
|
48
|
+
name:,
|
|
49
|
+
starts_at:,
|
|
49
50
|
ends_at: attrs["end"],
|
|
50
51
|
status: attrs["status"],
|
|
51
52
|
timezone: attrs["timezone"],
|
|
@@ -56,16 +57,6 @@ module Markbridge
|
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
private
|
|
59
|
-
|
|
60
|
-
def parse_attributes(attr_string)
|
|
61
|
-
attrs = {}
|
|
62
|
-
return attrs if attr_string.nil? || attr_string.empty?
|
|
63
|
-
|
|
64
|
-
# Match key="value" or key='value' patterns
|
|
65
|
-
attr_string.scan(/(\w+)=["']([^"']*)["']/) { |key, value| attrs[key.downcase] = value }
|
|
66
|
-
|
|
67
|
-
attrs
|
|
68
|
-
end
|
|
69
60
|
end
|
|
70
61
|
end
|
|
71
62
|
end
|
|
@@ -12,8 +12,7 @@ module Markbridge
|
|
|
12
12
|
# match = detector.detect(input, 0)
|
|
13
13
|
# match.node.type # => "regular"
|
|
14
14
|
class Poll < Base
|
|
15
|
-
|
|
16
|
-
CLOSE_TAG_PATTERN = %r{\[/poll\]}i
|
|
15
|
+
TAG_PATTERN = %r{\A\[poll(?<attrs>[^\]]*)\](?<content>.*?)\[/poll\]}im
|
|
17
16
|
|
|
18
17
|
# Attempt to detect a poll at the given position.
|
|
19
18
|
#
|
|
@@ -21,67 +20,30 @@ module Markbridge
|
|
|
21
20
|
# @param pos [Integer] current position to check
|
|
22
21
|
# @return [Match, nil] match result or nil if no match
|
|
23
22
|
def detect(input, pos)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Check for opening tag
|
|
27
|
-
remaining = input[pos..]
|
|
28
|
-
open_match = OPEN_TAG_PATTERN.match(remaining)
|
|
29
|
-
return nil unless open_match&.begin(0)&.zero?
|
|
30
|
-
|
|
31
|
-
# Find closing tag
|
|
32
|
-
close_match = CLOSE_TAG_PATTERN.match(remaining, open_match.end(0))
|
|
33
|
-
return nil unless close_match
|
|
34
|
-
|
|
35
|
-
# Extract raw content
|
|
36
|
-
end_pos = pos + close_match.end(0)
|
|
37
|
-
raw = input[pos...end_pos]
|
|
38
|
-
|
|
39
|
-
# Parse attributes from opening tag
|
|
40
|
-
attrs = parse_attributes(open_match[1])
|
|
41
|
-
|
|
42
|
-
# Extract options from content between tags
|
|
43
|
-
content = remaining[open_match.end(0)...close_match.begin(0)]
|
|
44
|
-
options = extract_options(content)
|
|
23
|
+
match = TAG_PATTERN.match(input[pos..])
|
|
24
|
+
return nil unless match
|
|
45
25
|
|
|
26
|
+
attrs = parse_attributes(match[:attrs])
|
|
46
27
|
node =
|
|
47
28
|
AST::Poll.new(
|
|
48
29
|
name: attrs["name"] || "poll",
|
|
49
30
|
type: attrs["type"],
|
|
50
31
|
results: attrs["results"],
|
|
51
32
|
public: attrs["public"] == "true",
|
|
52
|
-
chart_type: attrs["charttype"]
|
|
53
|
-
options
|
|
54
|
-
raw
|
|
33
|
+
chart_type: attrs["charttype"],
|
|
34
|
+
options: extract_options(match[:content]),
|
|
35
|
+
raw: match[0],
|
|
55
36
|
)
|
|
56
37
|
|
|
57
|
-
Match.new(start_pos: pos, end_pos
|
|
38
|
+
Match.new(start_pos: pos, end_pos: pos + match.end(0), node:)
|
|
58
39
|
end
|
|
59
40
|
|
|
60
41
|
private
|
|
61
42
|
|
|
62
|
-
|
|
63
|
-
attrs = {}
|
|
64
|
-
return attrs if attr_string.nil? || attr_string.empty?
|
|
65
|
-
|
|
66
|
-
# Match key="value" or key='value' patterns
|
|
67
|
-
attr_string.scan(/(\w+)=["']([^"']*)["']/) { |key, value| attrs[key.downcase] = value }
|
|
68
|
-
|
|
69
|
-
attrs
|
|
70
|
-
end
|
|
43
|
+
OPTION_PATTERN = /\A\s*(?:\*\s|-\s|\d+\.\s+)(?<value>.+?)\s*\z/
|
|
71
44
|
|
|
72
45
|
def extract_options(content)
|
|
73
|
-
|
|
74
|
-
content.each_line do |line|
|
|
75
|
-
line = line.strip
|
|
76
|
-
if line.start_with?("* ")
|
|
77
|
-
options << line[2..].strip
|
|
78
|
-
elsif line.start_with?("- ")
|
|
79
|
-
options << line[2..].strip
|
|
80
|
-
elsif line.match?(/^\d+\.\s/)
|
|
81
|
-
options << line.sub(/^\d+\.\s*/, "").strip
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
options
|
|
46
|
+
content.each_line.filter_map { |line| OPTION_PATTERN.match(line)&.[](:value) }
|
|
85
47
|
end
|
|
86
48
|
end
|
|
87
49
|
end
|
|
@@ -22,11 +22,18 @@ module Markbridge
|
|
|
22
22
|
# match = detector.detect(input, 0)
|
|
23
23
|
# match.node.type # => :attachment
|
|
24
24
|
class Upload < Base
|
|
25
|
-
#
|
|
26
|
-
IMAGE_PATTERN =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
# Image: 
|
|
26
|
+
IMAGE_PATTERN =
|
|
27
|
+
%r{\A!\[(?<alt>[^|\]]*)(?:\|(?<dimensions>[^\]]*))?\]\(upload://(?<url>[^)]+)\)}
|
|
28
|
+
|
|
29
|
+
# Attachment: [filename|attachment](upload://sha1.ext) (size)
|
|
30
|
+
ATTACHMENT_PATTERN =
|
|
31
|
+
%r{
|
|
32
|
+
\A
|
|
33
|
+
\[(?<filename>[^|\]]*)\|attachment\]
|
|
34
|
+
\(upload://(?<url>[^)]+)\)
|
|
35
|
+
(?:\s*\((?<size>[^)]+)\))?
|
|
36
|
+
}xi
|
|
30
37
|
|
|
31
38
|
# Attempt to detect an upload at the given position.
|
|
32
39
|
#
|
|
@@ -34,14 +41,11 @@ module Markbridge
|
|
|
34
41
|
# @param pos [Integer] current position to check
|
|
35
42
|
# @return [Match, nil] match result or nil if no match
|
|
36
43
|
def detect(input, pos)
|
|
37
|
-
char = input[pos]
|
|
38
|
-
return nil unless char == "!" || char == "["
|
|
39
|
-
|
|
40
44
|
remaining = input[pos..]
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
case input[pos]
|
|
46
|
+
when "!"
|
|
43
47
|
detect_image(remaining, pos)
|
|
44
|
-
|
|
48
|
+
when "["
|
|
45
49
|
detect_attachment(remaining, pos)
|
|
46
50
|
end
|
|
47
51
|
end
|
|
@@ -50,71 +54,42 @@ module Markbridge
|
|
|
50
54
|
|
|
51
55
|
def detect_image(remaining, pos)
|
|
52
56
|
match = IMAGE_PATTERN.match(remaining)
|
|
53
|
-
return nil unless match
|
|
54
|
-
|
|
55
|
-
raw = match[0]
|
|
56
|
-
alt_part = match[1]
|
|
57
|
-
url_part = match[2]
|
|
57
|
+
return nil unless match
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
alt
|
|
59
|
+
sha1, filename = parse_upload_url(match[:url])
|
|
60
|
+
alt = match[:alt]
|
|
61
|
+
alt = nil if alt.empty?
|
|
61
62
|
|
|
62
|
-
#
|
|
63
|
-
|
|
63
|
+
# `type: :image` is omitted because it is AST::Upload's default -
|
|
64
|
+
# passing it explicitly was an equivalent-mutation surface.
|
|
65
|
+
node =
|
|
66
|
+
AST::Upload.new(sha1:, filename:, alt:, dimensions: match[:dimensions], raw: match[0])
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Match.new(start_pos: pos, end_pos: pos + raw.length, node:)
|
|
68
|
+
Match.new(start_pos: pos, end_pos: pos + match[0].length, node:)
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
def detect_attachment(remaining, pos)
|
|
71
72
|
match = ATTACHMENT_PATTERN.match(remaining)
|
|
72
|
-
return nil unless match
|
|
73
|
-
|
|
74
|
-
raw = match[0]
|
|
75
|
-
name_part = match[1]
|
|
76
|
-
url_part = match[2]
|
|
77
|
-
size_part = match[3]
|
|
78
|
-
|
|
79
|
-
# Parse filename from "filename|attachment" format
|
|
80
|
-
filename = name_part.sub(/\|attachment$/i, "")
|
|
73
|
+
return nil unless match
|
|
81
74
|
|
|
82
|
-
|
|
83
|
-
sha1, _url_filename = parse_upload_url(url_part)
|
|
75
|
+
sha1, = parse_upload_url(match[:url])
|
|
84
76
|
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
node =
|
|
78
|
+
AST::Upload.new(
|
|
79
|
+
sha1:,
|
|
80
|
+
filename: match[:filename],
|
|
81
|
+
type: :attachment,
|
|
82
|
+
size: match[:size],
|
|
83
|
+
raw: match[0],
|
|
84
|
+
)
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
Match.new(start_pos: pos, end_pos: pos + raw.length, node:)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def parse_alt_dimensions(alt_part)
|
|
94
|
-
return nil, nil if alt_part.nil? || alt_part.empty?
|
|
95
|
-
|
|
96
|
-
if alt_part.include?("|")
|
|
97
|
-
parts = alt_part.split("|", 2)
|
|
98
|
-
alt = parts[0].empty? ? nil : parts[0]
|
|
99
|
-
dimensions = parts[1]
|
|
100
|
-
[alt, dimensions]
|
|
101
|
-
else
|
|
102
|
-
[alt_part, nil]
|
|
103
|
-
end
|
|
86
|
+
Match.new(start_pos: pos, end_pos: pos + match[0].length, node:)
|
|
104
87
|
end
|
|
105
88
|
|
|
89
|
+
# URL format: sha1.ext or just sha1. Returns [sha1, filename-or-nil].
|
|
106
90
|
def parse_upload_url(url_part)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
parts = url_part.split(".", 2)
|
|
110
|
-
sha1 = parts[0]
|
|
111
|
-
filename = url_part
|
|
112
|
-
else
|
|
113
|
-
sha1 = url_part
|
|
114
|
-
filename = nil
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
[sha1, filename]
|
|
91
|
+
sha1, _, ext = url_part.partition(".")
|
|
92
|
+
[sha1, ext.empty? ? nil : url_part]
|
|
118
93
|
end
|
|
119
94
|
end
|
|
120
95
|
end
|