markbridge 0.3.0 → 0.4.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/markbridge/ast/code.rb +8 -1
  3. data/lib/markbridge/ast/element.rb +28 -0
  4. data/lib/markbridge/ast/node.rb +41 -0
  5. data/lib/markbridge/normalizer/report.rb +41 -0
  6. data/lib/markbridge/normalizer/rule_set.rb +163 -0
  7. data/lib/markbridge/normalizer/text_projection.rb +37 -0
  8. data/lib/markbridge/normalizer/walker.rb +255 -0
  9. data/lib/markbridge/normalizer.rb +187 -0
  10. data/lib/markbridge/parsers/bbcode/handlers/code_handler.rb +17 -0
  11. data/lib/markbridge/parsers/html/handler_registry.rb +1 -0
  12. data/lib/markbridge/parsers/html/handlers/heading_handler.rb +29 -0
  13. data/lib/markbridge/parsers/html/handlers/raw_handler.rb +55 -4
  14. data/lib/markbridge/parsers/html.rb +1 -0
  15. data/lib/markbridge/parsers/media_wiki/parser.rb +2 -2
  16. data/lib/markbridge/parsers/text_formatter/handlers/code_handler.rb +3 -1
  17. data/lib/markbridge/renderers/discourse/html_block_safety.rb +31 -0
  18. data/lib/markbridge/renderers/discourse/renderer.rb +36 -6
  19. data/lib/markbridge/renderers/discourse/rendering_interface.rb +4 -0
  20. data/lib/markbridge/renderers/discourse/tag.rb +10 -0
  21. data/lib/markbridge/renderers/discourse/tag_library.rb +59 -4
  22. data/lib/markbridge/renderers/discourse/tags/code_tag.rb +4 -0
  23. data/lib/markbridge/renderers/discourse/tags/details_tag.rb +6 -2
  24. data/lib/markbridge/renderers/discourse/tags/event_tag.rb +2 -4
  25. data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +2 -4
  26. data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +4 -0
  27. data/lib/markbridge/renderers/discourse.rb +1 -0
  28. data/lib/markbridge/rspec.rb +46 -0
  29. data/lib/markbridge/version.rb +1 -1
  30. data/lib/markbridge.rb +65 -13
  31. metadata +9 -1
@@ -18,11 +18,9 @@ module Markbridge
18
18
  # end
19
19
  # end
20
20
  class PollTag < Tag
21
- def render(element, interface)
21
+ def render(element, _interface)
22
22
  body = element.raw || build_poll_bbcode(element)
23
- return "\n\n#{body}\n\n" if interface.html_mode?
24
-
25
- "#{body}\n\n"
23
+ "\n\n#{body}\n\n"
26
24
  end
27
25
 
28
26
  private
@@ -11,6 +11,10 @@ module Markbridge
11
11
  child_context = interface.with_parent(element)
12
12
  content = interface.render_children(element, context: child_context)
13
13
 
14
+ # A spoiler with nothing to hide renders to nothing — an empty
15
+ # [spoiler] shell would only add noise to the output.
16
+ return "" if content.empty?
17
+
14
18
  return render_html(element.title, content) if interface.html_mode?
15
19
 
16
20
  if element.title
@@ -7,6 +7,7 @@ require_relative "discourse/rendering_interface"
7
7
  require_relative "discourse/markdown_escaper"
8
8
  require_relative "discourse/identity_escaper"
9
9
  require_relative "discourse/html_escaper"
10
+ require_relative "discourse/html_block_safety"
10
11
  require_relative "discourse/postprocessor"
11
12
 
