markbridge 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa422d5e0a05eee7f32524e42823d02a552111132025fd9ede1fc5045a79412e
4
- data.tar.gz: cc7af6319a7c9dde74a764dc9edaab186ef2cca92bc6fabd842a147216adaec0
3
+ metadata.gz: 356955e6ba849a6acb08902465ed0d45c710020183d75eca66d3904b93995f3c
4
+ data.tar.gz: 4c1bf20ea2c024478cdfa6bbea5ceade531de96085a2cc911499a0c8d31d78ad
5
5
  SHA512:
6
- metadata.gz: 2eb57814326cf0fbaf2e356290d962a0a9e8bf2b9c38aa4e41ad7526d9494574162fc5384473b15c833b4e34bd7f322bf464098337bf9d535662f615590c19e4
7
- data.tar.gz: d4fc22c47cc54ab91a0d29b140bf69317c5c10aae3358758b0036b213f96ddca9318d723016854819a8a80dcf03ea5f66180eaccc8df2d550d06ab69b8f25a87
6
+ metadata.gz: 0fbb3282126a07f194e4c833bc2694b91a64b877144e6ea0317ff2f62785a28dd144a969cc5d4a3fc1eda3306fa1416496cc478b6fe51ffe4862dba9f468a288
7
+ data.tar.gz: 0624ec779e99b904de91eb2d519b65ee7c123d2b8e8d51c659b4ce5005afc521bd2290e07517a7f9aaf5ea9e19998354b31e7a6818e27fe55dceb02da22cad41
@@ -32,8 +32,13 @@ module Markbridge
32
32
  def register(tag_names, handler)
33
33
  element_class = handler.element_class
34
34
  Array(tag_names).each { |tag_name| @handlers[tag_name.to_s.downcase] = handler }
35
- @element_handlers[element_class] = handler
35
+ # The conditional write goes first. Both writes are independent and
36
+ # keyed by element_class, so the order makes no difference to a
37
+ # successful register. On a frozen registry it decides which
38
+ # collection raises, and only a write that can be skipped lets a
39
+ # spec reach the one behind it.
36
40
  @auto_closeable_elements << element_class if handler.auto_closeable?
41
+ @element_handlers[element_class] = handler
37
42
  self
38
43
  end
39
44
 
@@ -70,9 +70,21 @@ module Markbridge
70
70
  # a <pre> keep their semantics.
71
71
  @preserve_depth = initial_preserve_depth(doc)
72
72
 
73
+ # Whitespace collapsing follows the browser rule (CSS Text
74
+ # §4.1.1): a collapsible space is dropped at the start of a
75
+ # line, at the end of a line, and right after another
76
+ # collapsible space — wherever the inline boundaries fall, so
77
+ # `<b>a </b> b` keeps one space and `<p><b>x </b></p>` none.
78
+ # A line starts at the parse root and on every block-level tag
79
+ # boundary. @space_tail is the element whose last child is a
80
+ # collapsible Text ending in a space (the candidate for the
81
+ # end-of-line trim), or nil.
82
+ @line_start = true
83
+ @space_tail = nil
84
+
73
85
  # Process all nodes
74
86
  children.each { |node| process_node(node, document) }
75
- trim_trailing_whitespace(document)
87
+ end_line
76
88
 
77
89
  document
78
90
  end
@@ -117,17 +129,27 @@ module Markbridge
117
129
 
118
130
  if @preserve_depth.positive?
119
131
  parent << AST::Text.new(text)
132
+ # Preserved whitespace is content: it neither starts a line
133
+ # nor collapses against a space that follows it.
134
+ @line_start = false
135
+ @space_tail = nil
120
136
  return
121
137
  end
122
138
 
123
- text = text.gsub(WHITESPACE_RUN, " ") if text.match?(COLLAPSIBLE_WHITESPACE)
124
- # Drop leading whitespace at the start of an element's content,
125
- # matching the browser rule that whitespace at the beginning of a
126
- # block (or before any inline content) is collapsed away.
127
- text = text.lstrip if parent.children.empty?
139
+ text = collapse_whitespace(text)
140
+ text = text.lstrip if @line_start || @space_tail
128
141
  return if text.empty?
129
142
 
130
143
  parent << AST::Text.new(text)
144
+ @line_start = false
145
+ @space_tail = (parent if text.end_with?(" "))
146
+ end
147
+
148
+ # Collapse runs of whitespace to a single space.
149
+ # @param text [String]
150
+ # @return [String]
151
+ def collapse_whitespace(text)
152
+ text.match?(COLLAPSIBLE_WHITESPACE) ? text.gsub(WHITESPACE_RUN, " ") : text
131
153
  end
132
154
 
133
155
  # Process an element node
@@ -137,19 +159,21 @@ module Markbridge
137
159
  tag_name = node.name
