markbridge 0.1.1 → 0.1.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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/lib/markbridge/all.rb +4 -7
  3. data/lib/markbridge/ast/document.rb +1 -1
  4. data/lib/markbridge/ast/element.rb +2 -2
  5. data/lib/markbridge/ast/list.rb +2 -2
  6. data/lib/markbridge/ast/table.rb +6 -12
  7. data/lib/markbridge/ast/text.rb +5 -1
  8. data/lib/markbridge/bbcode.rb +4 -0
  9. data/lib/markbridge/gem_loader.rb +2 -3
  10. data/lib/markbridge/html.rb +4 -0
  11. data/lib/markbridge/mediawiki.rb +4 -0
  12. data/lib/markbridge/parsers/bbcode/closing_strategies/base.rb +0 -10
  13. data/lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb +17 -4
  14. data/lib/markbridge/parsers/bbcode/closing_strategies/tag_reconciler.rb +64 -44
  15. data/lib/markbridge/parsers/bbcode/handler_registry.rb +21 -11
  16. data/lib/markbridge/parsers/bbcode/handlers/attachment_handler.rb +17 -12
  17. data/lib/markbridge/parsers/bbcode/handlers/base_handler.rb +0 -10
  18. data/lib/markbridge/parsers/bbcode/handlers/code_handler.rb +6 -10
  19. data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +9 -17
  20. data/lib/markbridge/parsers/bbcode/handlers/list_handler.rb +1 -5
  21. data/lib/markbridge/parsers/bbcode/handlers/list_item_handler.rb +1 -2
  22. data/lib/markbridge/parsers/bbcode/handlers/quote_handler.rb +6 -18
  23. data/lib/markbridge/parsers/bbcode/handlers/raw_handler.rb +2 -6
  24. data/lib/markbridge/parsers/bbcode/handlers/self_closing_handler.rb +4 -4
  25. data/lib/markbridge/parsers/bbcode/handlers/table_cell_handler.rb +1 -1
  26. data/lib/markbridge/parsers/bbcode/handlers/table_handler.rb +2 -2
  27. data/lib/markbridge/parsers/bbcode/handlers/table_row_handler.rb +3 -3
  28. data/lib/markbridge/parsers/bbcode/parser.rb +5 -8
  29. data/lib/markbridge/parsers/bbcode/parser_state.rb +12 -18
  30. data/lib/markbridge/parsers/bbcode/peekable_enumerator.rb +9 -59
  31. data/lib/markbridge/parsers/bbcode/raw_content_collector.rb +2 -2
  32. data/lib/markbridge/parsers/bbcode/scanner.rb +49 -63
  33. data/lib/markbridge/parsers/bbcode/tokens/tag_end_token.rb +1 -5
  34. data/lib/markbridge/parsers/bbcode/tokens/tag_start_token.rb +1 -6
  35. data/lib/markbridge/parsers/bbcode/tokens/text_token.rb +1 -7
  36. data/lib/markbridge/parsers/bbcode/tokens/token.rb +1 -1
  37. data/lib/markbridge/parsers/bbcode.rb +1 -0
  38. data/lib/markbridge/parsers/html/handler_registry.rb +32 -49
  39. data/lib/markbridge/parsers/html/handlers/base_handler.rb +0 -2
  40. data/lib/markbridge/parsers/html/handlers/image_handler.rb +1 -4
  41. data/lib/markbridge/parsers/html/parser.rb +3 -13
  42. data/lib/markbridge/parsers/media_wiki/inline_parser.rb +56 -67
  43. data/lib/markbridge/parsers/media_wiki/inline_tag_registry.rb +103 -0
  44. data/lib/markbridge/parsers/media_wiki/parser.rb +51 -76
  45. data/lib/markbridge/parsers/media_wiki.rb +1 -0
  46. data/lib/markbridge/parsers/text_formatter/handler_registry.rb +5 -37
  47. data/lib/markbridge/parsers/text_formatter/parser.rb +3 -8
  48. data/lib/markbridge/processors/discourse_markdown/code_block_tracker.rb +24 -17
  49. data/lib/markbridge/processors/discourse_markdown/detectors/base.rb +9 -15
  50. data/lib/markbridge/processors/discourse_markdown/detectors/event.rb +11 -10
  51. data/lib/markbridge/processors/discourse_markdown/detectors/poll.rb +11 -39
  52. data/lib/markbridge/processors/discourse_markdown/detectors/upload.rb +38 -63
  53. data/lib/markbridge/processors/discourse_markdown/scanner.rb +25 -33
  54. data/lib/markbridge/renderers/discourse/builders/list_item_builder.rb +6 -6
  55. data/lib/markbridge/renderers/discourse/html_escaper.rb +20 -0
  56. data/lib/markbridge/renderers/discourse/markdown_escaper.rb +57 -50
  57. data/lib/markbridge/renderers/discourse/render_context.rb +23 -11
  58. data/lib/markbridge/renderers/discourse/renderer.rb +54 -12
  59. data/lib/markbridge/renderers/discourse/rendering_interface.rb +12 -4
  60. data/lib/markbridge/renderers/discourse/tag.rb +14 -1
  61. data/lib/markbridge/renderers/discourse/tag_library.rb +30 -25
  62. data/lib/markbridge/renderers/discourse/tags/align_tag.rb +15 -7
  63. data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +2 -0
  64. data/lib/markbridge/renderers/discourse/tags/code_tag.rb +14 -9
  65. data/lib/markbridge/renderers/discourse/tags/email_tag.rb +5 -3
  66. data/lib/markbridge/renderers/discourse/tags/event_tag.rb +3 -1
  67. data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +6 -2
  68. data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +2 -2
  69. data/lib/markbridge/renderers/discourse/tags/image_tag.rb +13 -2
  70. data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +2 -0
  71. data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +2 -2
  72. data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +24 -47
  73. data/lib/markbridge/renderers/discourse/tags/list_tag.rb +10 -15
  74. data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +5 -1
  75. data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +10 -0
  76. data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +9 -2
  77. data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +2 -0
  78. data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +9 -0
  79. data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +2 -0
  80. data/lib/markbridge/renderers/discourse/tags/table_tag.rb +12 -8
  81. data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +10 -3
  82. data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +29 -2
  83. data/lib/markbridge/renderers/discourse/tags/url_tag.rb +5 -3
  84. data/lib/markbridge/renderers/discourse.rb +1 -0
  85. data/lib/markbridge/textformatter.rb +4 -0
  86. data/lib/markbridge/version.rb +1 -1
  87. data/lib/markbridge.rb +8 -8
  88. metadata +8 -2