12
13
  # Builders
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../markbridge"
4
+
5
+ # Shared RSpec examples for consumers who write their own renderer
6
+ # tags. Require this file from your spec setup (RSpec itself must
7
+ # already be loaded):
8
+ #
9
+ # require "markbridge/rspec"
10
+ #
11
+ # and put a custom tag under the html_mode contract:
12
+ #
13
+ # RSpec.describe MyQuoteTag do
14
+ # it_behaves_like "an html_mode safe tag" do
15
+ # let(:tag) { described_class.new }
16
+ # let(:element) do
17
+ # element = MyQuote.new
18
+ # element << Markbridge::AST::Text.new("body *with* sigils")
19
+ # element
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # The example renders +element+ with +tag+ in html_mode and fails when
25
+ # the output would break inside a CommonMark HTML block — the same
26
+ # check Markbridge runs against its own tags. Give +element+ children
27
+ # whose text contains Markdown sigils, so a tag that passes them
28
+ # through unprotected is caught. When the tag needs a customized
29
+ # renderer to resolve its children (for example a custom tag library),
30
+ # override +markbridge_renderer+ with your configured renderer.
31
+ RSpec.shared_examples "an html_mode safe tag" do
32
+ let(:markbridge_renderer) { Markbridge::Renderers::Discourse::Renderer.new }
33
+
34
+ let(:markbridge_html_mode_interface) do
35
+ context = Markbridge::Renderers::Discourse::RenderContext.new([], html_mode: true)
36
+ Markbridge::Renderers::Discourse::RenderingInterface.new(markbridge_renderer, context)
37
+ end
38
+
39
+ it "renders html_mode output that is safe inside an HTML block" do
40
+ output = tag.render(element, markbridge_html_mode_interface)
41
+
42
+ expect(Markbridge::Renderers::Discourse::HtmlBlockSafety.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}"
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Markbridge
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/markbridge.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "markbridge/parse"
5
5
  require_relative "markbridge/conversion"
6
6
 
7
7
  require_relative "markbridge/ast"
8
+ require_relative "markbridge/normalizer"
8
9
  require_relative "markbridge/renderers/discourse"
9
10
 
10
11
  module Markbridge
@@ -44,11 +45,21 @@ module Markbridge
44
45
  # {Conversion} with an empty +markdown+ string, and surface the
45
46
  # exceptions via {Conversion#errors}.
46
47
  # @yieldparam ast [AST::Document] mutate before rendering (optional)
48
+ # @param normalize [Boolean, Normalizer] apply target-format nesting
49
+ # rules between the +yield+ hook and render. +true+ (default) uses the
50
+ # shared default normalizer; a {Normalizer} is used as-is; +false+
51
+ # skips normalization. See {Normalizer}.
47
52
  # @return [Conversion]
48
- def bbcode_to_markdown(input, handlers: nil, renderer: nil, raise_on_error: true)
53
+ def bbcode_to_markdown(
54
+ input,
55
+ handlers: nil,
56
+ renderer: nil,
57
+ raise_on_error: true,
58
+ normalize: true
59
+ )
49
60
  parse = parse_bbcode(input, handlers:)
50
61
  yield(parse.ast) if block_given?
51
- build_conversion(parse, renderer:, raise_on_error:)
62
+ build_conversion(parse, renderer:, raise_on_error:, normalize:)
52
63
  end
53
64
 
54
65
  # Parse HTML to AST.
@@ -85,11 +96,12 @@ module Markbridge
85
96
  # {Conversion} with an empty +markdown+ string, and surface the
86
97
  # exceptions via {Conversion#errors}.
87
98
  # @yieldparam ast [AST::Document] mutate before rendering (optional)
99
+ # @param normalize [Boolean, Normalizer] see {.bbcode_to_markdown}
88
100
  # @return [Conversion]
89
- def html_to_markdown(input, handlers: nil, renderer: nil, raise_on_error: true)
101
+ def html_to_markdown(input, handlers: nil, renderer: nil, raise_on_error: true, normalize: true)
90
102
  parse = parse_html(input, handlers:)
91
103
  yield(parse.ast) if block_given?
92
- build_conversion(parse, renderer:, raise_on_error:)
104
+ build_conversion(parse, renderer:, raise_on_error:, normalize:)
93
105
  end
94
106
 
95
107
  # Parse s9e/TextFormatter XML to AST.
@@ -122,11 +134,18 @@ module Markbridge
122
134
  # @param renderer [Renderers::Discourse::Renderer, nil] custom renderer
123
135
  # @param raise_on_error [Boolean] see {.bbcode_to_markdown}
