markbridge 0.1.0 → 0.1.2

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 (98) 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 +61 -0
  7. data/lib/markbridge/ast/text.rb +5 -1
  8. data/lib/markbridge/ast.rb +1 -0
  9. data/lib/markbridge/bbcode.rb +4 -0
  10. data/lib/markbridge/gem_loader.rb +2 -3
  11. data/lib/markbridge/html.rb +4 -0
  12. data/lib/markbridge/mediawiki.rb +4 -0
  13. data/lib/markbridge/parsers/bbcode/closing_strategies/base.rb +0 -10
  14. data/lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb +17 -4
  15. data/lib/markbridge/parsers/bbcode/closing_strategies/tag_reconciler.rb +64 -44
  16. data/lib/markbridge/parsers/bbcode/handler_registry.rb +26 -11
  17. data/lib/markbridge/parsers/bbcode/handlers/attachment_handler.rb +17 -12
  18. data/lib/markbridge/parsers/bbcode/handlers/base_handler.rb +0 -10
  19. data/lib/markbridge/parsers/bbcode/handlers/code_handler.rb +6 -10
  20. data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +13 -19
  21. data/lib/markbridge/parsers/bbcode/handlers/list_handler.rb +1 -5
  22. data/lib/markbridge/parsers/bbcode/handlers/list_item_handler.rb +1 -2
  23. data/lib/markbridge/parsers/bbcode/handlers/quote_handler.rb +30 -35
  24. data/lib/markbridge/parsers/bbcode/handlers/raw_handler.rb +2 -6
  25. data/lib/markbridge/parsers/bbcode/handlers/self_closing_handler.rb +4 -4
  26. data/lib/markbridge/parsers/bbcode/handlers/table_cell_handler.rb +26 -0
  27. data/lib/markbridge/parsers/bbcode/handlers/table_handler.rb +32 -0
  28. data/lib/markbridge/parsers/bbcode/handlers/table_row_handler.rb +35 -0
  29. data/lib/markbridge/parsers/bbcode/parser.rb +5 -8
  30. data/lib/markbridge/parsers/bbcode/parser_state.rb +12 -18
  31. data/lib/markbridge/parsers/bbcode/peekable_enumerator.rb +9 -59
  32. data/lib/markbridge/parsers/bbcode/raw_content_collector.rb +2 -2
  33. data/lib/markbridge/parsers/bbcode/scanner.rb +49 -63
  34. data/lib/markbridge/parsers/bbcode/tokens/tag_end_token.rb +1 -5
  35. data/lib/markbridge/parsers/bbcode/tokens/tag_start_token.rb +1 -6
  36. data/lib/markbridge/parsers/bbcode/tokens/text_token.rb +1 -7
  37. data/lib/markbridge/parsers/bbcode/tokens/token.rb +1 -1
  38. data/lib/markbridge/parsers/bbcode.rb +4 -0
  39. data/lib/markbridge/parsers/html/handler_registry.rb +32 -44
  40. data/lib/markbridge/parsers/html/handlers/base_handler.rb +0 -3
  41. data/lib/markbridge/parsers/html/handlers/image_handler.rb +1 -4
  42. data/lib/markbridge/parsers/html/handlers/table_cell_handler.rb +24 -0
  43. data/lib/markbridge/parsers/html/handlers/table_handler.rb +24 -0
  44. data/lib/markbridge/parsers/html/handlers/table_row_handler.rb +24 -0
  45. data/lib/markbridge/parsers/html/parser.rb +16 -15
  46. data/lib/markbridge/parsers/html.rb +3 -0
  47. data/lib/markbridge/parsers/media_wiki/inline_parser.rb +115 -151
  48. data/lib/markbridge/parsers/media_wiki/inline_tag_registry.rb +103 -0
  49. data/lib/markbridge/parsers/media_wiki/parser.rb +174 -71
  50. data/lib/markbridge/parsers/media_wiki.rb +1 -0
  51. data/lib/markbridge/parsers/text_formatter/handler_registry.rb +10 -36
  52. data/lib/markbridge/parsers/text_formatter/handlers/table_cell_handler.rb +26 -0
  53. data/lib/markbridge/parsers/text_formatter/parser.rb +3 -8
  54. data/lib/markbridge/parsers/text_formatter.rb +1 -0
  55. data/lib/markbridge/processors/discourse_markdown/code_block_tracker.rb +111 -92
  56. data/lib/markbridge/processors/discourse_markdown/detectors/base.rb +13 -7
  57. data/lib/markbridge/processors/discourse_markdown/detectors/event.rb +11 -20
  58. data/lib/markbridge/processors/discourse_markdown/detectors/poll.rb +10 -48
  59. data/lib/markbridge/processors/discourse_markdown/detectors/upload.rb +38 -63
  60. data/lib/markbridge/processors/discourse_markdown/scanner.rb +36 -41
  61. data/lib/markbridge/renderers/discourse/builders/list_item_builder.rb +6 -6
  62. data/lib/markbridge/renderers/discourse/html_escaper.rb +20 -0
  63. data/lib/markbridge/renderers/discourse/markdown_escaper.rb +262 -205
  64. data/lib/markbridge/renderers/discourse/render_context.rb +23 -11
  65. data/lib/markbridge/renderers/discourse/renderer.rb +54 -11
  66. data/lib/markbridge/renderers/discourse/rendering_interface.rb +12 -4
  67. data/lib/markbridge/renderers/discourse/tag.rb +14 -1
  68. data/lib/markbridge/renderers/discourse/tag_library.rb +30 -25
  69. data/lib/markbridge/renderers/discourse/tags/align_tag.rb +15 -7
  70. data/lib/markbridge/renderers/discourse/tags/attachment_tag.rb +1 -1
  71. data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +2 -0
  72. data/lib/markbridge/renderers/discourse/tags/code_tag.rb +14 -8
  73. data/lib/markbridge/renderers/discourse/tags/email_tag.rb +5 -3
  74. data/lib/markbridge/renderers/discourse/tags/event_tag.rb +3 -3
  75. data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +6 -2
  76. data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +2 -2
  77. data/lib/markbridge/renderers/discourse/tags/image_tag.rb +12 -1
  78. data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +2 -0
  79. data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +2 -2
  80. data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +24 -47
  81. data/lib/markbridge/renderers/discourse/tags/list_tag.rb +10 -15
  82. data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +6 -2
  83. data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +10 -0
  84. data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +9 -4
  85. data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +17 -11
  86. data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +9 -0
  87. data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +2 -0
  88. data/lib/markbridge/renderers/discourse/tags/table_cell_tag.rb +18 -0
  89. data/lib/markbridge/renderers/discourse/tags/table_row_tag.rb +18 -0
  90. data/lib/markbridge/renderers/discourse/tags/table_tag.rb +128 -0
  91. data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +10 -3
  92. data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +28 -1
  93. data/lib/markbridge/renderers/discourse/tags/url_tag.rb +5 -3
  94. data/lib/markbridge/renderers/discourse.rb +4 -0
  95. data/lib/markbridge/textformatter.rb +4 -0
  96. data/lib/markbridge/version.rb +1 -1
  97. data/lib/markbridge.rb +27 -62
  98. metadata +19 -2