@@ -18,8 +18,10 @@ 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
+
23
25
  "#{body}\n\n"
24
26
  end
25
27
 
@@ -32,9 +34,14 @@ module Markbridge
32
34
  "[poll#{attrs}]\n#{options}\n[/poll]"
33
35
  end
34
36
 
37
+ NAMES_WITHOUT_ATTRIBUTE = Set[nil, "poll"].freeze
38
+ private_constant :NAMES_WITHOUT_ATTRIBUTE
39
+
35
40
  def build_attributes(element)
36
41
  parts = []
37
- parts << %( name="#{element.name}") if element.name && element.name != "poll"
42
+ unless NAMES_WITHOUT_ATTRIBUTE.include?(element.name)
43
+ parts << %( name="#{element.name}")
44
+ end
38
45
  parts << %( type="#{element.type}") if element.type
39
46
  parts << %( results="#{element.results}") if element.results
40
47
  parts << %( public="true") if element.public
@@ -11,6 +11,8 @@ module Markbridge
11
11
  child_context = interface.with_parent(element)
12
12
  content = interface.render_children(element, context: child_context)
13
13
 
14
+ return "<blockquote>#{content}</blockquote>" if interface.html_mode?
15
+
14
16
  # Build Discourse quote BBCode
15
17
  # Format: [quote="username, post:123, topic:456"]content[/quote]
16
18
  body =
@@ -11,12 +11,21 @@ module Markbridge
11
11
  child_context = interface.with_parent(element)
12
12
  content = interface.render_children(element, context: child_context)
13
13
 
14
+ return render_html(element.title, content) if interface.html_mode?
15
+
14
16
  if element.title
15
17
  "[spoiler=#{element.title}]#{content}[/spoiler]"
16
18
  else
17
19
  "[spoiler]#{content}[/spoiler]"
18
20
  end
19
21
  end
22
+
23
+ private
24
+
25
+ def render_html(title, content)
26
+ summary = "<summary>#{title ? HtmlEscaper.escape(title) : "Spoiler"}</summary>"
27
+ "<details>#{summary}#{content}</details>"
28
+ end
20
29
  end