124
136
  # @yieldparam ast [AST::Document] mutate before rendering (optional)
137
+ # @param normalize [Boolean, Normalizer] see {.bbcode_to_markdown}
125
138
  # @return [Conversion]
126
- def text_formatter_xml_to_markdown(input, handlers: nil, renderer: nil, raise_on_error: true)
139
+ def text_formatter_xml_to_markdown(
140
+ input,
141
+ handlers: nil,
142
+ renderer: nil,
143
+ raise_on_error: true,
144
+ normalize: true
145
+ )
127
146
  parse = parse_text_formatter_xml(input, handlers:)
128
147
  yield(parse.ast) if block_given?
129
- build_conversion(parse, renderer:, raise_on_error:)
148
+ build_conversion(parse, renderer:, raise_on_error:, normalize:)
130
149
  end
131
150
 
132
151
  # Parse MediaWiki wikitext to AST.
@@ -155,11 +174,18 @@ module Markbridge
155
174
  # @param renderer [Renderers::Discourse::Renderer, nil] custom renderer
156
175
  # @param raise_on_error [Boolean] see {.bbcode_to_markdown}
157
176
  # @yieldparam ast [AST::Document] mutate before rendering (optional)
177
+ # @param normalize [Boolean, Normalizer] see {.bbcode_to_markdown}
158
178
  # @return [Conversion]
159
- def mediawiki_to_markdown(input, handlers: nil, renderer: nil, raise_on_error: true)
179
+ def mediawiki_to_markdown(
180
+ input,
181
+ handlers: nil,
182
+ renderer: nil,
183
+ raise_on_error: true,
184
+ normalize: true
185
+ )
160
186
  parse = parse_mediawiki(input, handlers:)
161
187
  yield(parse.ast) if block_given?
162
- build_conversion(parse, renderer:, raise_on_error:)
188
+ build_conversion(parse, renderer:, raise_on_error:, normalize:)
163
189
  end
164
190
 
165
191
  # Convert input in the given format. Thin dispatcher over the
@@ -211,8 +237,17 @@ module Markbridge
211
237
  # @param format [Symbol] :discourse (only renderer currently shipped)
212
238
  # @param renderer [Renderers::Discourse::Renderer, nil]
213
239
  # @param raise_on_error [Boolean]
240
+ # @param normalize [Boolean, Normalizer] see {.bbcode_to_markdown}.
241
+ # Normalization is idempotent, so re-rendering an already-normalized
242
+ # {Parse} is a no-op.
214
243
  # @return [Conversion]
215
- def render(parse_or_ast, format: :discourse, renderer: nil, raise_on_error: true)
244
+ def render(
245
+ parse_or_ast,
246
+ format: :discourse,
247
+ renderer: nil,
248
+ raise_on_error: true,
249
+ normalize: true
250
+ )
216
251
  raise ArgumentError, "unknown render format #{format.inspect}" unless format == :discourse
217
252
 
218
253
  parse =
@@ -228,7 +263,7 @@ module Markbridge
228
263
  raise ArgumentError, "expected Parse or AST::Node, got #{parse_or_ast.class}"
229
264
  end
230
265
 
231
- build_conversion(parse, renderer:, raise_on_error:)
266
+ build_conversion(parse, renderer:, raise_on_error:, normalize:)
232
267
  end
233
268
 
234
269
  # Build a configured Discourse {Renderers::Discourse::Renderer}
@@ -241,8 +276,10 @@ module Markbridge
241
276
  # library to start from. Defaults to a fresh {TagLibrary.default}.
242
277
  # When supplied, it is +dup+'d before any +tags:+ / +unregister:+
243
278
  # mutation, so the caller's library is left untouched.
244
- # @param unregister [Array<Class>, nil] AST classes to drop from
245
- # the library so they fall through to +render_children+.
279
+ # @param unregister [Array<Class>, nil] AST classes whose binding to
280
+ # drop from the library. A built-in class then renders as just its
281
+ # children; a subclass of a bound class falls back to the nearest
282
+ # ancestor's tag (see {Renderers::Discourse::TagLibrary#unregister}).
246
283
  # @param escaper [#escape, nil] when given, used as-is; +escape:+,