@@ -19,10 +19,10 @@ module Markbridge
19
19
  # end
20
20
  class PollTag < Tag
21
21
  def render(element, interface)
22
- # Return raw BBCode if available, otherwise reconstruct
23
- return element.raw if element.raw
22
+ body = element.raw || build_poll_bbcode(element)
23
+ return "\n\n#{body}\n\n" if interface.html_mode?
24
24
 
25
- build_poll_bbcode(element)
25
+ "#{body}\n\n"
26
26
  end
27
27
 
28
28
  private
@@ -34,9 +34,14 @@ module Markbridge
34
34
  "[poll#{attrs}]\n#{options}\n[/poll]"
35
35
  end
36
36
 
37
+ NAMES_WITHOUT_ATTRIBUTE = Set[nil, "poll"].freeze
38
+ private_constant :NAMES_WITHOUT_ATTRIBUTE
39
+
37
40
  def build_attributes(element)
38
41
  parts = []
39
- 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
40
45
  parts << %( type="#{element.type}") if element.type
41
46
  parts << %( results="#{element.results}") if element.results
42
47
  parts << %( public="true") if element.public
@@ -11,19 +11,25 @@ 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
- if element.post && element.topic && element.username
17
- # Full Discourse quote with context
18
- "[quote=\"#{element.username}, post:#{element.post}, topic:#{element.topic}\"]\n#{content}\n[/quote]"
19
- elsif element.author
20
- # Quote with author attribution only
21
- "[quote=\"#{element.author}\"]\n#{content}\n[/quote]"
22
- else
23
- # Plain quote - could use Markdown blockquote or BBCode
24
- # Using Markdown blockquote for plain quotes
25
- content.split("\n").map { |line| "> #{line}" }.join("\n")
26
- end
18
+ body =
19
+ if element.post && element.topic && element.username
20
+ # Full Discourse quote with context
21
+ "[quote=\"#{element.username}, post:#{element.post}, topic:#{element.topic}\"]\n#{content}\n[/quote]"
22
+ elsif element.author
23
+ # Quote with author attribution only
24
+ "[quote=\"#{element.author}\"]\n#{content}\n[/quote]"
25
+ else
26
+ # Plain quote rendered as Markdown blockquote
27
+ content.split("\n").map { |line| "> #{line}" }.join("\n")
28
+ end
29
+
30
+ # Trailing blank line so consecutive quotes don't merge and
31
+ # following content starts a new paragraph.
32
+ "#{body}\n\n"
27
33
  end