21
30
  end
22
31
  end
@@ -9,6 +9,8 @@ module Markbridge
9
9
  def render(element, interface)
10
10
  child_context = interface.with_parent(element)
11
11
  content = interface.render_children(element, context: child_context)
12
+ return "<s>#{content}</s>" if interface.html_mode?
13
+
12
14
  interface.wrap_inline(content, "~~")
13
15
  end
14
16
  end
@@ -15,7 +15,10 @@ module Markbridge
15
15
  if markdown_compatible?(rows_data, interface)
16
16
  render_markdown(rows_data)
17
17
  else
18
- render_html(rows_data)
18
+ # Re-render cells in html_mode so inline Markdown like **bold** becomes
19
+ # <strong>bold</strong>; CommonMark would not parse Markdown inside an HTML block.
20
+ html_rows = extract_rows(element, interface, child_context.with_html_mode(true))
21
+ render_html(html_rows)
19
22
  end
20
23
  end
21
24
 
@@ -25,13 +28,15 @@ module Markbridge
25
28
  # @return [Array<Hash>] array of {cells: [{content:, header:}], ...}
26
29
  def extract_rows(element, interface, child_context)
27
30
  element.children.filter_map do |child|
28
- next unless child.is_a?(AST::TableRow)
31
+ next unless child.instance_of?(AST::TableRow)
29
32
 
30
33
  cells =
31
34
  child.children.filter_map do |cell|
32
- next unless cell.is_a?(AST::TableCell)
35
+ next unless cell.instance_of?(AST::TableCell)
33
36
 
34
- cell_context = child_context.with_parent(child)
37
+ # Push the cell itself into the parent chain so descendants
38
+ # can detect they're inside a cell via has_parent?.
39
+ cell_context = child_context.with_parent(cell)
35
40
  content = interface.render_children(cell, context: cell_context).strip
36
41
  { content:, header: cell.header? }
37
42
  end
@@ -42,7 +47,6 @@ module Markbridge
42
47
 
43
48
  # Check if the table can be rendered as Markdown
44
49
  def markdown_compatible?(rows_data, interface)
45
- return false if rows_data.empty?
46
50
  return false if interface.has_parent?(AST::Table)
47
51
 
48
52
  cell_count = rows_data.first[:cells].length
@@ -90,7 +94,7 @@ module Markbridge
90
94
 
91
95
  unless header_rows.empty?
92
96
  lines << "<thead>"
93
- header_rows.each { |row| lines << html_row(row, force_header: true) }
97
+ header_rows.each { |row| lines << html_row(row) }
94
98
  lines << "</thead>"
95
99
  end
96
100
 
@@ -108,10 +112,10 @@ module Markbridge
108
112
  end
109
113
 
110
114
  # Render a single HTML table row
111
- def html_row(row, force_header: false)
115
+ def html_row(row)
112
116
  cells_html =
113
117
  row[:cells].map do |cell|
114
- tag = (cell[:header] || force_header) ? "th" : "td"
118
+ tag = cell[:header] ? "th" : "td"
115
119
  "<#{tag}>#{cell[:content]}</#{tag}>"
116
120
  end
117
121
 
@@ -4,13 +4,20 @@ module Markbridge
4
4
  module Renderers
5
5
  module Discourse
6
6
  module Tags
7
- # Tag for rendering underline text
7
+ # Discourse's HTML sanitizer strips raw `<u>`, but `[u]…[/u]` is
8
+ # cooked by the BBCode plugin into `<span class="bbcode-u">`. The
9
+ # BBCode plugin runs on Markdown source, not on raw HTML inside an
10
+ # HTML block, so in html_mode we emit the cooked form directly.
8
11
  class UnderlineTag < Tag
9
12
  def render(element, interface)
10
13
  child_context = interface.with_parent(element)
11
14
  content = interface.render_children(element, context: child_context)
