markbridge 0.4.1 → 0.4.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/renderers/discourse/builders/list_item_builder.rb +15 -63
- data/lib/markbridge/renderers/discourse/html_block.rb +142 -0
- data/lib/markbridge/renderers/discourse/renderer.rb +22 -3
- data/lib/markbridge/renderers/discourse/rendering_interface.rb +4 -2
- data/lib/markbridge/renderers/discourse/tags/align_tag.rb +24 -4
- data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +38 -33
- data/lib/markbridge/renderers/discourse.rb +1 -1
- data/lib/markbridge/rspec.rb +1 -1
- data/lib/markbridge/version.rb +1 -1
- metadata +2 -2
- data/lib/markbridge/renderers/discourse/html_block_safety.rb +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa422d5e0a05eee7f32524e42823d02a552111132025fd9ede1fc5045a79412e
|
|
4
|
+
data.tar.gz: cc7af6319a7c9dde74a764dc9edaab186ef2cca92bc6fabd842a147216adaec0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2eb57814326cf0fbaf2e356290d962a0a9e8bf2b9c38aa4e41ad7526d9494574162fc5384473b15c833b4e34bd7f322bf464098337bf9d535662f615590c19e4
|
|
7
|
+
data.tar.gz: d4fc22c47cc54ab91a0d29b140bf69317c5c10aae3358758b0036b213f96ddca9318d723016854819a8a80dcf03ea5f66180eaccc8df2d550d06ab69b8f25a87
|
|
@@ -4,78 +4,30 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Builders
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Formats one list item: the marker goes in front of the first
|
|
8
|
+
# line, every other line moves right by the width of the marker.
|
|
9
|
+
# The content is indented as one unit, so nested lists, code
|
|
10
|
+
# fences and HTML blocks inside it keep the shape they already
|
|
11
|
+
# have. Blank lines stay blank; the postprocessor clears
|
|
12
|
+
# whitespace-only lines anyway.
|
|
10
13
|
class ListItemBuilder
|
|
11
|
-
# Build a formatted list item string
|
|
12
14
|
# @param content [String] the item content
|
|
13
15
|
# @param marker [String] the list marker ("- " or "1. ")
|
|
14
|
-
# @param indent [String] the indentation string
|
|
15
16
|
# @return [String]
|
|
16
|
-
|
|
17
|
+
# @example
|
|
18
|
+
# builder.build("a\nb", marker: "- ") # => "- a\n b\n"
|
|
19
|
+
def build(content, marker:)
|
|
17
20
|
lines = content.split("\n")
|
|
18
|
-
first_line = "#{
|
|
19
|
-
|
|
21
|
+
first_line = "#{marker}#{lines.first}"
|
|
20
22
|
return "#{first_line}\n" if lines.size < 2
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# Format multi-line content with proper indentation
|
|
28
|
-
# @param lines [Array<String>] content lines
|
|
29
|
-
# @param first_line [String] the formatted first line
|
|
30
|
-
# @param indent [String] base indentation
|
|
31
|
-
# @return [String]
|
|
32
|
-
def format_multiline(lines, first_line, indent)
|
|
33
|
-
continuation_indent = "#{indent} "
|
|
34
|
-
continuation_lines = lines[1..]
|
|
35
|
-
|
|
36
|
-
rest =
|
|
37
|
-
continuation_lines.each_with_index.filter_map do |line, idx|
|
|
38
|
-
format_continuation_line(line, idx, continuation_lines, continuation_indent)
|
|
39
|
-
end
|
|
40
|
-
|
|
24
|
+
indent = " " * marker.length
|
|
25
|
+
# An empty line maps to nil, and join turns that back into
|
|
26
|
+
# an empty line. Indenting it instead would leave a line
|
|
27
|
+
# with nothing but spaces on it.
|
|
28
|
+
rest = lines[1..].map { |line| "#{indent}#{line}" unless line.empty? }
|
|
41
29
|
"#{([first_line] + rest).join("\n")}\n"
|
|
42
30
|
end
|
|
43
|
-
|
|
44
|
-
# Format a single continuation line
|
|
45
|
-
# @param line [String] the line to format
|
|
46
|
-
# @param idx [Integer] index in continuation_lines array
|
|
47
|
-
# @param continuation_lines [Array<String>] all continuation lines
|
|
48
|
-
# @param continuation_indent [String] indent for continuation
|
|
49
|
-
# @return [String, nil] formatted line or nil to skip
|
|
50
|
-
def format_continuation_line(line, idx, continuation_lines, continuation_indent)
|
|
51
|
-
# Handle empty lines
|
|
52
|
-
return handle_empty_line(idx, continuation_lines, continuation_indent) if line.empty?
|
|
53
|
-
|
|
54
|
-
# Check if line is already a list item (has indentation + marker)
|
|
55
|
-
if line.match?(/\A\s*(?:-|\d+\.)\s/)
|
|
56
|
-
# Already a list item - don't add extra indentation
|
|
57
|
-
line
|
|
58
|
-
else
|
|
59
|
-
# Regular continuation line - add indentation
|
|
60
|
-
"#{continuation_indent}#{line}"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Handle empty lines in continuation. Caller (format_continuation_line)
|
|
65
|
-
# only invokes this when `line.empty?`, and `content.split("\n")`
|
|
66
|
-
# trims trailing empty strings, so the LAST continuation line is
|
|
67
|
-
# never empty — `idx + 1` is always in bounds when we get here.
|
|
68
|
-
# @param idx [Integer] index in continuation_lines
|
|
69
|
-
# @param continuation_lines [Array<String>] all continuation lines
|
|
70
|
-
# @param continuation_indent [String] indent for continuation
|
|
71
|
-
# @return [String, nil] formatted line or nil to skip
|
|
72
|
-
def handle_empty_line(idx, continuation_lines, continuation_indent)
|
|
73
|
-
# Skip empty lines that come before nested list items (structural blanks)
|
|
74
|
-
return nil if continuation_lines[idx + 1].match?(/\A\s*(?:-|\d+\.)\s/)
|
|
75
|
-
|
|
76
|
-
# Preserve empty lines within text content (paragraph breaks) with indentation
|
|
77
|
-
continuation_indent
|
|
78
|
-
end
|
|
79
31
|
end
|
|
80
32
|
end
|
|
81
33
|
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module Renderers
|
|
5
|
+
module Discourse
|
|
6
|
+
# The one place that knows how CommonMark HTML blocks (spec §4.6)
|
|
7
|
+
# and Markdown meet in the rendered output.
|
|
8
|
+
#
|
|
9
|
+
# Inside an HTML block the content passes through as raw HTML.
|
|
10
|
+
# Markdown is parsed again only across blank lines. A tag that
|
|
11
|
+
# renders into such a block therefore picks one of two forms:
|
|
12
|
+
#
|
|
13
|
+
# 1. raw HTML — an HTML equivalent of the tag's Markdown, spliced
|
|
14
|
+
# into the block as it is;
|
|
15
|
+
# 2. a Markdown island — the tag's normal Markdown surrounded by
|
|
16
|
+
# blank lines, which end the block and start it again:
|
|
17
|
+
#
|
|
18
|
+
# <div align="center">
|
|
19
|
+
#
|
|
20
|
+
# [a link](https://example.com)
|
|
21
|
+
#
|
|
22
|
+
# </div>
|
|
23
|
+
module HtmlBlock
|
|
24
|
+
# Tag names that start a type 6 HTML block when a line begins
|
|
25
|
+
# with `<name` or `</name` followed by a space, a tab, `>`, `/>`
|
|
26
|
+
# or the end of the line.
|
|
27
|
+
BLOCK_TAGS = %w[
|
|
28
|
+
address
|
|
29
|
+
article
|
|
30
|
+
aside
|
|
31
|
+
base
|
|
32
|
+
basefont
|
|
33
|
+
blockquote
|
|
34
|
+
body
|
|
35
|
+
caption
|
|
36
|
+
center
|
|
37
|
+
col
|
|
38
|
+
colgroup
|
|
39
|
+
dd
|
|
40
|
+
details
|
|
41
|
+
dialog
|
|
42
|
+
dir
|
|
43
|
+
div
|
|
44
|
+
dl
|
|
45
|
+
dt
|
|
46
|
+
fieldset
|
|
47
|
+
figcaption
|
|
48
|
+
figure
|
|
49
|
+
footer
|
|
50
|
+
form
|
|
51
|
+
frame
|
|
52
|
+
frameset
|
|
53
|
+
h1
|
|
54
|
+
h2
|
|
55
|
+
h3
|
|
56
|
+
h4
|
|
57
|
+
h5
|
|
58
|
+
h6
|
|
59
|
+
head
|
|
60
|
+
header
|
|
61
|
+
hr
|
|
62
|
+
html
|
|
63
|
+
iframe
|
|
64
|
+
legend
|
|
65
|
+
li
|
|
66
|
+
link
|
|
67
|
+
main
|
|
68
|
+
menu
|
|
69
|
+
menuitem
|
|
70
|
+
nav
|
|
71
|
+
noframes
|
|
72
|
+
ol
|
|
73
|
+
optgroup
|
|
74
|
+
option
|
|
75
|
+
p
|
|
76
|
+
param
|
|
77
|
+
search
|
|
78
|
+
section
|
|
79
|
+
summary
|
|
80
|
+
table
|
|
81
|
+
tbody
|
|
82
|
+
td
|
|
83
|
+
tfoot
|
|
84
|
+
th
|
|
85
|
+
thead
|
|
86
|
+
title
|
|
87
|
+
tr
|
|
88
|
+
track
|
|
89
|
+
ul
|
|
90
|
+
].freeze
|
|
91
|
+
|
|
92
|
+
# The spec allows up to three leading spaces. We match any amount
|
|
93
|
+
# of leading whitespace instead, because the lines this runs on
|
|
94
|
+
# are item content that the list builder has not indented yet.
|
|
95
|
+
OPENER = %r{\A[ \t]*</?(?:#{Regexp.union(BLOCK_TAGS).source})(?:[ \t>]|/>|\z)}i
|
|
96
|
+
|
|
97
|
+
# Markdown sigils that would surface as literal text inside an
|
|
98
|
+
# HTML block: emphasis (`*`, `_`, `~`) and link middles (`](`).
|
|
99
|
+
MARKDOWN_SIGILS = /[*_~]|\]\(/
|
|
100
|
+
|
|
101
|
+
# Blank lines at the start or at the end of a fragment. The first
|
|
102
|
+
# content line keeps its own indentation, e.g. a nested ` - a`.
|
|
103
|
+
BLANK_EDGES = /\A(?:[ \t]*\n)+|(?:\n[ \t]*)+\z/
|
|
104
|
+
|
|
105
|
+
private_constant :BLOCK_TAGS, :OPENER, :MARKDOWN_SIGILS, :BLANK_EDGES
|
|
106
|
+
|
|
107
|
+
# Whether +line+ starts an HTML block. A closing tag such as
|
|
108
|
+
# `</div>` starts one too. The block runs until the next blank
|
|
109
|
+
# line.
|
|
110
|
+
# @param line [String]
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def self.opens?(line)
|
|
113
|
+
OPENER.match?(line)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Wrap Markdown so that CommonMark parses it even inside an HTML
|
|
117
|
+
# block. Blank lines that the fragment already has at its edges
|
|
118
|
+
# are folded into the wrap.
|
|
119
|
+
# @param markdown [String]
|
|
120
|
+
# @return [String]
|
|
121
|
+
# @example
|
|
122
|
+
# HtmlBlock.island("- a\n") # => "\n\n- a\n\n"
|
|
123
|
+
def self.island(markdown)
|
|
124
|
+
"\n\n#{markdown.gsub(BLANK_EDGES, "")}\n\n"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Whether a fragment can be spliced into an HTML block: raw HTML
|
|
128
|
+
# or plain text without Markdown sigils, or an island.
|
|
129
|
+
#
|
|
130
|
+
# Used by the html_mode contract check that ships in
|
|
131
|
+
# +markbridge/rspec+ and by this repo's own contract spec.
|
|
132
|
+
# @param output [String] a tag's html_mode render result
|
|
133
|
+
# @return [Boolean]
|
|
134
|
+
def self.safe?(output)
|
|
135
|
+
return true if output.start_with?("\n\n") && output.end_with?("\n\n")
|
|
136
|
+
|
|
137
|
+
!output.match?(MARKDOWN_SIGILS)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -87,19 +87,38 @@ module Markbridge
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
# Render all children of a node
|
|
90
|
+
#
|
|
91
|
+
# With a block, the block runs at every join point, right before
|
|
92
|
+
# a non-empty child output is appended. It receives the buffer
|
|
93
|
+
# built so far and the child about to be rendered into it, and
|
|
94
|
+
# may change the buffer in place. A tag uses this to adjust the
|
|
95
|
+
# text in front of a specific child without iterating the
|
|
96
|
+
# children itself, which would lose the emphasis-boundary rule
|
|
97
|
+
# below.
|
|
98
|
+
#
|
|
90
99
|
# @param node [AST::Element]
|
|
91
100
|
# @param context [RenderContext] rendering context
|
|
101
|
+
# @yieldparam result [String] the buffer built so far
|
|
102
|
+
# @yieldparam child [AST::Node] the child about to be appended
|
|
92
103
|
# @return [String]
|
|
104
|
+
# @example Attach a nested list directly to the text in front of it
|
|
105
|
+
# interface.render_children(item, context:) do |buffer, child|
|
|
106
|
+
# buffer.rstrip! if child.is_a?(AST::List)
|
|
107
|
+
# end
|
|
93
108
|
def render_children(node, context:)
|
|
94
109
|
result = +""
|
|
95
110
|
node.children.each do |child|
|
|
96
111
|
part = render(child, context:)
|
|
97
112
|
next if part.empty?
|
|
98
113
|
|
|
114
|
+
yield(result, child) if block_given?
|
|
115
|
+
|
|
99
116
|
# Integer-byte check avoids allocating substrings for the
|
|
100
117
|
# per-child adjacency probe. EMPHASIS_DELIMITER_BYTES.include?
|
|
101
|
-
# over a 4-element Set is O(1).
|
|
102
|
-
|
|
118
|
+
# over a 4-element Set is O(1). On an empty buffer getbyte
|
|
119
|
+
# returns nil, which matches no byte of a non-empty part, so
|
|
120
|
+
# the first child needs no extra guard.
|
|
121
|
+
if (last_byte = result.getbyte(-1)) == part.getbyte(0) &&
|
|
103
122
|
EMPHASIS_DELIMITER_BYTES.include?(last_byte)
|
|
104
123
|
result << EMPHASIS_BOUNDARY
|
|
105
124
|
end
|
|
@@ -168,7 +187,7 @@ module Markbridge
|
|
|
168
187
|
# parses the content as Markdown before the closing tags reopen another
|
|
169
188
|
# HTML block.
|
|
170
189
|
def render_markdown_text(node, context)
|
|
171
|
-
context.html_mode? ?
|
|
190
|
+
context.html_mode? ? HtmlBlock.island(node.text) : node.text
|
|
172
191
|
end
|
|
173
192
|
|
|
174
193
|
def render_text(node, context)
|
|
@@ -26,8 +26,10 @@ module Markbridge
|
|
|
26
26
|
@renderer.render_default(node, context:)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
# An optional block runs at every join point, before a non-empty
|
|
30
|
+
# child output is appended — see Renderer#render_children.
|
|
31
|
+
def render_children(element, context: @context, &block)
|
|
32
|
+
@renderer.render_children(element, context:, &block)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# Context operations
|
|
@@ -20,10 +20,30 @@ module Markbridge
|
|
|
20
20
|
|
|
21
21
|
return content unless ALLOWED_ALIGNMENTS.include?(element.alignment)
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
|
|
23
|
+
return html_block_form(element, content) if interface.html_mode?
|
|
24
|
+
|
|
25
|
+
# Keeps consecutive aligned blocks from merging.
|
|
26
|
+
"\n\n#{markdown_island_form(element, content)}\n\n"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Children already render as raw HTML here, and a blank line
|
|
32
|
+
# would terminate the enclosing block (e.g. a <table>).
|
|
33
|
+
def html_block_form(element, content)
|
|
34
|
+
%(<div align="#{element.alignment}">#{content}</div>)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A `<div>` opens an HTML block (CommonMark §4.6), and Markdown
|
|
38
|
+
# inside one is only parsed across blank lines. Without them a
|
|
39
|
+
# link in the content shows up as its own source text. The blank
|
|
40
|
+
# lines do mean CommonMark wraps the content in a `<p>`, so
|
|
41
|
+
# inline content picks up a paragraph margin.
|
|
42
|
+
def markdown_island_form(element, content)
|
|
43
|
+
open_tag = %(<div align="#{element.alignment}">)
|
|
44
|
+
return "#{open_tag}</div>" if content.match?(/\A\s*\z/)
|
|
45
|
+
|
|
46
|
+
"#{open_tag}#{HtmlBlock.island(content)}</div>"
|
|
27
47
|
end
|
|
28
48
|
end
|
|
29
49
|
end
|
|
@@ -4,7 +4,13 @@ module Markbridge
|
|
|
4
4
|
module Renderers
|
|
5
5
|
module Discourse
|
|
6
6
|
module Tags
|
|
7
|
-
# Tag for rendering list items
|
|
7
|
+
# Tag for rendering list items.
|
|
8
|
+
#
|
|
9
|
+
# An item does not indent itself. It renders its content at
|
|
10
|
+
# column zero and hands it to the builder, which moves the whole
|
|
11
|
+
# content right by the width of the marker. A nested list then
|
|
12
|
+
# lands at the content column of the item that holds it, whatever
|
|
13
|
+
# the nesting depth is.
|
|
8
14
|
class ListItemTag < Tag
|
|
9
15
|
def initialize
|
|
10
16
|
@builder = Builders::ListItemBuilder.new
|
|
@@ -12,50 +18,49 @@ module Markbridge
|
|
|
12
18
|
|
|
13
19
|
def render(element, interface)
|
|
14
20
|
child_context = interface.with_parent(element)
|
|
15
|
-
|
|
16
|
-
return "" if content.empty?
|
|
21
|
+
return render_html(element, interface, child_context) if interface.html_mode?
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
content = render_content(element, interface, child_context)
|
|
24
|
+
return "" if content.empty?
|
|
19
25
|
|
|
20
26
|
parent_list = interface.find_parent(AST::List)
|
|
21
|
-
@builder.build(
|
|
22
|
-
content,
|
|
23
|
-
marker: determine_marker(parent_list),
|
|
24
|
-
indent: calculate_indent(interface),
|
|
25
|
-
)
|
|
27
|
+
@builder.build(content, marker: determine_marker(parent_list))
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
|
-
# @param parent_list [AST::List, nil]
|
|
31
32
|
# @return [String]
|
|
32
|
-
def
|
|
33
|
-
|
|
33
|
+
def render_html(element, interface, child_context)
|
|
34
|
+
content = interface.render_children(element, context: child_context).strip
|
|
35
|
+
content.empty? ? "" : "<li>#{content}</li>"
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
38
|
+
# A nested list is attached directly to the content in front of
|
|
39
|
+
# it. A blank line there would make the whole list loose, and
|
|
40
|
+
# CommonMark then wraps every item in a <p>. Blank lines inside
|
|
41
|
+
# the output of a child are never touched.
|
|
40
42
|
# @return [String]
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
def render_content(element, interface, child_context)
|
|
44
|
+
content =
|
|
45
|
+
interface.render_children(element, context: child_context) do |buffer, child|
|
|
46
|
+
tighten(buffer) if child.is_a?(AST::List)
|
|
47
|
+
end
|
|
48
|
+
content.strip
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Drops the blank lines in front of a nested list, but keeps one
|
|
52
|
+
# when the line before it opens an HTML block (for example a
|
|
53
|
+
# `</table>`), because only a blank line ends such a block.
|
|
54
|
+
def tighten(buffer)
|
|
55
|
+
buffer.rstrip!
|
|
56
|
+
last_line = buffer[(buffer.rindex("\n") || -1) + 1..]
|
|
57
|
+
buffer << "\n" if HtmlBlock.opens?(last_line)
|
|
58
|
+
end
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
58
|
-
indent
|
|
60
|
+
# @param parent_list [AST::List, nil]
|
|
61
|
+
# @return [String]
|
|
62
|
+
def determine_marker(parent_list)
|
|
63
|
+
parent_list&.ordered? ? "1. " : "- "
|
|
59
64
|
end
|
|
60
65
|
end
|
|
61
66
|
end
|
|
@@ -7,7 +7,7 @@ require_relative "discourse/rendering_interface"
|
|
|
7
7
|
require_relative "discourse/markdown_escaper"
|
|
8
8
|
require_relative "discourse/identity_escaper"
|
|
9
9
|
require_relative "discourse/html_escaper"
|
|
10
|
-
require_relative "discourse/
|
|
10
|
+
require_relative "discourse/html_block"
|
|
11
11
|
require_relative "discourse/postprocessor"
|
|
12
12
|
|
|
13
13
|
# Builders
|
data/lib/markbridge/rspec.rb
CHANGED
|
@@ -39,7 +39,7 @@ RSpec.shared_examples "an html_mode safe tag" do
|
|
|
39
39
|
it "renders html_mode output that is safe inside an HTML block" do
|
|
40
40
|
output = tag.render(element, markbridge_html_mode_interface)
|
|
41
41
|
|
|
42
|
-
expect(Markbridge::Renderers::Discourse::
|
|
42
|
+
expect(Markbridge::Renderers::Discourse::HtmlBlock.safe?(output)).to be(true),
|
|
43
43
|
"Expected #{tag.class} to render raw HTML or a \\n\\n-wrapped " \
|
|
44
44
|
"Markdown island in html_mode, got: #{output.inspect}"
|
|
45
45
|
end
|
data/lib/markbridge/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: markbridge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Discourse Team
|
|
@@ -138,7 +138,7 @@ files:
|
|
|
138
138
|
- lib/markbridge/parsers/text_formatter/parser.rb
|
|
139
139
|
- lib/markbridge/renderers/discourse.rb
|
|
140
140
|
- lib/markbridge/renderers/discourse/builders/list_item_builder.rb
|
|
141
|
-
- lib/markbridge/renderers/discourse/
|
|
141
|
+
- lib/markbridge/renderers/discourse/html_block.rb
|
|
142
142
|
- lib/markbridge/renderers/discourse/html_escaper.rb
|
|
143
143
|
- lib/markbridge/renderers/discourse/identity_escaper.rb
|
|
144
144
|
- lib/markbridge/renderers/discourse/markdown_escaper.rb
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Markbridge
|
|
4
|
-
module Renderers
|
|
5
|
-
module Discourse
|
|
6
|
-
# Decides whether a rendered fragment is safe to splice into a
|
|
7
|
-
# CommonMark HTML block (spec §4.6). Inside such a block the
|
|
8
|
-
# content passes through as raw HTML; Markdown is only parsed
|
|
9
|
-
# again across blank lines. Safe output is therefore: a raw HTML
|
|
10
|
-
# or plain-text fragment without Markdown sigils, or a
|
|
11
|
-
# +\n\n…\n\n+ wrap — a deliberate Markdown island.
|
|
12
|
-
#
|
|
13
|
-
# Used by the html_mode contract check that ships in
|
|
14
|
-
# +markbridge/rspec+ and by this repo's own contract spec.
|
|
15
|
-
module HtmlBlockSafety
|
|
16
|
-
# Markdown sigils that would surface as literal text inside an
|
|
17
|
-
# HTML block: emphasis (`*`, `_`, `~`) and link middles (`](`).
|
|
18
|
-
MARKDOWN_SIGILS = /[*_~]|\]\(/
|
|
19
|
-
private_constant :MARKDOWN_SIGILS
|
|
20
|
-
|
|
21
|
-
# @param output [String] a tag's html_mode render result
|
|
22
|
-
# @return [Boolean]
|
|
23
|
-
def self.safe?(output)
|
|
24
|
-
return true if output.start_with?("\n\n") && output.end_with?("\n\n")
|
|
25
|
-
|
|
26
|
-
!output.match?(MARKDOWN_SIGILS)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|