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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/markbridge/ast/image.rb +9 -1
  3. data/lib/markbridge/ast/url.rb +29 -4
  4. data/lib/markbridge/errors.rb +9 -0
  5. data/lib/markbridge/parsers/bbcode/handler_registry.rb +6 -1
  6. data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +2 -1
  7. data/lib/markbridge/parsers/bbcode/handlers/url_handler.rb +26 -2
  8. data/lib/markbridge/parsers/html/handlers/image_handler.rb +4 -1
  9. data/lib/markbridge/parsers/html/parser.rb +57 -19
  10. data/lib/markbridge/parsers/text_formatter/handlers/attachment_handler.rb +3 -2
  11. data/lib/markbridge/parsers/text_formatter/handlers/image_handler.rb +4 -2
  12. data/lib/markbridge/parsers/text_formatter/parser.rb +107 -14
  13. data/lib/markbridge/renderers/discourse/html_block.rb +50 -7
  14. data/lib/markbridge/renderers/discourse/inline/joins.rb +382 -0
  15. data/lib/markbridge/renderers/discourse/inline/serializer.rb +738 -0
  16. data/lib/markbridge/renderers/discourse/inline/tokens.rb +182 -0
  17. data/lib/markbridge/renderers/discourse/link_destination.rb +43 -0
  18. data/lib/markbridge/renderers/discourse/markdown_escaper.rb +105 -45
  19. data/lib/markbridge/renderers/discourse/paragraphs.rb +88 -0
  20. data/lib/markbridge/renderers/discourse/postprocessor.rb +183 -7
  21. data/lib/markbridge/renderers/discourse/render_context.rb +38 -12
  22. data/lib/markbridge/renderers/discourse/renderer.rb +336 -92
  23. data/lib/markbridge/renderers/discourse/rendering_interface.rb +123 -44
  24. data/lib/markbridge/renderers/discourse/tag.rb +53 -9
  25. data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +4 -6
  26. data/lib/markbridge/renderers/discourse/tags/code_tag.rb +48 -10
  27. data/lib/markbridge/renderers/discourse/tags/color_tag.rb +73 -13
  28. data/lib/markbridge/renderers/discourse/tags/details_tag.rb +62 -15
  29. data/lib/markbridge/renderers/discourse/tags/email_tag.rb +8 -9
  30. data/lib/markbridge/renderers/discourse/tags/event_tag.rb +31 -9
  31. data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +20 -1
  32. data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +12 -1
  33. data/lib/markbridge/renderers/discourse/tags/image_tag.rb +13 -27
  34. data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +4 -6
  35. data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +9 -2
  36. data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +75 -9
  37. data/lib/markbridge/renderers/discourse/tags/list_tag.rb +29 -7
  38. data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +12 -0
  39. data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +13 -6
  40. data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +52 -9
  41. data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +26 -1
  42. data/lib/markbridge/renderers/discourse/tags/size_tag.rb +78 -13
  43. data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +72 -14
  44. data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +5 -6
  45. data/lib/markbridge/renderers/discourse/tags/subscript_tag.rb +11 -5
  46. data/lib/markbridge/renderers/discourse/tags/superscript_tag.rb +11 -5
  47. data/lib/markbridge/renderers/discourse/tags/table_tag.rb +48 -28
  48. data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +3 -10
  49. data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +90 -43
  50. data/lib/markbridge/renderers/discourse/tags/url_tag.rb +43 -26
  51. data/lib/markbridge/renderers/discourse.rb +6 -0
  52. data/lib/markbridge/rspec.rb +4 -2
  53. data/lib/markbridge/version.rb +1 -1
  54. data/lib/markbridge.rb +90 -1
  55. metadata +7 -1
@@ -4,12 +4,12 @@ module Markbridge
4
4
  module Renderers
5
5
  module Discourse
6
6
  module Tags
7
- # Tag for rendering URLs
8
- #
9
- # Bare URLs (a single Text child equal to the href, or no link
10
- # text at all) render as the plain href instead of a Markdown
11
- # link — in Discourse a bare URL on its own line oneboxes, a
12
- # `[text](url)` link does not.
7
+ # Links. A bare link (no text of its own, or a text that is the
8
+ # href, or a label with nothing visible in it) is a bare URL token:
9
+ # the serializer decides from the neighbors whether it can stay
10
+ # bare, which oneboxes in Discourse, or has to be written as
11
+ # `[url](url)`. Any other link is a link token pair around the
12
+ # children.
13
13
  class UrlTag < Tag
