qiita-markdown 0.43.0 → 0.44.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +14 -200
- data/CHANGELOG.md +10 -0
- data/README.md +2 -2
- data/benchmark/heading_anchor_rendering.rb +1 -1
- data/lib/qiita/markdown/embed/figma.rb +11 -0
- data/lib/qiita/markdown/filters/checkbox.rb +1 -1
- data/lib/qiita/markdown/filters/code_block.rb +13 -13
- data/lib/qiita/markdown/filters/custom_block.rb +1 -0
- data/lib/qiita/markdown/filters/external_link.rb +2 -0
- data/lib/qiita/markdown/filters/final_sanitizer.rb +118 -117
- data/lib/qiita/markdown/filters/footnote.rb +2 -0
- data/lib/qiita/markdown/filters/group_mention.rb +2 -2
- data/lib/qiita/markdown/filters/image_link.rb +6 -6
- data/lib/qiita/markdown/filters/inline_code_color.rb +8 -8
- data/lib/qiita/markdown/filters/mention.rb +11 -9
- data/lib/qiita/markdown/filters/simplify.rb +1 -0
- data/lib/qiita/markdown/filters/syntax_highlight.rb +4 -4
- data/lib/qiita/markdown/filters/truncate.rb +1 -3
- data/lib/qiita/markdown/filters/user_input_sanitizer.rb +26 -23
- data/lib/qiita/markdown/greenmat/heading_rendering.rb +2 -2
- data/lib/qiita/markdown/greenmat/html_toc_renderer.rb +1 -1
- data/lib/qiita/markdown/transformers/filter_attributes.rb +1 -0
- data/lib/qiita/markdown/transformers/filter_iframe.rb +2 -2
- data/lib/qiita/markdown/transformers/filter_script.rb +1 -1
- data/lib/qiita/markdown/transformers/strip_invalid_node.rb +1 -3
- data/lib/qiita/markdown/version.rb +1 -1
- data/lib/qiita/markdown.rb +1 -0
- data/qiita-markdown.gemspec +6 -6
- data/spec/qiita/markdown/filters/checkbox_spec.rb +42 -0
- data/spec/qiita/markdown/processor_spec.rb +54 -16
- data/spec/qiita/markdown/summary_processor_spec.rb +2 -2
- metadata +96 -99
@@ -12,41 +12,41 @@ module Qiita
|
|
12
12
|
class FinalSanitizer < HTML::Pipeline::Filter
|
13
13
|
RULE = {
|
14
14
|
attributes: {
|
15
|
-
"a" => [
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
"a" => %w[
|
16
|
+
data-hovercard-target-name
|
17
|
+
data-hovercard-target-type
|
18
|
+
href
|
19
|
+
rel
|
20
20
|
],
|
21
21
|
"blockquote" => Embed::Tweet::ATTRIBUTES,
|
22
|
-
"iframe" => [
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
22
|
+
"iframe" => %w[
|
23
|
+
allowfullscreen
|
24
|
+
frameborder
|
25
|
+
height
|
26
|
+
marginheight
|
27
|
+
marginwidth
|
28
|
+
scrolling
|
29
|
+
src
|
30
|
+
style
|
31
|
+
width
|
32
32
|
],
|
33
33
|
"img" => [
|
34
34
|
"src",
|
35
35
|
],
|
36
|
-
"input" => [
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
"input" => %w[
|
37
|
+
checked
|
38
|
+
disabled
|
39
|
+
type
|
40
40
|
],
|
41
|
-
"div" => [
|
42
|
-
|
43
|
-
|
41
|
+
"div" => %w[
|
42
|
+
itemscope
|
43
|
+
itemtype
|
44
44
|
],
|
45
45
|
"p" => Embed::CodePen::ATTRIBUTES,
|
46
|
-
"script" => [
|
47
|
-
|
48
|
-
|
49
|
-
|
46
|
+
"script" => %w[
|
47
|
+
async
|
48
|
+
src
|
49
|
+
type
|
50
50
|
].concat(
|
51
51
|
Embed::SpeekerDeck::ATTRIBUTES,
|
52
52
|
Embed::Docswell::ATTRIBUTES,
|
@@ -60,103 +60,104 @@ module Qiita
|
|
60
60
|
"th" => [
|
61
61
|
"style",
|
62
62
|
],
|
63
|
-
"video" => [
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
63
|
+
"video" => %w[
|
64
|
+
src
|
65
|
+
autoplay
|
66
|
+
controls
|
67
|
+
loop
|
68
|
+
muted
|
69
|
+
poster
|
70
70
|
],
|
71
|
-
all: [
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
71
|
+
all: %w[
|
72
|
+
abbr
|
73
|
+
align
|
74
|
+
alt
|
75
|
+
border
|
76
|
+
cellpadding
|
77
|
+
cellspacing
|
78
|
+
cite
|
79
|
+
class
|
80
|
+
color
|
81
|
+
cols
|
82
|
+
colspan
|
83
|
+
data-lang
|
84
|
+
datetime
|
85
|
+
height
|
86
|
+
hreflang
|
87
|
+
id
|
88
|
+
itemprop
|
89
|
+
lang
|
90
|
+
name
|
91
|
+
rowspan
|
92
|
+
tabindex
|
93
|
+
target
|
94
|
+
title
|
95
|
+
width
|
96
96
|
],
|
97
97
|
},
|
98
98
|
css: {
|
99
|
-
properties: [
|
100
|
-
|
101
|
-
|
99
|
+
properties: %w[
|
100
|
+
background-color
|
101
|
+
border
|
102
|
+
text-align
|
102
103
|
],
|
103
104
|
},
|
104
|
-
elements: [
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
105
|
+
elements: %w[
|
106
|
+
a
|
107
|
+
b
|
108
|
+
blockquote
|
109
|
+
br
|
110
|
+
code
|
111
|
+
dd
|
112
|
+
del
|
113
|
+
details
|
114
|
+
div
|
115
|
+
dl
|
116
|
+
dt
|
117
|
+
em
|
118
|
+
font
|
119
|
+
h1
|
120
|
+
h2
|
121
|
+
h3
|
122
|
+
h4
|
123
|
+
h5
|
124
|
+
h6
|
125
|
+
h7
|
126
|
+
h8
|
127
|
+
hr
|
128
|
+
i
|
129
|
+
img
|
130
|
+
input
|
131
|
+
ins
|
132
|
+
kbd
|
133
|
+
li
|
134
|
+
ol
|
135
|
+
p
|
136
|
+
pre
|
137
|
+
q
|
138
|
+
rp
|
139
|
+
rt
|
140
|
+
ruby
|
141
|
+
s
|
142
|
+
samp
|
143
|
+
script
|
144
|
+
iframe
|
145
|
+
span
|
146
|
+
strike
|
147
|
+
strong
|
148
|
+
sub
|
149
|
+
summary
|
150
|
+
sup
|
151
|
+
table
|
152
|
+
tbody
|
153
|
+
td
|
154
|
+
tfoot
|
155
|
+
th
|
156
|
+
thead
|
157
|
+
tr
|
158
|
+
tt
|
159
|
+
ul
|
160
|
+
var
|
160
161
|
],
|
161
162
|
protocols: {
|
162
163
|
"a" => {
|
@@ -6,6 +6,7 @@ module Qiita
|
|
6
6
|
doc.search("sup > a").each do |a|
|
7
7
|
footnote = find_footnote(a)
|
8
8
|
next unless footnote
|
9
|
+
|
9
10
|
a[:title] = footnote.text.gsub(/\A\n/, "").gsub(/ ↩\n\z/, "")
|
10
11
|
end
|
11
12
|
doc
|
@@ -16,6 +17,7 @@ module Qiita
|
|
16
17
|
def find_footnote(a)
|
17
18
|
href = a["href"]
|
18
19
|
return nil if !href || href.match(/\A#fn\d+\z/).nil?
|
20
|
+
|
19
21
|
doc.search(href).first
|
20
22
|
end
|
21
23
|
end
|
@@ -4,12 +4,12 @@ module Qiita
|
|
4
4
|
class ImageLink < HTML::Pipeline::Filter
|
5
5
|
def call
|
6
6
|
doc.search("img").each do |img|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
next if img.ancestors.any? { |ancestor| ancestor.name == "a" }
|
8
|
+
|
9
|
+
outer = Nokogiri::HTML.fragment(%(<a href="#{img['src']}" target="_blank"></a>))
|
10
|
+
inner = img.clone
|
11
|
+
outer.at("a").add_child(inner)
|
12
|
+
img.replace(outer)
|
13
13
|
end
|
14
14
|
doc
|
15
15
|
end
|
@@ -6,15 +6,15 @@ module Qiita
|
|
6
6
|
|
7
7
|
REGEXPS = Regexp.union(
|
8
8
|
/\#(?:\h{3}|\h{6})/,
|
9
|
-
/rgba?\(\s*(?:\d+(
|
10
|
-
/rgba?\(\s*(?:\d+%(
|
11
|
-
/rgba?\(\s*(?:\d
|
12
|
-
|
13
|
-
|
14
|
-
/hsla?\(\s*\d+(?:deg|rad|grad|turn)
|
9
|
+
/rgba?\(\s*(?:\d+(?:,|\s)\s*){2}\d+\s*\)/,
|
10
|
+
/rgba?\(\s*(?:\d+%(?:,|\s)\s*){2}\d+%\s*\)/,
|
11
|
+
/rgba?\(\s*(?:\d+,\s*){3}\d*\.?\d+%?\s*\)/,
|
12
|
+
%r{rgba?\(\s*(?:\d+\s*){2}\d+\s*/\s*\d?\.?\d+%?\s*\)},
|
13
|
+
%r{rgba?\(\s*(?:\d+%\s*){2}\d+%\s*/\s*\d?\.?\d+%?\s*\)},
|
14
|
+
/hsla?\(\s*\d+(?:deg|rad|grad|turn)?,\s*\d+%,\s*\d+%\s*\)/,
|
15
15
|
/hsla?\(\s*\d+(?:deg|rad|grad|turn)?\s+\d+%\s+\d+%\s*\)/,
|
16
|
-
/hsla?\(\s*\d+(?:deg|rad|grad|turn)
|
17
|
-
|
16
|
+
/hsla?\(\s*\d+(?:deg|rad|grad|turn)?,\s*(?:\d+%,\s*){2}\d?\.?\d+%?\s*\)/,
|
17
|
+
%r{hsla?\(\s*\d+(?:deg|rad|grad|turn)?\s+\d+%\s+\d+%\s*/\s*\d?\.?\d+%?\s*\)},
|
18
18
|
)
|
19
19
|
|
20
20
|
COLOR_CODE_PATTERN = /\A\s*(#{REGEXPS})\s*\z/
|
@@ -8,17 +8,17 @@ module Qiita
|
|
8
8
|
class Mention < HTML::Pipeline::MentionFilter
|
9
9
|
IGNORE_PARENTS = ::HTML::Pipeline::MentionFilter::IGNORE_PARENTS + Set["blockquote"]
|
10
10
|
|
11
|
-
MentionPattern =
|
11
|
+
MentionPattern = %r{
|
12
12
|
(?:^|\W)
|
13
|
-
@((
|
14
|
-
(
|
13
|
+
@((?>\w[\w-]{0,30}\w(?:@github)?))
|
14
|
+
(?!/)
|
15
15
|
(?=
|
16
16
|
\.+[ \t\W]|
|
17
17
|
\.+$|
|
18
18
|
[^0-9a-zA-Z_.]|
|
19
19
|
$
|
20
20
|
)
|
21
|
-
|
21
|
+
}ix
|
22
22
|
|
23
23
|
# @note Override to use another IGNORE_PARENTS
|
24
24
|
def call
|
@@ -28,8 +28,10 @@ module Qiita
|
|
28
28
|
content = node.to_html
|
29
29
|
next unless content.include?("@")
|
30
30
|
next if has_ancestor?(node, IGNORE_PARENTS)
|
31
|
+
|
31
32
|
html = mention_link_filter(content, base_url, info_url, username_pattern)
|
32
33
|
next if html == content
|
34
|
+
|
33
35
|
node.replace(html)
|
34
36
|
end
|
35
37
|
doc
|
@@ -38,22 +40,22 @@ module Qiita
|
|
38
40
|
# @note Override to use customized MentionPattern and allowed_usernames logic.
|
39
41
|
def mention_link_filter(text, _, _, _)
|
40
42
|
text.gsub(MentionPattern) do |match|
|
41
|
-
name =
|
43
|
+
name = ::Regexp.last_match(1)
|
42
44
|
case
|
43
45
|
when allowed_usernames && name == "all"
|
44
46
|
result[:mentioned_usernames] |= allowed_usernames
|
45
47
|
match.sub(
|
46
48
|
"@#{name}",
|
47
|
-
%
|
49
|
+
%(<a href="/" class="user-mention" title="#{name}">@#{name}</a>),
|
48
50
|
)
|
49
|
-
when allowed_usernames && !allowed_usernames.include?(name) || name == "all"
|
51
|
+
when (allowed_usernames && !allowed_usernames.include?(name)) || name == "all"
|
50
52
|
match
|
51
53
|
else
|
52
54
|
result[:mentioned_usernames] |= [name]
|
53
55
|
url = File.join(base_url, name)
|
54
56
|
match.sub(
|
55
57
|
"@#{name}",
|
56
|
-
%
|
58
|
+
%(<a href="#{url}" class="user-mention js-hovercard" title="#{name}" data-hovercard-target-type="user" data-hovercard-target-name="#{name}">@#{name}</a>),
|
57
59
|
)
|
58
60
|
end
|
59
61
|
end
|
@@ -66,7 +68,7 @@ module Qiita
|
|
66
68
|
end
|
67
69
|
|
68
70
|
def has_ancestor?(node, tags)
|
69
|
-
super || node.parent.parent && node.parent.parent["class"] == "code-lang"
|
71
|
+
super || (node.parent.parent && node.parent.parent["class"] == "code-lang")
|
70
72
|
end
|
71
73
|
end
|
72
74
|
end
|
@@ -54,7 +54,7 @@ module Qiita
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def call
|
57
|
-
outer = Nokogiri::HTML.fragment(%
|
57
|
+
outer = Nokogiri::HTML.fragment(%(<div class="code-frame" data-lang="#{language}">))
|
58
58
|
frame = outer.at("div")
|
59
59
|
frame.add_child(filename_node) if filename
|
60
60
|
frame.add_child(highlighted_node)
|
@@ -72,7 +72,7 @@ module Qiita
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def filename_node
|
75
|
-
%
|
75
|
+
%(<div class="code-lang"><span class="bold">#{filename}</span></div>)
|
76
76
|
end
|
77
77
|
|
78
78
|
def has_inline_php?
|
@@ -87,7 +87,7 @@ module Qiita
|
|
87
87
|
if specific_language && Rouge::Lexer.find(specific_language)
|
88
88
|
begin
|
89
89
|
highlight(specific_language).presence or raise
|
90
|
-
rescue
|
90
|
+
rescue StandardError
|
91
91
|
highlight(@default_language)
|
92
92
|
end
|
93
93
|
else
|
@@ -100,7 +100,7 @@ module Qiita
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def language_node
|
103
|
-
Nokogiri::HTML.fragment(%
|
103
|
+
Nokogiri::HTML.fragment(%(<div class="code-frame" data-lang="#{language}"></div>))
|
104
104
|
end
|
105
105
|
|
106
106
|
def specific_language
|
@@ -51,9 +51,7 @@ module Qiita
|
|
51
51
|
node.content.each_char.with_index do |char, index|
|
52
52
|
current_char_is_blank = char.strip.empty?
|
53
53
|
|
54
|
-
if !@previous_char_was_blank || !current_char_is_blank
|
55
|
-
@current_length += 1
|
56
|
-
end
|
54
|
+
@current_length += 1 if !@previous_char_was_blank || !current_char_is_blank
|
57
55
|
|
58
56
|
@previous_char_was_blank = current_char_is_blank
|
59
57
|
|
@@ -10,24 +10,24 @@ module Qiita
|
|
10
10
|
summary sup table tbody td tfoot th thead tr ul var
|
11
11
|
],
|
12
12
|
attributes: {
|
13
|
-
"a"
|
13
|
+
"a" => %w[class href rel title],
|
14
14
|
"blockquote" => %w[cite] + Embed::Tweet::ATTRIBUTES,
|
15
|
-
"code"
|
16
|
-
"div"
|
17
|
-
"font"
|
18
|
-
"h1"
|
19
|
-
"h2"
|
20
|
-
"h3"
|
21
|
-
"h4"
|
22
|
-
"h5"
|
23
|
-
"h6"
|
24
|
-
"img"
|
25
|
-
"ins"
|
26
|
-
"li"
|
27
|
-
"p"
|
28
|
-
"q"
|
29
|
-
"script"
|
30
|
-
"iframe"
|
15
|
+
"code" => %w[data-metadata],
|
16
|
+
"div" => %w[class data-type data-metadata],
|
17
|
+
"font" => %w[color],
|
18
|
+
"h1" => %w[id],
|
19
|
+
"h2" => %w[id],
|
20
|
+
"h3" => %w[id],
|
21
|
+
"h4" => %w[id],
|
22
|
+
"h5" => %w[id],
|
23
|
+
"h6" => %w[id],
|
24
|
+
"img" => %w[alt height src title width],
|
25
|
+
"ins" => %w[cite datetime],
|
26
|
+
"li" => %w[id],
|
27
|
+
"p" => Embed::CodePen::ATTRIBUTES,
|
28
|
+
"q" => %w[cite],
|
29
|
+
"script" => %w[async src id].concat(Embed::SpeekerDeck::ATTRIBUTES, Embed::Docswell::ATTRIBUTES),
|
30
|
+
"iframe" => %w[
|
31
31
|
allowfullscreen
|
32
32
|
frameborder
|
33
33
|
height
|
@@ -38,17 +38,20 @@ module Qiita
|
|
38
38
|
style
|
39
39
|
width
|
40
40
|
],
|
41
|
-
"sup"
|
42
|
-
"td"
|
43
|
-
"th"
|
41
|
+
"sup" => %w[id],
|
42
|
+
"td" => %w[colspan rowspan style],
|
43
|
+
"th" => %w[colspan rowspan style],
|
44
44
|
},
|
45
45
|
protocols: {
|
46
|
-
"a"
|
46
|
+
"a" => { "href" => ["http", "https", "mailto", :relative] },
|
47
47
|
"blockquote" => { "cite" => ["http", "https", :relative] },
|
48
|
-
"q"
|
48
|
+
"q" => { "cite" => ["http", "https", :relative] },
|
49
49
|
},
|
50
50
|
css: {
|
51
|
-
properties: %w[
|
51
|
+
properties: %w[
|
52
|
+
text-align
|
53
|
+
border
|
54
|
+
],
|
52
55
|
},
|
53
56
|
transformers: [
|
54
57
|
Transformers::FilterAttributes,
|
@@ -2,14 +2,14 @@ module Qiita
|
|
2
2
|
module Markdown
|
3
3
|
module Transformers
|
4
4
|
class FilterIframe
|
5
|
-
URL_WHITE_LIST = [
|
6
|
-
].flatten.freeze
|
5
|
+
URL_WHITE_LIST = [].flatten.freeze
|
7
6
|
|
8
7
|
HOST_WHITE_LIST = [
|
9
8
|
Embed::Youtube::SCRIPT_HOSTS,
|
10
9
|
Embed::SlideShare::SCRIPT_HOST,
|
11
10
|
Embed::GoogleSlide::SCRIPT_HOST,
|
12
11
|
Embed::Docswell::SCRIPT_HOSTS,
|
12
|
+
Embed::Figma::SCRIPT_HOST,
|
13
13
|
].flatten.freeze
|
14
14
|
|
15
15
|
def self.call(**args)
|
@@ -45,7 +45,7 @@ module Qiita
|
|
45
45
|
def host_of(url)
|
46
46
|
if url
|
47
47
|
scheme = URI.parse(url).scheme
|
48
|
-
Addressable::URI.parse(url).host if [
|
48
|
+
Addressable::URI.parse(url).host if %w[http https].include? scheme
|
49
49
|
end
|
50
50
|
rescue Addressable::URI::InvalidURIError, URI::InvalidURIError
|
51
51
|
nil
|
data/lib/qiita/markdown.rb
CHANGED
@@ -15,6 +15,7 @@ require "qiita/markdown/embed/slide_share"
|
|
15
15
|
require "qiita/markdown/embed/google_slide"
|
16
16
|
require "qiita/markdown/embed/speeker_deck"
|
17
17
|
require "qiita/markdown/embed/docswell"
|
18
|
+
require "qiita/markdown/embed/figma"
|
18
19
|
require "qiita/markdown/transformers/filter_attributes"
|
19
20
|
require "qiita/markdown/transformers/filter_script"
|
20
21
|
require "qiita/markdown/transformers/filter_iframe"
|