markbridge 0.1.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 (144) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/lib/markbridge/all.rb +9 -0
  4. data/lib/markbridge/ast/align.rb +24 -0
  5. data/lib/markbridge/ast/attachment.rb +42 -0
  6. data/lib/markbridge/ast/bold.rb +13 -0
  7. data/lib/markbridge/ast/code.rb +27 -0
  8. data/lib/markbridge/ast/color.rb +25 -0
  9. data/lib/markbridge/ast/document.rb +27 -0
  10. data/lib/markbridge/ast/element.rb +47 -0
  11. data/lib/markbridge/ast/email.rb +27 -0
  12. data/lib/markbridge/ast/event.rb +59 -0
  13. data/lib/markbridge/ast/heading.rb +23 -0
  14. data/lib/markbridge/ast/horizontal_rule.rb +12 -0
  15. data/lib/markbridge/ast/image.rb +35 -0
  16. data/lib/markbridge/ast/italic.rb +13 -0
  17. data/lib/markbridge/ast/line_break.rb +12 -0
  18. data/lib/markbridge/ast/list.rb +52 -0
  19. data/lib/markbridge/ast/list_item.rb +13 -0
  20. data/lib/markbridge/ast/markdown_text.rb +37 -0
  21. data/lib/markbridge/ast/mention.rb +29 -0
  22. data/lib/markbridge/ast/node.rb +19 -0
  23. data/lib/markbridge/ast/paragraph.rb +13 -0
  24. data/lib/markbridge/ast/poll.rb +74 -0
  25. data/lib/markbridge/ast/quote.rb +46 -0
  26. data/lib/markbridge/ast/size.rb +25 -0
  27. data/lib/markbridge/ast/spoiler.rb +27 -0
  28. data/lib/markbridge/ast/strikethrough.rb +13 -0
  29. data/lib/markbridge/ast/subscript.rb +13 -0
  30. data/lib/markbridge/ast/superscript.rb +13 -0
  31. data/lib/markbridge/ast/text.rb +38 -0
  32. data/lib/markbridge/ast/underline.rb +13 -0
  33. data/lib/markbridge/ast/upload.rb +74 -0
  34. data/lib/markbridge/ast/url.rb +27 -0
  35. data/lib/markbridge/ast.rb +42 -0
  36. data/lib/markbridge/configuration.rb +11 -0
  37. data/lib/markbridge/gem_loader.rb +23 -0
  38. data/lib/markbridge/parsers/bbcode/closing_strategies/base.rb +37 -0
  39. data/lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb +17 -0
  40. data/lib/markbridge/parsers/bbcode/closing_strategies/strict.rb +12 -0
  41. data/lib/markbridge/parsers/bbcode/closing_strategies/tag_reconciler.rb +121 -0
  42. data/lib/markbridge/parsers/bbcode/errors/max_depth_exceeded_error.rb +13 -0
  43. data/lib/markbridge/parsers/bbcode/handler_registry.rb +160 -0
  44. data/lib/markbridge/parsers/bbcode/handlers/align_handler.rb +26 -0
  45. data/lib/markbridge/parsers/bbcode/handlers/attachment_handler.rb +104 -0
  46. data/lib/markbridge/parsers/bbcode/handlers/base_handler.rb +44 -0
  47. data/lib/markbridge/parsers/bbcode/handlers/code_handler.rb +25 -0
  48. data/lib/markbridge/parsers/bbcode/handlers/color_handler.rb +31 -0
  49. data/lib/markbridge/parsers/bbcode/handlers/email_handler.rb +25 -0
  50. data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +51 -0
  51. data/lib/markbridge/parsers/bbcode/handlers/list_handler.rb +36 -0
  52. data/lib/markbridge/parsers/bbcode/handlers/list_item_handler.rb +26 -0
  53. data/lib/markbridge/parsers/bbcode/handlers/quote_handler.rb +64 -0
  54. data/lib/markbridge/parsers/bbcode/handlers/raw_handler.rb +48 -0
  55. data/lib/markbridge/parsers/bbcode/handlers/self_closing_handler.rb +28 -0
  56. data/lib/markbridge/parsers/bbcode/handlers/simple_handler.rb +28 -0
  57. data/lib/markbridge/parsers/bbcode/handlers/size_handler.rb +31 -0
  58. data/lib/markbridge/parsers/bbcode/handlers/spoiler_handler.rb +28 -0
  59. data/lib/markbridge/parsers/bbcode/handlers/url_handler.rb +24 -0
  60. data/lib/markbridge/parsers/bbcode/parser.rb +123 -0
  61. data/lib/markbridge/parsers/bbcode/parser_state.rb +93 -0
  62. data/lib/markbridge/parsers/bbcode/peekable_enumerator.rb +126 -0
  63. data/lib/markbridge/parsers/bbcode/raw_content_collector.rb +35 -0
  64. data/lib/markbridge/parsers/bbcode/raw_content_result.rb +25 -0
  65. data/lib/markbridge/parsers/bbcode/scanner.rb +231 -0
  66. data/lib/markbridge/parsers/bbcode/tokens/tag_end_token.rb +21 -0
  67. data/lib/markbridge/parsers/bbcode/tokens/tag_start_token.rb +23 -0
  68. data/lib/markbridge/parsers/bbcode/tokens/text_token.rb +23 -0
  69. data/lib/markbridge/parsers/bbcode/tokens/token.rb +16 -0
  70. data/lib/markbridge/parsers/bbcode.rb +56 -0
  71. data/lib/markbridge/parsers/html/handler_registry.rb +87 -0
  72. data/lib/markbridge/parsers/html/handlers/base_handler.rb +27 -0
  73. data/lib/markbridge/parsers/html/handlers/image_handler.rb +40 -0
  74. data/lib/markbridge/parsers/html/handlers/list_handler.rb +29 -0
  75. data/lib/markbridge/parsers/html/handlers/list_item_handler.rb +26 -0
  76. data/lib/markbridge/parsers/html/handlers/paragraph_handler.rb +17 -0
  77. data/lib/markbridge/parsers/html/handlers/quote_handler.rb +28 -0
  78. data/lib/markbridge/parsers/html/handlers/raw_handler.rb +33 -0
  79. data/lib/markbridge/parsers/html/handlers/simple_handler.rb +26 -0
  80. data/lib/markbridge/parsers/html/handlers/url_handler.rb +27 -0
  81. data/lib/markbridge/parsers/html/parser.rb +113 -0
  82. data/lib/markbridge/parsers/html.rb +30 -0
  83. data/lib/markbridge/parsers/media_wiki/inline_parser.rb +332 -0
  84. data/lib/markbridge/parsers/media_wiki/parser.rb +279 -0
  85. data/lib/markbridge/parsers/media_wiki.rb +15 -0
  86. data/lib/markbridge/parsers/text_formatter/handler_registry.rb +130 -0
  87. data/lib/markbridge/parsers/text_formatter/handlers/attachment_handler.rb +33 -0
  88. data/lib/markbridge/parsers/text_formatter/handlers/attribute_handler.rb +40 -0
  89. data/lib/markbridge/parsers/text_formatter/handlers/base_handler.rb +45 -0
  90. data/lib/markbridge/parsers/text_formatter/handlers/code_handler.rb +28 -0
  91. data/lib/markbridge/parsers/text_formatter/handlers/email_handler.rb +27 -0
  92. data/lib/markbridge/parsers/text_formatter/handlers/image_handler.rb +32 -0
  93. data/lib/markbridge/parsers/text_formatter/handlers/list_handler.rb +31 -0
  94. data/lib/markbridge/parsers/text_formatter/handlers/quote_handler.rb +33 -0
  95. data/lib/markbridge/parsers/text_formatter/handlers/simple_handler.rb +37 -0
  96. data/lib/markbridge/parsers/text_formatter/handlers/url_handler.rb +29 -0
  97. data/lib/markbridge/parsers/text_formatter/parser.rb +132 -0
  98. data/lib/markbridge/parsers/text_formatter.rb +31 -0
  99. data/lib/markbridge/processors/discourse_markdown/code_block_tracker.rb +199 -0
  100. data/lib/markbridge/processors/discourse_markdown/detectors/base.rb +57 -0
  101. data/lib/markbridge/processors/discourse_markdown/detectors/event.rb +73 -0
  102. data/lib/markbridge/processors/discourse_markdown/detectors/mention.rb +57 -0
  103. data/lib/markbridge/processors/discourse_markdown/detectors/poll.rb +90 -0
  104. data/lib/markbridge/processors/discourse_markdown/detectors/upload.rb +123 -0
  105. data/lib/markbridge/processors/discourse_markdown/scanner.rb +199 -0
  106. data/lib/markbridge/processors/discourse_markdown.rb +16 -0
  107. data/lib/markbridge/processors.rb +8 -0
  108. data/lib/markbridge/renderers/discourse/builders/list_item_builder.rb +83 -0
  109. data/lib/markbridge/renderers/discourse/markdown_escaper.rb +468 -0
  110. data/lib/markbridge/renderers/discourse/render_context.rb +80 -0
  111. data/lib/markbridge/renderers/discourse/renderer.rb +63 -0
  112. data/lib/markbridge/renderers/discourse/rendering_interface.rb +86 -0
  113. data/lib/markbridge/renderers/discourse/tag.rb +29 -0
  114. data/lib/markbridge/renderers/discourse/tag_library.rb +67 -0
  115. data/lib/markbridge/renderers/discourse/tags/align_tag.rb +24 -0
  116. data/lib/markbridge/renderers/discourse/tags/attachment_tag.rb +46 -0
  117. data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +18 -0
  118. data/lib/markbridge/renderers/discourse/tags/code_tag.rb +54 -0
  119. data/lib/markbridge/renderers/discourse/tags/color_tag.rb +27 -0
  120. data/lib/markbridge/renderers/discourse/tags/email_tag.rb +24 -0
  121. data/lib/markbridge/renderers/discourse/tags/event_tag.rb +49 -0
  122. data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +21 -0
  123. data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +16 -0
  124. data/lib/markbridge/renderers/discourse/tags/image_tag.rb +29 -0
  125. data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +18 -0
  126. data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +16 -0
  127. data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +87 -0
  128. data/lib/markbridge/renderers/discourse/tags/list_tag.rb +39 -0
  129. data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +34 -0
  130. data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +21 -0
  131. data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +51 -0
  132. data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +32 -0
  133. data/lib/markbridge/renderers/discourse/tags/size_tag.rb +27 -0
  134. data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +24 -0
  135. data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +18 -0
  136. data/lib/markbridge/renderers/discourse/tags/subscript_tag.rb +19 -0
  137. data/lib/markbridge/renderers/discourse/tags/superscript_tag.rb +19 -0
  138. data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +19 -0
  139. data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +80 -0
  140. data/lib/markbridge/renderers/discourse/tags/url_tag.rb +24 -0
  141. data/lib/markbridge/renderers/discourse.rb +50 -0
  142. data/lib/markbridge/version.rb +5 -0
  143. data/lib/markbridge.rb +201 -0
  144. metadata +186 -0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Placeholder tag for rendering polls.
