markbridge 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/markbridge/ast/image.rb +9 -1
- data/lib/markbridge/ast/url.rb +29 -4
- data/lib/markbridge/errors.rb +9 -0
- data/lib/markbridge/parsers/bbcode/handler_registry.rb +6 -1
- data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +2 -1
- data/lib/markbridge/parsers/bbcode/handlers/url_handler.rb +26 -2
- data/lib/markbridge/parsers/html/handlers/image_handler.rb +4 -1
- data/lib/markbridge/parsers/html/parser.rb +57 -19
- data/lib/markbridge/parsers/text_formatter/handlers/attachment_handler.rb +3 -2
- data/lib/markbridge/parsers/text_formatter/handlers/image_handler.rb +4 -2
- data/lib/markbridge/parsers/text_formatter/parser.rb +107 -14
- data/lib/markbridge/renderers/discourse/html_block.rb +50 -7
- data/lib/markbridge/renderers/discourse/inline/joins.rb +382 -0
- data/lib/markbridge/renderers/discourse/inline/serializer.rb +738 -0
- data/lib/markbridge/renderers/discourse/inline/tokens.rb +182 -0
- data/lib/markbridge/renderers/discourse/link_destination.rb +43 -0
- data/lib/markbridge/renderers/discourse/markdown_escaper.rb +105 -45
- data/lib/markbridge/renderers/discourse/paragraphs.rb +88 -0
- data/lib/markbridge/renderers/discourse/postprocessor.rb +183 -7
- data/lib/markbridge/renderers/discourse/render_context.rb +38 -12
- data/lib/markbridge/renderers/discourse/renderer.rb +336 -92
- data/lib/markbridge/renderers/discourse/rendering_interface.rb +123 -44
- data/lib/markbridge/renderers/discourse/tag.rb +53 -9
- data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +4 -6
- data/lib/markbridge/renderers/discourse/tags/code_tag.rb +48 -10
- data/lib/markbridge/renderers/discourse/tags/color_tag.rb +73 -13
- data/lib/markbridge/renderers/discourse/tags/details_tag.rb +62 -15
- data/lib/markbridge/renderers/discourse/tags/email_tag.rb +8 -9
- data/lib/markbridge/renderers/discourse/tags/event_tag.rb +31 -9
- data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +20 -1
- data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +12 -1
- data/lib/markbridge/renderers/discourse/tags/image_tag.rb +13 -27
- data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +4 -6
- data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +9 -2
- data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +75 -9
- data/lib/markbridge/renderers/discourse/tags/list_tag.rb +29 -7
- data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +12 -0
- data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +13 -6
- data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +52 -9
- data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +26 -1
- data/lib/markbridge/renderers/discourse/tags/size_tag.rb +78 -13
- data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +72 -14
- data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +5 -6
- data/lib/markbridge/renderers/discourse/tags/subscript_tag.rb +11 -5
- data/lib/markbridge/renderers/discourse/tags/superscript_tag.rb +11 -5
- data/lib/markbridge/renderers/discourse/tags/table_tag.rb +48 -28
- data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +3 -10
- data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +90 -43
- data/lib/markbridge/renderers/discourse/tags/url_tag.rb +43 -26
- data/lib/markbridge/renderers/discourse.rb +6 -0
- data/lib/markbridge/rspec.rb +4 -2
- data/lib/markbridge/version.rb +1 -1
- data/lib/markbridge.rb +90 -1
- metadata +7 -1
|
@@ -5,6 +5,8 @@ module Markbridge
|
|
|
5
5
|
module Discourse
|
|
6
6
|
# Renders AST to Discourse-flavored Markdown in-memory.
|
|
7
7
|
class Renderer
|
|
8
|
+
# @return [TagLibrary] the library the renderer renders with
|
|
9
|
+
attr_reader :tag_library
|
|
8
10
|
attr_reader :postprocessor
|
|
9
11
|
|
|
10
12
|
def initialize(tag_library: nil, escaper: nil, html_escaper: nil, postprocessor: nil)
|
|
@@ -12,49 +14,30 @@ module Markbridge
|
|
|
12
14
|
@escaper = escaper || MarkdownEscaper.new
|
|
13
15
|
@html_escaper = html_escaper || HtmlEscaper
|
|
14
16
|
@postprocessor = postprocessor || Postprocessor::DEFAULT
|
|
15
|
-
|
|
16
|
-
# are
|
|
17
|
-
#
|
|
17
|
+
@serializer = Inline::Serializer.new(escaper: @escaper, html_escaper: @html_escaper)
|
|
18
|
+
# @interface_cache and @resolved_tags are set up by a top-level
|
|
19
|
+
# call and reset when it completes (see #reset_caches);
|
|
20
|
+
# @resolved_default_tags stays for the life of the renderer.
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
#
|
|
23
|
+
# Renders a node to Markdown.
|
|
24
|
+
#
|
|
21
25
|
# @param node [AST::Node]
|
|
22
26
|
# @param context [RenderContext] rendering context with parent chain
|
|
23
27
|
# @return [String]
|
|
24
28
|
# @raise [TypeError] when the tag bound to the node's class returns
|
|
25
|
-
# something other than a String (a nil from a
|
|
26
|
-
# otherwise surface as an inscrutable
|
|
27
|
-
# inside render_children)
|
|
29
|
+
# something other than a String or inline tokens (a nil from a
|
|
30
|
+
# custom tag would otherwise surface as an inscrutable
|
|
31
|
+
# concatenation error deep inside render_children)
|
|
28
32
|
def render(node, context: RenderContext.new)
|
|
29
|
-
root_call =
|
|
30
|
-
@interface_cache = {} if root_call
|
|
31
|
-
|
|
32
|
-
# Exact-class hit first (a single Hash lookup, the common case),
|
|
33
|
-
# then the memoized ancestry fallback so a subclass without its
|
|
34
|
-
# own tag renders through its nearest ancestor's tag.
|
|
35
|
-
tag = @tag_library[node.class] || resolved_tag(node.class)
|
|
36
|
-
if tag
|
|
37
|
-
result = tag.render(node, interface_for(context))
|
|
38
|
-
unless result.is_a?(String)
|
|
39
|
-
raise TypeError,
|
|
40
|
-
"#{tag.class} rendered #{node.class} to " \
|
|
41
|
-
"#{result.inspect} — tags must return a String " \
|
|
42
|
-
"(use interface.render_default(node) to fall back " \
|
|
43
|
-
"to the stock rendering)"
|
|
44
|
-
end
|
|
45
|
-
return result
|
|
46
|
-
end
|
|
33
|
+
root_call = enter
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
serialize(render_output(node, context), context)
|
|
49
36
|
ensure
|
|
50
|
-
if root_call
|
|
51
|
-
@interface_cache = nil
|
|
52
|
-
@resolved_tags = nil
|
|
53
|
-
@resolved_default_tags = nil
|
|
54
|
-
end
|
|
37
|
+
reset_caches if root_call
|
|
55
38
|
end
|
|
56
39
|
|
|
57
|
-
#
|
|
40
|
+
# Renders a node with the stock tag for its class, ignoring any
|
|
58
41
|
# override registered in this renderer's tag library. Lets a
|
|
59
42
|
# custom tag intercept only the nodes it cares about and delegate
|
|
60
43
|
# the rest:
|
|
@@ -71,30 +54,93 @@ module Markbridge
|
|
|
71
54
|
# @param context [RenderContext] rendering context with parent chain
|
|
72
55
|
# @return [String]
|
|
73
56
|
def render_default(node, context: RenderContext.new)
|
|
74
|
-
root_call =
|
|
75
|
-
@interface_cache = {} if root_call
|
|
57
|
+
root_call = enter
|
|
76
58
|
|
|
77
59
|
tag = default_tag_library[node.class] || resolved_default_tag(node.class)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
60
|
+
output =
|
|
61
|
+
tag ? tag.render(node, interface_for(context)) : render_without_tag(node, context)
|
|
62
|
+
serialize(output, context)
|
|
81
63
|
ensure
|
|
82
|
-
if root_call
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
64
|
+
reset_caches if root_call
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Writes inline tokens as Markdown, or as HTML in html_mode. A
|
|
68
|
+
# String is returned as it is.
|
|
69
|
+
#
|
|
70
|
+
# @param output [String, Array<Array>] what a tag returned
|
|
71
|
+
# @param context [RenderContext]
|
|
72
|
+
# @return [String]
|
|
73
|
+
def serialize(output, context)
|
|
74
|
+
if output.instance_of?(String)
|
|
75
|
+
output
|
|
76
|
+
else
|
|
77
|
+
@serializer.write(output, html_mode: context.html_mode?)
|
|
86
78
|
end
|
|
87
79
|
end
|
|
88
80
|
|
|
89
|
-
#
|
|
81
|
+
# The output of a node as its tag returns it: finished Markdown,
|
|
82
|
+
# or inline tokens (see Inline::Tokens) that the serializer writes
|
|
83
|
+
# together with the output of the node's siblings.
|
|
84
|
+
#
|
|
85
|
+
# @param node [AST::Node]
|
|
86
|
+
# @param context [RenderContext]
|
|
87
|
+
# @return [String, Array<Array>]
|
|
88
|
+
def render_output(node, context)
|
|
89
|
+
# Exact-class hit first (a single Hash lookup, the common case),
|
|
90
|
+
# then the memoized ancestry fallback so a subclass without its
|
|
91
|
+
# own tag renders through its nearest ancestor's tag.
|
|
92
|
+
tag = @tag_library[node.class] || resolved_tag(node.class)
|
|
93
|
+
return render_without_tag(node, context) unless tag
|
|
94
|
+
|
|
95
|
+
output = tag.render(node, interface_for(context))
|
|
96
|
+
unless output.instance_of?(String) || output.instance_of?(Array)
|
|
97
|
+
raise TypeError,
|
|
98
|
+
"#{tag.class} rendered #{node.class} to #{output.inspect} — tags must return " \
|
|
99
|
+
"a String or inline tokens (use interface.render_default(node) to fall " \
|
|
100
|
+
"back to the stock rendering)"
|
|
101
|
+
end
|
|
102
|
+
output
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The output of the children of a node as inline tokens, for a tag
|
|
106
|
+
# that returns tokens itself. Finished Markdown from a child is a
|
|
107
|
+
# token as it is.
|
|
108
|
+
#
|
|
109
|
+
# @param node [AST::Element]
|
|
110
|
+
# @param context [RenderContext]
|
|
111
|
+
# @return [Array<Array>]
|
|
112
|
+
def render_tokens(node, context:)
|
|
113
|
+
root_call = enter
|
|
114
|
+
outer_parent = @child_parent
|
|
115
|
+
outer_index = @child_index
|
|
116
|
+
outer_list = @previous_list
|
|
117
|
+
@child_parent = node
|
|
118
|
+
@previous_list = continued_list(node, context, outer_list)
|
|
119
|
+
tokens = []
|
|
120
|
+
index = -1
|
|
121
|
+
node.children.each do |child|
|
|
122
|
+
@child_index = (index += 1)
|
|
123
|
+
output = render_child(child, context)
|
|
124
|
+
output.instance_of?(String) ? tokens << output : tokens.concat(output)
|
|
125
|
+
end
|
|
126
|
+
@child_parent = outer_parent
|
|
127
|
+
@child_index = outer_index
|
|
128
|
+
@previous_list = outer_list
|
|
129
|
+
tokens
|
|
130
|
+
ensure
|
|
131
|
+
reset_caches if root_call
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Renders all children of a node and writes their output one after
|
|
135
|
+
# the other with the Inline::Serializer, which knows what has to
|
|
136
|
+
# happen where two outputs meet (see Inline::Joins).
|
|
90
137
|
#
|
|
91
138
|
# With a block, the block runs at every join point, right before
|
|
92
139
|
# a non-empty child output is appended. It receives the buffer
|
|
93
140
|
# built so far and the child about to be rendered into it, and
|
|
94
141
|
# may change the buffer in place. A tag uses this to adjust the
|
|
95
142
|
# text in front of a specific child without iterating the
|
|
96
|
-
# children itself, which would lose the
|
|
97
|
-
# below.
|
|
143
|
+
# children itself, which would lose the join rules.
|
|
98
144
|
#
|
|
99
145
|
# @param node [AST::Element]
|
|
100
146
|
# @param context [RenderContext] rendering context
|
|
@@ -105,45 +151,223 @@ module Markbridge
|
|
|
105
151
|
# interface.render_children(item, context:) do |buffer, child|
|
|
106
152
|
# buffer.rstrip! if child.is_a?(AST::List)
|
|
107
153
|
# end
|
|
108
|
-
def render_children(node, context
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
def render_children(node, context:, &block)
|
|
155
|
+
root_call = enter
|
|
156
|
+
outer_parent = @child_parent
|
|
157
|
+
outer_index = @child_index
|
|
158
|
+
outer_list = @previous_list
|
|
159
|
+
@child_parent = node
|
|
160
|
+
@previous_list = continued_list(node, context, outer_list)
|
|
161
|
+
index = -1
|
|
162
|
+
result =
|
|
163
|
+
@serializer.write_groups(
|
|
164
|
+
node.children,
|
|
165
|
+
html_mode: context.html_mode?,
|
|
166
|
+
before: block,
|
|
167
|
+
) do |child|
|
|
168
|
+
@child_index = (index += 1)
|
|
169
|
+
render_child(child, context)
|
|
124
170
|
end
|
|
125
|
-
|
|
126
|
-
|
|
171
|
+
@child_parent = outer_parent
|
|
172
|
+
@child_index = outer_index
|
|
173
|
+
@previous_list = outer_list
|
|
127
174
|
result
|
|
175
|
+
ensure
|
|
176
|
+
reset_caches if root_call
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Renders one child of the element whose children are being
|
|
180
|
+
# rendered, and remembers the last list among them for
|
|
181
|
+
# #previous_list: a list, or a child whose output ends with one
|
|
182
|
+
# (Tag#trailing_list). Text after it with something other than
|
|
183
|
+
# whitespace in it keeps two lists apart; a child that renders to
|
|
184
|
+
# nothing does not count.
|
|
185
|
+
def render_child(child, context)
|
|
186
|
+
output = render_output(child, context)
|
|
187
|
+
list = child.is_a?(AST::Text) ? nil : trailing_list(child, context)
|
|
188
|
+
if list
|
|
189
|
+
@previous_list = list unless output.empty?
|
|
190
|
+
elsif @previous_list && visible?(output)
|
|
191
|
+
@previous_list = nil
|
|
192
|
+
end
|
|
193
|
+
output
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def visible?(output)
|
|
197
|
+
if output.instance_of?(String)
|
|
198
|
+
Inline::Joins.visible?(output)
|
|
199
|
+
else
|
|
200
|
+
Inline::Tokens.visible?(output)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The element whose children #render_children is rendering right
|
|
205
|
+
# now. Unlike the parent chain of the context it also knows an
|
|
206
|
+
# element without a tag of its own, which never goes on the chain.
|
|
207
|
+
# @return [AST::Element, nil]
|
|
208
|
+
attr_reader :child_parent
|
|
209
|
+
|
|
210
|
+
# The position of the child that #render_children is rendering
|
|
211
|
+
# right now, among the children of its parent. A nested call
|
|
212
|
+
# sets it back when it returns, so a tag still finds its own
|
|
213
|
+
# position after it rendered its children. Only a hint for
|
|
214
|
+
# RenderingInterface#sibling, which checks it before use: a node
|
|
215
|
+
# rendered on its own, outside #render_children, sees the
|
|
216
|
+
# position of some other node.
|
|
217
|
+
# @return [Integer, nil]
|
|
218
|
+
attr_reader :child_index
|
|
219
|
+
|
|
220
|
+
# The last list among the siblings in front of +element+, or the
|
|
221
|
+
# sibling whose output ends with a list (Tag#trailing_list), when
|
|
222
|
+
# nothing but whitespace or nodes that rendered to nothing came
|
|
223
|
+
# after it. nil when there is none, or when +element+ is not the
|
|
224
|
+
# child that #render_children is rendering right now.
|
|
225
|
+
# @param element [AST::Node]
|
|
226
|
+
# @return [AST::Node, nil]
|
|
227
|
+
def previous_list(element)
|
|
228
|
+
@previous_list if current_child?(element)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The bullet characters and the ordered delimiters the lists are
|
|
232
|
+
# written with, in the order they are tried.
|
|
233
|
+
BULLET_MARKERS = ["- ", "* "].freeze
|
|
234
|
+
ORDERED_MARKERS = ["1. ", "1) "].freeze
|
|
235
|
+
|
|
236
|
+
# The marker for the items of +node+, a list or a node whose
|
|
237
|
+
# output is a list (a poll without the plugin). Two lists of one
|
|
238
|
+
# kind with the same marker and only whitespace between them cook
|
|
239
|
+
# as one list, a different bullet character or ordered delimiter
|
|
240
|
+
# starts a new list, so the list after #previous_list gets the
|
|
241
|
+
# marker of its kind that list did not use. The marker is
|
|
242
|
+
# remembered for +node+, see #list_marker.
|
|
243
|
+
# @param node [AST::Node] the child #render_children is at;
|
|
244
|
+
# any other node gets the first marker of its kind
|
|
245
|
+
# @param ordered [Boolean]
|
|
246
|
+
# @return [String]
|
|
247
|
+
def choose_list_marker(node, ordered:)
|
|
248
|
+
root_call = enter
|
|
249
|
+
markers = ordered ? ORDERED_MARKERS : BULLET_MARKERS
|
|
250
|
+
# The marker of this kind that the list in front did not use;
|
|
251
|
+
# the first one when none is in front.
|
|
252
|
+
before = @list_markers[previous_list(node)]
|
|
253
|
+
@list_markers[node] = (markers - [before]).first
|
|
254
|
+
ensure
|
|
255
|
+
reset_caches if root_call
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# The marker #choose_list_marker picked for +node+ during this
|
|
259
|
+
# render call, nil when it picked none. ListItemTag writes it in
|
|
260
|
+
# front of the items, HorizontalRuleTag picks the characters of
|
|
261
|
+
# its rule so they differ from it.
|
|
262
|
+
# @param node [AST::Node]
|
|
263
|
+
# @return [String, nil]
|
|
264
|
+
def list_marker(node)
|
|
265
|
+
@list_markers&.[](node)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Whether +element+ is the child that #render_children is
|
|
269
|
+
# rendering right now. The state above describes that child only.
|
|
270
|
+
# @param element [AST::Node]
|
|
271
|
+
# @return [Boolean]
|
|
272
|
+
def current_child?(element)
|
|
273
|
+
parent = child_parent
|
|
274
|
+
!parent.nil? && parent.children.at(child_index).equal?(element)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# The list the output of +node+ starts with, as its tag says (see
|
|
278
|
+
# Tag#leading_list). An element without a tag writes nothing but
|
|
279
|
+
# its children, so its output starts with what the first child
|
|
280
|
+
# with something in it starts with; +children: true+ asks that
|
|
281
|
+
# for an element with a tag, for a tag whose output starts with
|
|
282
|
+
# the children's.
|
|
283
|
+
# @param node [AST::Node]
|
|
284
|
+
# @param context [RenderContext]
|
|
285
|
+
# @param children [Boolean]
|
|
286
|
+
# @return [AST::List, nil]
|
|
287
|
+
def leading_list(node, context, children: false)
|
|
288
|
+
root_call = enter
|
|
289
|
+
unless children
|
|
290
|
+
tag = edge_tag(node)
|
|
291
|
+
return tag.leading_list(node, interface_for(context)) if tag
|
|
292
|
+
end
|
|
293
|
+
first = node.is_a?(AST::Element) && node.children.find { |child| !whitespace?(child) }
|
|
294
|
+
leading_list(first, context) if first
|
|
295
|
+
ensure
|
|
296
|
+
reset_caches if root_call
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# The list the output of +node+ ends with, see #leading_list.
|
|
300
|
+
# @param node [AST::Node]
|
|
301
|
+
# @param context [RenderContext]
|
|
302
|
+
# @param children [Boolean]
|
|
303
|
+
# @return [AST::List, nil]
|
|
304
|
+
def trailing_list(node, context, children: false)
|
|
305
|
+
root_call = enter
|
|
306
|
+
unless children
|
|
307
|
+
tag = edge_tag(node)
|
|
308
|
+
return tag.trailing_list(node, interface_for(context)) if tag
|
|
309
|
+
end
|
|
310
|
+
last =
|
|
311
|
+
node.is_a?(AST::Element) &&
|
|
312
|
+
node.children.reverse_each.find { |child| !whitespace?(child) }
|
|
313
|
+
trailing_list(last, context) if last
|
|
314
|
+
ensure
|
|
315
|
+
reset_caches if root_call
|
|
128
316
|
end
|
|
129
317
|
|
|
130
318
|
private
|
|
131
319
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
320
|
+
# Something other than whitespace.
|
|
321
|
+
|
|
322
|
+
# Sets up the caches for a top-level call and says whether this is
|
|
323
|
+
# one: a call from inside a tag finds them in place.
|
|
324
|
+
def enter
|
|
325
|
+
root_call = @interface_cache.nil?
|
|
326
|
+
if root_call
|
|
327
|
+
@interface_cache = {}
|
|
328
|
+
@list_markers = {}.compare_by_identity
|
|
329
|
+
end
|
|
330
|
+
root_call
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# The caches live for one top-level call: the tag library can
|
|
334
|
+
# change between calls, and an interface is cached by the object
|
|
335
|
+
# id of its context. The default library never changes, so its
|
|
336
|
+
# resolved tags stay.
|
|
337
|
+
def reset_caches
|
|
338
|
+
@interface_cache = nil
|
|
339
|
+
@resolved_tags = nil
|
|
340
|
+
@list_markers = nil
|
|
341
|
+
end
|
|
142
342
|
|
|
143
343
|
def interface_for(context)
|
|
144
344
|
@interface_cache[context.object_id] ||= RenderingInterface.new(self, context)
|
|
145
345
|
end
|
|
146
346
|
|
|
347
|
+
# The list in front of +node+ that the first list inside +node+
|
|
348
|
+
# continues: the output of +node+ starts with that inner list
|
|
349
|
+
# (Tag#leading_list), so the inner list gets the other marker.
|
|
350
|
+
# nil when +node+ writes something of its own first, or when no
|
|
351
|
+
# list came before it; the question is asked only then, it costs
|
|
352
|
+
# a tag lookup on every element.
|
|
353
|
+
# @param outer_list [AST::Node, nil] the last list in front of
|
|
354
|
+
# +node+, among the children of its parent
|
|
355
|
+
# @return [AST::Node, nil]
|
|
356
|
+
def continued_list(node, context, outer_list)
|
|
357
|
+
outer_list if outer_list && leading_list(node, context)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# The tag of +node+ as #render_output finds it.
|
|
361
|
+
def edge_tag(node)
|
|
362
|
+
@tag_library[node.class] || resolved_tag(node.class)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Text with nothing but whitespace writes nothing that keeps two
|
|
366
|
+
# lists apart, see #render_child.
|
|
367
|
+
def whitespace?(child)
|
|
368
|
+
child.instance_of?(AST::Text) && child.text.strip.empty?
|
|
369
|
+
end
|
|
370
|
+
|
|
147
371
|
# Ancestry fallback for tag dispatch (see TagLibrary#resolve),
|
|
148
372
|
# memoized per top-level render call — the tag library can change
|
|
149
373
|
# between calls, so the cache must not outlive one call (it is
|
|
@@ -156,7 +380,7 @@ module Markbridge
|
|
|
156
380
|
end
|
|
157
381
|
|
|
158
382
|
# Same as {#resolved_tag}, against the default library backing
|
|
159
|
-
# #render_default.
|
|
383
|
+
# #render_default, kept for the life of the renderer.
|
|
160
384
|
def resolved_default_tag(node_class)
|
|
161
385
|
cache = @resolved_default_tags ||= {}
|
|
162
386
|
cache.fetch(node_class) { cache[node_class] = default_tag_library.resolve(node_class) }
|
|
@@ -169,9 +393,15 @@ module Markbridge
|
|
|
169
393
|
end
|
|
170
394
|
|
|
171
395
|
# The tag-less rendering paths shared by #render and #render_default.
|
|
396
|
+
# An element without a tag does not go on the parent chain (every
|
|
397
|
+
# chain walk would get one step longer, and text nodes walk it
|
|
398
|
+
# several times). The first one becomes the context's root element
|
|
399
|
+
# instead, so top-level children can still ask for their siblings.
|
|
400
|
+
# The Document is the usual case.
|
|
172
401
|
def render_without_tag(node, context)
|
|
173
402
|
case node
|
|
174
403
|
when AST::Element # Document is an Element subclass
|
|
404
|
+
context = context.with_root(node) if context.root.nil?
|
|
175
405
|
render_children(node, context:)
|
|
176
406
|
when AST::MarkdownText
|
|
177
407
|
render_markdown_text(node, context)
|
|
@@ -182,32 +412,46 @@ module Markbridge
|
|
|
182
412
|
end
|
|
183
413
|
end
|
|
184
414
|
|
|
185
|
-
# In html_mode,
|
|
186
|
-
#
|
|
187
|
-
# parses the content as Markdown
|
|
188
|
-
#
|
|
415
|
+
# In html_mode, pre-formatted Markdown is a Markdown island in a
|
|
416
|
+
# `<div>`: the blank lines end the enclosing HTML block (e.g. a
|
|
417
|
+
# <table>) so that CommonMark parses the content as Markdown, and
|
|
418
|
+
# the `</div>` line opens the block again for the rest of the cell.
|
|
189
419
|
def render_markdown_text(node, context)
|
|
190
|
-
context.html_mode? ? HtmlBlock.
|
|
420
|
+
context.html_mode? ? HtmlBlock.enclosed_island(node.text) : node.text
|
|
191
421
|
end
|
|
192
422
|
|
|
423
|
+
# Text nodes are the most frequent nodes, so this walks the parent
|
|
424
|
+
# chain once and takes the first answer it can: in html_mode the
|
|
425
|
+
# chain does not matter at all, and under a Code ancestor nothing
|
|
426
|
+
# after it does. The checks are spelled out instead of going
|
|
427
|
+
# through a block, and the walk builds no array — every step
|
|
428
|
+
# of the chain costs.
|
|
429
|
+
#
|
|
430
|
+
# A link label is where `]` is structural and must be escaped: a
|
|
431
|
+
# link, a mail link, and the alt text of an image or an upload
|
|
432
|
+
# (ImageTag and UploadTag render it as a Text node under their
|
|
433
|
+
# element). The tags write their own brackets around it.
|
|
193
434
|
def render_text(node, context)
|
|
194
435
|
# In html_mode even inside a code block we must HTML-escape, otherwise a
|
|
195
|
-
# stray `<` in a code cell would break the surrounding <td>.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
@html_escaper.escape(node.text)
|
|
200
|
-
|
|
201
|
-
|
|
436
|
+
# stray `<` in a code cell would break the surrounding <td>. A
|
|
437
|
+
# blank line would end the block, like the serializer's text
|
|
438
|
+
# it is written as the entity.
|
|
439
|
+
if context.html_mode?
|
|
440
|
+
return HtmlBlock.without_blank_lines(@html_escaper.escape(node.text))
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
in_link_label = false
|
|
444
|
+
|
|
445
|
+
# The chain always ends in a root context whose element is nil.
|
|
446
|
+
while (parent = context.element)
|
|
447
|
+
return node.text if parent.is_a?(AST::Code)
|
|
448
|
+
|
|
449
|
+
in_link_label = true if parent.is_a?(AST::Url) || parent.is_a?(AST::Email) ||
|
|
450
|
+
parent.is_a?(AST::Image) || parent.is_a?(AST::Upload)
|
|
451
|
+
context = context.parent_context
|
|
202
452
|
end
|
|
203
|
-
end
|
|
204
453
|
|
|
205
|
-
|
|
206
|
-
# rendered under an Url/Email ancestor must escape it. Tags that emit
|
|
207
|
-
# their own bracketed markup (ImageTag, UploadTag, etc.) skip this
|
|
208
|
-
# path entirely, so their structural brackets are preserved.
|
|
209
|
-
def in_link_label?(context)
|
|
210
|
-
context.has_parent?(AST::Url) || context.has_parent?(AST::Email)
|
|
454
|
+
@escaper.escape(node.text, in_link_label:)
|
|
211
455
|
end
|
|
212
456
|
end
|
|
213
457
|
end
|