markbridge 0.1.1 → 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.
- checksums.yaml +4 -4
- data/lib/markbridge/all.rb +4 -7
- data/lib/markbridge/ast/document.rb +1 -1
- data/lib/markbridge/ast/element.rb +2 -2
- data/lib/markbridge/ast/list.rb +2 -2
- data/lib/markbridge/ast/table.rb +6 -12
- data/lib/markbridge/ast/text.rb +5 -1
- data/lib/markbridge/bbcode.rb +4 -0
- data/lib/markbridge/gem_loader.rb +2 -3
- data/lib/markbridge/html.rb +4 -0
- data/lib/markbridge/mediawiki.rb +4 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/base.rb +0 -10
- data/lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb +17 -4
- data/lib/markbridge/parsers/bbcode/closing_strategies/tag_reconciler.rb +64 -44
- data/lib/markbridge/parsers/bbcode/handler_registry.rb +21 -11
- data/lib/markbridge/parsers/bbcode/handlers/attachment_handler.rb +17 -12
- data/lib/markbridge/parsers/bbcode/handlers/base_handler.rb +0 -10
- data/lib/markbridge/parsers/bbcode/handlers/code_handler.rb +6 -10
- data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +9 -17
- data/lib/markbridge/parsers/bbcode/handlers/list_handler.rb +1 -5
- data/lib/markbridge/parsers/bbcode/handlers/list_item_handler.rb +1 -2
- data/lib/markbridge/parsers/bbcode/handlers/quote_handler.rb +6 -18
- data/lib/markbridge/parsers/bbcode/handlers/raw_handler.rb +2 -6
- data/lib/markbridge/parsers/bbcode/handlers/self_closing_handler.rb +4 -4
- data/lib/markbridge/parsers/bbcode/handlers/table_cell_handler.rb +1 -1
- data/lib/markbridge/parsers/bbcode/handlers/table_handler.rb +2 -2
- data/lib/markbridge/parsers/bbcode/handlers/table_row_handler.rb +3 -3
- data/lib/markbridge/parsers/bbcode/parser.rb +5 -8
- data/lib/markbridge/parsers/bbcode/parser_state.rb +12 -18
- data/lib/markbridge/parsers/bbcode/peekable_enumerator.rb +9 -59
- data/lib/markbridge/parsers/bbcode/raw_content_collector.rb +2 -2
- data/lib/markbridge/parsers/bbcode/scanner.rb +49 -63
- data/lib/markbridge/parsers/bbcode/tokens/tag_end_token.rb +1 -5
- data/lib/markbridge/parsers/bbcode/tokens/tag_start_token.rb +1 -6
- data/lib/markbridge/parsers/bbcode/tokens/text_token.rb +1 -7
- data/lib/markbridge/parsers/bbcode/tokens/token.rb +1 -1
- data/lib/markbridge/parsers/bbcode.rb +1 -0
- data/lib/markbridge/parsers/html/handler_registry.rb +32 -49
- data/lib/markbridge/parsers/html/handlers/base_handler.rb +0 -2
- data/lib/markbridge/parsers/html/handlers/image_handler.rb +1 -4
- data/lib/markbridge/parsers/html/parser.rb +3 -13
- data/lib/markbridge/parsers/media_wiki/inline_parser.rb +56 -67
- data/lib/markbridge/parsers/media_wiki/inline_tag_registry.rb +103 -0
- data/lib/markbridge/parsers/media_wiki/parser.rb +51 -76
- data/lib/markbridge/parsers/media_wiki.rb +1 -0
- data/lib/markbridge/parsers/text_formatter/handler_registry.rb +5 -37
- data/lib/markbridge/parsers/text_formatter/parser.rb +3 -8
- data/lib/markbridge/processors/discourse_markdown/code_block_tracker.rb +24 -17
- data/lib/markbridge/processors/discourse_markdown/detectors/base.rb +9 -15
- data/lib/markbridge/processors/discourse_markdown/detectors/event.rb +11 -10
- data/lib/markbridge/processors/discourse_markdown/detectors/poll.rb +11 -39
- data/lib/markbridge/processors/discourse_markdown/detectors/upload.rb +38 -63
- data/lib/markbridge/processors/discourse_markdown/scanner.rb +25 -33
- data/lib/markbridge/renderers/discourse/builders/list_item_builder.rb +6 -6
- data/lib/markbridge/renderers/discourse/html_escaper.rb +20 -0
- data/lib/markbridge/renderers/discourse/markdown_escaper.rb +49 -49
- data/lib/markbridge/renderers/discourse/render_context.rb +23 -11
- data/lib/markbridge/renderers/discourse/renderer.rb +54 -12
- data/lib/markbridge/renderers/discourse/rendering_interface.rb +12 -4
- data/lib/markbridge/renderers/discourse/tag.rb +14 -1
- data/lib/markbridge/renderers/discourse/tag_library.rb +30 -25
- data/lib/markbridge/renderers/discourse/tags/align_tag.rb +15 -7
- data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +2 -0
- data/lib/markbridge/renderers/discourse/tags/code_tag.rb +14 -9
- data/lib/markbridge/renderers/discourse/tags/email_tag.rb +5 -3
- data/lib/markbridge/renderers/discourse/tags/event_tag.rb +3 -1
- data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +6 -2
- data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +2 -2
- data/lib/markbridge/renderers/discourse/tags/image_tag.rb +13 -2
- data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +2 -0
- data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +2 -2
- data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +24 -47
- data/lib/markbridge/renderers/discourse/tags/list_tag.rb +10 -15
- data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +5 -1
- data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +10 -0
- data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +9 -2
- data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +2 -0
- data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +9 -0
- data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +2 -0
- data/lib/markbridge/renderers/discourse/tags/table_tag.rb +12 -8
- data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +10 -3
- data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +29 -2
- data/lib/markbridge/renderers/discourse/tags/url_tag.rb +5 -3
- data/lib/markbridge/renderers/discourse.rb +1 -0
- data/lib/markbridge/textformatter.rb +4 -0
- data/lib/markbridge/version.rb +1 -1
- data/lib/markbridge.rb +8 -8
- metadata +8 -2
|
@@ -27,6 +27,14 @@ module Markbridge
|
|
|
27
27
|
@context.with_parent(element)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def with_html_mode(value)
|
|
31
|
+
@context.with_html_mode(value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def html_mode?
|
|
35
|
+
@context.html_mode?
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
def find_parent(klass)
|
|
31
39
|
@context.find_parent(klass)
|
|
32
40
|
end
|
|
@@ -48,18 +56,18 @@ module Markbridge
|
|
|
48
56
|
# @return [Boolean]
|
|
49
57
|
def block_context?(node)
|
|
50
58
|
# Check if it's a block-level element type (but not code, which can be inline)
|
|
51
|
-
return true if node.
|
|
59
|
+
return true if node.instance_of?(AST::List) || node.instance_of?(AST::HorizontalRule)
|
|
52
60
|
return false unless node.is_a?(AST::Element)
|
|
53
61
|
|
|
54
62
|
# Check if content has newlines
|
|
55
|
-
node.children.any? { |c| c.
|
|
63
|
+
node.children.any? { |c| c.instance_of?(AST::Text) && c.text.include?("\n") }
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
# Helper: wrap inline content with markers
|
|
59
67
|
# Handles edge cases like existing markers and whitespace
|
|
60
68
|
def wrap_inline(content, open_marker, close_marker = nil)
|
|
61
69
|
close_marker ||= open_marker
|
|
62
|
-
return content
|
|
70
|
+
return content unless content.match?(/\S/)
|
|
63
71
|
|
|
64
72
|
# Handle conflicts with existing markers
|
|
65
73
|
if content.include?(open_marker) || content.include?(close_marker)
|
|
@@ -75,7 +83,7 @@ module Markbridge
|
|
|
75
83
|
end
|
|
76
84
|
|
|
77
85
|
# Preserve leading/trailing whitespace
|
|
78
|
-
content.sub(
|
|
86
|
+
content.sub(/\A(\s*)(.+?)(\s*)\z/m) do
|
|
79
87
|
match = Regexp.last_match
|
|
80
88
|
"#{match[1]}#{open_marker}#{match[2]}#{close_marker}#{match[3]}"
|
|
81
89
|
end
|
|
@@ -12,7 +12,20 @@ module Markbridge
|
|
|
12
12
|
@render_block = block
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# Render a node to Discourse Markdown
|
|
15
|
+
# Render a node to Discourse Markdown.
|
|
16
|
+
#
|
|
17
|
+
# When `interface.html_mode?` is true the surrounding output is
|
|
18
|
+
# a CommonMark HTML block (§4.6): content is treated as raw HTML
|
|
19
|
+
# and is not re-parsed for Markdown except across blank lines.
|
|
20
|
+
# Every tag must pick one of two contracts:
|
|
21
|
+
#
|
|
22
|
+
# 1. Emit raw HTML (e.g. `<strong>` for `**`).
|
|
23
|
+
# 2. Wrap Markdown output in `\n\n…\n\n` so the blank lines close
|
|
24
|
+
# the HTML block, CommonMark parses the content as a Markdown
|
|
25
|
+
# island, then re-opens. Visible: blank-line wrapping forces a
|
|
26
|
+
# `<p>` margin around inline content, so prefer (1) when the
|
|
27
|
+
# tag has an HTML form.
|
|
28
|
+
#
|
|
16
29
|
# @param element [AST::Node] the node to render
|
|
17
30
|
# @param interface [RenderingInterface] the rendering interface
|
|
18
31
|
# @return [String] the rendered markdown
|
|
@@ -5,6 +5,8 @@ module Markbridge
|
|
|
5
5
|
module Discourse
|
|
6
6
|
# Library of rendering tags for different element types
|
|
7
7
|
class TagLibrary
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
8
10
|
def initialize
|
|
9
11
|
@tags = {}
|
|
10
12
|
end
|
|
@@ -24,42 +26,45 @@ module Markbridge
|
|
|
24
26
|
@tags[element_class]
|
|
25
27
|
end
|
|
26
28
|
|
|
29
|
+
# Iterate over registered (element_class, tag) pairs.
|
|
30
|
+
# Useful for debugging custom libraries — e.g. confirming an override
|
|
31
|
+
# has stuck. Iteration order matches registration order.
|
|
32
|
+
# @yieldparam element_class [Class]
|
|
33
|
+
# @yieldparam tag [Tag]
|
|
34
|
+
# @return [Enumerator] when no block is given
|
|
35
|
+
def each(&block)
|
|
36
|
+
@tags.each(&block)
|
|
37
|
+
end
|
|
38
|
+
|
|
27
39
|
# Auto-register all tags using naming convention
|
|
28
40
|
# Convention: BoldTag handles AST::Bold, ItalicTag handles AST::Italic, etc.
|
|
29
41
|
# @return [self]
|
|
30
42
|
def auto_register!
|
|
31
43
|
Tags.constants.each do |tag_constant|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
44
|
+
element_class = ast_class_for(tag_constant)
|
|
45
|
+
register(element_class, Tags.const_get(tag_constant).new) if element_class
|
|
45
46
|
end
|
|
46
|
-
|
|
47
47
|
self
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
#
|
|
50
|
+
# Look up the AST element class matching a +XxxTag+ constant via the
|
|
51
|
+
# +XxxTag → AST::Xxx+ naming convention.
|
|
52
|
+
# @return [Class, nil]
|
|
53
|
+
def ast_class_for(tag_constant)
|
|
54
|
+
AST.const_get(tag_constant.to_s.sub(/Tag\z/, ""))
|
|
55
|
+
rescue NameError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Create the default tag library for Discourse Markdown.
|
|
60
|
+
#
|
|
61
|
+
# Each call returns a *fresh* instance — mutations made to one will
|
|
62
|
+
# not be visible to another. If you want a process-wide singleton,
|
|
63
|
+
# use {Markbridge.default_tag_library} instead, which memoizes.
|
|
64
|
+
#
|
|
51
65
|
# @return [TagLibrary]
|
|
52
66
|
def self.default
|
|
53
|
-
|
|
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
|
|
67
|
+
new.auto_register!
|
|
63
68
|
end
|
|
64
69
|
end
|
|
65
70
|
end
|
|
@@ -4,18 +4,26 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Discourse's HTML sanitizer allows the (HTML5-deprecated) `align`
|
|
8
|
+
# attribute on `<div>` but strips inline `style`, so we emit the
|
|
9
|
+
# legacy form. Alignment is constrained to a known keyword set
|
|
10
|
+
# for defense in depth — anything outside the set falls through
|
|
11
|
+
# to bare content rather than getting interpolated into the
|
|
12
|
+
# attribute.
|
|
9
13
|
class AlignTag < Tag
|
|
14
|
+
ALLOWED_ALIGNMENTS = Set["left", "right", "center", "justify"].freeze
|
|
15
|
+
private_constant :ALLOWED_ALIGNMENTS
|
|
16
|
+
|
|
10
17
|
def render(element, interface)
|
|
11
18
|
child_context = interface.with_parent(element)
|
|
12
19
|
content = interface.render_children(element, context: child_context)
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
return content unless ALLOWED_ALIGNMENTS.include?(element.alignment)
|
|
22
|
+
|
|
23
|
+
wrapper = %(<div align="#{element.alignment}">#{content}</div>)
|
|
24
|
+
# Skip the trailing blank line in html_mode: a blank line would
|
|
25
|
+
# terminate the surrounding HTML block (e.g. an enclosing <table>).
|
|
26
|
+
interface.html_mode? ? wrapper : "#{wrapper}\n\n"
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
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 "<strong>#{content}</strong>" if interface.html_mode?
|
|
13
|
+
|
|
12
14
|
interface.wrap_inline(content, "**")
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -4,15 +4,19 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
# Tag for rendering code
|
|
8
7
|
class CodeTag < Tag
|
|
9
8
|
def render(element, interface)
|
|
10
9
|
child_context = interface.with_parent(element)
|
|
11
10
|
content = interface.render_children(element, context: child_context)
|
|
12
11
|
|
|
13
|
-
# Determine if inline or block based on context
|
|
14
12
|
if interface.block_context?(element)
|
|
15
|
-
|
|
13
|
+
if interface.html_mode?
|
|
14
|
+
render_html_block(content, element.language)
|
|
15
|
+
else
|
|
16
|
+
render_block(content, element.language)
|
|
17
|
+
end
|
|
18
|
+
elsif interface.html_mode?
|
|
19
|
+
"<code>#{content}</code>"
|
|
16
20
|
else
|
|
17
21
|
render_inline(content)
|
|
18
22
|
end
|
|
@@ -24,24 +28,25 @@ module Markbridge
|
|
|
24
28
|
"`#{content}`"
|
|
25
29
|
end
|
|
26
30
|
|
|
31
|
+
# Trailing blank line keeps an adjacent fence on the next block from
|
|
32
|
+
# being parsed as a continuation of this one.
|
|
27
33
|
def render_block(content, language)
|
|
28
34
|
fence = calculate_fence(content)
|
|
29
|
-
|
|
35
|
+
"#{fence}#{language}\n#{content}\n#{fence}\n\n"
|
|
36
|
+
end
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
"
|
|
38
|
+
def render_html_block(content, language)
|
|
39
|
+
class_attr = %( class="language-#{HtmlEscaper.escape(language)}") if language
|
|
40
|
+
"<pre><code#{class_attr}>#{content}</code></pre>"
|
|
33
41
|
end
|
|
34
42
|
|
|
35
43
|
def calculate_fence(content)
|
|
36
|
-
# Find longest sequence of backticks and tildes
|
|
37
44
|
max_backticks = content.scan(/`+/).map(&:length).max || 0
|
|
38
45
|
max_tildes = content.scan(/~+/).map(&:length).max || 0
|
|
39
46
|
|
|
40
|
-
# Need fence longer than any sequence in content (minimum 3)
|
|
41
47
|
required_backticks = [3, max_backticks + 1].max
|
|
42
48
|
required_tildes = [3, max_tildes + 1].max
|
|
43
49
|
|
|
44
|
-
# Choose whichever requires fewer characters
|
|
45
50
|
if required_backticks <= required_tildes
|
|
46
51
|
"`" * required_backticks
|
|
47
52
|
else
|
|
@@ -11,10 +11,12 @@ module Markbridge
|
|
|
11
11
|
text = interface.render_children(element, context: child_context)
|
|
12
12
|
address = element.address
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
return text unless address
|
|
15
|
+
|
|
16
|
+
if interface.html_mode?
|
|
17
|
+
%(<a href="mailto:#{HtmlEscaper.escape(address)}">#{text}</a>)
|
|
16
18
|
else
|
|
17
|
-
text
|
|
19
|
+
"[#{text}](mailto:#{address})"
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
end
|
|
@@ -18,8 +18,10 @@ module Markbridge
|
|
|
18
18
|
# end
|
|
19
19
|
# end
|
|
20
20
|
class EventTag < Tag
|
|
21
|
-
def render(element,
|
|
21
|
+
def render(element, interface)
|
|
22
22
|
body = element.raw || build_event_bbcode(element)
|
|
23
|
+
return "\n\n#{body}\n\n" if interface.html_mode?
|
|
24
|
+
|
|
23
25
|
"#{body}\n\n"
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -4,12 +4,16 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
# Tag for rendering headings
|
|
8
|
-
# Renders as ATX-style Markdown headings (# through ######)
|
|
9
7
|
class HeadingTag < Tag
|
|
10
8
|
def render(element, interface)
|
|
11
9
|
child_context = interface.with_parent(element)
|
|
12
10
|
content = interface.render_children(element, context: child_context)
|
|
11
|
+
|
|
12
|
+
if interface.html_mode?
|
|
13
|
+
level = element.level.clamp(1, 6)
|
|
14
|
+
return "<h#{level}>#{content}</h#{level}>"
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
prefix = "#" * element.level
|
|
14
18
|
|
|
15
19
|
"#{prefix} #{content}\n\n"
|
|
@@ -7,11 +7,13 @@ module Markbridge
|
|
|
7
7
|
# Tag for rendering images
|
|
8
8
|
# Renders to Markdown image syntax with optional Discourse sizing
|
|
9
9
|
class ImageTag < Tag
|
|
10
|
-
def render(element,
|
|
11
|
-
src = element.src
|
|
10
|
+
def render(element, interface)
|
|
11
|
+
src = element.src
|
|
12
12
|
width = element.width
|
|
13
13
|
height = element.height
|
|
14
14
|
|
|
15
|
+
return render_html(src, width, height) if interface.html_mode?
|
|
16
|
+
|
|
15
17
|
# Build Discourse image syntax with dimensions
|
|
16
18
|
# Format:  or 
|
|
17
19
|
if width && height
|
|
@@ -22,6 +24,15 @@ module Markbridge
|
|
|
22
24
|
""
|
|
23
25
|
end
|
|
24
26
|
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def render_html(src, width, height)
|
|
31
|
+
attrs = %(src="#{HtmlEscaper.escape(src)}" alt="")
|
|
32
|
+
attrs << %( width="#{width}") if width
|
|
33
|
+
attrs << %( height="#{height}") if height
|
|
34
|
+
"<img #{attrs}>"
|
|
35
|
+
end
|
|
25
36
|
end
|
|
26
37
|
end
|
|
27
38
|
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 "<em>#{content}</em>" if interface.html_mode?
|
|
13
|
+
|
|
12
14
|
interface.wrap_inline(content, "*")
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -7,77 +7,54 @@ module Markbridge
|
|
|
7
7
|
# Tag for rendering list items
|
|
8
8
|
class ListItemTag < Tag
|
|
9
9
|
def initialize
|
|
10
|
-
super
|
|
11
10
|
@builder = Builders::ListItemBuilder.new
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def render(element, interface)
|
|
15
|
-
# Create new context with this list item as parent
|
|
16
14
|
child_context = interface.with_parent(element)
|
|
17
|
-
|
|
18
|
-
# Render children with updated context
|
|
19
15
|
content = interface.render_children(element, context: child_context).strip
|
|
20
16
|
return "" if content.empty?
|
|
21
17
|
|
|
22
|
-
|
|
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)
|
|
18
|
+
return "<li>#{content}</li>" if interface.html_mode?
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
@builder.build(
|
|
20
|
+
parent_list = interface.find_parent(AST::List)
|
|
21
|
+
@builder.build(
|
|
22
|
+
content,
|
|
23
|
+
marker: determine_marker(parent_list),
|
|
24
|
+
indent: calculate_indent(interface),
|
|
25
|
+
)
|
|
31
26
|
end
|
|
32
27
|
|
|
33
28
|
private
|
|
34
29
|
|
|
35
|
-
# Determine the list marker based on parent list type
|
|
36
30
|
# @param parent_list [AST::List, nil]
|
|
37
31
|
# @return [String]
|
|
38
32
|
def determine_marker(parent_list)
|
|
39
33
|
parent_list&.ordered? ? "1. " : "- "
|
|
40
34
|
end
|
|
41
35
|
|
|
42
|
-
#
|
|
36
|
+
# Each ancestor List (excluding the direct parent) contributes
|
|
37
|
+
# indentation matching its own marker width:
|
|
38
|
+
# ordered = " " (3 chars for "1. "), unordered = " " (2 chars for "- ").
|
|
43
39
|
# @param interface [RenderingInterface]
|
|
44
40
|
# @return [String]
|
|
45
41
|
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
42
|
list_count = interface.count_parents(AST::List)
|
|
49
|
-
#
|
|
50
|
-
|
|
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
|
|
43
|
+
# Top-level list — no ancestor lists contribute indent. The
|
|
44
|
+
# common case; short-circuit before any array allocations.
|
|
45
|
+
return "" if list_count <= 1
|
|
80
46
|
|
|
47
|
+
indent = +""
|
|
48
|
+
found = 0
|
|
49
|
+
interface.context.parents.each do |parent|
|
|
50
|
+
next unless parent.instance_of?(AST::List)
|
|
51
|
+
found += 1
|
|
52
|
+
# The immediate parent List is the LAST one in parents; its
|
|
53
|
+
# marker width is already accounted for by the item's own
|
|
54
|
+
# marker, so it doesn't contribute to indent.
|
|
55
|
+
break if found == list_count
|
|
56
|
+
indent << (parent.ordered? ? " " : " ")
|
|
57
|
+
end
|
|
81
58
|
indent
|
|
82
59
|
end
|
|
83
60
|
end
|
|
@@ -4,31 +4,26 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
# Tag for rendering lists
|
|
8
7
|
class ListTag < Tag
|
|
9
8
|
def render(element, interface)
|
|
10
|
-
# Create new context with this list as parent
|
|
11
9
|
child_context = interface.with_parent(element)
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
11
|
+
content =
|
|
12
|
+
element
|
|
13
|
+
.children
|
|
14
|
+
.map { |child| interface.render_node(child, context: child_context) }
|
|
15
|
+
.join
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
if interface.html_mode?
|
|
18
|
+
tag_name = element.ordered? ? "ol" : "ul"
|
|
19
|
+
return "<#{tag_name}>#{content}</#{tag_name}>"
|
|
20
|
+
end
|
|
21
21
|
|
|
22
|
-
|
|
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
|
|
22
|
+
nested = interface.has_parent?(AST::List) || interface.has_parent?(AST::ListItem)
|
|
26
23
|
|
|
27
24
|
if nested
|
|
28
|
-
# Nested list: add leading newline so it starts on its own line
|
|
29
25
|
"\n#{content}"
|
|
30
26
|
else
|
|
31
|
-
# Top-level list: add spacing
|
|
32
27
|
"\n\n#{content}\n\n"
|
|
33
28
|
end
|
|
34
29
|
end
|
|
@@ -25,7 +25,11 @@ module Markbridge
|
|
|
25
25
|
# end
|
|
26
26
|
class MentionTag < Tag
|
|
27
27
|
def render(element, _interface)
|
|
28
|
-
|
|
28
|
+
# Escape unconditionally: realistic Discourse usernames have no
|
|
29
|
+
# HTML-special characters so the Markdown path is unaffected,
|
|
30
|
+
# and the html_mode path needs the escape to splice safely into
|
|
31
|
+
# a raw HTML block.
|
|
32
|
+
"@#{HtmlEscaper.escape(element.name)}"
|
|
29
33
|
end
|
|
30
34
|
end
|
|
31
35
|
end
|
|
@@ -11,6 +11,16 @@ module Markbridge
|
|
|
11
11
|
child_context = interface.with_parent(element)
|
|
12
12
|
content = interface.render_children(element, context: child_context)
|
|
13
13
|
|
|
14
|
+
if interface.html_mode?
|
|
15
|
+
# Inside a table cell the surrounding <td> already provides the
|
|
16
|
+
# block context, so a <p> wrapper just adds vertical margin —
|
|
17
|
+
# and if the paragraph contains a block element (e.g. a list),
|
|
18
|
+
# `<p><ul>…</ul></p>` is invalid per HTML5. Drop the wrapper.
|
|
19
|
+
return content if interface.has_parent?(AST::TableCell)
|
|
20
|
+
|
|
21
|
+
return "<p>#{content}</p>"
|
|
22
|
+
end
|
|
23
|
+
|
|
14
24
|
# Paragraph followed by blank line (two newlines)
|
|
15
25
|
"#{content}\n\n"
|
|
16
26
|
end
|
|
@@ -18,8 +18,10 @@ module Markbridge
|
|
|
18
18
|
# end
|
|
19
19
|
# end
|
|
20
20
|
class PollTag < Tag
|
|
21
|
-
def render(element,
|
|
21
|
+
def render(element, interface)
|
|
22
22
|
body = element.raw || build_poll_bbcode(element)
|
|
23
|
+
return "\n\n#{body}\n\n" if interface.html_mode?
|
|
24
|
+
|
|
23
25
|
"#{body}\n\n"
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -32,9 +34,14 @@ module Markbridge
|
|
|
32
34
|
"[poll#{attrs}]\n#{options}\n[/poll]"
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
NAMES_WITHOUT_ATTRIBUTE = Set[nil, "poll"].freeze
|
|
38
|
+
private_constant :NAMES_WITHOUT_ATTRIBUTE
|
|
39
|
+
|
|
35
40
|
def build_attributes(element)
|
|
36
41
|
parts = []
|
|
37
|
-
|
|
42
|
+
unless NAMES_WITHOUT_ATTRIBUTE.include?(element.name)
|
|
43
|
+
parts << %( name="#{element.name}")
|
|
44
|
+
end
|
|
38
45
|
parts << %( type="#{element.type}") if element.type
|
|
39
46
|
parts << %( results="#{element.results}") if element.results
|
|
40
47
|
parts << %( public="true") if element.public
|
|
@@ -11,6 +11,8 @@ module Markbridge
|
|
|
11
11
|
child_context = interface.with_parent(element)
|
|
12
12
|
content = interface.render_children(element, context: child_context)
|
|
13
13
|
|
|
14
|
+
return "<blockquote>#{content}</blockquote>" if interface.html_mode?
|
|
15
|
+
|
|
14
16
|
# Build Discourse quote BBCode
|
|
15
17
|
# Format: [quote="username, post:123, topic:456"]content[/quote]
|
|
16
18
|
body =
|
|
@@ -11,12 +11,21 @@ module Markbridge
|
|
|
11
11
|
child_context = interface.with_parent(element)
|
|
12
12
|
content = interface.render_children(element, context: child_context)
|
|
13
13
|
|
|
14
|
+
return render_html(element.title, content) if interface.html_mode?
|
|
15
|
+
|
|
14
16
|
if element.title
|
|
15
17
|
"[spoiler=#{element.title}]#{content}[/spoiler]"
|
|
16
18
|
else
|
|
17
19
|
"[spoiler]#{content}[/spoiler]"
|
|
18
20
|
end
|
|
19
21
|
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def render_html(title, content)
|
|
26
|
+
summary = "<summary>#{title ? HtmlEscaper.escape(title) : "Spoiler"}</summary>"
|
|
27
|
+
"<details>#{summary}#{content}</details>"
|
|
28
|
+
end
|
|
20
29
|
end
|
|
21
30
|
end
|
|
22
31
|
end
|
|
@@ -9,6 +9,8 @@ module Markbridge
|
|
|
9
9
|
def render(element, interface)
|
|
10
10
|
child_context = interface.with_parent(element)
|
|
11
11
|
content = interface.render_children(element, context: child_context)
|
|
12
|
+
return "<s>#{content}</s>" if interface.html_mode?
|
|
13
|
+
|
|
12
14
|
interface.wrap_inline(content, "~~")
|
|
13
15
|
end
|
|
14
16
|
end
|