8
+ #
9
+ # This is a STUB implementation that outputs the original raw BBCode.
10
+ # Applications using Markbridge should provide their own custom tag
11
+ # to render polls as placeholders or convert them to another format.
12
+ #
13
+ # @example Custom renderer with placeholder
14
+ # class MyPollTag < Markbridge::Renderers::Discourse::Tags::PollTag
15
+ # def render(element, interface)
16
+ # id = register_poll(element)
17
+ # "<<POLL:#{id}>>"
18
+ # end
19
+ # end
20
+ class PollTag < Tag
21
+ def render(element, interface)
22
+ # Return raw BBCode if available, otherwise reconstruct
23
+ return element.raw if element.raw
24
+
25
+ build_poll_bbcode(element)
26
+ end
27
+
28
+ private
29
+
30
+ def build_poll_bbcode(element)
31
+ attrs = build_attributes(element)
32
+ options = element.options.map { |opt| "* #{opt}" }.join("\n")
33
+
34
+ "[poll#{attrs}]\n#{options}\n[/poll]"
35
+ end
36
+
37
+ def build_attributes(element)
38
+ parts = []
39
+ parts << %( name="#{element.name}") if element.name && element.name != "poll"
40
+ parts << %( type="#{element.type}") if element.type
41
+ parts << %( results="#{element.results}") if element.results
42
+ parts << %( public="true") if element.public
43
+ parts << %( chartType="#{element.chart_type}") if element.chart_type
44
+
45
+ parts.join
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering quotes
8
+ # Renders to Discourse BBCode quote format to preserve attribution
9
+ class QuoteTag < Tag
10
+ def render(element, interface)
11
+ child_context = interface.with_parent(element)
12
+ content = interface.render_children(element, context: child_context)
13
+
14
+ # Build Discourse quote BBCode
15
+ # 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
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering sized text
8
+ # Note: Discourse doesn't support inline size changes by default
9
+ # Renders as plain text with HTML comment noting the size was lost
10
+ class SizeTag < Tag
11
+ def render(element, interface)
12
+ child_context = interface.with_parent(element)
13
+ content = interface.render_children(element, context: child_context)
14
+
15
+ if element.size
16
+ # Render as HTML span with style - requires HTML to be enabled
17
+ # Alternative: just output the text without size
18
+ "<span style=\"font-size: #{element.size}px\">#{content}</span>"
19
+ else
20
+ content
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering spoilers
8
+ # Renders to Discourse BBCode spoiler format
9
+ class SpoilerTag < Tag
10
+ def render(element, interface)
11
+ child_context = interface.with_parent(element)
12
+ content = interface.render_children(element, context: child_context)
13
+
14
+ if element.title
15
+ "[spoiler=#{element.title}]#{content}[/spoiler]"
16
+ else
17
+ "[spoiler]#{content}[/spoiler]"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ 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 strikethrough text
8
+ class StrikethroughTag < Tag
9
+ def render(element, interface)
10
+ child_context = interface.with_parent(element)
11
+ content = interface.render_children(element, context: child_context)
12
+ interface.wrap_inline(content, "~~")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering subscript text
8
+ # Renders to HTML <sub> tag
9
+ class SubscriptTag < Tag
10
+ def render(element, interface)
11
+ child_context = interface.with_parent(element)
12
+ content = interface.render_children(element, context: child_context)
13
+ "<sub>#{content}</sub>"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering superscript text
8
+ # Renders to HTML <sup> tag
9
+ class SuperscriptTag < Tag
10
+ def render(element, interface)
11
+ child_context = interface.with_parent(element)
12
+ content = interface.render_children(element, context: child_context)
13
+ "<sup>#{content}</sup>"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering underline text
8
+ class UnderlineTag < Tag
9
+ def render(element, interface)
10
+ child_context = interface.with_parent(element)
11
+ content = interface.render_children(element, context: child_context)
12
+ # Discourse uses HTML for underline
13
+ "<u>#{content}</u>"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Placeholder tag for rendering Discourse uploads.
8
+ #
9
+ # This is a STUB implementation that outputs the original raw Markdown.
10
+ # Applications using Markbridge should provide their own custom tag
11
+ # to render uploads as placeholders or resolve upload:// URLs.
12
+ #
13
+ # @example Custom renderer with placeholder
14
+ # class MyUploadTag < Markbridge::Renderers::Discourse::Tags::UploadTag
15
+ # def render(element, interface)
16
+ # "<<UPLOAD:#{element.sha1}>>"
17
+ # end
18
+ # end
19
+ #
20
+ # @example Custom renderer that resolves URLs
21
+ # class MyUploadTag < Markbridge::Renderers::Discourse::Tags::UploadTag
22
+ # def render(element, interface)
23
+ # url = resolve_upload(element.sha1)
24
+ # if element.type == :image
25
+ # "![#{element.alt}](#{url})"
26
+ # else
27
+ # "[#{element.filename}](#{url})"
28
+ # end
29
+ # end
30
+ # end
31
+ class UploadTag < Tag
32
+ def render(element, interface)
33
+ # Return raw Markdown if available, otherwise reconstruct
34
+ return element.raw if element.raw
35
+
36
+ build_upload_markdown(element)
37
+ end
38
+
39
+ private
40
+
41
+ def build_upload_markdown(element)
42
+ if element.type == :image
43
+ build_image_markdown(element)
44
+ else
45
+ build_attachment_markdown(element)
46
+ end
47
+ end
48
+
49
+ def build_image_markdown(element)
50
+ alt = build_alt(element)
51
+ url = build_upload_url(element)
52
+
53
+ "![#{alt}](#{url})"
54
+ end
55
+
56
+ def build_attachment_markdown(element)
57
+ filename = element.filename || "attachment"
58
+ url = build_upload_url(element)
59
+ size_part = element.size ? " (#{element.size})" : ""
60
+
61
+ "[#{filename}|attachment](#{url})#{size_part}"
62
+ end
63
+
64
+ def build_alt(element)
65
+ parts = []
66
+ parts << element.alt if element.alt
67
+ parts << element.dimensions if element.dimensions
68
+
69
+ parts.join("|")
70
+ end
71
+
72
+ def build_upload_url(element)
73
+ filename = element.filename || element.sha1
74
+ "upload://#{filename}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering URLs
8
+ class UrlTag < Tag
9
+ def render(element, interface)
10
+ child_context = interface.with_parent(element)
11
+ text = interface.render_children(element, context: child_context)
12
+ href = element.href
13
+
14
+ if href&.match?(/^(https?|ftps?|mailto):/i)
15
+ "[#{text}](#{href})"
16
+ else
17
+ text
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "discourse/tag"
4
+ require_relative "discourse/tag_library"
5
+ require_relative "discourse/render_context"
6
+ require_relative "discourse/rendering_interface"
7
+ require_relative "discourse/markdown_escaper"
8
+
9
+ # Builders
10
+ require_relative "discourse/builders/list_item_builder"
11
+
12
+ # Tags
13
+ require_relative "discourse/tags/align_tag"
14
+ require_relative "discourse/tags/attachment_tag"
15
+ require_relative "discourse/tags/bold_tag"
16
+ require_relative "discourse/tags/code_tag"
17
+ require_relative "discourse/tags/color_tag"
18
+ require_relative "discourse/tags/email_tag"
19
+ require_relative "discourse/tags/heading_tag"
20
+ require_relative "discourse/tags/horizontal_rule_tag"
21
+ require_relative "discourse/tags/line_break_tag"
22
+ require_relative "discourse/tags/image_tag"
23
+ require_relative "discourse/tags/italic_tag"
24
+ require_relative "discourse/tags/list_tag"
25
+ require_relative "discourse/tags/list_item_tag"
26
+ require_relative "discourse/tags/paragraph_tag"
27
+ require_relative "discourse/tags/quote_tag"
28
+ require_relative "discourse/tags/size_tag"
29
+ require_relative "discourse/tags/spoiler_tag"
30
+ require_relative "discourse/tags/strikethrough_tag"
31
+ require_relative "discourse/tags/subscript_tag"
32
+ require_relative "discourse/tags/superscript_tag"
33
+ require_relative "discourse/tags/underline_tag"
34
+ require_relative "discourse/tags/url_tag"
35
+
36
+ # Discourse-specific tags
37
+ require_relative "discourse/tags/event_tag"
38
+ require_relative "discourse/tags/mention_tag"
39
+ require_relative "discourse/tags/poll_tag"
40
+ require_relative "discourse/tags/upload_tag"
41
+
42
+ # Renderer itself
43
+ require_relative "discourse/renderer"
44
+
45
+ module Markbridge
46
+ module Renderers
47
+ module Discourse
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ VERSION = "0.1.0"
5
+ end
data/lib/markbridge.rb ADDED
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "markbridge/version"
4
+ require_relative "markbridge/configuration"
5
+
6
+ require_relative "markbridge/ast"
7
+ require_relative "markbridge/renderers/discourse"
8
+ require_relative "markbridge/parsers/media_wiki"
9
+ require_relative "markbridge/parsers/text_formatter"
10
+ require_relative "markbridge/processors"
11
+
12
+ module Markbridge
13
+ class << self
14
+ # Parse BBCode to AST
15
+ # @param input [String] BBCode source
16
+ # @param handlers [HandlerRegistry, nil] custom handler registry or use default
17
+ # @return [AST::Document]
18
+ 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
+ handlers ||= default_handlers
23
+
24
+ parser = Parsers::BBCode::Parser.new(handlers:)
25
+ parser.parse(input)
26
+ end
27
+
28
+ # Convert BBCode to Discourse Markdown
29
+ # @param input [String] BBCode source
30
+ # @param handlers [HandlerRegistry, nil] custom handler registry or use default
31
+ # @param tag_library [TagLibrary, nil] custom tag library or use default
32
+ # @return [String] Markdown output
33
+ 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
+ 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)
45
+ end
46
+
47
+ # Parse HTML to AST
48
+ # @param input [String] HTML source
49
+ # @param handlers [HandlerRegistry, nil] custom handler registry or use default
50
+ # @return [AST::Document]
51
+ 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
+ handlers ||= default_html_handlers
56
+
57
+ parser = Parsers::HTML::Parser.new(handlers:)
58
+ parser.parse(input)
59
+ end
60
+
61
+ # Convert HTML to Discourse Markdown
62
+ # @param input [String] HTML source
63
+ # @param handlers [HandlerRegistry, nil] custom handler registry or use default
64
+ # @param tag_library [TagLibrary, nil] custom tag library or use default
65
+ # @return [String] Markdown output
66
+ 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
+ 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)
78
+ end
79
+
80
+ # Parse s9e/TextFormatter XML to AST
81
+ # @param input [String] XML source in s9e/TextFormatter format
82
+ # @param handlers [Parsers::TextFormatter::HandlerRegistry, nil] custom handler registry or use default
83
+ # @return [AST::Document]
84
+ 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
+ handlers ||= default_text_formatter_handlers
89
+
90
+ parser = Parsers::TextFormatter::Parser.new(handlers:)
91
+ parser.parse(input)
92
+ end
93
+
94
+ # Convert s9e/TextFormatter XML to Discourse Markdown
95
+ # @param input [String] XML source in s9e/TextFormatter format
96
+ # @param handlers [Parsers::TextFormatter::HandlerRegistry, nil] custom handler registry or use default
97
+ # @param tag_library [TagLibrary, nil] custom tag library or use default
98
+ # @return [String] Markdown output
99
+ 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
+ ast = parse_text_formatter_xml(input, handlers:)
106
+ renderer = build_renderer(tag_library:)
107
+
108
+ result = renderer.render(ast)
109
+ cleanup_markdown(result)
110
+ end
111
+
112
+ # Parse MediaWiki wikitext to AST
113
+ # @param input [String] MediaWiki source
114
+ # @return [AST::Document]
115
+ def parse_mediawiki(input)
116
+ raise ArgumentError, "input cannot be nil" if input.nil?
117
+
118
+ input = input.to_s
119
+ parser = Parsers::MediaWiki::Parser.new
120
+ parser.parse(input)
121
+ end
122
+
123
+ # Convert MediaWiki wikitext to Discourse Markdown
124
+ # @param input [String] MediaWiki source
125
+ # @param tag_library [TagLibrary, nil] custom tag library or use default
126
+ # @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)
137
+ end
138
+
139
+ # Get default handler registry
140
+ # @return [Parsers::BBCode::HandlerRegistry]
141
+ def default_handlers
142
+ @default_handlers ||= Parsers::BBCode::HandlerRegistry.default
143
+ end
144
+
145
+ # Get default HTML handler registry
146
+ # @return [Parsers::HTML::HandlerRegistry]
147
+ def default_html_handlers
148
+ @default_html_handlers ||= Parsers::HTML::HandlerRegistry.default
149
+ end
150
+
151
+ # Get default tag library
152
+ # @return [Renderers::Discourse::TagLibrary]
153
+ def default_tag_library
154
+ @default_tag_library ||= Renderers::Discourse::TagLibrary.default
155
+ end
156
+
157
+ # Get default s9e/TextFormatter handler registry
158
+ # @return [Parsers::TextFormatter::HandlerRegistry]
159
+ def default_text_formatter_handlers
160
+ @default_text_formatter_handlers ||= Parsers::TextFormatter::HandlerRegistry.default
161
+ end
162
+
163
+ # Get the global configuration
164
+ # @return [Configuration]
165
+ def configuration
166
+ @configuration ||= Configuration.new
167
+ end
168
+
169
+ # Configure Markbridge with a block
170
+ # @yield [Configuration]
171
+ def configure
172
+ yield configuration
173
+ end
174
+
175
+ # Reset defaults (useful for testing)
176
+ def reset_defaults!
177
+ @default_handlers = nil
178
+ @default_html_handlers = nil
179
+ @default_tag_library = nil
180
+ @default_text_formatter_handlers = nil
181
+ @configuration = nil
182
+ end
183
+
184
+ private
185
+
186
+ def build_renderer(tag_library:)
187
+ escaper =
188
+ Renderers::Discourse::MarkdownEscaper.new(
189
+ escape_hard_line_breaks: configuration.escape_hard_line_breaks,
190
+ )
191
+ Renderers::Discourse::Renderer.new(tag_library:, escaper:)
192
+ end
193
+
194
+ def cleanup_markdown(text)
195
+ text
196
+ .gsub(/\n{3,}/, "\n\n") # Max 2 consecutive newlines
197
+ .gsub(/^[ \t]+$/m, "") # Remove whitespace-only lines
198
+ .strip # Trim leading/trailing whitespace
199
+ end
200
+ end
201
+ end