markbridge 0.4.3 → 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/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/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 +41 -31
- 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
|
@@ -4,27 +4,49 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
#
|
|
7
|
+
# Renders {AST::Event} as the +[event]+ BBCode of the discourse-events
|
|
8
|
+
# plugin: the raw BBCode of the source when the parser kept it,
|
|
9
|
+
# otherwise the BBCode built from the attributes.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
11
|
+
# With +bbcode: false+, for a site without the plugin, the event is
|
|
12
|
+
# a paragraph with the name and the dates, so the reader still sees
|
|
13
|
+
# it.
|
|
12
14
|
#
|
|
13
|
-
#
|
|
15
|
+
# The BBCode has no HTML form, so it goes through
|
|
16
|
+
# RenderingInterface#markdown_island: a block with blank lines
|
|
17
|
+
# around it, and a Markdown island in a `<div>` inside an HTML
|
|
18
|
+
# table. A placeholder for an event is written the same way (see
|
|
19
|
+
# "Placeholders" in the migration guide).
|
|
20
|
+
#
|
|
21
|
+
# @example A placeholder for the importer
|
|
14
22
|
# class MyEventTag < Markbridge::Renderers::Discourse::Tags::EventTag
|
|
15
23
|
# def render(element, interface)
|
|
16
24
|
# id = register_event(element)
|
|
17
|
-
# "<<EVENT:#{id}>>"
|
|
25
|
+
# interface.markdown_island("<<EVENT:#{id}>>", block: true)
|
|
18
26
|
# end
|
|
19
27
|
# end
|
|
20
28
|
class EventTag < Tag
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
# @param bbcode [Boolean] write the BBCode of the plugin
|
|
30
|
+
def initialize(bbcode: true)
|
|
31
|
+
@bbcode = bbcode
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render(element, interface)
|
|
35
|
+
return without_plugin(element, interface) unless @bbcode
|
|
36
|
+
|
|
37
|
+
interface.markdown_island(element.raw || build_event_bbcode(element), block: true)
|
|
24
38
|
end
|
|
25
39
|
|
|
26
40
|
private
|
|
27
41
|
|
|
42
|
+
def without_plugin(element, interface)
|
|
43
|
+
dates = [element.starts_at, element.ends_at].map(&:to_s).reject(&:empty?).join(" to ")
|
|
44
|
+
text = [element.name.to_s, dates].reject(&:empty?).join(", ")
|
|
45
|
+
return "" if text.empty?
|
|
46
|
+
|
|
47
|
+
interface.render_node(AST::Paragraph.new << AST::Text.new(text))
|
|
48
|
+
end
|
|
49
|
+
|
|
28
50
|
def build_event_bbcode(element)
|
|
29
51
|
attrs = build_attributes(element)
|
|
30
52
|
"[event#{attrs}]\n[/event]"
|
|
@@ -5,6 +5,17 @@ module Markbridge
|
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
7
|
class HeadingTag < Tag
|
|
8
|
+
# A run of # at the end of an ATX heading, after whitespace, is
|
|
9
|
+
# the optional closing sequence and disappears when cooked. The
|
|
10
|
+
# space after the prefix counts, so a run that is the whole
|
|
11
|
+
# content is one too.
|
|
12
|
+
CLOSING_SEQUENCE = /(?:\A|(?<=[ \t]))#+[ \t]*\z/
|
|
13
|
+
# A heading is one line. A newline in its content, from a text
|
|
14
|
+
# node, is a soft break and becomes a space; LineBreakTag writes
|
|
15
|
+
# a hard break in a heading as <br>.
|
|
16
|
+
LINE_BREAK = /[ \t]*\n[ \t]*/
|
|
17
|
+
private_constant :CLOSING_SEQUENCE, :LINE_BREAK
|
|
18
|
+
|
|
8
19
|
def render(element, interface)
|
|
9
20
|
child_context = interface.with_parent(element)
|
|
10
21
|
content = interface.render_children(element, context: child_context)
|
|
@@ -16,7 +27,15 @@ module Markbridge
|
|
|
16
27
|
|
|
17
28
|
prefix = "#" * element.level
|
|
18
29
|
|
|
19
|
-
"\n\n#{prefix} #{content}\n\n"
|
|
30
|
+
"\n\n#{prefix} #{keep_trailing_hashes(content.gsub(LINE_BREAK, " "))}\n\n"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# A backslash in front of the run keeps it as text. The escaper
|
|
36
|
+
# does not do this, it only knows about # at the start of a line.
|
|
37
|
+
def keep_trailing_hashes(content)
|
|
38
|
+
content.sub(CLOSING_SEQUENCE) { |run| "\\#{run}" }
|
|
20
39
|
end
|
|
21
40
|
end
|
|
22
41
|
end
|
|
@@ -7,7 +7,18 @@ module Markbridge
|
|
|
7
7
|
# Tag for rendering horizontal rules
|
|
8
8
|
class HorizontalRuleTag < Tag
|
|
9
9
|
def render(_element, interface)
|
|
10
|
-
|
|
10
|
+
return "<hr>" if interface.html_mode?
|
|
11
|
+
|
|
12
|
+
list = interface.find_parent(AST::List)
|
|
13
|
+
return "\n\n---\n\n" unless list
|
|
14
|
+
|
|
15
|
+
# `- ---` is a thematic break that ends the list, because the
|
|
16
|
+
# marker and the dashes together are one run of dashes and
|
|
17
|
+
# spaces. `- * * *` is a list item that holds a thematic break
|
|
18
|
+
# (CommonMark example 61), so inside a list item the rule is
|
|
19
|
+
# written with the character the list's marker does not use;
|
|
20
|
+
# `* * * *` would be the rule alone again.
|
|
21
|
+
interface.list_marker(list)&.start_with?("*") ? "\n\n- - -\n\n" : "\n\n* * *\n\n"
|
|
11
22
|
end
|
|
12
23
|
end
|
|
13
24
|
end
|
|
@@ -4,34 +4,20 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Images: an image token. The serializer writes
|
|
8
|
+
# ``, `` or ``
|
|
9
|
+
# with the alt text escaped like a link label, and <img> in an
|
|
10
|
+
# HTML block.
|
|
9
11
|
class ImageTag < Tag
|
|
10
|
-
def render(element,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if width && height
|
|
20
|
-
""
|
|
21
|
-
elsif width
|
|
22
|
-
""
|
|
23
|
-
else
|
|
24
|
-
""
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def render_html(src, width, height)
|
|
31
|
-
attrs = %(src="#{HtmlEscaper.escape(src)}" alt="")
|
|
32
|
-
attrs << %( width="#{width}") if width
|
|
33
|
-
attrs << %( height="#{height}") if height
|
|
34
|
-
"<img #{attrs}>"
|
|
12
|
+
def render(element, _interface)
|
|
13
|
+
[
|
|
14
|
+
Inline::Tokens.image(
|
|
15
|
+
src: element.src,
|
|
16
|
+
alt: element.alt,
|
|
17
|
+
width: element.width,
|
|
18
|
+
height: element.height,
|
|
19
|
+
),
|
|
20
|
+
]
|
|
35
21
|
end
|
|
36
22
|
end
|
|
37
23
|
end
|
|
@@ -4,14 +4,12 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
#
|
|
7
|
+
# Emphasis. The serializer writes the markers, or the HTML tags
|
|
8
|
+
# where the markers would not work.
|
|
8
9
|
class ItalicTag < Tag
|
|
9
10
|
def render(element, interface)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return "<em>#{content}</em>" if interface.html_mode?
|
|
13
|
-
|
|
14
|
-
interface.wrap_inline(content, "*")
|
|
11
|
+
tokens = interface.render_tokens(element, context: interface.with_parent(element))
|
|
12
|
+
tokens.unshift(Inline::Tokens.open(:em)) << Inline::Tokens.close(:em)
|
|
15
13
|
end
|
|
16
14
|
end
|
|
17
15
|
end
|
|
@@ -4,10 +4,17 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
#
|
|
7
|
+
# Line breaks: a hard break token, which the serializer writes as a
|
|
8
|
+
# newline, as <br> on a line that holds nothing else (a newline
|
|
9
|
+
# there would be a blank line, which ends the paragraph and makes
|
|
10
|
+
# a list loose), and as <br> in an HTML block. A heading is one
|
|
11
|
+
# line, so a break in a heading is <br> as well.
|
|
8
12
|
class LineBreakTag < Tag
|
|
13
|
+
HARD_BREAK = [Inline::Tokens.hard_break].freeze
|
|
14
|
+
private_constant :HARD_BREAK
|
|
15
|
+
|
|
9
16
|
def render(_element, interface)
|
|
10
|
-
interface.
|
|
17
|
+
interface.has_parent?(AST::Heading) ? "<br>" : HARD_BREAK
|
|
11
18
|
end
|
|
12
19
|
end
|
|
13
20
|
end
|
|
@@ -12,6 +12,16 @@ module Markbridge
|
|
|
12
12
|
# lands at the content column of the item that holds it, whatever
|
|
13
13
|
# the nesting depth is.
|
|
14
14
|
class ListItemTag < Tag
|
|
15
|
+
# Ends a nested list in front of more content of the item.
|
|
16
|
+
COMMENT = "<!---->"
|
|
17
|
+
LIST_END = "\n<!---->"
|
|
18
|
+
NEWLINE = 10
|
|
19
|
+
SPACE = 32
|
|
20
|
+
# Whitespace and the <br> the serializer writes for a line break
|
|
21
|
+
# on a line that holds nothing else.
|
|
22
|
+
TRAILING_BREAKS = /(?:[[:space:]]|<br>)+\z/
|
|
23
|
+
private_constant :COMMENT, :LIST_END, :NEWLINE, :SPACE, :TRAILING_BREAKS
|
|
24
|
+
|
|
15
25
|
def initialize
|
|
16
26
|
@builder = Builders::ListItemBuilder.new
|
|
17
27
|
end
|
|
@@ -24,7 +34,7 @@ module Markbridge
|
|
|
24
34
|
return "" if content.empty?
|
|
25
35
|
|
|
26
36
|
parent_list = interface.find_parent(AST::List)
|
|
27
|
-
@builder.build(content, marker: determine_marker(parent_list))
|
|
37
|
+
@builder.build(content, marker: determine_marker(parent_list, interface))
|
|
28
38
|
end
|
|
29
39
|
|
|
30
40
|
private
|
|
@@ -39,28 +49,84 @@ module Markbridge
|
|
|
39
49
|
# it. A blank line there would make the whole list loose, and
|
|
40
50
|
# CommonMark then wraps every item in a <p>. Blank lines inside
|
|
41
51
|
# the output of a child are never touched.
|
|
52
|
+
#
|
|
53
|
+
# Text after a nested list would become a continuation line of
|
|
54
|
+
# its last item, and a blank line in between would make the
|
|
55
|
+
# list loose again. An empty HTML comment on a line of its own
|
|
56
|
+
# ends the nested list without one; the whitespace between the
|
|
57
|
+
# list and the text goes.
|
|
42
58
|
# @return [String]
|
|
43
59
|
def render_content(element, interface, child_context)
|
|
60
|
+
after_list = false
|
|
44
61
|
content =
|
|
45
62
|
interface.render_children(element, context: child_context) do |buffer, child|
|
|
46
|
-
|
|
63
|
+
if child.is_a?(AST::List)
|
|
64
|
+
tighten(buffer)
|
|
65
|
+
after_list = true
|
|
66
|
+
elsif after_list && visible?(child)
|
|
67
|
+
end_list(buffer, before: child)
|
|
68
|
+
after_list = false
|
|
69
|
+
end
|
|
47
70
|
end
|
|
48
71
|
content.strip
|
|
49
72
|
end
|
|
50
73
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
74
|
+
# The comment needs a line of its own: after it on the same line,
|
|
75
|
+
# the text would be part of the HTML block. Text that starts with
|
|
76
|
+
# a newline brings the line break itself. +before+ is the child
|
|
77
|
+
# that comes next.
|
|
78
|
+
def end_list(buffer, before:)
|
|
79
|
+
buffer.sub!(TRAILING_BREAKS, "")
|
|
80
|
+
buffer << LIST_END
|
|
81
|
+
buffer << "\n" unless text_of(before)&.start_with?("\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Whitespace and line breaks between a nested list and the text
|
|
85
|
+
# after it do not count, the text does.
|
|
86
|
+
def visible?(child)
|
|
87
|
+
text = text_of(child)
|
|
88
|
+
return text.match?(/[^[:space:]]/) if text
|
|
89
|
+
|
|
90
|
+
!child.is_a?(AST::LineBreak)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The text of a text child, as it is written to the output.
|
|
94
|
+
def text_of(child)
|
|
95
|
+
child.text if child.is_a?(AST::Text) || child.is_a?(AST::MarkdownText)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Drops the blank lines in front of a nested list. A blank line
|
|
99
|
+
# is what ends an HTML block, so a line in front that opens one
|
|
100
|
+
# gets a different treatment: a line that starts a block of its
|
|
101
|
+
# own (the line in front of it is blank, as with the `</div>` of
|
|
102
|
+
# an aligned block) gets an empty comment in front, which makes
|
|
103
|
+
# the line a comment block that ends on the same line; a line
|
|
104
|
+
# inside a block an earlier line opened (the `</table>` of a
|
|
105
|
+
# table) keeps the blank line, and the outer list is loose.
|
|
54
106
|
def tighten(buffer)
|
|
55
107
|
buffer.rstrip!
|
|
56
|
-
|
|
57
|
-
|
|
108
|
+
# Byte offsets throughout: the content in front may hold
|
|
109
|
+
# multi-byte characters.
|
|
110
|
+
start = (buffer.byterindex("\n") || -1) + 1
|
|
111
|
+
return unless HtmlBlock.opens?(buffer.byteslice(start..))
|
|
112
|
+
|
|
113
|
+
if start.zero? || buffer.getbyte(start - 2) == NEWLINE
|
|
114
|
+
# After the indentation of the line: a nested list has
|
|
115
|
+
# indented its lines already.
|
|
116
|
+
start += 1 while buffer.getbyte(start) == SPACE
|
|
117
|
+
buffer.bytesplice(start, 0, COMMENT)
|
|
118
|
+
else
|
|
119
|
+
buffer << "\n"
|
|
120
|
+
end
|
|
58
121
|
end
|
|
59
122
|
|
|
60
123
|
# @param parent_list [AST::List, nil]
|
|
61
124
|
# @return [String]
|
|
62
|
-
|
|
63
|
-
|
|
125
|
+
# The marker the renderer chose for the list (see
|
|
126
|
+
# RenderingInterface#choose_list_marker), or the first one of
|
|
127
|
+
# its kind for an item rendered without its list.
|
|
128
|
+
def determine_marker(parent_list, interface)
|
|
129
|
+
interface.list_marker(parent_list) || (parent_list&.ordered? ? "1. " : "- ")
|
|
64
130
|
end
|
|
65
131
|
end
|
|
66
132
|
end
|
|
@@ -5,27 +5,49 @@ module Markbridge
|
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
7
|
class ListTag < Tag
|
|
8
|
+
# The output is the list, see Tag#leading_list.
|
|
9
|
+
def leading_list(element, _interface)
|
|
10
|
+
element
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def trailing_list(element, _interface)
|
|
14
|
+
element
|
|
15
|
+
end
|
|
16
|
+
|
|
8
17
|
def render(element, interface)
|
|
9
18
|
child_context = interface.with_parent(element)
|
|
10
19
|
|
|
20
|
+
# Two lists of the same kind in a row would cook as one list,
|
|
21
|
+
# and the blank line between them would make it loose. A
|
|
22
|
+
# different marker starts a new list, so the renderer picks
|
|
23
|
+
# the one the list in front did not use, and the items write
|
|
24
|
+
# it (ListItemTag). In html_mode nothing reads it.
|
|
25
|
+
interface.choose_list_marker(element, ordered: element.ordered?)
|
|
26
|
+
|
|
11
27
|
content =
|
|
12
28
|
element
|
|
13
29
|
.children
|
|
14
30
|
.map { |child| interface.render_node(child, context: child_context) }
|
|
15
31
|
.join
|
|
16
32
|
|
|
33
|
+
# A list without items renders to nothing, like the other
|
|
34
|
+
# containers. Blank lines alone would still count as a list
|
|
35
|
+
# for the separator between two lists of the same kind.
|
|
36
|
+
return "" if content.empty?
|
|
37
|
+
|
|
17
38
|
if interface.html_mode?
|
|
18
39
|
tag_name = element.ordered? ? "ol" : "ul"
|
|
19
40
|
return "<#{tag_name}>#{content}</#{tag_name}>"
|
|
20
41
|
end
|
|
21
42
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
# A list right inside a list item starts on the line after the
|
|
44
|
+
# item text, so the list stays tight. ListItemTag takes care of
|
|
45
|
+
# what follows it in the item. Everywhere else (also inside a
|
|
46
|
+
# quote or an aligned block that sits in an item) the list is a
|
|
47
|
+
# block with blank lines on both sides.
|
|
48
|
+
parent = interface.context.element
|
|
49
|
+
nested = parent.is_a?(AST::ListItem) || parent.is_a?(AST::List)
|
|
50
|
+
nested ? "\n#{content}" : "\n\n#{content}\n\n"
|
|
29
51
|
end
|
|
30
52
|
end
|
|
31
53
|
end
|
|
@@ -17,6 +17,18 @@ module Markbridge
|
|
|
17
17
|
# end
|
|
18
18
|
# end
|
|
19
19
|
#
|
|
20
|
+
# @example Custom renderer with placeholder
|
|
21
|
+
# # The importer replaces the placeholder with the mention, so the
|
|
22
|
+
# # renderer joins it with its neighbors like a mention (see
|
|
23
|
+
# # "Placeholders" in the migration guide).
|
|
24
|
+
# Tokens = Markbridge::Renderers::Discourse::Inline::Tokens
|
|
25
|
+
#
|
|
26
|
+
# class MyMentionTag < Markbridge::Renderers::Discourse::Tags::MentionTag
|
|
27
|
+
# def render(element, interface)
|
|
28
|
+
# [Tokens.placeholder("[mention|#{element.name}]", like: "@#{element.name}")]
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
20
32
|
# @example Custom renderer that links to user
|
|
21
33
|
# class MyMentionTag < Markbridge::Renderers::Discourse::Tags::MentionTag
|
|
22
34
|
# def render(element, interface)
|
|
@@ -7,17 +7,20 @@ module Markbridge
|
|
|
7
7
|
# Tag for rendering paragraphs
|
|
8
8
|
# Paragraphs are separated by blank lines in Markdown
|
|
9
9
|
class ParagraphTag < Tag
|
|
10
|
+
# Spaces and tabs at the start of the paragraph. Not a no-break
|
|
11
|
+
# space: the escaper writes one where it wants indentation kept.
|
|
12
|
+
LEADING_BLANKS = /\A[ \t]+/
|
|
13
|
+
private_constant :LEADING_BLANKS
|
|
14
|
+
|
|
10
15
|
def render(element, interface)
|
|
11
16
|
child_context = interface.with_parent(element)
|
|
12
17
|
content = interface.render_children(element, context: child_context)
|
|
13
18
|
|
|
14
19
|
if interface.html_mode?
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
return content if interface.has_parent?(AST::TableCell)
|
|
20
|
-
|
|
20
|
+
# Also inside a table cell: without the <p> the paragraph
|
|
21
|
+
# runs into the text next to it. A block inside the
|
|
22
|
+
# paragraph closes the <p> early in the HTML parser, which
|
|
23
|
+
# is what the Markdown form does as well.
|
|
21
24
|
return "<p>#{content}</p>"
|
|
22
25
|
end
|
|
23
26
|
|
|
@@ -25,6 +28,10 @@ module Markbridge
|
|
|
25
28
|
# non-block content (raw text, inline elements) stays separated
|
|
26
29
|
# from the paragraph. cleanup_markdown collapses any duplicate
|
|
27
30
|
# newlines that result when neighbours are themselves block tags.
|
|
31
|
+
# Spaces at the start go: CommonMark drops up to three of them
|
|
32
|
+
# anyway, and after a list they would make the paragraph part
|
|
33
|
+
# of the last item.
|
|
34
|
+
content = content.sub(LEADING_BLANKS, "") if content.match?(LEADING_BLANKS)
|
|
28
35
|
"\n\n#{content}\n\n"
|
|
29
36
|
end
|
|
30
37
|
end
|
|
@@ -4,27 +4,70 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
#
|
|
7
|
+
# Renders {AST::Poll} as the +[poll]+ BBCode of the poll plugin: the
|
|
8
|
+
# raw BBCode of the source when the parser kept it, otherwise the
|
|
9
|
+
# BBCode built from the attributes and the options.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# to render polls as placeholders or convert them to another format.
|
|
11
|
+
# With +bbcode: false+, for a site without the plugin, the options
|
|
12
|
+
# are written as a bullet list. The raw BBCode is of no use there.
|
|
12
13
|
#
|
|
13
|
-
#
|
|
14
|
+
# The BBCode has no HTML form, so it goes through
|
|
15
|
+
# RenderingInterface#markdown_island: a block with blank lines
|
|
16
|
+
# around it, and a Markdown island in a `<div>` inside an HTML
|
|
17
|
+
# table. A placeholder for a poll is written the same way (see
|
|
18
|
+
# "Placeholders" in the migration guide).
|
|
19
|
+
#
|
|
20
|
+
# @example A placeholder for the importer
|
|
14
21
|
# class MyPollTag < Markbridge::Renderers::Discourse::Tags::PollTag
|
|
15
22
|
# def render(element, interface)
|
|
16
23
|
# id = register_poll(element)
|
|
17
|
-
# "<<POLL:#{id}>>"
|
|
24
|
+
# interface.markdown_island("<<POLL:#{id}>>", block: true)
|
|
18
25
|
# end
|
|
19
26
|
# end
|
|
20
27
|
class PollTag < Tag
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
# @param bbcode [Boolean] write the BBCode of the plugin
|
|
29
|
+
def initialize(bbcode: true)
|
|
30
|
+
@bbcode = bbcode
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render(element, interface)
|
|
34
|
+
return without_plugin(element, interface) unless @bbcode
|
|
35
|
+
|
|
36
|
+
interface.markdown_island(element.raw || build_poll_bbcode(element), block: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The plain form is a bullet list, so a bullet list next to the
|
|
40
|
+
# poll would continue it (see Tag#leading_list). The poll stands
|
|
41
|
+
# for the list, the marker is chosen for it.
|
|
42
|
+
def leading_list(element, _interface)
|
|
43
|
+
element unless @bbcode
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def trailing_list(element, _interface)
|
|
47
|
+
element unless @bbcode
|
|
24
48
|
end
|
|
25
49
|
|
|
26
50
|
private
|
|
27
51
|
|
|
52
|
+
# The options as the items of a bullet list, one line each; a
|
|
53
|
+
# poll without options renders to nothing, like an empty list.
|
|
54
|
+
# The poll writes the lines itself, with the marker the renderer
|
|
55
|
+
# picks for it against the list in front.
|
|
56
|
+
def without_plugin(element, interface)
|
|
57
|
+
return "" if element.options.empty?
|
|
58
|
+
if interface.html_mode?
|
|
59
|
+
items = element.options.map { |option| "<li>#{HtmlEscaper.escape(option)}</li>" }
|
|
60
|
+
return "<ul>#{items.join}</ul>"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
marker = interface.choose_list_marker(element, ordered: false)
|
|
64
|
+
lines =
|
|
65
|
+
element.options.map do |option|
|
|
66
|
+
"#{marker}#{interface.render_node(AST::Text.new(option))}"
|
|
67
|
+
end
|
|
68
|
+
"\n\n#{lines.join("\n")}\n\n"
|
|
69
|
+
end
|
|
70
|
+
|
|
28
71
|
def build_poll_bbcode(element)
|
|
29
72
|
attrs = build_attributes(element)
|
|
30
73
|
options = element.options.map { |opt| "* #{opt}" }.join("\n")
|
|
@@ -10,9 +10,15 @@ module Markbridge
|
|
|
10
10
|
def render(element, interface)
|
|
11
11
|
child_context = interface.with_parent(element)
|
|
12
12
|
content = interface.render_children(element, context: child_context)
|
|
13
|
-
|
|
13
|
+
# An empty quote shows nothing, but it is still a block: the
|
|
14
|
+
# blank line keeps the text in front and after it apart. In an
|
|
15
|
+
# HTML block a blank line would end the block.
|
|
16
|
+
return interface.html_mode? ? "" : "\n\n" if content.empty?
|
|
14
17
|
return "<blockquote>#{content}</blockquote>" if interface.html_mode?
|
|
15
18
|
|
|
19
|
+
# A block at the start or the end brings its own blank lines.
|
|
20
|
+
content = content.gsub(/\A\n+|\n+\z/, "")
|
|
21
|
+
|
|
16
22
|
# Build Discourse quote BBCode
|
|
17
23
|
# Format: [quote="username, post:2, topic:456"]content[/quote]
|
|
18
24
|
# (post: is the post number within the topic, topic: the topic id)
|
|
@@ -24,6 +30,10 @@ module Markbridge
|
|
|
24
30
|
# Name-only attribution; a bare post_id/user_id can't
|
|
25
31
|
# produce a valid Discourse post reference.
|
|
26
32
|
"[quote=\"#{element.author || element.username}\"]\n#{content}\n[/quote]"
|
|
33
|
+
elsif nests_attributed_quote?(element)
|
|
34
|
+
# Discourse doesn't render a [quote="…"] inside a Markdown
|
|
35
|
+
# blockquote, so the outer quote is BBCode too.
|
|
36
|
+
"[quote]\n#{content}\n[/quote]"
|
|
27
37
|
else
|
|
28
38
|
# Plain quote rendered as Markdown blockquote
|
|
29
39
|
content.split("\n").map { |line| "> #{line}" }.join("\n")
|
|
@@ -34,6 +44,21 @@ module Markbridge
|
|
|
34
44
|
# inline elements) starts a new paragraph against the quote.
|
|
35
45
|
"\n\n#{body}\n\n"
|
|
36
46
|
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# @param element [AST::Element]
|
|
51
|
+
# @return [Boolean] whether a quote that renders as [quote="…"]
|
|
52
|
+
# is somewhere inside +element+
|
|
53
|
+
def nests_attributed_quote?(element)
|
|
54
|
+
element.children.any? do |child|
|
|
55
|
+
child.is_a?(AST::Element) &&
|
|
56
|
+
(
|
|
57
|
+
(child.is_a?(AST::Quote) && (child.author || child.username)) ||
|
|
58
|
+
nests_attributed_quote?(child)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
37
62
|
end
|
|
38
63
|
end
|
|
39
64
|
end
|