138
160
  return if IGNORED_TAGS.include?(tag_name)
139
161
 
140
- # Drop whitespace that sits between content and the start of a
141
- # block-level tag, matching browser behavior where such whitespace
142
- # collapses against the block boundary. Applies whether or not a
143
- # handler is registered, so unknown tags like <div> or <section>
144
- # still collapse the whitespace before them.
145
- trim_trailing_whitespace(parent) if @handlers.block_level_tags.include?(tag_name)
162
+ # A block-level tag ends the line before it and the line inside
163
+ # it, matching browser behavior where whitespace collapses
164
+ # against the block boundary. Applies whether or not a handler
165
+ # is registered, so unknown tags like <div> or <section> still
166
+ # end lines.
167
+ block = @handlers.block_level_tags.include?(tag_name)
168
+ end_line if block
146
169
 
147
170
  preserving = @handlers.whitespace_preserving_tags.include?(tag_name)
148
171
  @preserve_depth += 1 if preserving
149
172
 
150
- dispatch_element(node, tag_name, parent, preserving)
173
+ dispatch_element(node, tag_name, parent)
151
174
 
152
175
  @preserve_depth -= 1 if preserving
176
+ end_line if block
153
177
  end
154
178
 
155
179
  # Dispatch an element to its handler (or the unknown-tag path) and
@@ -157,18 +181,22 @@ module Markbridge
157
181
  # @param node [Nokogiri::XML::Element]
158
182
  # @param tag_name [String]
159
183
  # @param parent [AST::Element]
160
- # @param preserving [Boolean] whether this tag preserves whitespace
161
- def dispatch_element(node, tag_name, parent, preserving)
184
+ def dispatch_element(node, tag_name, parent)
162
185
  handler = @handlers[tag_name]
163
186
  return handle_unknown_tag(node, parent) unless handler
164
187
 
165
188
  # Handler returns element if children should be processed, nil otherwise
189
+ size = parent.children.size
166
190
  ast_element = handler.process(element: node, parent:)
167
191
 
168
- return unless ast_element
169
-
170
- process_children(node, ast_element)
171
- trim_trailing_whitespace(ast_element) unless preserving
192
+ if ast_element
193
+ process_children(node, ast_element)
194
+ elsif parent.children.size > size
195
+ # The handler appended a leaf (image, line break, ...), which
196
+ # is content on the current line like a word.
197
+ @line_start = false
198
+ @space_tail = nil
199
+ end
172
200
  end
173
201
 
174
202
  # Handle unknown tag by tracking it and ignoring the wrapper
@@ -180,6 +208,16 @@ module Markbridge
180
208
  process_children(node, parent)
181
209
  end
182
210
 
211
+ # End the current line: the collapsible space that closed it goes,
212
+ # and the next collapsible space starts a line. @space_tail is
213
+ # not cleared here: the trim is idempotent, so a stale tail is
214
+ # harmless until the next text or leaf on the new line resets it,
215
+ # and @line_start already covers the lstrip.
216
+ def end_line
217
+ trim_trailing_whitespace(@space_tail) if @space_tail
218
+ @line_start = true
219
+ end
220
+
183
221
  # Number of whitespace-preserving elements enclosing the parse
184
222
  # root, the root itself included. Computed once per parse; from
185
223
  # there the counter is maintained during descent. Documents and
@@ -28,18 +28,25 @@ module Markbridge
28
28
 
29
29
  if interface.html_mode?
30
30
  %(<a href="#{HtmlEscaper.escape(href)}">#{text}</a>)
31
- elsif element.bare? || text.empty?
31
+ elsif element.bare? || blank?(text)
32
32
  # Url#bare? judges the AST (so label escaping can't confuse
33
33
  # it); the rendered-text check additionally catches labels
34
- # that render to nothing (e.g. an empty formatting child).
34
+ # that render to nothing (e.g. an empty formatting child)
35
+ # or to whitespace only, which wrap_inline would leave
36
+ # unlinked.
35
37
  href
36
38
  else
37
- "[#{text}](#{markdown_destination(href)})"
39
+ interface.wrap_inline(text, "[", "](#{markdown_destination(href)})")
38
40
  end
39
41
  end
40
42
 
41
43
  private
42
44
 
45
+ # Unicode-aware, matching the guard in RenderingInterface#wrap_inline.
46
+ def blank?(text)
47
+ !text.match?(/[^[:space:]]/)
48
+ end
49
+
43
50
  # CommonMark link destinations cannot contain whitespace unless
44
51
  # wrapped in <> — relevant for relative targets like MediaWiki
45
52
  # page names ("Main Page").
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Markbridge
4
- VERSION = "0.4.2"
4
+ VERSION = "0.4.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markbridge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Discourse Team