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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ # Base class for rendering tags
7
+ # Can be subclassed for complex tags or initialized with a block for simple tags
8
+ class Tag
9
+ # Initialize a tag
10
+ # @param block [Proc, nil] optional block for rendering
11
+ def initialize(&block)
12
+ @render_block = block
13
+ end
14
+
15
+ # Render a node to Discourse Markdown
16
+ # @param element [AST::Node] the node to render
17
+ # @param interface [RenderingInterface] the rendering interface
18
+ # @return [String] the rendered markdown
19
+ def render(element, interface)
20
+ if @render_block
21
+ @render_block.call(element, interface)
22
+ else
23
+ raise NotImplementedError, "#{self.class} must implement #render or provide a block"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ # Library of rendering tags for different element types
7
+ class TagLibrary
8
+ def initialize
9
+ @tags = {}
10
+ end
11
+
12
+ # Register a tag for an element class
13
+ # @param element_class [Class] the element class
14
+ # @param tag [Tag] the tag instance
15
+ def register(element_class, tag)
16
+ @tags[element_class] = tag
17
+ self
18
+ end
19
+
20
+ # Get tag for an element class
21
+ # @param element_class [Class]
22
+ # @return [Tag, nil]
23
+ def [](element_class)
24
+ @tags[element_class]
25
+ end
26
+
27
+ # Auto-register all tags using naming convention
28
+ # Convention: BoldTag handles AST::Bold, ItalicTag handles AST::Italic, etc.
29
+ # @return [self]
30
+ def auto_register!
31
+ Tags.constants.each do |tag_constant|
32
+ tag_class = Tags.const_get(tag_constant)
33
+ next unless tag_class.is_a?(Class) && tag_class < Tag
34
+
35
+ # Extract element name from tag name: BoldTag → Bold
36
+ element_name = tag_constant.to_s.sub(/Tag$/, "")
37
+ element_class =
38
+ begin
39
+ AST.const_get(element_name)
40
+ rescue StandardError
41
+ nil
42
+ end
43
+
44
+ register(element_class, tag_class.new) if element_class
45
+ end
46
+
47
+ self
48
+ end
49
+
50
+ # Create the default tag library for Discourse Markdown
51
+ # @return [TagLibrary]
52
+ def self.default
53
+ library = new
54
+
55
+ # Auto-register tags based on naming convention
56
+ library.auto_register!
57
+
58
+ # Special cases: inline tags that don't follow the convention
59
+ library.register AST::LineBreak, Tags::LineBreakTag.new
60
+ library.register AST::HorizontalRule, Tags::HorizontalRuleTag.new
61
+
62
+ library
63
+ end
64
+ end
65
+ end
66
+ end
67
+ 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 aligned text
8
+ # Renders as HTML div with align attribute
9
+ class AlignTag < 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.alignment
15
+ "<div align=\"#{element.alignment}\">#{content}</div>"
16
+ else
17
+ content
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Placeholder tag for rendering attachments.
8
+ #
9
+ # This is a STUB implementation that outputs metadata as a comment.
10
+ # Applications using Markbridge should provide their own custom renderer
11
+ # that maps attachment IDs/indices to actual upload URLs.
12
+ #
13
+ # @example Custom renderer
14
+ # class MyAttachmentTag < Markbridge::Renderers::Discourse::Tags::AttachmentTag
15
+ # def render(element, interface)
16
+ # url = lookup_attachment_url(element.id || element.index)
17
+ # alt = element.alt || element.filename || ""
18
+ # "![#{alt}](#{url})"
19
+ # end
20
+ # end
21
+ #
22
+ # library = Markbridge::Renderers::Discourse::TagLibrary.default
23
+ # library.register(Markbridge::AST::Attachment, MyAttachmentTag.new)
24
+ class AttachmentTag < Tag
25
+ def render(element, interface)
26
+ # Build metadata comment for downstream processing
27
+ metadata = build_metadata(element)
28
+ "<!-- ATTACHMENT: #{metadata} -->"
29
+ end
30
+
31
+ private
32
+
33
+ def build_metadata(element)
34
+ parts = []
35
+ parts << "id=#{element.id}" if element.id
36
+ parts << "index=#{element.index}" if element.index
37
+ parts << "filename=#{element.filename}" if element.filename
38
+ parts << "alt=#{element.alt}" if element.alt
39
+
40
+ parts.empty? ? "UNIDENTIFIED" : parts.join(" ")
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ 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 bold text
8
+ class BoldTag < 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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering code
8
+ class CodeTag < Tag
9
+ def render(element, interface)
10
+ child_context = interface.with_parent(element)
11
+ content = interface.render_children(element, context: child_context)
12
+
13
+ # Determine if inline or block based on context
14
+ if interface.block_context?(element)
15
+ render_block(content, element.language)
16
+ else
17
+ render_inline(content)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def render_inline(content)
24
+ "`#{content}`"
25
+ end
26
+
27
+ def render_block(content, language)
28
+ fence = calculate_fence(content)
29
+ lang = language || ""
30
+
31
+ "#{fence}#{lang}\n#{content}\n#{fence}"
32
+ end
33
+
34
+ def calculate_fence(content)
35
+ # Find longest sequence of backticks and tildes
36
+ max_backticks = content.scan(/`+/).map(&:length).max || 0
37
+ max_tildes = content.scan(/~+/).map(&:length).max || 0
38
+
39
+ # Need fence longer than any sequence in content (minimum 3)
40
+ required_backticks = [3, max_backticks + 1].max
41
+ required_tildes = [3, max_tildes + 1].max
42
+
43
+ # Choose whichever requires fewer characters
44
+ if required_backticks <= required_tildes
45
+ "`" * required_backticks
46
+ else
47
+ "~" * required_tildes
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ 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 colored text
8
+ # Note: Discourse doesn't support inline color by default
9
+ # Renders as plain text with HTML comment noting the color was lost
10
+ class ColorTag < 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.color
16
+ # Render as HTML span with style - requires HTML to be enabled
17
+ # Alternative: just output the text without color
18
+ "<span style=\"color: #{element.color}\">#{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 email links
8
+ class EmailTag < Tag
9
+ def render(element, interface)
10
+ child_context = interface.with_parent(element)
11
+ text = interface.render_children(element, context: child_context)
12
+ address = element.address
13
+
14
+ if address
15
+ "[#{text}](mailto:#{address})"
16
+ else
17
+ text
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Placeholder tag for rendering events.
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 events as placeholders or convert them to another format.
12
+ #
13
+ # @example Custom renderer with placeholder
14
+ # class MyEventTag < Markbridge::Renderers::Discourse::Tags::EventTag
15
+ # def render(element, interface)
16
+ # id = register_event(element)
17
+ # "<<EVENT:#{id}>>"
18
+ # end
19
+ # end
20
+ class EventTag < Tag
21
+ def render(element, interface)
22
+ # Return raw BBCode if available, otherwise reconstruct
23
+ return element.raw if element.raw
24
+
25
+ build_event_bbcode(element)
26
+ end
27
+
28
+ private
29
+
30
+ def build_event_bbcode(element)
31
+ attrs = build_attributes(element)
32
+ "[event#{attrs}]\n[/event]"
33
+ end
34
+
35
+ def build_attributes(element)
36
+ parts = []
37
+ parts << %( name="#{element.name}")
38
+ parts << %( start="#{element.starts_at}")
39
+ parts << %( end="#{element.ends_at}") if element.ends_at
40
+ parts << %( status="#{element.status}") if element.status
41
+ parts << %( timezone="#{element.timezone}") if element.timezone
42
+
43
+ parts.join
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering headings
8
+ # Renders as ATX-style Markdown headings (# through ######)
9
+ class HeadingTag < Tag
10
+ def render(element, interface)
11
+ child_context = interface.with_parent(element)
12
+ content = interface.render_children(element, context: child_context)
13
+ prefix = "#" * element.level
14
+
15
+ "#{prefix} #{content}\n\n"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering horizontal rules
8
+ class HorizontalRuleTag < Tag
9
+ def render(_element, _interface)
10
+ "\n\n---\n\n"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering images
8
+ # Renders to Markdown image syntax with optional Discourse sizing
9
+ class ImageTag < Tag
10
+ def render(element, interface)
11
+ src = element.src || ""
12
+ width = element.width
13
+ height = element.height
14
+
15
+ # Build Discourse image syntax with dimensions
16
+ # Format: ![alt|WIDTHxHEIGHT](url) or ![alt|WIDTH](url)
17
+ if width && height
18
+ "![|#{width}x#{height}](#{src})"
19
+ elsif width
20
+ "![|#{width}](#{src})"
21
+ else
22
+ "![](#{src})"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ 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 italic text
8
+ class ItalicTag < 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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering line breaks
8
+ class LineBreakTag < Tag
9
+ def render(_element, _interface)
10
+ "\n"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering list items
8
+ class ListItemTag < Tag
9
+ def initialize
10
+ super
11
+ @builder = Builders::ListItemBuilder.new
12
+ end
13
+
14
+ def render(element, interface)
15
+ # Create new context with this list item as parent
16
+ child_context = interface.with_parent(element)
17
+
18
+ # Render children with updated context
19
+ content = interface.render_children(element, context: child_context).strip
20
+ return "" if content.empty?
21
+
22
+ # Get parent list to determine marker
23
+ parent_list = interface.find_parent(AST::List)
24
+ marker = determine_marker(parent_list)
25
+
26
+ # Calculate indentation based on ancestor lists
27
+ indent = calculate_indent(interface)
28
+
29
+ # Delegate formatting to builder
30
+ @builder.build(content, marker:, indent:)
31
+ end
32
+
33
+ private
34
+
35
+ # Determine the list marker based on parent list type
36
+ # @param parent_list [AST::List, nil]
37
+ # @return [String]
38
+ def determine_marker(parent_list)
39
+ parent_list&.ordered? ? "1. " : "- "
40
+ end
41
+
42
+ # Calculate indentation for this list item
43
+ # @param interface [RenderingInterface]
44
+ # @return [String]
45
+ def calculate_indent(interface)
46
+ # Calculate indentation: count ancestor Lists (not including direct parent)
47
+ # The direct parent List shouldn't add indentation, only grandparent Lists
48
+ list_count = interface.count_parents(AST::List)
49
+ # Subtract 1 because the immediate parent list doesn't contribute to indent
50
+ ancestor_lists = list_count.positive? ? list_count - 1 : 0
51
+
52
+ # Indentation width depends on markers of ancestor lists
53
+ # Walk up the context to determine correct indentation
54
+ calculate_indent_from_ancestors(interface.context, ancestor_lists)
55
+ end
56
+
57
+ # Calculate the correct indentation for nested list items
58
+ # Each level matches the marker width of its parent: ordered="1. " (3 chars), unordered="- " (2 chars)
59
+ # @param context [RenderContext] the rendering context
60
+ # @param ancestor_count [Integer] number of ancestor lists
61
+ # @return [String] the indentation string
62
+ def calculate_indent_from_ancestors(context, ancestor_count)
63
+ return "" if ancestor_count.zero?
64
+
65
+ # Walk through parents from outermost to innermost to build indentation
66
+ # Each ancestor contributes indentation based on ITS OWN marker width
67
+ lists = context.parents.select { |p| p.is_a?(AST::List) }
68
+
69
+ # Skip the immediate parent (last list in the array)
70
+ ancestor_lists = lists[0...-1]
71
+
72
+ # Build indentation string
73
+ indent = ""
74
+ ancestor_lists
75
+ .first(ancestor_count)
76
+ .each do |list|
77
+ # Each level's indentation matches that list's marker width
78
+ indent += list.ordered? ? " " : " "
79
+ end
80
+
81
+ indent
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering lists
8
+ class ListTag < Tag
9
+ def render(element, interface)
10
+ # Create new context with this list as parent
11
+ child_context = interface.with_parent(element)
12
+
13
+ # Render children with updated context, dropping empty items
14
+ rendered_items =
15
+ element.children.filter_map do |child|
16
+ rendered = interface.render_node(child, context: child_context)
17
+ rendered unless rendered.strip.empty?
18
+ end
19
+
20
+ content = rendered_items.join
21
+
22
+ # Check if we're nested - either inside another List OR inside a ListItem
23
+ has_list_parent = interface.has_parent?(AST::List)
24
+ has_list_item_parent = interface.has_parent?(AST::ListItem)
25
+ nested = has_list_parent || has_list_item_parent
26
+
27
+ if nested
28
+ # Nested list: add leading newline so it starts on its own line
29
+ "\n#{content}"
30
+ else
31
+ # Top-level list: add spacing
32
+ "\n\n#{content}\n\n"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Placeholder tag for rendering mentions.
8
+ #
9
+ # This is a STUB implementation that outputs a placeholder.
10
+ # Applications using Markbridge should provide their own custom tag
11
+ # or use the raw mention format.
12
+ #
13
+ # @example Custom renderer that preserves original format
14
+ # class MyMentionTag < Markbridge::Renderers::Discourse::Tags::MentionTag
15
+ # def render(element, interface)
16
+ # "@#{element.name}"
17
+ # end
18
+ # end
19
+ #
20
+ # @example Custom renderer that links to user
21
+ # class MyMentionTag < Markbridge::Renderers::Discourse::Tags::MentionTag
22
+ # def render(element, interface)
23
+ # "[@#{element.name}](/u/#{element.name})"
24
+ # end
25
+ # end
26
+ class MentionTag < Tag
27
+ def render(element, interface)
28
+ "@#{element.name}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markbridge
4
+ module Renderers
5
+ module Discourse
6
+ module Tags
7
+ # Tag for rendering paragraphs
8
+ # Paragraphs are separated by blank lines in Markdown
9
+ class ParagraphTag < Tag
10
+ def render(element, interface)
11
+ child_context = interface.with_parent(element)
12
+ content = interface.render_children(element, context: child_context)
13
+
14
+ # Paragraph followed by blank line (two newlines)
15
+ "#{content}\n\n"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end