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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9a481ff76e961a6f9c93e9cea998a6cdbe02ee46000c3acfba4e00023c12fd4
|
|
4
|
+
data.tar.gz: ffcc31afdae7383a83a2a51a26bfacfcd1c482dd5e779144cb6263b8547ed63c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46ba479a92c780d448385002567fea76f51478d18af74b6e7675bf916d01e5bbf61d56fd4f6b9f8ce1fd702662a9d41f15f1d43d86ec605e62604f2171b17795
|
|
7
|
+
data.tar.gz: 1eb764723017c5ca0afefb538945b37343dddf9aefb891947a7c3357451078bea5b045588c66275e517a95343ebef0a9bb9bfaef17639ded233333a819af0c9f
|
data/lib/markbridge/ast/image.rb
CHANGED
|
@@ -9,6 +9,9 @@ module Markbridge
|
|
|
9
9
|
#
|
|
10
10
|
# @example Image with dimensions
|
|
11
11
|
# image = AST::Image.new(src: "https://example.com/img.png", width: 100, height: 100)
|
|
12
|
+
#
|
|
13
|
+
# @example Image with alternative text
|
|
14
|
+
# image = AST::Image.new(src: "https://example.com/img.png", alt: "a cat")
|
|
12
15
|
class Image < Element
|
|
13
16
|
# @return [String, nil] the image source URL
|
|
14
17
|
attr_reader :src
|
|
@@ -19,16 +22,21 @@ module Markbridge
|
|
|
19
22
|
# @return [Integer, nil] the image height
|
|
20
23
|
attr_reader :height
|
|
21
24
|
|
|
25
|
+
# @return [String, nil] the alternative text
|
|
26
|
+
attr_reader :alt
|
|
27
|
+
|
|
22
28
|
# Create a new Image element.
|
|
23
29
|
#
|
|
24
30
|
# @param src [String, nil] the image source URL
|
|
25
31
|
# @param width [Integer, nil] the image width
|
|
26
32
|
# @param height [Integer, nil] the image height
|
|
27
|
-
|
|
33
|
+
# @param alt [String, nil] the alternative text
|
|
34
|
+
def initialize(src: nil, width: nil, height: nil, alt: nil)
|
|
28
35
|
super()
|
|
29
36
|
@src = src
|
|
30
37
|
@width = width
|
|
31
38
|
@height = height
|
|
39
|
+
@alt = alt
|
|
32
40
|
end
|
|
33
41
|
end
|
|
34
42
|
end
|
data/lib/markbridge/ast/url.rb
CHANGED
|
@@ -12,6 +12,10 @@ module Markbridge
|
|
|
12
12
|
# url = AST::Url.new(href: "https://example.com")
|
|
13
13
|
# url << AST::Text.new("https://example.com")
|
|
14
14
|
class Url < Element
|
|
15
|
+
# Whitespace at the start or at the end of the text.
|
|
16
|
+
EDGE_SPACE = /\A[[:space:]]+|[[:space:]]+\z/
|
|
17
|
+
private_constant :EDGE_SPACE
|
|
18
|
+
|
|
15
19
|
# @return [String, nil] the URL/href for this link
|
|
16
20
|
attr_reader :href
|
|
17
21
|
|
|
@@ -24,18 +28,39 @@ module Markbridge
|
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
# Whether this link is "bare" — it has no link text of its own:
|
|
27
|
-
# either no children, or a single Text child whose content is
|
|
28
|
-
#
|
|
29
|
-
#
|
|
31
|
+
# either no children, or a single Text child whose content is the
|
|
32
|
+
# href (how parsers model a URL that stands for itself).
|
|
33
|
+
# Whitespace at the edges of the text and line breaks in front of
|
|
34
|
+
# it or after it don't count: forum posts often have
|
|
35
|
+
# `[url] https://example.com [/url]` or a line break right after
|
|
36
|
+
# `[url]`. Consumers that record or rewrite links need this
|
|
30
37
|
# judgment too (a bare URL must stay bare to keep autolinking
|
|
31
38
|
# and oneboxing working), so it lives on the node rather than
|
|
32
39
|
# in the renderer.
|
|
33
40
|
#
|
|
34
41
|
# @return [Boolean]
|
|
42
|
+
# @example
|
|
43
|
+
# url = AST::Url.new(href: "https://example.com")
|
|
44
|
+
# url << AST::LineBreak.new << AST::Text.new(" https://example.com ")
|
|
45
|
+
# url.bare? # => true
|
|
35
46
|
def bare?
|
|
36
47
|
return true if children.empty?
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
bare_text = nil
|
|
50
|
+
children.each do |child|
|
|
51
|
+
if child.instance_of?(Text)
|
|
52
|
+
return false if bare_text
|
|
53
|
+
|
|
54
|
+
bare_text = child.text
|
|
55
|
+
elsif !child.instance_of?(LineBreak)
|
|
56
|
+
return false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
return false if bare_text.nil?
|
|
60
|
+
# Most bare links have no whitespace to take off.
|
|
61
|
+
return true if bare_text == href
|
|
62
|
+
|
|
63
|
+
bare_text.gsub(EDGE_SPACE, "") == href
|
|
39
64
|
end
|
|
40
65
|
end
|
|
41
66
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
# Raised when a post holds something whose output depends on a plugin
|
|
5
|
+
# of the target site and the renderer was not told whether the site
|
|
6
|
+
# has it. See the +features:+ option of {Markbridge.discourse_renderer}.
|
|
7
|
+
class UndecidedFeatureError < StandardError
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -10,6 +10,7 @@ module Markbridge
|
|
|
10
10
|
# - [img=100x100]url[/img]
|
|
11
11
|
# - [img width=100]url[/img]
|
|
12
12
|
# - [img width=100 height=50]url[/img]
|
|
13
|
+
# - [img alt="a cat"]url[/img]
|
|
13
14
|
class ImageHandler < RawHandler
|
|
14
15
|
def initialize
|
|
15
16
|
super(AST::Image)
|
|
@@ -19,7 +20,7 @@ module Markbridge
|
|
|
19
20
|
|
|
20
21
|
def create_element(token:, content:)
|
|
21
22
|
width, height = extract_dimensions(token)
|
|
22
|
-
AST::Image.new(src: content, width:, height:)
|
|
23
|
+
AST::Image.new(src: content, width:, height:, alt: token.attrs[:alt])
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
OPTION_DIMENSIONS_PATTERN = /\A(?<width>\d+)(?:x(?<height>\d+))?\z/i
|
|
@@ -5,6 +5,9 @@ module Markbridge
|
|
|
5
5
|
module BBCode
|
|
6
6
|
module Handlers
|
|
7
7
|
# Handler for URL tags
|
|
8
|
+
# Supports:
|
|
9
|
+
# - [url=https://example.com]text[/url]
|
|
10
|
+
# - [url]https://example.com[/url], where the text is the href
|
|
8
11
|
class UrlHandler < BaseHandler
|
|
9
12
|
def initialize
|
|
10
13
|
@element_class = AST::Url
|
|
@@ -12,11 +15,32 @@ module Markbridge
|
|
|
12
15
|
|
|
13
16
|
def on_open(token:, context:, registry:, tokens: nil)
|
|
14
17
|
href = token.attrs[:href] || token.attrs[:url] || token.attrs[:option]
|
|
15
|
-
|
|
16
|
-
context.push(
|
|
18
|
+
content = href ? nil : content_href(token:, tokens:)
|
|
19
|
+
return context.push(AST::Url.new(href:), token:) unless content
|
|
20
|
+
|
|
21
|
+
# The text and the closing tag that content_href looked at.
|
|
22
|
+
tokens.next
|
|
23
|
+
tokens.next
|
|
24
|
+
context.add_child(AST::Url.new(href: content) << AST::Text.new(content))
|
|
17
25
|
end
|
|
18
26
|
|
|
19
27
|
attr_reader :element_class
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# [url]https://example.com[/url] has its href in the text. Only
|
|
32
|
+
# plain text followed by the closing tag counts: with tags inside
|
|
33
|
+
# the text is no URL, and the element keeps a nil href.
|
|
34
|
+
#
|
|
35
|
+
# @return [String, nil]
|
|
36
|
+
def content_href(token:, tokens:)
|
|
37
|
+
text, close = tokens&.peek_ahead(2)
|
|
38
|
+
return unless text.instance_of?(TextToken) && close.instance_of?(TagEndToken)
|
|
39
|
+
return unless close.tag == token.tag
|
|
40
|
+
|
|
41
|
+
href = text.text.strip
|
|
42
|
+
href unless href.empty?
|
|
43
|
+
end
|
|
20
44
|
end
|
|
21
45
|
end
|
|
22
46
|
end
|
|
@@ -14,8 +14,11 @@ module Markbridge
|
|
|
14
14
|
src = element["src"]
|
|
15
15
|
width = sanitize_dimension(element["width"])
|
|
16
16
|
height = sanitize_dimension(element["height"])
|
|
17
|
+
# An empty alt attribute means "no text", the same as a missing one.
|
|
18
|
+
alt = element["alt"]
|
|
19
|
+
alt = nil if alt&.empty?
|
|
17
20
|
|
|
18
|
-
ast_element = AST::Image.new(src:, width:, height:)
|
|
21
|
+
ast_element = AST::Image.new(src:, width:, height:, alt:)
|
|
19
22
|
parent << ast_element
|
|
20
23
|
|
|
21
24
|
# Return nil to signal: don't process children (void element)
|
|
@@ -21,8 +21,9 @@ module Markbridge
|
|
|
21
21
|
)
|
|
22
22
|
parent << node
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
|
|
24
|
+
# The content is only fallback text (the filename), so there are
|
|
25
|
+
# no children to process.
|
|
26
|
+
nil
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
attr_reader :element_class
|
|
@@ -17,11 +17,13 @@ module Markbridge
|
|
|
17
17
|
src: attrs[:src],
|
|
18
18
|
width: attrs[:width]&.to_i,
|
|
19
19
|
height: attrs[:height]&.to_i,
|
|
20
|
+
alt: attrs[:alt],
|
|
20
21
|
)
|
|
21
22
|
parent << node
|
|
22
23
|
|
|
23
|
-
#
|
|
24
|
-
|
|
24
|
+
# The content is only fallback text (the URL), so there are
|
|
25
|
+
# no children to process.
|
|
26
|
+
nil
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
attr_reader :element_class
|
|
@@ -9,6 +9,12 @@ module Markbridge
|
|
|
9
9
|
# - Plain text: <t>text content</t>
|
|
10
10
|
# - Rich text: <r><B>bold</B> <URL url="...">link</URL></r>
|
|
11
11
|
# - Markup preservation: <s> and <e> elements (ignored during parsing)
|
|
12
|
+
# - Ignored text: <i> (lowercase) holds text s9e did not use, like the
|
|
13
|
+
# newline after [list] or a stray [/b]. It is skipped too; <I> is italic.
|
|
14
|
+
# - Line breaks: <br/> followed by a newline in the next text node. The
|
|
15
|
+
# newline is only source formatting, like any whitespace at the start
|
|
16
|
+
# of a line, because phpBB renders the XML as HTML. Inside <CODE> the
|
|
17
|
+
# text is kept as it is.
|
|
12
18
|
#
|
|
13
19
|
# This format is used by phpBB 3.2+ and other forum software.
|
|
14
20
|
#
|
|
@@ -36,6 +42,12 @@ module Markbridge
|
|
|
36
42
|
handlers || HandlerRegistry.shared_default
|
|
37
43
|
end
|
|
38
44
|
@unknown_tags = Hash.new(0)
|
|
45
|
+
# Whether an AST class is one of the BLOCK_NODES, computed once
|
|
46
|
+
# per class, so an inline element doesn't check all of them.
|
|
47
|
+
@block_classes =
|
|
48
|
+
Hash.new do |block_class_cache, klass|
|
|
49
|
+
block_class_cache[klass] = BLOCK_NODES.any? { klass <= _1 }
|
|
50
|
+
end
|
|
39
51
|
end
|
|
40
52
|
|
|
41
53
|
# Parse s9e/TextFormatter XML into an AST.
|
|
@@ -49,11 +61,13 @@ module Markbridge
|
|
|
49
61
|
# @return [AST::Document]
|
|
50
62
|
def parse(input)
|
|
51
63
|
@unknown_tags.clear
|
|
64
|
+
@line_start = true
|
|
52
65
|
|
|
53
66
|
if input.is_a?(Nokogiri::XML::Node)
|
|
54
67
|
root = input.is_a?(Nokogiri::XML::Document) ? input.root : input
|
|
55
68
|
document = AST::Document.new
|
|
56
69
|
process_node(root, document) if root
|
|
70
|
+
flush_line_break
|
|
57
71
|
return document
|
|
58
72
|
end
|
|
59
73
|
|
|
@@ -70,17 +84,38 @@ module Markbridge
|
|
|
70
84
|
|
|
71
85
|
document = AST::Document.new
|
|
72
86
|
process_node(root, document)
|
|
87
|
+
flush_line_break
|
|
73
88
|
document
|
|
74
89
|
rescue Nokogiri::XML::SyntaxError
|
|
75
90
|
AST::Document.new << AST::Text.new(input)
|
|
76
91
|
end
|
|
77
92
|
|
|
78
|
-
# <s>/<e> hold the original BBCode markup for unparsing
|
|
79
|
-
# are the plain-text/rich-text roots.
|
|
80
|
-
# membership checks run once per element
|
|
81
|
-
|
|
93
|
+
# <s>/<e> hold the original BBCode markup for unparsing, <i> holds
|
|
94
|
+
# text that s9e ignored; <t>/<r> are the plain-text/rich-text roots.
|
|
95
|
+
# Frozen constants — the membership checks run once per element
|
|
96
|
+
# node. The check is case-sensitive: <I> is italic.
|
|
97
|
+
MARKUP_PRESERVATION_TAGS = %w[s e i].freeze
|
|
82
98
|
ROOT_TAGS = %w[t r].freeze
|
|
83
|
-
|
|
99
|
+
|
|
100
|
+
# AST nodes that phpBB shows as blocks. A new line starts before and
|
|
101
|
+
# after them, so whitespace at the start of their content and of the
|
|
102
|
+
# text after them is not visible.
|
|
103
|
+
BLOCK_NODES = [
|
|
104
|
+
AST::Align,
|
|
105
|
+
AST::Code,
|
|
106
|
+
AST::Details,
|
|
107
|
+
AST::Heading,
|
|
108
|
+
AST::HorizontalRule,
|
|
109
|
+
AST::List,
|
|
110
|
+
AST::ListItem,
|
|
111
|
+
AST::Paragraph,
|
|
112
|
+
AST::Quote,
|
|
113
|
+
AST::Spoiler,
|
|
114
|
+
AST::Table,
|
|
115
|
+
AST::TableCell,
|
|
116
|
+
AST::TableRow,
|
|
117
|
+
].freeze
|
|
118
|
+
private_constant :MARKUP_PRESERVATION_TAGS, :ROOT_TAGS, :BLOCK_NODES
|
|
84
119
|
|
|
85
120
|
# Process children of an XML element (public for handler access)
|
|
86
121
|
# @param element [Nokogiri::XML::Element]
|
|
@@ -114,33 +149,91 @@ module Markbridge
|
|
|
114
149
|
# Handle root nodes
|
|
115
150
|
return process_children(element, ast_parent) if ROOT_TAGS.include?(tag_name)
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
if tag_name == "br"
|
|
119
|
-
ast_parent << AST::LineBreak.new
|
|
120
|
-
return
|
|
121
|
-
end
|
|
152
|
+
return process_line_break(ast_parent) if tag_name == "br"
|
|
122
153
|
|
|
123
154
|
# Process element with registered handler
|
|
124
155
|
# Handler returns element if children should be processed, nil otherwise
|
|
156
|
+
flush_line_break
|
|
125
157
|
result_element = @handlers.process_element(element, ast_parent, self)
|
|
126
158
|
|
|
127
159
|
if result_element
|
|
128
160
|
# Handler succeeded and returned element - process children into it
|
|
129
|
-
|
|
130
|
-
elsif
|
|
161
|
+
process_handled_children(element, result_element)
|
|
162
|
+
elsif @handlers.has_handler?(tag_name)
|
|
163
|
+
# The handler returned nil intentionally: a leaf like an image
|
|
164
|
+
# or an attachment, whose content is only fallback text. It is
|
|
165
|
+
# visible, so the text after it is no longer at the line start.
|
|
166
|
+
@line_start = false
|
|
167
|
+
else
|
|
131
168
|
# No handler found - track as unknown and process children directly
|
|
132
169
|
@unknown_tags[tag_name] += 1
|
|
133
170
|
process_children(element, ast_parent)
|
|
134
171
|
end
|
|
135
|
-
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Two line breaks in a row are an empty line in phpBB. In Markdown
|
|
175
|
+
# that is a paragraph break, so a line break waits in
|
|
176
|
+
# +@pending_break+ until something visible follows it. When the
|
|
177
|
+
# next line break comes first, the two become a blank line. A
|
|
178
|
+
# third one comes after the blank line and is not visible.
|
|
179
|
+
#
|
|
180
|
+
# @param ast_parent [AST::Element]
|
|
181
|
+
def process_line_break(ast_parent)
|
|
182
|
+
if @pending_break.equal?(ast_parent)
|
|
183
|
+
@pending_break = nil
|
|
184
|
+
ast_parent << AST::Text.new("\n\n")
|
|
185
|
+
else
|
|
186
|
+
flush_line_break
|
|
187
|
+
@pending_break = ast_parent
|
|
188
|
+
end
|
|
189
|
+
@line_start = true
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Adds the line break that waits, if there is one, before anything
|
|
193
|
+
# visible is added after it.
|
|
194
|
+
def flush_line_break
|
|
195
|
+
@pending_break&.<<(AST::LineBreak.new)
|
|
196
|
+
@pending_break = nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# @param element [Nokogiri::XML::Element]
|
|
200
|
+
# @param ast_element [AST::Element]
|
|
201
|
+
def process_handled_children(element, ast_element)
|
|
202
|
+
if @block_classes[ast_element.class]
|
|
203
|
+
@line_start = true
|
|
204
|
+
process_children(element, ast_element)
|
|
205
|
+
@line_start = true
|
|
206
|
+
else
|
|
207
|
+
process_children(element, ast_element)
|
|
208
|
+
end
|
|
136
209
|
end
|
|
137
210
|
|
|
138
211
|
# Process text node
|
|
212
|
+
#
|
|
213
|
+
# Outside of code, whitespace at the start of a line is not visible
|
|
214
|
+
# in phpBB, but in Markdown a newline after a line break would end
|
|
215
|
+
# the paragraph and four spaces would start a code block, so it is
|
|
216
|
+
# removed. Whitespace-only text between two inline elements (the
|
|
217
|
+
# space in "<B>a</B> <I>b</I>") is kept; with a newline in it, it
|
|
218
|
+
# is only formatting between blocks.
|
|
219
|
+
#
|
|
139
220
|
# @param text_node [Nokogiri::XML::Text]
|
|
140
221
|
# @param ast_parent [AST::Element]
|
|
141
222
|
def process_text(text_node, ast_parent)
|
|
142
223
|
text = text_node.content
|
|
143
|
-
|
|
224
|
+
|
|
225
|
+
if ast_parent.is_a?(AST::Code)
|
|
226
|
+
ast_parent << AST::Text.new(text) if text.match?(/\S/)
|
|
227
|
+
elsif @line_start
|
|
228
|
+
text = text.lstrip
|
|
229
|
+
return if text.empty?
|
|
230
|
+
|
|
231
|
+
@line_start = false
|
|
232
|
+
flush_line_break
|
|
233
|
+
ast_parent << AST::Text.new(text)
|
|
234
|
+
elsif text.match?(/\S/) || !text.include?("\n")
|
|
235
|
+
ast_parent << AST::Text.new(text)
|
|
236
|
+
end
|
|
144
237
|
end
|
|
145
238
|
end
|
|
146
239
|
end
|
|
@@ -11,15 +11,21 @@ module Markbridge
|
|
|
11
11
|
# renders into such a block therefore picks one of two forms:
|
|
12
12
|
#
|
|
13
13
|
# 1. raw HTML — an HTML equivalent of the tag's Markdown, spliced
|
|
14
|
-
# into the block as it is;
|
|
14
|
+
# into the block as it is, with no blank line in it;
|
|
15
15
|
# 2. a Markdown island — the tag's normal Markdown surrounded by
|
|
16
|
-
# blank lines, which end the block and
|
|
16
|
+
# blank lines, which end the block, and a line that opens it
|
|
17
|
+
# again after them:
|
|
17
18
|
#
|
|
18
19
|
# <div align="center">
|
|
19
20
|
#
|
|
20
21
|
# [a link](https://example.com)
|
|
21
22
|
#
|
|
22
23
|
# </div>
|
|
24
|
+
#
|
|
25
|
+
# Inside an HTML block the island is always in a `<div>`
|
|
26
|
+
# (.enclosed_island, which a tag reaches through
|
|
27
|
+
# RenderingInterface#markdown_island). After a bare island the rest
|
|
28
|
+
# of the cell would be Markdown too.
|
|
23
29
|
module HtmlBlock
|
|
24
30
|
# Tag names that start a type 6 HTML block when a line begins
|
|
25
31
|
# with `<name` or `</name` followed by a space, a tab, `>`, `/>`
|
|
@@ -94,6 +100,10 @@ module Markbridge
|
|
|
94
100
|
# are item content that the list builder has not indented yet.
|
|
95
101
|
OPENER = %r{\A[ \t]*</?(?:#{Regexp.union(BLOCK_TAGS).source})(?:[ \t>]|/>|\z)}i
|
|
96
102
|
|
|
103
|
+
# The newline in front of a line that holds nothing but spaces and
|
|
104
|
+
# tabs: with the line it makes a blank line.
|
|
105
|
+
BLANK_LINE_NEWLINE = /\n(?=[ \t]*\n)/
|
|
106
|
+
|
|
97
107
|
# Markdown sigils that would surface as literal text inside an
|
|
98
108
|
# HTML block: emphasis (`*`, `_`, `~`) and link middles (`](`).
|
|
99
109
|
MARKDOWN_SIGILS = /[*_~]|\]\(/
|
|
@@ -102,7 +112,7 @@ module Markbridge
|
|
|
102
112
|
# content line keeps its own indentation, e.g. a nested ` - a`.
|
|
103
113
|
BLANK_EDGES = /\A(?:[ \t]*\n)+|(?:\n[ \t]*)+\z/
|
|
104
114
|
|
|
105
|
-
private_constant :BLOCK_TAGS, :OPENER, :MARKDOWN_SIGILS, :BLANK_EDGES
|
|
115
|
+
private_constant :BLOCK_TAGS, :OPENER, :BLANK_LINE_NEWLINE, :MARKDOWN_SIGILS, :BLANK_EDGES
|
|
106
116
|
|
|
107
117
|
# Whether +line+ starts an HTML block. A closing tag such as
|
|
108
118
|
# `</div>` starts one too. The block runs until the next blank
|
|
@@ -124,17 +134,50 @@ module Markbridge
|
|
|
124
134
|
"\n\n#{markdown.gsub(BLANK_EDGES, "")}\n\n"
|
|
125
135
|
end
|
|
126
136
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
137
|
+
# An island for the inside of an HTML block, in a `<div>`. After
|
|
138
|
+
# a bare island what follows on the same lines is Markdown until
|
|
139
|
+
# a line opens an HTML block again, so text after it in a table
|
|
140
|
+
# cell would get Markdown meaning; the `</div>` line opens the
|
|
141
|
+
# block again. A `div` is a block in the HTML as well, so the
|
|
142
|
+
# island shows as a paragraph of its own, like a bare island
|
|
143
|
+
# does.
|
|
144
|
+
# @param markdown [String]
|
|
145
|
+
# @return [String]
|
|
146
|
+
# @example
|
|
147
|
+
# HtmlBlock.enclosed_island("") # => "<div>\n\n\n\n</div>"
|
|
148
|
+
def self.enclosed_island(markdown)
|
|
149
|
+
"<div>#{island(markdown)}</div>"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Text for the inside of an HTML block, with no blank line in it:
|
|
153
|
+
# a blank line ends the block, and what follows is Markdown
|
|
154
|
+
# again. The newline that would start a blank line is written as
|
|
155
|
+
# the entity, which the HTML parser reads back as a newline.
|
|
156
|
+
# @param text [String] text or HTML, already escaped
|
|
157
|
+
# @return [String]
|
|
158
|
+
# @example
|
|
159
|
+
# HtmlBlock.without_blank_lines("a\n\nb") # => "a \nb"
|
|
160
|
+
def self.without_blank_lines(text)
|
|
161
|
+
return text unless text.include?("\n")
|
|
162
|
+
|
|
163
|
+
text.gsub(BLANK_LINE_NEWLINE, " ")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Whether a fragment can be spliced into an HTML block: an island
|
|
167
|
+
# in a `<div>`, or raw HTML or plain text without Markdown sigils
|
|
168
|
+
# and without a blank line. A blank line anywhere else ends the
|
|
169
|
+
# block, and what follows it is Markdown until a line opens a
|
|
170
|
+
# block again; a bare island, `\n\n…\n\n`, is not accepted for
|
|
171
|
+
# that reason.
|
|
129
172
|
#
|
|
130
173
|
# Used by the html_mode contract check that ships in
|
|
131
174
|
# +markbridge/rspec+ and by this repo's own contract spec.
|
|
132
175
|
# @param output [String] a tag's html_mode render result
|
|
133
176
|
# @return [Boolean]
|
|
134
177
|
def self.safe?(output)
|
|
135
|
-
return true if output.start_with?("
|
|
178
|
+
return true if output.start_with?("<div>\n\n") && output.end_with?("\n\n</div>")
|
|
136
179
|
|
|
137
|
-
!output.match?(MARKDOWN_SIGILS)
|
|
180
|
+
!output.match?(MARKDOWN_SIGILS) && !output.match?(BLANK_LINE_NEWLINE)
|
|
138
181
|
end
|
|
139
182
|
end
|
|
140
183
|
end
|