12
- # Discourse uses HTML for underline
13
- "<u>#{content}</u>"
15
+
16
+ if interface.html_mode?
17
+ %(<span class="bbcode-u">#{content}</span>)
18
+ else
19
+ "[u]#{content}[/u]"
20
+ end
14
21
  end
15
22
  end
16
23
  end
@@ -29,7 +29,9 @@ module Markbridge
29
29
  # end
30
30
  # end
31
31
  class UploadTag < Tag
32
- def render(element, _interface)
32
+ def render(element, interface)
33
+ return build_upload_html(element) if interface.html_mode?
34
+
33
35
  # Return raw Markdown if available, otherwise reconstruct
34
36
  return element.raw if element.raw
35
37
 
@@ -56,7 +58,7 @@ module Markbridge
56
58
  def build_attachment_markdown(element)
57
59
  filename = element.filename || "attachment"
58
60
  url = build_upload_url(element)
59
- size_part = element.size ? " (#{element.size})" : ""
61
+ size_part = " (#{element.size})" if element.size
60
62
 
61
63
  "[#{filename}|attachment](#{url})#{size_part}"
62
64
  end
@@ -73,6 +75,31 @@ module Markbridge
73
75
  filename = element.filename || element.sha1
74
76
  "upload://#{filename}"
75
77
  end
78
+
79
+ # html_mode reconstructs from the AST fields rather than reusing
80
+ # element.raw — raw is Markdown, which CommonMark passes through
81
+ # unchanged inside an HTML block (so the user would see the
82
+ # literal "![…](upload://…)" string instead of an image).
83
+ def build_upload_html(element)
84
+ if element.type == :image
85
+ build_image_html(element)
86
+ else
87
+ build_attachment_html(element)
88
+ end
89
+ end
90
+
91
+ def build_image_html(element)
92
+ src = HtmlEscaper.escape(build_upload_url(element))
93
+ alt = HtmlEscaper.escape(element.alt)
94
+ %(<img src="#{src}" alt="#{alt}">)
95
+ end
96
+
97
+ def build_attachment_html(element)
98
+ href = HtmlEscaper.escape(build_upload_url(element))
99
+ filename = HtmlEscaper.escape(element.filename || "attachment")
100
+ size_part = " (#{HtmlEscaper.escape(element.size)})" if element.size
101
+ %(<a href="#{href}">#{filename}</a>#{size_part})
102
+ end
76
103
  end
77
104
  end
78
105
  end
@@ -11,10 +11,12 @@ module Markbridge
11
11
  text = interface.render_children(element, context: child_context)
12
12
  href = element.href
13
13
 
14
- if href&.match?(/^(https?|ftps?|mailto):/i)
15
- "[#{text}](#{href})"
14
+ return text unless href&.match?(/\A(?:https?|ftps?|mailto):/i)
15
+
16
+ if interface.html_mode?
17
+ %(<a href="#{HtmlEscaper.escape(href)}">#{text}</a>)
16
18
  else
17
- text
19
+ "[#{text}](#{href})"
18
20
  end
19
21
  end
20
22
  end
@@ -5,6 +5,7 @@ require_relative "discourse/tag_library"
5
5
  require_relative "discourse/render_context"
6
6
  require_relative "discourse/rendering_interface"
7
7
  require_relative "discourse/markdown_escaper"
8
+ require_relative "discourse/html_escaper"
8
9
 
9
10
  # Builders
10
11
  require_relative "discourse/builders/list_item_builder"
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../markbridge"
4
+ require_relative "parsers/text_formatter"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Markbridge
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/markbridge.rb CHANGED
@@ -5,8 +5,6 @@ require_relative "markbridge/configuration"
5
5
 
6
6
  require_relative "markbridge/ast"
7
7
  require_relative "markbridge/renderers/discourse"
8
- require_relative "markbridge/parsers/media_wiki"
9
- require_relative "markbridge/parsers/text_formatter"
10
8
  require_relative "markbridge/processors"
11
9
 
12
10
  module Markbridge
@@ -70,21 +68,23 @@ module Markbridge
70
68
 
71
69
  # Parse MediaWiki wikitext to AST
72
70
  # @param input [String] MediaWiki source
71
+ # @param inline_tag_registry [Parsers::MediaWiki::InlineTagRegistry, nil] custom registry
73
72
  # @return [AST::Document]
74
- def parse_mediawiki(input)
73
+ def parse_mediawiki(input, inline_tag_registry: nil)
75
74
  raise ArgumentError, "input cannot be nil" if input.nil?
76
75
 
77
76
  input = input.to_s
78
- parser = Parsers::MediaWiki::Parser.new
77
+ parser = Parsers::MediaWiki::Parser.new(inline_tag_registry:)
79
78
  parser.parse(input)
80
79
  end
81
80
 
82
81
  # Convert MediaWiki wikitext to Discourse Markdown
83
82
  # @param input [String] MediaWiki source
83
+ # @param inline_tag_registry [Parsers::MediaWiki::InlineTagRegistry, nil] custom registry
84
84
  # @param tag_library [TagLibrary, nil] custom tag library or use default
85
85
  # @return [String] Markdown output
86
- def mediawiki_to_markdown(input, tag_library: nil)
87
- ast = parse_mediawiki(input)
86
+ def mediawiki_to_markdown(input, inline_tag_registry: nil, tag_library: nil)
87
+ ast = parse_mediawiki(input, inline_tag_registry:)
88
88
  render_to_markdown(ast, tag_library:)
89
89
  end
90
90
 
@@ -142,7 +142,7 @@ module Markbridge
142
142
  parser.parse(input.to_s)
143
143
  end
144
144
 
145
- def render_to_markdown(ast, tag_library: nil)
145
+ def render_to_markdown(ast, tag_library:)
146
146
  tag_library ||= default_tag_library
147
147
  renderer = build_renderer(tag_library:)
148
148
  cleanup_markdown(renderer.render(ast))
@@ -159,7 +159,7 @@ module Markbridge
159
159
  def cleanup_markdown(text)
160
160
  text
161
161
  .gsub(/\n{3,}/, "\n\n") # Max 2 consecutive newlines
162
- .gsub(/^[ \t]+$/m, "") # Remove whitespace-only lines
162
+ .gsub(/^[ \t]+$/, "") # Remove whitespace-only lines
163
163
  .strip # Trim leading/trailing whitespace
164
164
  end
165
165
  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.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Discourse Team
@@ -52,8 +52,11 @@ files:
52
52
  - lib/markbridge/ast/underline.rb
53
53
  - lib/markbridge/ast/upload.rb
54
54
  - lib/markbridge/ast/url.rb
55
+ - lib/markbridge/bbcode.rb
55
56
  - lib/markbridge/configuration.rb
56
57
  - lib/markbridge/gem_loader.rb
58
+ - lib/markbridge/html.rb
59
+ - lib/markbridge/mediawiki.rb
57
60
  - lib/markbridge/parsers/bbcode.rb
58
61
  - lib/markbridge/parsers/bbcode/closing_strategies/base.rb
59
62
  - lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb
@@ -107,6 +110,7 @@ files:
107
110
  - lib/markbridge/parsers/html/parser.rb
108
111
  - lib/markbridge/parsers/media_wiki.rb
109
112
  - lib/markbridge/parsers/media_wiki/inline_parser.rb
113
+ - lib/markbridge/parsers/media_wiki/inline_tag_registry.rb
110
114
  - lib/markbridge/parsers/media_wiki/parser.rb
111
115
  - lib/markbridge/parsers/text_formatter.rb
112
116
  - lib/markbridge/parsers/text_formatter/handler_registry.rb
@@ -133,6 +137,7 @@ files:
133
137
  - lib/markbridge/processors/discourse_markdown/scanner.rb
134
138
  - lib/markbridge/renderers/discourse.rb
135
139
  - lib/markbridge/renderers/discourse/builders/list_item_builder.rb
140
+ - lib/markbridge/renderers/discourse/html_escaper.rb
136
141
  - lib/markbridge/renderers/discourse/markdown_escaper.rb
137
142
  - lib/markbridge/renderers/discourse/render_context.rb
138
143
  - lib/markbridge/renderers/discourse/renderer.rb
@@ -168,6 +173,7 @@ files:
168
173
  - lib/markbridge/renderers/discourse/tags/underline_tag.rb
169
174
  - lib/markbridge/renderers/discourse/tags/upload_tag.rb
170
175
  - lib/markbridge/renderers/discourse/tags/url_tag.rb
176
+ - lib/markbridge/textformatter.rb
171
177
  - lib/markbridge/version.rb
172
178
  homepage: https://github.com/discourse/markbridge
173
179
  licenses:
@@ -183,7 +189,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
183
189
  requirements:
184
190
  - - ">="
185
191
  - !ruby/object:Gem::Version
186
- version: 3.2.0
192
+ version: 3.3.0
187
193
  required_rubygems_version: !ruby/object:Gem::Requirement
188
194
  requirements:
189
195
  - - ">="