markbridge 0.1.3 → 0.2.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 +4 -4
- data/LICENSE.txt +1 -1
- data/lib/markbridge/ast/details.rb +24 -0
- data/lib/markbridge/ast/element.rb +63 -0
- data/lib/markbridge/ast.rb +1 -0
- data/lib/markbridge/conversion.rb +40 -0
- data/lib/markbridge/parse.rb +20 -0
- data/lib/markbridge/parsers/bbcode/handler_registry.rb +25 -2
- data/lib/markbridge/parsers/bbcode/handlers/raw_handler.rb +13 -2
- data/lib/markbridge/parsers/html/handler_registry.rb +97 -17
- data/lib/markbridge/parsers/html/handlers/self_closing_handler.rb +26 -0
- data/lib/markbridge/parsers/html/handlers/span_handler.rb +74 -0
- data/lib/markbridge/parsers/html/parser.rb +88 -18
- data/lib/markbridge/parsers/html.rb +2 -0
- data/lib/markbridge/parsers/media_wiki/inline_parser.rb +21 -8
- data/lib/markbridge/parsers/media_wiki/parser.rb +13 -5
- data/lib/markbridge/parsers/text_formatter/handler_registry.rb +27 -4
- data/lib/markbridge/parsers/text_formatter/handlers/attachment_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/attribute_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/base_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/code_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/email_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/image_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/list_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/quote_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/simple_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/table_cell_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/handlers/url_handler.rb +1 -1
- data/lib/markbridge/parsers/text_formatter/parser.rb +17 -3
- data/lib/markbridge/renderers/discourse/identity_escaper.rb +37 -0
- data/lib/markbridge/renderers/discourse/markdown_escaper.rb +83 -8
- data/lib/markbridge/renderers/discourse/postprocessor.rb +53 -0
- data/lib/markbridge/renderers/discourse/render_context.rb +14 -40
- data/lib/markbridge/renderers/discourse/renderer.rb +15 -5
- data/lib/markbridge/renderers/discourse/rendering_interface.rb +4 -3
- data/lib/markbridge/renderers/discourse/tag_library.rb +42 -2
- data/lib/markbridge/renderers/discourse/tags/align_tag.rb +2 -2
- data/lib/markbridge/renderers/discourse/tags/code_tag.rb +5 -3
- data/lib/markbridge/renderers/discourse/tags/details_tag.rb +46 -0
- data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +1 -1
- data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +5 -2
- data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +4 -3
- data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +13 -0
- data/lib/markbridge/renderers/discourse.rb +3 -0
- data/lib/markbridge/version.rb +1 -1
- data/lib/markbridge.rb +274 -110
- metadata +9 -2
- data/lib/markbridge/configuration.rb +0 -11
|
@@ -5,13 +5,15 @@ module Markbridge
|
|
|
5
5
|
module Discourse
|
|
6
6
|
# Renders AST to Discourse-flavored Markdown in-memory.
|
|
7
7
|
class Renderer
|
|
8
|
-
|
|
8
|
+
attr_reader :postprocessor
|
|
9
|
+
|
|
10
|
+
def initialize(tag_library: nil, escaper: nil, html_escaper: nil, postprocessor: nil)
|
|
9
11
|
@tag_library = tag_library || TagLibrary.default
|
|
10
12
|
@escaper = escaper || MarkdownEscaper.new
|
|
11
13
|
@html_escaper = html_escaper || HtmlEscaper
|
|
14
|
+
@postprocessor = postprocessor || Postprocessor::DEFAULT
|
|
12
15
|
# @interface_cache is lazily initialized in #render's top-level
|
|
13
|
-
# call and reset to nil after the call completes.
|
|
14
|
-
# needed here — unset ivar returns nil under `.nil?` check.
|
|
16
|
+
# call and reset to nil after the call completes.
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
# Render a node to Markdown
|
|
@@ -20,7 +22,7 @@ module Markbridge
|
|
|
20
22
|
# @return [String]
|
|
21
23
|
def render(node, context: RenderContext.new)
|
|
22
24
|
root_call = @interface_cache.nil?
|
|
23
|
-
@interface_cache
|
|
25
|
+
@interface_cache = {} if root_call
|
|
24
26
|
|
|
25
27
|
tag = @tag_library[node.class]
|
|
26
28
|
if tag
|
|
@@ -97,9 +99,17 @@ module Markbridge
|
|
|
97
99
|
elsif context.html_mode?
|
|
98
100
|
@html_escaper.escape(node.text)
|
|
99
101
|
else
|
|
100
|
-
@escaper.escape(node.text)
|
|
102
|
+
@escaper.escape(node.text, in_link_label: in_link_label?(context))
|
|
101
103
|
end
|
|
102
104
|
end
|
|
105
|
+
|
|
106
|
+
# `]` is structural inside a Markdown link label, so any plain text
|
|
107
|
+
# rendered under an Url/Email ancestor must escape it. Tags that emit
|
|
108
|
+
# their own bracketed markup (ImageTag, UploadTag, etc.) skip this
|
|
109
|
+
# path entirely, so their structural brackets are preserved.
|
|
110
|
+
def in_link_label?(context)
|
|
111
|
+
context.has_parent?(AST::Url) || context.has_parent?(AST::Email)
|
|
112
|
+
end
|
|
103
113
|
end
|
|
104
114
|
end
|
|
105
115
|
end
|
|
@@ -67,7 +67,7 @@ module Markbridge
|
|
|
67
67
|
# Handles edge cases like existing markers and whitespace
|
|
68
68
|
def wrap_inline(content, open_marker, close_marker = nil)
|
|
69
69
|
close_marker ||= open_marker
|
|
70
|
-
return content unless content.match?(
|
|
70
|
+
return content unless content.match?(/[^[:space:]]/)
|
|
71
71
|
|
|
72
72
|
# Handle conflicts with existing markers
|
|
73
73
|
if content.include?(open_marker) || content.include?(close_marker)
|
|
@@ -82,8 +82,9 @@ module Markbridge
|
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
# Preserve leading/trailing whitespace
|
|
86
|
-
|
|
85
|
+
# Preserve leading/trailing whitespace (Unicode-aware, since
|
|
86
|
+
# CommonMark's flanking rules treat e.g. nbsp as whitespace).
|
|
87
|
+
content.sub(/\A([[:space:]]*)(.+?)([[:space:]]*)\z/m) do
|
|
87
88
|
match = Regexp.last_match
|
|
88
89
|
"#{match[1]}#{open_marker}#{match[2]}#{close_marker}#{match[3]}"
|
|
89
90
|
end
|
|
@@ -11,6 +11,15 @@ module Markbridge
|
|
|
11
11
|
@tags = {}
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# When a TagLibrary is +dup+'d / +clone+'d, ensure the
|
|
15
|
+
# internal +@tags+ Hash is independent of the source. Without
|
|
16
|
+
# this, both copies would share the same underlying Hash and
|
|
17
|
+
# mutations to one would silently affect the other.
|
|
18
|
+
def initialize_copy(other)
|
|
19
|
+
super
|
|
20
|
+
@tags = @tags.dup
|
|
21
|
+
end
|
|
22
|
+
|
|
14
23
|
# Register a tag for an element class
|
|
15
24
|
# @param element_class [Class] the element class
|
|
16
25
|
# @param tag [Tag] the tag instance
|
|
@@ -19,6 +28,38 @@ module Markbridge
|
|
|
19
28
|
self
|
|
20
29
|
end
|
|
21
30
|
|
|
31
|
+
# Remove a tag binding so the renderer falls through to
|
|
32
|
+
# +render_children+ for that element class. See
|
|
33
|
+
# +Renderer#render+ for the auto-passthrough path.
|
|
34
|
+
#
|
|
35
|
+
# @param element_class [Class]
|
|
36
|
+
# @return [self]
|
|
37
|
+
def unregister(element_class)
|
|
38
|
+
@tags.delete(element_class)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Merge a Hash of class → Tag mappings on top of this library
|
|
43
|
+
# in-place. A +nil+ value unregisters the corresponding class
|
|
44
|
+
# (so the default auto-passthrough kicks in).
|
|
45
|
+
#
|
|
46
|
+
# Named with a trailing +!+ because it mutates +self+ —
|
|
47
|
+
# mirroring Ruby's Hash#merge / Hash#merge! convention. Use
|
|
48
|
+
# +dup+ first if you need a non-destructive merge.
|
|
49
|
+
#
|
|
50
|
+
# @param mapping [Hash{Class => Tag, nil}]
|
|
51
|
+
# @return [self]
|
|
52
|
+
def merge!(mapping)
|
|
53
|
+
mapping.each_pair do |klass, tag|
|
|
54
|
+
if tag.nil?
|
|
55
|
+
unregister(klass)
|
|
56
|
+
else
|
|
57
|
+
register(klass, tag)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
22
63
|
# Get tag for an element class
|
|
23
64
|
# @param element_class [Class]
|
|
24
65
|
# @return [Tag, nil]
|
|
@@ -59,8 +100,7 @@ module Markbridge
|
|
|
59
100
|
# Create the default tag library for Discourse Markdown.
|
|
60
101
|
#
|
|
61
102
|
# Each call returns a *fresh* instance — mutations made to one will
|
|
62
|
-
# not be visible to another.
|
|
63
|
-
# use {Markbridge.default_tag_library} instead, which memoizes.
|
|
103
|
+
# not be visible to another.
|
|
64
104
|
#
|
|
65
105
|
# @return [TagLibrary]
|
|
66
106
|
def self.default
|
|
@@ -21,9 +21,9 @@ module Markbridge
|
|
|
21
21
|
return content unless ALLOWED_ALIGNMENTS.include?(element.alignment)
|
|
22
22
|
|
|
23
23
|
wrapper = %(<div align="#{element.alignment}">#{content}</div>)
|
|
24
|
-
# Skip the
|
|
24
|
+
# Skip the blank-line bracketing in html_mode: a blank line would
|
|
25
25
|
# terminate the surrounding HTML block (e.g. an enclosing <table>).
|
|
26
|
-
interface.html_mode? ? wrapper : "#{wrapper}\n\n"
|
|
26
|
+
interface.html_mode? ? wrapper : "\n\n#{wrapper}\n\n"
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -28,11 +28,13 @@ module Markbridge
|
|
|
28
28
|
"`#{content}`"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
# being parsed as a
|
|
31
|
+
# Leading and trailing blank lines: the trailing one keeps an
|
|
32
|
+
# adjacent fence on the next block from being parsed as a
|
|
33
|
+
# continuation of this one; the leading one separates the fence
|
|
34
|
+
# from prior raw text or inline content.
|
|
33
35
|
def render_block(content, language)
|
|
34
36
|
fence = calculate_fence(content)
|
|
35
|
-
"#{fence}#{language}\n#{content}\n#{fence}\n\n"
|
|
37
|
+
"\n\n#{fence}#{language}\n#{content}\n#{fence}\n\n"
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def render_html_block(content, language)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module Renderers
|
|
5
|
+
module Discourse
|
|
6
|
+
module Tags
|
|
7
|
+
# Renders {AST::Details} as a Discourse +[details=…]…[/details]+
|
|
8
|
+
# collapsible block.
|
|
9
|
+
#
|
|
10
|
+
# Markdown form: the BBCode is bracketed with leading and trailing
|
|
11
|
+
# blank lines so consecutive details blocks don't merge and
|
|
12
|
+
# adjacent inline content starts a new paragraph against the
|
|
13
|
+
# block. Inside, the rendered children are stripped so the
|
|
14
|
+
# +[details]+ opener and the body sit on adjacent lines without
|
|
15
|
+
# a stray blank between them — matching Discourse's BBCode
|
|
16
|
+
# parser expectations.
|
|
17
|
+
#
|
|
18
|
+
# HTML-block form (inside a CommonMark HTML block — when
|
|
19
|
+
# +interface.html_mode?+ is +true+): a raw
|
|
20
|
+
# +<details><summary>…</summary>…</details>+ element. The
|
|
21
|
+
# +title+ is HTML-escaped for the +<summary>+ text.
|
|
22
|
+
class DetailsTag < Tag
|
|
23
|
+
DEFAULT_TITLE = "Summary"
|
|
24
|
+
private_constant :DEFAULT_TITLE
|
|
25
|
+
|
|
26
|
+
def render(element, interface)
|
|
27
|
+
child_context = interface.with_parent(element)
|
|
28
|
+
content = interface.render_children(element, context: child_context)
|
|
29
|
+
|
|
30
|
+
return render_html(element.title, content) if interface.html_mode?
|
|
31
|
+
|
|
32
|
+
opener = element.title ? %([details="#{element.title}"]) : "[details]"
|
|
33
|
+
"\n\n#{opener}\n#{content.strip}\n[/details]\n\n"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def render_html(title, content)
|
|
39
|
+
label = title ? HtmlEscaper.escape(title) : DEFAULT_TITLE
|
|
40
|
+
"<details><summary>#{label}</summary>#{content}</details>"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -21,8 +21,11 @@ module Markbridge
|
|
|
21
21
|
return "<p>#{content}</p>"
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
|
|
24
|
+
# Bracket with leading and trailing blank lines so adjacent
|
|
25
|
+
# non-block content (raw text, inline elements) stays separated
|
|
26
|
+
# from the paragraph. cleanup_markdown collapses any duplicate
|
|
27
|
+
# newlines that result when neighbours are themselves block tags.
|
|
28
|
+
"\n\n#{content}\n\n"
|
|
26
29
|
end
|
|
27
30
|
end
|
|
28
31
|
end
|
|
@@ -27,9 +27,10 @@ module Markbridge
|
|
|
27
27
|
content.split("\n").map { |line| "> #{line}" }.join("\n")
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
30
|
+
# Bracket with leading and trailing blank lines so consecutive
|
|
31
|
+
# quotes don't merge and adjacent non-block content (raw text,
|
|
32
|
+
# inline elements) starts a new paragraph against the quote.
|
|
33
|
+
"\n\n#{body}\n\n"
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
end
|
|
@@ -8,17 +8,30 @@ module Markbridge
|
|
|
8
8
|
# cooked by the BBCode plugin into `<span class="bbcode-u">`. The
|
|
9
9
|
# BBCode plugin runs on Markdown source, not on raw HTML inside an
|
|
10
10
|
# HTML block, so in html_mode we emit the cooked form directly.
|
|
11
|
+
#
|
|
12
|
+
# Inside Markdown link text (`[text](url)`) the BBCode plugin also
|
|
13
|
+
# does not re-cook nested BBCode, so `[[u]X[/u]](url)` would render
|
|
14
|
+
# literally. Drop the wrapper when rendering under a link ancestor.
|
|
11
15
|
class UnderlineTag < Tag
|
|
12
16
|
def render(element, interface)
|
|
13
17
|
child_context = interface.with_parent(element)
|
|
14
18
|
content = interface.render_children(element, context: child_context)
|
|
19
|
+
return content unless content.match?(/[^[:space:]]/)
|
|
15
20
|
|
|
16
21
|
if interface.html_mode?
|
|
17
22
|
%(<span class="bbcode-u">#{content}</span>)
|
|
23
|
+
elsif inside_link?(interface)
|
|
24
|
+
content
|
|
18
25
|
else
|
|
19
26
|
"[u]#{content}[/u]"
|
|
20
27
|
end
|
|
21
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def inside_link?(interface)
|
|
33
|
+
interface.has_parent?(AST::Url) || interface.has_parent?(AST::Email)
|
|
34
|
+
end
|
|
22
35
|
end
|
|
23
36
|
end
|
|
24
37
|
end
|
|
@@ -5,7 +5,9 @@ require_relative "discourse/tag_library"
|
|
|
5
5
|
require_relative "discourse/render_context"
|
|
6
6
|
require_relative "discourse/rendering_interface"
|
|
7
7
|
require_relative "discourse/markdown_escaper"
|
|
8
|
+
require_relative "discourse/identity_escaper"
|
|
8
9
|
require_relative "discourse/html_escaper"
|
|
10
|
+
require_relative "discourse/postprocessor"
|
|
9
11
|
|
|
10
12
|
# Builders
|
|
11
13
|
require_relative "discourse/builders/list_item_builder"
|
|
@@ -16,6 +18,7 @@ require_relative "discourse/tags/attachment_tag"
|
|
|
16
18
|
require_relative "discourse/tags/bold_tag"
|
|
17
19
|
require_relative "discourse/tags/code_tag"
|
|
18
20
|
require_relative "discourse/tags/color_tag"
|
|
21
|
+
require_relative "discourse/tags/details_tag"
|
|
19
22
|
require_relative "discourse/tags/email_tag"
|
|
20
23
|
require_relative "discourse/tags/heading_tag"
|
|
21
24
|
require_relative "discourse/tags/horizontal_rule_tag"
|
data/lib/markbridge/version.rb
CHANGED