28
34
  end
29
35
  end
@@ -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
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering table cells (passthrough - renders children only)
8
+ # The TableTag handles cells directly; this is a safety net for standalone rendering.
9
+ class TableCellTag < Tag
10
+ def render(element, interface)
11
+ child_context = interface.with_parent(element)
12
+ interface.render_children(element, context: child_context)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering table rows (passthrough - renders children only)
8
+ # The TableTag handles rows directly; this is a safety net for standalone rendering.
9
+ class TableRowTag < Tag
10
+ def render(element, interface)
11
+ child_context = interface.with_parent(element)
12
+ interface.render_children(element, context: child_context)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering tables as Markdown pipe tables with HTML fallback
8
+ class TableTag < Tag
9
+ def render(element, interface)
10
+ child_context = interface.with_parent(element)
11
+ rows_data = extract_rows(element, interface, child_context)
12
+
13
+ return "" if rows_data.empty?
14
+
15
+ if markdown_compatible?(rows_data, interface)
16
+ render_markdown(rows_data)
17
+ else
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)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Extract rendered cell data from each row
28
+ # @return [Array<Hash>] array of {cells: [{content:, header:}], ...}
29
+ def extract_rows(element, interface, child_context)
30
+ element.children.filter_map do |child|
31
+ next unless child.instance_of?(AST::TableRow)
32
+
33
+ cells =
34
+ child.children.filter_map do |cell|
35
+ next unless cell.instance_of?(AST::TableCell)
36
+
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)
40
+ content = interface.render_children(cell, context: cell_context).strip
41
+ { content:, header: cell.header? }
42
+ end
43
+
44
+ { cells: } unless cells.empty?
45
+ end
46
+ end
47
+
48
+ # Check if the table can be rendered as Markdown
49
+ def markdown_compatible?(rows_data, interface)
50
+ return false if interface.has_parent?(AST::Table)
51
+
52
+ cell_count = rows_data.first[:cells].length
53
+ rows_data.all? do |row|
54
+ row[:cells].length == cell_count &&
55
+ row[:cells].none? { |c| c[:content].include?("\n") }
56
+ end
57
+ end
58
+
59
+ # Render as Markdown pipe table
60
+ def render_markdown(rows_data)
61
+ header_idx = rows_data.index { |r| r[:cells].all? { |c| c[:header] } }
62
+ header_row = header_idx ? rows_data[header_idx] : rows_data.first
63
+ data_rows =
64
+ (
65
+ if header_idx
66
+ rows_data[0...header_idx] + rows_data[(header_idx + 1)..]
67
+ else
68
+ rows_data[1..]
69
+ end
70
+ )
71
+
72
+ col_count = header_row[:cells].length
73
+ lines = []
74
+ lines << format_row(header_row[:cells])
75
+ lines << "| #{(["---"] * col_count).join(" | ")} |"
76
+ data_rows.each { |row| lines << format_row(row[:cells]) }
77
+
78
+ "\n\n#{lines.join("\n")}\n\n"
79
+ end
80
+
81
+ # Format a single row as a Markdown pipe row
82
+ def format_row(cells)
83
+ # Pipe characters in cell content are already escaped by the markdown escaper
84
+ "| #{cells.map { |c| c[:content] }.join(" | ")} |"
85
+ end
86
+
87
+ # Render as HTML table
88
+ def render_html(rows_data)
89
+ has_header = rows_data.any? { |r| r[:cells].any? { |c| c[:header] } }
90
+ lines = ["<table>"]
91
+
92
+ if has_header
93
+ header_rows, body_rows = rows_data.partition { |r| r[:cells].all? { |c| c[:header] } }
94
+
95
+ unless header_rows.empty?
96
+ lines << "<thead>"
97
+ header_rows.each { |row| lines << html_row(row) }
98
+ lines << "</thead>"
99
+ end
100
+
101
+ unless body_rows.empty?
102
+ lines << "<tbody>"
103
+ body_rows.each { |row| lines << html_row(row) }
104
+ lines << "</tbody>"
105
+ end
106
+ else
107
+ rows_data.each { |row| lines << html_row(row) }
108
+ end
109
+
110
+ lines << "</table>"
111
+ "\n\n#{lines.join("\n")}\n\n"
112
+ end
113
+
114
+ # Render a single HTML table row
115
+ def html_row(row)
116
+ cells_html =
117
+ row[:cells].map do |cell|
118
+ tag = cell[:header] ? "th" : "td"
119
+ "<#{tag}>#{cell[:content]}</#{tag}>"
120
+ end
121
+
122
+ "<tr>#{cells_html.join}</tr>"
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -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
@@ -30,6 +30,8 @@ module Markbridge
30
30
  # end