14
14
  # Schemes that are safe to link. Anything else that *looks*
15
15
  # like a scheme (javascript:, data:, vbscript:, …) is dropped;
@@ -17,34 +17,51 @@ module Markbridge
17
17
  # URLs) pass through — common in forum exports and harmless.
18
18
  ALLOWED_SCHEMES = /\A(?:https?|ftps?|mailto):/i
19
19
  SCHEME_LIKE = /\A[a-z][a-z0-9+.-]*:/i
20
- private_constant :ALLOWED_SCHEMES, :SCHEME_LIKE
20
+ # The whitespace in front of the text and after it.
21
+ EDGES = /\A([[:space:]]*).*?([[:space:]]*)\z/m
22
+ private_constant :ALLOWED_SCHEMES, :SCHEME_LIKE, :EDGES
21
23
 
22
24
  def render(element, interface)
23
- child_context = interface.with_parent(element)
24
- text = interface.render_children(element, context: child_context)
25
25
  href = element.href
26
+ linkable = linkable?(href)
27
+ # Url#bare? judges the AST, so label escaping cannot confuse it.
28
+ return bare_tokens(element, href) if linkable && element.bare?
26
29
 
27
- return text unless linkable?(href)
28
-
29
- if interface.html_mode?
30
- %(<a href="#{HtmlEscaper.escape(href)}">#{text}</a>)
31
- elsif element.bare? || text.empty?
32
- # Url#bare? judges the AST (so label escaping can't confuse
33
- # it); the rendered-text check additionally catches labels
34
- # that render to nothing (e.g. an empty formatting child).
35
- href
36
- else
37
- "[#{text}](#{markdown_destination(href)})"
38
- end
30
+ tokens = interface.render_tokens(element, context: interface.with_parent(element))
31
+ return tokens unless linkable
32
+ # A label that renders to nothing (an empty formatting child)
33
+ # or to whitespace only is no label either.
34
+ return [Inline::Tokens.bare_url(href)] unless Inline::Tokens.visible?(tokens)
35
+
36
+ tokens.unshift(Inline::Tokens.link_open(href)) << Inline::Tokens.link_close
39
37
  end
40
38
 
41
39
  private
42
40
 
43
- # CommonMark link destinations cannot contain whitespace unless
44
- # wrapped in <> — relevant for relative targets like MediaWiki
45
- # page names ("Main Page").
46
- def markdown_destination(href)
47
- href.match?(/\s/) ? "<#{href}>" : href
41
+ # The bare URL in place of the text. Whitespace at the edges of
42
+ # the text and the line breaks in front of it and after it stay
43
+ # outside the URL, so `[url] https://e.com [/url]` keeps its
44
+ # spaces. A bare link has only Text and LineBreak children (see
45
+ # Url#bare?); the line breaks are written as hard break tokens
46
+ # here, the serializer picks their form.
47
+ def bare_tokens(element, href)
48
+ bare = Inline::Tokens.bare_url(href)
49
+ return [bare] if element.children.empty?
50
+
51
+ element
52
+ .children
53
+ .each_with_object([]) do |child, tokens|
54
+ next tokens << Inline::Tokens.hard_break if child.instance_of?(AST::LineBreak)
55
+
56
+ label_text = child.text
57
+ # Most bare links have no whitespace to keep.
58
+ next tokens << bare if label_text == href
59
+
60
+ lead, trail = label_text.match(EDGES).captures
61
+ tokens << Inline::Tokens.text(lead) unless lead.empty?
62
+ tokens << bare
63
+ tokens << Inline::Tokens.text(trail) unless trail.empty?
64
+ end
48
65
  end
49
66
 
50
67
  def linkable?(href)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../errors"
3
4
  require_relative "discourse/tag"
4
5
  require_relative "discourse/tag_library"
5
6
  require_relative "discourse/render_context"
@@ -8,6 +9,11 @@ require_relative "discourse/markdown_escaper"
8
9
  require_relative "discourse/identity_escaper"
9
10
  require_relative "discourse/html_escaper"
10
11
  require_relative "discourse/html_block"
12
+ require_relative "discourse/paragraphs"
13
+ require_relative "discourse/link_destination"
14
+ require_relative "discourse/inline/tokens"
15
+ require_relative "discourse/inline/joins"
16
+ require_relative "discourse/inline/serializer"
11
17
  require_relative "discourse/postprocessor"
12
18
 
13
19
  # Builders
@@ -38,9 +38,11 @@ RSpec.shared_examples "an html_mode safe tag" do
38
38
 
39
39
  it "renders html_mode output that is safe inside an HTML block" do
40
40
  output = tag.render(element, markbridge_html_mode_interface)
41
+ output = markbridge_renderer.serialize(output, markbridge_html_mode_interface.context)
41
42
 
42
43
  expect(Markbridge::Renderers::Discourse::HtmlBlock.safe?(output)).to be(true),
43
- "Expected #{tag.class} to render raw HTML or a \\n\\n-wrapped " \
44
- "Markdown island in html_mode, got: #{output.inspect}"
44
+ "Expected #{tag.class} to render raw HTML without a blank line, or a " \
45
+ "Markdown island in a <div> (interface.markdown_island) in html_mode, " \
46
+ "got: #{output.inspect}"
45
47
  end
46
48
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Markbridge
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/markbridge.rb CHANGED
@@ -9,6 +9,26 @@ require_relative "markbridge/normalizer"
9
9
  require_relative "markbridge/renderers/discourse"
10
10
 
11
11
  module Markbridge
12
+ # The tags of the plugins bundled with Discourse, by feature. The
13
+ # discourse-bbcode plugin (:bbcode) has its own registration, its tags
14
+ # take the mappings.
15
+ FEATURE_TAGS = {
16
+ details: {
17
+ AST::Details => Renderers::Discourse::Tags::DetailsTag,
18
+ },
19
+ spoiler: {
20
+ AST::Spoiler => Renderers::Discourse::Tags::SpoilerTag,
21
+ },
22
+ poll: {
23
+ AST::Poll => Renderers::Discourse::Tags::PollTag,
24
+ },
25
+ post_event: {
26
+ AST::Event => Renderers::Discourse::Tags::EventTag,
27
+ },
28
+ }.freeze
29
+ FEATURES = (FEATURE_TAGS.keys + [:bbcode]).freeze
30
+ private_constant :FEATURE_TAGS, :FEATURES
31
+
12
32
  class << self
13
33
  # Parse BBCode to AST.
14
34
  #
@@ -297,7 +317,32 @@ module Markbridge
297
317
  # {Renderers::Discourse::Postprocessor} when no explicit
298
318
  # +postprocessor:+ is given. Strips NBSP and zero-width format
299
319
  # characters from the end of each line.
320
+ # @param features [Hash{Symbol => Boolean}, nil] the plugins of the
321
+ # target site. +:details+, +:spoiler+, +:poll+ and +:post_event+ are
322
+ # bundled with Discourse and on by default. With one of them off,
323
+ # the tag writes a plain form instead of the plugin's BBCode: the
324
+ # title in bold and the content for a details block, the content
325
+ # for a spoiler, the options as a list for a poll, a paragraph with
326
+ # the name and the dates for an event. +:bbcode+ is the
327
+ # discourse-bbcode plugin, which needs an install, and starts
328
+ # undecided: a color or size in a post raises
329
+ # {UndecidedFeatureError} until it is +false+ (the text only) or
330
+ # +true+ (the BBCode, with +sizes:+). Mappings in +tags:+ still win.
331
+ # @param sizes [Hash, #call, nil] with +bbcode: true+, the mapping
332
+ # from the size values of the source forum to the percent value
333
+ # the plugin reads: a whole number from 1 to 999, or nil to drop a
334
+ # size. A Hash is read with +fetch+, so an unknown value raises
335
+ # KeyError. Required with +bbcode: true+, because every forum has
336
+ # its own scale.
337
+ # @param colors [Hash, #call, nil] with +bbcode: true+, a mapping from
338
+ # the color values of the source forum to +#rgb+, +#rrggbb+ or a
339
+ # name, or nil to drop a color. Without it the values are written
340
+ # as they are, and a value the plugin does not accept raises
341
+ # ArgumentError.
300
342
  # @return [Renderers::Discourse::Renderer]
343
+ # @raise [ArgumentError] for a feature that does not exist, for
344
+ # +bbcode: true+ without +sizes:+, and for a mapping without
345
+ # +bbcode: true+
301
346
  def discourse_renderer(
302
347
  tags: nil,
303
348
  tag_library: nil,
@@ -307,7 +352,10 @@ module Markbridge
307
352
  escape_hard_line_breaks: false,
308
353
  allow: nil,
309
354
  postprocessor: nil,
310
- strip_trailing_invisibles: false
355
+ strip_trailing_invisibles: false,
356
+ features: nil,
357
+ sizes: nil,
358
+ colors: nil
311
359
  )
312
360
  # Dup the caller's library before mutating so successive
313
361
  # +discourse_renderer+ calls against the same +tag_library:+ don't
@@ -315,6 +363,7 @@ module Markbridge
315
363
  # a fresh instance, so the dup is only needed in the explicit
316
364
  # +tag_library:+ branch.
317
365
  library = tag_library ? tag_library.dup : Renderers::Discourse::TagLibrary.default
366
+ register_features(library, features || {}, sizes:, colors:)
318
367
  library.merge!(tags) if tags
319
368
  Array(unregister).each { |klass| library.unregister(klass) }
320
369
 
@@ -326,6 +375,46 @@ module Markbridge
326
375
 
327
376
  private
328
377
 
378
+ # Registers the tags of the features named, so the other tags of the
379
+ # library stay as they are.
380
+ def register_features(library, features, sizes:, colors:)
381
+ features.each do |feature, enabled|
382
+ unless FEATURES.include?(feature)
383
+ raise ArgumentError,
384
+ "unknown feature #{feature.inspect}, the features are #{FEATURES.join(", ")}"
385
+ end
386
+ unless [true, false].include?(enabled)
387
+ raise ArgumentError, "feature #{feature} must be true or false, got #{enabled.inspect}"
388
+ end
389
+ next if feature == :bbcode
390
+
391
+ FEATURE_TAGS
392
+ .fetch(feature)
393
+ .each { |klass, tag_class| library.register(klass, tag_class.new(bbcode: enabled)) }
394
+ end
395
+
396
+ register_bbcode_feature(library, features[:bbcode], sizes:, colors:)
397
+ end
398
+
399
+ def register_bbcode_feature(library, bbcode, sizes:, colors:)
400
+ if bbcode && sizes.nil?
401
+ raise ArgumentError,
402
+ "features: { bbcode: true } needs sizes:, the mapping from the size values of " \
403
+ "the source forum to percent"
404
+ end
405
+ { sizes:, colors: }.each do |name, mapping|
406
+ next if mapping.nil?
407
+ unless mapping.respond_to?(:call) || mapping.respond_to?(:fetch)
408
+ raise ArgumentError, "#{name}: must be a Hash or respond to call"
409
+ end
410
+ raise ArgumentError, "#{name}: needs features: { bbcode: true }" unless bbcode
411
+ end
412
+ return if bbcode.nil?
413
+
414
+ library.register(AST::Color, Renderers::Discourse::Tags::ColorTag.new(bbcode:, colors:))
415
+ library.register(AST::Size, Renderers::Discourse::Tags::SizeTag.new(bbcode:, sizes:))
416
+ end
417
+
329
418
  def bbcode_diagnostics(parser)
330
419
  {
331
420
  auto_closed_tags_count: parser.auto_closed_tags_count,
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.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Discourse Team
@@ -55,6 +55,7 @@ files:
55
55
  - lib/markbridge/ast/url.rb
56
56
  - lib/markbridge/bbcode.rb
57
57
  - lib/markbridge/conversion.rb
58
+ - lib/markbridge/errors.rb
58
59
  - lib/markbridge/gem_loader.rb
59
60
  - lib/markbridge/html.rb
60
61
  - lib/markbridge/mediawiki.rb
@@ -141,7 +142,12 @@ files:
141
142
  - lib/markbridge/renderers/discourse/html_block.rb
142
143
  - lib/markbridge/renderers/discourse/html_escaper.rb
143
144
  - lib/markbridge/renderers/discourse/identity_escaper.rb
145
+ - lib/markbridge/renderers/discourse/inline/joins.rb
146
+ - lib/markbridge/renderers/discourse/inline/serializer.rb
147
+ - lib/markbridge/renderers/discourse/inline/tokens.rb
148
+ - lib/markbridge/renderers/discourse/link_destination.rb
144
149
  - lib/markbridge/renderers/discourse/markdown_escaper.rb
150
+ - lib/markbridge/renderers/discourse/paragraphs.rb
145
151
  - lib/markbridge/renderers/discourse/postprocessor.rb
146
152
  - lib/markbridge/renderers/discourse/render_context.rb
147
153
  - lib/markbridge/renderers/discourse/renderer.rb