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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/lib/markbridge/all.rb +9 -0
- data/lib/markbridge/ast/align.rb +24 -0
- data/lib/markbridge/ast/attachment.rb +42 -0
- data/lib/markbridge/ast/bold.rb +13 -0
- data/lib/markbridge/ast/code.rb +27 -0
- data/lib/markbridge/ast/color.rb +25 -0
- data/lib/markbridge/ast/document.rb +27 -0
- data/lib/markbridge/ast/element.rb +47 -0
- data/lib/markbridge/ast/email.rb +27 -0
- data/lib/markbridge/ast/event.rb +59 -0
- data/lib/markbridge/ast/heading.rb +23 -0
- data/lib/markbridge/ast/horizontal_rule.rb +12 -0
- data/lib/markbridge/ast/image.rb +35 -0
- data/lib/markbridge/ast/italic.rb +13 -0
- data/lib/markbridge/ast/line_break.rb +12 -0
- data/lib/markbridge/ast/list.rb +52 -0
- data/lib/markbridge/ast/list_item.rb +13 -0
- data/lib/markbridge/ast/markdown_text.rb +37 -0
- data/lib/markbridge/ast/mention.rb +29 -0
- data/lib/markbridge/ast/node.rb +19 -0
- data/lib/markbridge/ast/paragraph.rb +13 -0
- data/lib/markbridge/ast/poll.rb +74 -0
- data/lib/markbridge/ast/quote.rb +46 -0
- data/lib/markbridge/ast/size.rb +25 -0
- data/lib/markbridge/ast/spoiler.rb +27 -0
- data/lib/markbridge/ast/strikethrough.rb +13 -0
- data/lib/markbridge/ast/subscript.rb +13 -0
- data/lib/markbridge/ast/superscript.rb +13 -0
- data/lib/markbridge/ast/text.rb +38 -0
- data/lib/markbridge/ast/underline.rb +13 -0
- data/lib/markbridge/ast/upload.rb +74 -0
- data/lib/markbridge/ast/url.rb +27 -0
- data/lib/markbridge/ast.rb +42 -0
- data/lib/markbridge/configuration.rb +11 -0
- data/lib/markbridge/gem_loader.rb +23 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/base.rb +37 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb +17 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/strict.rb +12 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/tag_reconciler.rb +121 -0
- data/lib/markbridge/parsers/bbcode/errors/max_depth_exceeded_error.rb +13 -0
- data/lib/markbridge/parsers/bbcode/handler_registry.rb +160 -0
- data/lib/markbridge/parsers/bbcode/handlers/align_handler.rb +26 -0
- data/lib/markbridge/parsers/bbcode/handlers/attachment_handler.rb +104 -0
- data/lib/markbridge/parsers/bbcode/handlers/base_handler.rb +44 -0
- data/lib/markbridge/parsers/bbcode/handlers/code_handler.rb +25 -0
- data/lib/markbridge/parsers/bbcode/handlers/color_handler.rb +31 -0
- data/lib/markbridge/parsers/bbcode/handlers/email_handler.rb +25 -0
- data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +51 -0
- data/lib/markbridge/parsers/bbcode/handlers/list_handler.rb +36 -0
- data/lib/markbridge/parsers/bbcode/handlers/list_item_handler.rb +26 -0
- data/lib/markbridge/parsers/bbcode/handlers/quote_handler.rb +64 -0
- data/lib/markbridge/parsers/bbcode/handlers/raw_handler.rb +48 -0
- data/lib/markbridge/parsers/bbcode/handlers/self_closing_handler.rb +28 -0
- data/lib/markbridge/parsers/bbcode/handlers/simple_handler.rb +28 -0
- data/lib/markbridge/parsers/bbcode/handlers/size_handler.rb +31 -0
- data/lib/markbridge/parsers/bbcode/handlers/spoiler_handler.rb +28 -0
- data/lib/markbridge/parsers/bbcode/handlers/url_handler.rb +24 -0
- data/lib/markbridge/parsers/bbcode/parser.rb +123 -0
- data/lib/markbridge/parsers/bbcode/parser_state.rb +93 -0
- data/lib/markbridge/parsers/bbcode/peekable_enumerator.rb +126 -0
- data/lib/markbridge/parsers/bbcode/raw_content_collector.rb +35 -0
- data/lib/markbridge/parsers/bbcode/raw_content_result.rb +25 -0
- data/lib/markbridge/parsers/bbcode/scanner.rb +231 -0
- data/lib/markbridge/parsers/bbcode/tokens/tag_end_token.rb +21 -0
- data/lib/markbridge/parsers/bbcode/tokens/tag_start_token.rb +23 -0
- data/lib/markbridge/parsers/bbcode/tokens/text_token.rb +23 -0
- data/lib/markbridge/parsers/bbcode/tokens/token.rb +16 -0
- data/lib/markbridge/parsers/bbcode.rb +56 -0
- data/lib/markbridge/parsers/html/handler_registry.rb +87 -0
- data/lib/markbridge/parsers/html/handlers/base_handler.rb +27 -0
- data/lib/markbridge/parsers/html/handlers/image_handler.rb +40 -0
- data/lib/markbridge/parsers/html/handlers/list_handler.rb +29 -0
- data/lib/markbridge/parsers/html/handlers/list_item_handler.rb +26 -0
- data/lib/markbridge/parsers/html/handlers/paragraph_handler.rb +17 -0
- data/lib/markbridge/parsers/html/handlers/quote_handler.rb +28 -0
- data/lib/markbridge/parsers/html/handlers/raw_handler.rb +33 -0
- data/lib/markbridge/parsers/html/handlers/simple_handler.rb +26 -0
- data/lib/markbridge/parsers/html/handlers/url_handler.rb +27 -0
- data/lib/markbridge/parsers/html/parser.rb +113 -0
- data/lib/markbridge/parsers/html.rb +30 -0
- data/lib/markbridge/parsers/media_wiki/inline_parser.rb +332 -0
- data/lib/markbridge/parsers/media_wiki/parser.rb +279 -0
- data/lib/markbridge/parsers/media_wiki.rb +15 -0
- data/lib/markbridge/parsers/text_formatter/handler_registry.rb +130 -0
- data/lib/markbridge/parsers/text_formatter/handlers/attachment_handler.rb +33 -0
- data/lib/markbridge/parsers/text_formatter/handlers/attribute_handler.rb +40 -0
- data/lib/markbridge/parsers/text_formatter/handlers/base_handler.rb +45 -0
- data/lib/markbridge/parsers/text_formatter/handlers/code_handler.rb +28 -0
- data/lib/markbridge/parsers/text_formatter/handlers/email_handler.rb +27 -0
- data/lib/markbridge/parsers/text_formatter/handlers/image_handler.rb +32 -0
- data/lib/markbridge/parsers/text_formatter/handlers/list_handler.rb +31 -0
- data/lib/markbridge/parsers/text_formatter/handlers/quote_handler.rb +33 -0
- data/lib/markbridge/parsers/text_formatter/handlers/simple_handler.rb +37 -0
- data/lib/markbridge/parsers/text_formatter/handlers/url_handler.rb +29 -0
- data/lib/markbridge/parsers/text_formatter/parser.rb +132 -0
- data/lib/markbridge/parsers/text_formatter.rb +31 -0
- data/lib/markbridge/processors/discourse_markdown/code_block_tracker.rb +199 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/base.rb +57 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/event.rb +73 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/mention.rb +57 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/poll.rb +90 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/upload.rb +123 -0
- data/lib/markbridge/processors/discourse_markdown/scanner.rb +199 -0
- data/lib/markbridge/processors/discourse_markdown.rb +16 -0
- data/lib/markbridge/processors.rb +8 -0
- data/lib/markbridge/renderers/discourse/builders/list_item_builder.rb +83 -0
- data/lib/markbridge/renderers/discourse/markdown_escaper.rb +468 -0
- data/lib/markbridge/renderers/discourse/render_context.rb +80 -0
- data/lib/markbridge/renderers/discourse/renderer.rb +63 -0
- data/lib/markbridge/renderers/discourse/rendering_interface.rb +86 -0
- data/lib/markbridge/renderers/discourse/tag.rb +29 -0
- data/lib/markbridge/renderers/discourse/tag_library.rb +67 -0
- data/lib/markbridge/renderers/discourse/tags/align_tag.rb +24 -0
- data/lib/markbridge/renderers/discourse/tags/attachment_tag.rb +46 -0
- data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +18 -0
- data/lib/markbridge/renderers/discourse/tags/code_tag.rb +54 -0
- data/lib/markbridge/renderers/discourse/tags/color_tag.rb +27 -0
- data/lib/markbridge/renderers/discourse/tags/email_tag.rb +24 -0
- data/lib/markbridge/renderers/discourse/tags/event_tag.rb +49 -0
- data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +21 -0
- data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +16 -0
- data/lib/markbridge/renderers/discourse/tags/image_tag.rb +29 -0
- data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +18 -0
- data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +16 -0
- data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +87 -0
- data/lib/markbridge/renderers/discourse/tags/list_tag.rb +39 -0
- data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +34 -0
- data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +21 -0
- data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +51 -0
- data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +32 -0
- data/lib/markbridge/renderers/discourse/tags/size_tag.rb +27 -0
- data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +24 -0
- data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +18 -0
- data/lib/markbridge/renderers/discourse/tags/subscript_tag.rb +19 -0
- data/lib/markbridge/renderers/discourse/tags/superscript_tag.rb +19 -0
- data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +19 -0
- data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +80 -0
- data/lib/markbridge/renderers/discourse/tags/url_tag.rb +24 -0
- data/lib/markbridge/renderers/discourse.rb +50 -0
- data/lib/markbridge/version.rb +5 -0
- data/lib/markbridge.rb +201 -0
- 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
|
+
# ""
|
|
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:  or 
|
|
17
|
+
if width && height
|
|
18
|
+
""
|
|
19
|
+
elsif width
|
|
20
|
+
""
|
|
21
|
+
else
|
|
22
|
+
""
|
|
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
|