31
31
  class UploadTag < Tag
32
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"
@@ -30,6 +31,9 @@ require_relative "discourse/tags/spoiler_tag"
30
31
  require_relative "discourse/tags/strikethrough_tag"
31
32
  require_relative "discourse/tags/subscript_tag"
32
33
  require_relative "discourse/tags/superscript_tag"
34
+ require_relative "discourse/tags/table_tag"
35
+ require_relative "discourse/tags/table_row_tag"
36
+ require_relative "discourse/tags/table_cell_tag"
33
37
  require_relative "discourse/tags/underline_tag"
34
38
  require_relative "discourse/tags/url_tag"
35
39
 
@@ -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.0"
4
+ VERSION = "0.1.2"
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
@@ -16,13 +14,8 @@ module Markbridge
16
14
  # @param handlers [HandlerRegistry, nil] custom handler registry or use default
17
15
  # @return [AST::Document]
18
16
  def parse_bbcode(input, handlers: nil)
19
- raise ArgumentError, "input cannot be nil" if input.nil?
20
-
21
- input = input.to_s # Coerce to string
22
17
  handlers ||= default_handlers
23
-
24
- parser = Parsers::BBCode::Parser.new(handlers:)
25
- parser.parse(input)
18
+ parse_with(Parsers::BBCode::Parser, input, handlers:)
26
19
  end
27
20
 
28
21
  # Convert BBCode to Discourse Markdown
@@ -31,17 +24,8 @@ module Markbridge
31
24
  # @param tag_library [TagLibrary, nil] custom tag library or use default
32
25
  # @return [String] Markdown output