247
284
  # +escape_hard_line_breaks:+, and +allow:+ are then ignored.
248
285
  # @param escape [Boolean] when +false+, the renderer is built with
@@ -297,13 +334,28 @@ module Markbridge
297
334
  }
298
335
  end
299
336
 
300
- def build_conversion(parse, renderer:, raise_on_error:)
337
+ def build_conversion(parse, renderer:, raise_on_error:, normalize:)
338
+ parse = apply_normalization(parse, normalize)
301
339
  renderer ||= Renderers::Discourse::Renderer.new
302
340
  markdown, errors = render_through(renderer, parse.ast, raise_on_error:)
303
341
 
304
342
  Conversion.new(parsed: parse, markdown:, errors:)
305
343
  end
306
344
 
345
+ # Normalize +parse.ast+ in place (target-format nesting rules) and fold
346
+ # any report into +diagnostics[:normalization]+. Returns the +parse+ to
347
+ # render — a new one carrying the report when something changed, else
348
+ # the original unchanged.
349
+ def apply_normalization(parse, normalize)
350
+ return parse unless normalize
351
+
352
+ normalizer = normalize.is_a?(Normalizer) ? normalize : Normalizer.shared_default
353
+ report = normalizer.normalize(parse.ast)
354
+ return parse if report.empty?
355
+
356
+ parse.with(diagnostics: parse.diagnostics.merge(normalization: report))
357
+ end
358
+
307
359
  def build_escaper(escape:, escape_hard_line_breaks:, allow:)
308
360
  if escape == false
309
361
  if escape_hard_line_breaks || allow
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Discourse Team
@@ -58,6 +58,11 @@ files:
58
58
  - lib/markbridge/gem_loader.rb
59
59
  - lib/markbridge/html.rb
60
60
  - lib/markbridge/mediawiki.rb
61
+ - lib/markbridge/normalizer.rb
62
+ - lib/markbridge/normalizer/report.rb
63
+ - lib/markbridge/normalizer/rule_set.rb
64
+ - lib/markbridge/normalizer/text_projection.rb
65
+ - lib/markbridge/normalizer/walker.rb
61
66
  - lib/markbridge/parse.rb
62
67
  - lib/markbridge/parsers/bbcode.rb
63
68
  - lib/markbridge/parsers/bbcode/closing_strategies/base.rb
@@ -98,6 +103,7 @@ files:
98
103
  - lib/markbridge/parsers/html.rb
99
104
  - lib/markbridge/parsers/html/handler_registry.rb
100
105
  - lib/markbridge/parsers/html/handlers/base_handler.rb
106
+ - lib/markbridge/parsers/html/handlers/heading_handler.rb
101
107
  - lib/markbridge/parsers/html/handlers/image_handler.rb
102
108
  - lib/markbridge/parsers/html/handlers/list_handler.rb
103
109
  - lib/markbridge/parsers/html/handlers/list_item_handler.rb
@@ -132,6 +138,7 @@ files:
132
138
  - lib/markbridge/parsers/text_formatter/parser.rb
133
139
  - lib/markbridge/renderers/discourse.rb
134
140
  - lib/markbridge/renderers/discourse/builders/list_item_builder.rb
141
+ - lib/markbridge/renderers/discourse/html_block_safety.rb
135
142
  - lib/markbridge/renderers/discourse/html_escaper.rb
136
143
  - lib/markbridge/renderers/discourse/identity_escaper.rb
137
144
  - lib/markbridge/renderers/discourse/markdown_escaper.rb
@@ -171,6 +178,7 @@ files:
171
178
  - lib/markbridge/renderers/discourse/tags/underline_tag.rb
172
179
  - lib/markbridge/renderers/discourse/tags/upload_tag.rb
173
180
  - lib/markbridge/renderers/discourse/tags/url_tag.rb
181
+ - lib/markbridge/rspec.rb
174
182
  - lib/markbridge/textformatter.rb
175
183
  - lib/markbridge/version.rb
176
184
  homepage: https://github.com/discourse/markbridge