33
26
  def bbcode_to_markdown(input, handlers: nil, tag_library: nil)
34
- raise ArgumentError, "input cannot be nil" if input.nil?
35
-
36
- handlers ||= default_handlers
37
- tag_library ||= default_tag_library
38
-
39
27
  ast = parse_bbcode(input, handlers:)
40
- renderer = build_renderer(tag_library:)
41
-
42
- # Clean up output
43
- result = renderer.render(ast)
44
- cleanup_markdown(result)
28
+ render_to_markdown(ast, tag_library:)
45
29
  end
46
30
 
47
31
  # Parse HTML to AST
@@ -49,13 +33,8 @@ module Markbridge
49
33
  # @param handlers [HandlerRegistry, nil] custom handler registry or use default
50
34
  # @return [AST::Document]
51
35
  def parse_html(input, handlers: nil)
52
- raise ArgumentError, "input cannot be nil" if input.nil?
53
-
54
- input = input.to_s # Coerce to string
55
36
  handlers ||= default_html_handlers
56
-
57
- parser = Parsers::HTML::Parser.new(handlers:)
58
- parser.parse(input)
37
+ parse_with(Parsers::HTML::Parser, input, handlers:)
59
38
  end
60
39
 
61
40
  # Convert HTML to Discourse Markdown
@@ -64,17 +43,8 @@ module Markbridge
64
43
  # @param tag_library [TagLibrary, nil] custom tag library or use default
65
44
  # @return [String] Markdown output
66
45
  def html_to_markdown(input, handlers: nil, tag_library: nil)
67
- raise ArgumentError, "input cannot be nil" if input.nil?
68
-
69
- handlers ||= default_html_handlers
70
- tag_library ||= default_tag_library
71
-
72
46
  ast = parse_html(input, handlers:)
73
- renderer = build_renderer(tag_library:)
74
-
75
- # Clean up output
76
- result = renderer.render(ast)
77
- cleanup_markdown(result)
47
+ render_to_markdown(ast, tag_library:)
78
48
  end
79
49
 
80
50
  # Parse s9e/TextFormatter XML to AST
@@ -82,13 +52,8 @@ module Markbridge
82
52
  # @param handlers [Parsers::TextFormatter::HandlerRegistry, nil] custom handler registry or use default
83
53
  # @return [AST::Document]
84
54
  def parse_text_formatter_xml(input, handlers: nil)
85
- raise ArgumentError, "input cannot be nil" if input.nil?
86
-
87
- input = input.to_s
88
55
  handlers ||= default_text_formatter_handlers
89
-
90
- parser = Parsers::TextFormatter::Parser.new(handlers:)
91
- parser.parse(input)
56
+ parse_with(Parsers::TextFormatter::Parser, input, handlers:)
92
57
  end
93
58
 
94
59
  # Convert s9e/TextFormatter XML to Discourse Markdown
@@ -97,43 +62,30 @@ module Markbridge
97
62
  # @param tag_library [TagLibrary, nil] custom tag library or use default
98
63
  # @return [String] Markdown output
99
64
  def text_formatter_xml_to_markdown(input, handlers: nil, tag_library: nil)
100
- raise ArgumentError, "input cannot be nil" if input.nil?
101
-
102
- handlers ||= default_text_formatter_handlers
103
- tag_library ||= default_tag_library
104
-
105
65
  ast = parse_text_formatter_xml(input, handlers:)
106
- renderer = build_renderer(tag_library:)
107
-
108
- result = renderer.render(ast)
109
- cleanup_markdown(result)
66
+ render_to_markdown(ast, tag_library:)
110
67
  end
111
68
 
112
69
  # Parse MediaWiki wikitext to AST
113
70
  # @param input [String] MediaWiki source
71
+ # @param inline_tag_registry [Parsers::MediaWiki::InlineTagRegistry, nil] custom registry
114
72
  # @return [AST::Document]
115
- def parse_mediawiki(input)
73
+ def parse_mediawiki(input, inline_tag_registry: nil)
116
74
  raise ArgumentError, "input cannot be nil" if input.nil?
117
75
 
118
76
  input = input.to_s
119
- parser = Parsers::MediaWiki::Parser.new
77
+ parser = Parsers::MediaWiki::Parser.new(inline_tag_registry:)
120
78
  parser.parse(input)
121
79
  end
122
80
 
123
81
  # Convert MediaWiki wikitext to Discourse Markdown
124
82
  # @param input [String] MediaWiki source
83
+ # @param inline_tag_registry [Parsers::MediaWiki::InlineTagRegistry, nil] custom registry
125
84
  # @param tag_library [TagLibrary, nil] custom tag library or use default
126
85
  # @return [String] Markdown output
127
- def mediawiki_to_markdown(input, tag_library: nil)
128
- raise ArgumentError, "input cannot be nil" if input.nil?
129
-
130
- tag_library ||= default_tag_library
131
-
132
- ast = parse_mediawiki(input)
133
- renderer = build_renderer(tag_library:)
134
-
135
- result = renderer.render(ast)
136
- cleanup_markdown(result)
86
+ def mediawiki_to_markdown(input, inline_tag_registry: nil, tag_library: nil)
87
+ ast = parse_mediawiki(input, inline_tag_registry:)
88
+ render_to_markdown(ast, tag_library:)
137
89
  end
138
90
 
139
91
  # Get default handler registry
@@ -183,6 +135,19 @@ module Markbridge
183
135
 
184
136
  private
185
137
 
138
+ def parse_with(parser_class, input, handlers:)
139
+ raise ArgumentError, "input cannot be nil" if input.nil?
140
+
141
+ parser = parser_class.new(handlers:)
142
+ parser.parse(input.to_s)
143
+ end
144
+
145
+ def render_to_markdown(ast, tag_library:)
146
+ tag_library ||= default_tag_library
147
+ renderer = build_renderer(tag_library:)
148
+ cleanup_markdown(renderer.render(ast))
149
+ end
150
+
186
151
  def build_renderer(tag_library:)
187
152
  escaper =
188
153
  Renderers::Discourse::MarkdownEscaper.new(
@@ -194,7 +159,7 @@ module Markbridge
194
159
  def cleanup_markdown(text)
195
160
  text
196
161
  .gsub(/\n{3,}/, "\n\n") # Max 2 consecutive newlines
197
- .gsub(/^[ \t]+$/m, "") # Remove whitespace-only lines
162
+ .gsub(/^[ \t]+$/, "") # Remove whitespace-only lines
198
163
  .strip # Trim leading/trailing whitespace
199
164
  end
200
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.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Discourse Team
@@ -47,12 +47,16 @@ files:
47
47
  - lib/markbridge/ast/strikethrough.rb
48
48
  - lib/markbridge/ast/subscript.rb
49
49
  - lib/markbridge/ast/superscript.rb
50
+ - lib/markbridge/ast/table.rb
50
51
  - lib/markbridge/ast/text.rb
51
52
  - lib/markbridge/ast/underline.rb
52
53
  - lib/markbridge/ast/upload.rb
53
54
  - lib/markbridge/ast/url.rb
55
+ - lib/markbridge/bbcode.rb
54
56
  - lib/markbridge/configuration.rb
55
57
  - lib/markbridge/gem_loader.rb
58
+ - lib/markbridge/html.rb
59
+ - lib/markbridge/mediawiki.rb
56
60
  - lib/markbridge/parsers/bbcode.rb
57
61
  - lib/markbridge/parsers/bbcode/closing_strategies/base.rb
58
62
  - lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb
@@ -75,6 +79,9 @@ files:
75
79
  - lib/markbridge/parsers/bbcode/handlers/simple_handler.rb
76
80
  - lib/markbridge/parsers/bbcode/handlers/size_handler.rb
77
81
  - lib/markbridge/parsers/bbcode/handlers/spoiler_handler.rb
82
+ - lib/markbridge/parsers/bbcode/handlers/table_cell_handler.rb
83
+ - lib/markbridge/parsers/bbcode/handlers/table_handler.rb
84
+ - lib/markbridge/parsers/bbcode/handlers/table_row_handler.rb
78
85
  - lib/markbridge/parsers/bbcode/handlers/url_handler.rb
79
86
  - lib/markbridge/parsers/bbcode/parser.rb
80
87
  - lib/markbridge/parsers/bbcode/parser_state.rb
@@ -96,10 +103,14 @@ files:
96
103
  - lib/markbridge/parsers/html/handlers/quote_handler.rb
97
104
  - lib/markbridge/parsers/html/handlers/raw_handler.rb
98
105
  - lib/markbridge/parsers/html/handlers/simple_handler.rb
106
+ - lib/markbridge/parsers/html/handlers/table_cell_handler.rb
107
+ - lib/markbridge/parsers/html/handlers/table_handler.rb
108
+ - lib/markbridge/parsers/html/handlers/table_row_handler.rb
99
109
  - lib/markbridge/parsers/html/handlers/url_handler.rb
100
110
  - lib/markbridge/parsers/html/parser.rb
101
111
  - lib/markbridge/parsers/media_wiki.rb
102
112
  - lib/markbridge/parsers/media_wiki/inline_parser.rb
113
+ - lib/markbridge/parsers/media_wiki/inline_tag_registry.rb
103
114
  - lib/markbridge/parsers/media_wiki/parser.rb
104
115
  - lib/markbridge/parsers/text_formatter.rb
105
116
  - lib/markbridge/parsers/text_formatter/handler_registry.rb
@@ -112,6 +123,7 @@ files:
112
123
  - lib/markbridge/parsers/text_formatter/handlers/list_handler.rb
113
124
  - lib/markbridge/parsers/text_formatter/handlers/quote_handler.rb
114
125
  - lib/markbridge/parsers/text_formatter/handlers/simple_handler.rb
126
+ - lib/markbridge/parsers/text_formatter/handlers/table_cell_handler.rb
115
127
  - lib/markbridge/parsers/text_formatter/handlers/url_handler.rb
116
128
  - lib/markbridge/parsers/text_formatter/parser.rb
117
129
  - lib/markbridge/processors.rb
@@ -125,6 +137,7 @@ files:
125
137
  - lib/markbridge/processors/discourse_markdown/scanner.rb
126
138
  - lib/markbridge/renderers/discourse.rb
127
139
  - lib/markbridge/renderers/discourse/builders/list_item_builder.rb
140
+ - lib/markbridge/renderers/discourse/html_escaper.rb
128
141
  - lib/markbridge/renderers/discourse/markdown_escaper.rb
129
142
  - lib/markbridge/renderers/discourse/render_context.rb
130
143
  - lib/markbridge/renderers/discourse/renderer.rb
@@ -154,9 +167,13 @@ files:
154
167
  - lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb
155
168
  - lib/markbridge/renderers/discourse/tags/subscript_tag.rb
156
169
  - lib/markbridge/renderers/discourse/tags/superscript_tag.rb
170
+ - lib/markbridge/renderers/discourse/tags/table_cell_tag.rb
171
+ - lib/markbridge/renderers/discourse/tags/table_row_tag.rb
172
+ - lib/markbridge/renderers/discourse/tags/table_tag.rb
157
173
  - lib/markbridge/renderers/discourse/tags/underline_tag.rb
158
174
  - lib/markbridge/renderers/discourse/tags/upload_tag.rb
159
175
  - lib/markbridge/renderers/discourse/tags/url_tag.rb
176
+ - lib/markbridge/textformatter.rb
160
177
  - lib/markbridge/version.rb
161
178
  homepage: https://github.com/discourse/markbridge
162
179
  licenses:
@@ -172,7 +189,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
172
189
  requirements:
173
190
  - - ">="
174
191
  - !ruby/object:Gem::Version
175
- version: 3.2.0
192
+ version: 3.3.0
176
193
  required_rubygems_version: !ruby/object:Gem::Requirement
177
194
  requirements:
178
195
  - - ">="