qiita-markdown 0.21.0 → 0.22.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.
Potentially problematic release.
This version of qiita-markdown might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +1 -0
- data/CHANGELOG.md +4 -0
- data/lib/qiita/markdown.rb +4 -0
- data/lib/qiita/markdown/code_pen.rb +8 -0
- data/lib/qiita/markdown/filters/final_sanitizer.rb +6 -43
- data/lib/qiita/markdown/filters/user_input_sanitizer.rb +6 -59
- data/lib/qiita/markdown/transformers/filter_attributes.rb +63 -0
- data/lib/qiita/markdown/transformers/filter_script.rb +40 -0
- data/lib/qiita/markdown/transformers/strip_invalid_node.rb +44 -0
- data/lib/qiita/markdown/version.rb +1 -1
- data/spec/qiita/markdown/processor_spec.rb +76 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff3cf2974ec3776febf776a8509ea134b7e6b94d
|
4
|
+
data.tar.gz: 16d059b677b0cf622be0d3c85e4ad9e293ffcdef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab3e0be1351d8872fc3cc8d75502a3f8bd95f24a9870af84c164676b52e84f0ee34a72f7321db28c3889d133d167ec14c1ef32d0de9da3a918b0f4cdc4c43496
|
7
|
+
data.tar.gz: b91800c0d2282cc001d7fa9df45f1688f721c47d82ab5a01b62e656a10f2ccb7d8a18901076261bbc0ef88945ba87f042fd9cc54001b2638def2ebbfa83fe47e
|
data/.rubocop_todo.yml
CHANGED
@@ -57,6 +57,7 @@ Style/MutableConstant:
|
|
57
57
|
- 'lib/qiita/markdown/filters/sanitize.rb'
|
58
58
|
- 'lib/qiita/markdown/filters/simplify.rb'
|
59
59
|
- 'lib/qiita/markdown/filters/syntax_highlight.rb'
|
60
|
+
- 'lib/qiita/markdown/code_pen.rb'
|
60
61
|
- 'lib/qiita/markdown/processor.rb'
|
61
62
|
- 'lib/qiita/markdown/summary_processor.rb'
|
62
63
|
- 'lib/qiita/markdown/version.rb'
|
data/CHANGELOG.md
CHANGED
data/lib/qiita/markdown.rb
CHANGED
@@ -7,6 +7,10 @@ require "nokogiri"
|
|
7
7
|
require "pygments"
|
8
8
|
require "sanitize"
|
9
9
|
|
10
|
+
require "qiita/markdown/code_pen"
|
11
|
+
require "qiita/markdown/transformers/filter_attributes"
|
12
|
+
require "qiita/markdown/transformers/filter_script"
|
13
|
+
require "qiita/markdown/transformers/strip_invalid_node"
|
10
14
|
require "qiita/markdown/filters/checkbox"
|
11
15
|
require "qiita/markdown/filters/code_block"
|
12
16
|
require "qiita/markdown/filters/emoji"
|
@@ -10,45 +10,6 @@ module Qiita
|
|
10
10
|
#
|
11
11
|
# @see Qiita::Markdown::Filters::UserInputSanitizerr
|
12
12
|
class FinalSanitizer < HTML::Pipeline::Filter
|
13
|
-
# Wraps a node env to transform invalid node.
|
14
|
-
class TransformableNode
|
15
|
-
def self.call(*args)
|
16
|
-
new(*args).transform
|
17
|
-
end
|
18
|
-
|
19
|
-
def initialize(env)
|
20
|
-
@env = env
|
21
|
-
end
|
22
|
-
|
23
|
-
def transform
|
24
|
-
if has_invalid_list_node? || has_invalid_table_node?
|
25
|
-
node.replace(node.children)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def has_invalid_list_node?
|
32
|
-
name == "li" && node.ancestors.none? do |ancestor|
|
33
|
-
%w[ol ul].include?(ancestor.name)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def has_invalid_table_node?
|
38
|
-
%w[thead tbody tfoot tr td th].include?(name) && node.ancestors.none? do |ancestor|
|
39
|
-
ancestor.name == "table"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def name
|
44
|
-
@env[:node_name]
|
45
|
-
end
|
46
|
-
|
47
|
-
def node
|
48
|
-
@env[:node]
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
13
|
RULE = {
|
53
14
|
attributes: {
|
54
15
|
"a" => [
|
@@ -80,6 +41,7 @@ module Qiita
|
|
80
41
|
"itemscope",
|
81
42
|
"itemtype",
|
82
43
|
],
|
44
|
+
"p" => CodePen::ATTRIBUTES,
|
83
45
|
"script" => [
|
84
46
|
"async",
|
85
47
|
"src",
|
@@ -173,6 +135,7 @@ module Qiita
|
|
173
135
|
"ruby",
|
174
136
|
"s",
|
175
137
|
"samp",
|
138
|
+
"script",
|
176
139
|
"span",
|
177
140
|
"strike",
|
178
141
|
"strong",
|
@@ -219,17 +182,17 @@ module Qiita
|
|
219
182
|
],
|
220
183
|
},
|
221
184
|
},
|
222
|
-
|
223
|
-
|
185
|
+
transformers: [
|
186
|
+
Transformers::StripInvalidNode,
|
187
|
+
Transformers::FilterScript,
|
224
188
|
],
|
225
|
-
transformers: TransformableNode,
|
226
189
|
}.freeze
|
227
190
|
|
228
191
|
SCRIPTABLE_RULE = RULE.dup.tap do |rule|
|
229
192
|
rule[:attributes] = RULE[:attributes].dup
|
230
193
|
rule[:attributes][:all] = rule[:attributes][:all] + [:data]
|
231
194
|
rule[:elements] = RULE[:elements] + ["iframe", "script", "video"]
|
232
|
-
rule[:
|
195
|
+
rule[:transformers] = rule[:transformers] - [Transformers::FilterScript]
|
233
196
|
end
|
234
197
|
|
235
198
|
def call
|
@@ -3,65 +3,10 @@ module Qiita
|
|
3
3
|
module Filters
|
4
4
|
# Sanitizes user input if :strict context is given.
|
5
5
|
class UserInputSanitizer < HTML::Pipeline::Filter
|
6
|
-
class AttributeFilter
|
7
|
-
FILTERS = {
|
8
|
-
"a" => {
|
9
|
-
"class" => %w[autolink],
|
10
|
-
"rel" => %w[footnote url],
|
11
|
-
"rev" => %w[footnote],
|
12
|
-
},
|
13
|
-
"blockquote" => {
|
14
|
-
"class" => %w[twitter-tweet],
|
15
|
-
},
|
16
|
-
"div" => {
|
17
|
-
"class" => %w[footnotes],
|
18
|
-
},
|
19
|
-
"sup" => {
|
20
|
-
"id" => /\Afnref\d+\z/,
|
21
|
-
},
|
22
|
-
"li" => {
|
23
|
-
"id" => /\Afn\d+\z/,
|
24
|
-
},
|
25
|
-
}.freeze
|
26
|
-
|
27
|
-
DELIMITER = " ".freeze
|
28
|
-
|
29
|
-
def self.call(*args)
|
30
|
-
new(*args).transform
|
31
|
-
end
|
32
|
-
|
33
|
-
def initialize(env)
|
34
|
-
@env = env
|
35
|
-
end
|
36
|
-
|
37
|
-
def transform
|
38
|
-
return unless FILTERS.key?(name)
|
39
|
-
FILTERS[name].each_pair do |attr, pattern|
|
40
|
-
filter_attribute(attr, pattern) if node.attributes.key?(attr)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
def filter_attribute(attr, pattern)
|
47
|
-
node[attr] = node[attr].split(DELIMITER).select do |value|
|
48
|
-
pattern.is_a?(Array) ? pattern.include?(value) : (pattern =~ value)
|
49
|
-
end.join(DELIMITER)
|
50
|
-
end
|
51
|
-
|
52
|
-
def name
|
53
|
-
@env[:node_name]
|
54
|
-
end
|
55
|
-
|
56
|
-
def node
|
57
|
-
@env[:node]
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
6
|
RULE = {
|
62
7
|
elements: %w[
|
63
8
|
a b blockquote br code dd del details div dl dt em font h1 h2 h3 h4 h5 h6
|
64
|
-
hr i img ins kbd li ol p pre q rp rt ruby s samp strike strong sub
|
9
|
+
hr i img ins kbd li ol p pre q rp rt ruby s samp script strike strong sub
|
65
10
|
summary sup table tbody td tfoot th thead tr ul var
|
66
11
|
],
|
67
12
|
attributes: {
|
@@ -79,7 +24,9 @@ module Qiita
|
|
79
24
|
"img" => %w[alt height src title width],
|
80
25
|
"ins" => %w[cite datetime],
|
81
26
|
"li" => %w[id],
|
27
|
+
"p" => CodePen::ATTRIBUTES,
|
82
28
|
"q" => %w[cite],
|
29
|
+
"script" => %w[async src],
|
83
30
|
"sup" => %w[id],
|
84
31
|
"td" => %w[colspan rowspan style],
|
85
32
|
"th" => %w[colspan rowspan style],
|
@@ -92,10 +39,10 @@ module Qiita
|
|
92
39
|
css: {
|
93
40
|
properties: %w[text-align],
|
94
41
|
},
|
95
|
-
|
96
|
-
|
42
|
+
transformers: [
|
43
|
+
Transformers::FilterAttributes,
|
44
|
+
Transformers::FilterScript,
|
97
45
|
],
|
98
|
-
transformers: AttributeFilter,
|
99
46
|
}.freeze
|
100
47
|
|
101
48
|
def call
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Qiita
|
2
|
+
module Markdown
|
3
|
+
module Transformers
|
4
|
+
class FilterAttributes
|
5
|
+
FILTERS = {
|
6
|
+
"a" => {
|
7
|
+
"class" => %w[autolink],
|
8
|
+
"rel" => %w[footnote url],
|
9
|
+
"rev" => %w[footnote],
|
10
|
+
},
|
11
|
+
"blockquote" => {
|
12
|
+
"class" => %w[twitter-tweet],
|
13
|
+
},
|
14
|
+
"div" => {
|
15
|
+
"class" => %w[footnotes],
|
16
|
+
},
|
17
|
+
"p" => {
|
18
|
+
"class" => %w[codepen],
|
19
|
+
},
|
20
|
+
"sup" => {
|
21
|
+
"id" => /\Afnref\d+\z/,
|
22
|
+
},
|
23
|
+
"li" => {
|
24
|
+
"id" => /\Afn\d+\z/,
|
25
|
+
},
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
DELIMITER = " ".freeze
|
29
|
+
|
30
|
+
def self.call(*args)
|
31
|
+
new(*args).transform
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(env)
|
35
|
+
@env = env
|
36
|
+
end
|
37
|
+
|
38
|
+
def transform
|
39
|
+
return unless FILTERS.key?(name)
|
40
|
+
FILTERS[name].each_pair do |attr, pattern|
|
41
|
+
filter_attribute(attr, pattern) if node.attributes.key?(attr)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def filter_attribute(attr, pattern)
|
48
|
+
node[attr] = node[attr].split(DELIMITER).select do |value|
|
49
|
+
pattern.is_a?(Array) ? pattern.include?(value) : (pattern =~ value)
|
50
|
+
end.join(DELIMITER)
|
51
|
+
end
|
52
|
+
|
53
|
+
def name
|
54
|
+
@env[:node_name]
|
55
|
+
end
|
56
|
+
|
57
|
+
def node
|
58
|
+
@env[:node]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Qiita
|
2
|
+
module Markdown
|
3
|
+
module Transformers
|
4
|
+
class FilterScript
|
5
|
+
WHITE_LIST = [
|
6
|
+
CodePen::SCRIPT_URL,
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
def self.call(*args)
|
10
|
+
new(*args).transform
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(env)
|
14
|
+
@env = env
|
15
|
+
end
|
16
|
+
|
17
|
+
def transform
|
18
|
+
if name == "script"
|
19
|
+
if WHITE_LIST.include?(node["src"])
|
20
|
+
node["async"] = "async" unless node.attributes.key?("async")
|
21
|
+
node.children.unlink
|
22
|
+
else
|
23
|
+
node.unlink
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def name
|
31
|
+
@env[:node_name]
|
32
|
+
end
|
33
|
+
|
34
|
+
def node
|
35
|
+
@env[:node]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Qiita
|
2
|
+
module Markdown
|
3
|
+
module Transformers
|
4
|
+
# Wraps a node env to transform invalid node.
|
5
|
+
class StripInvalidNode
|
6
|
+
def self.call(*args)
|
7
|
+
new(*args).transform
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(env)
|
11
|
+
@env = env
|
12
|
+
end
|
13
|
+
|
14
|
+
def transform
|
15
|
+
if has_invalid_list_node? || has_invalid_table_node?
|
16
|
+
node.replace(node.children)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def has_invalid_list_node?
|
23
|
+
name == "li" && node.ancestors.none? do |ancestor|
|
24
|
+
%w[ol ul].include?(ancestor.name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def has_invalid_table_node?
|
29
|
+
%w[thead tbody tfoot tr td th].include?(name) && node.ancestors.none? do |ancestor|
|
30
|
+
ancestor.name == "table"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def name
|
35
|
+
@env[:node_name]
|
36
|
+
end
|
37
|
+
|
38
|
+
def node
|
39
|
+
@env[:node]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -1140,7 +1140,7 @@ describe Qiita::Markdown::Processor do
|
|
1140
1140
|
end
|
1141
1141
|
|
1142
1142
|
shared_examples_for "data-attributes" do |allowed:|
|
1143
|
-
context "with data-attributes" do
|
1143
|
+
context "with data-attributes for general tags" do
|
1144
1144
|
let(:markdown) do
|
1145
1145
|
<<-EOS.strip_heredoc
|
1146
1146
|
<div data-a="b"></div>
|
@@ -1161,6 +1161,28 @@ describe Qiita::Markdown::Processor do
|
|
1161
1161
|
end
|
1162
1162
|
end
|
1163
1163
|
end
|
1164
|
+
|
1165
|
+
context "with data-attributes for <p> tag" do
|
1166
|
+
let(:markdown) do
|
1167
|
+
<<-EOS.strip_heredoc
|
1168
|
+
<p data-slug-hash="a" data-malicious="b"></p>
|
1169
|
+
EOS
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
if allowed
|
1173
|
+
it "does not sanitize data-attributes" do
|
1174
|
+
should eq <<-EOS.strip_heredoc
|
1175
|
+
<p data-slug-hash="a" data-malicious="b"></p>
|
1176
|
+
EOS
|
1177
|
+
end
|
1178
|
+
else
|
1179
|
+
it "sanitizes data-attributes except the attributes used by codepen" do
|
1180
|
+
should eq <<-EOS.strip_heredoc
|
1181
|
+
<p data-slug-hash="a"></p>
|
1182
|
+
EOS
|
1183
|
+
end
|
1184
|
+
end
|
1185
|
+
end
|
1164
1186
|
end
|
1165
1187
|
|
1166
1188
|
shared_examples_for "class attribute" do |allowed:|
|
@@ -1248,6 +1270,28 @@ describe Qiita::Markdown::Processor do
|
|
1248
1270
|
end
|
1249
1271
|
end
|
1250
1272
|
end
|
1273
|
+
|
1274
|
+
context "with class attribute for <p> tag" do
|
1275
|
+
let(:markdown) do
|
1276
|
+
<<-EOS.strip_heredoc
|
1277
|
+
<p class="codepen malicious-class">foo</p>
|
1278
|
+
EOS
|
1279
|
+
end
|
1280
|
+
|
1281
|
+
if allowed
|
1282
|
+
it "does not sanitize the classes" do
|
1283
|
+
should eq <<-EOS.strip_heredoc
|
1284
|
+
<p class="codepen malicious-class">foo</p>
|
1285
|
+
EOS
|
1286
|
+
end
|
1287
|
+
else
|
1288
|
+
it "sanitizes classes except `codepen`" do
|
1289
|
+
should eq <<-EOS.strip_heredoc
|
1290
|
+
<p class="codepen">foo</p>
|
1291
|
+
EOS
|
1292
|
+
end
|
1293
|
+
end
|
1294
|
+
end
|
1251
1295
|
end
|
1252
1296
|
|
1253
1297
|
shared_examples_for "background-color" do |allowed:|
|
@@ -1268,6 +1312,33 @@ describe Qiita::Markdown::Processor do
|
|
1268
1312
|
end
|
1269
1313
|
end
|
1270
1314
|
|
1315
|
+
shared_examples_for "override codepen attributes" do |allowed:|
|
1316
|
+
context "with HTML embed code for CodePen" do
|
1317
|
+
let(:markdown) do
|
1318
|
+
<<-EOS.strip_heredoc
|
1319
|
+
<p data-height="1" data-theme-id="0" data-slug-hash="foo" data-default-tab="bar" data-user="baz" data-embed-version="2" data-pen-title="qux" class="codepen"></p>
|
1320
|
+
<script src="https://production-assets.codepen.io/assets/embed/ei.js"></script>
|
1321
|
+
EOS
|
1322
|
+
end
|
1323
|
+
|
1324
|
+
if allowed
|
1325
|
+
it "does not sanitize embed code" do
|
1326
|
+
should eq <<-EOS.strip_heredoc
|
1327
|
+
<p data-height="1" data-theme-id="0" data-slug-hash="foo" data-default-tab="bar" data-user="baz" data-embed-version="2" data-pen-title="qux" class="codepen"></p>\n
|
1328
|
+
<script src="https://production-assets.codepen.io/assets/embed/ei.js"></script>
|
1329
|
+
EOS
|
1330
|
+
end
|
1331
|
+
else
|
1332
|
+
it "sanitizes data-attributes except the minimum attributes and force async attribute" do
|
1333
|
+
should eq <<-EOS.strip_heredoc
|
1334
|
+
<p data-slug-hash="foo" data-embed-version="2" class="codepen"></p>\n
|
1335
|
+
<script src="https://production-assets.codepen.io/assets/embed/ei.js" async="async"></script>
|
1336
|
+
EOS
|
1337
|
+
end
|
1338
|
+
end
|
1339
|
+
end
|
1340
|
+
end
|
1341
|
+
|
1271
1342
|
context "without script and strict context" do
|
1272
1343
|
let(:context) do
|
1273
1344
|
super().merge(script: false, strict: false)
|
@@ -1281,6 +1352,7 @@ describe Qiita::Markdown::Processor do
|
|
1281
1352
|
include_examples "data-attributes", allowed: false
|
1282
1353
|
include_examples "class attribute", allowed: true
|
1283
1354
|
include_examples "background-color", allowed: true
|
1355
|
+
include_examples "override codepen attributes", allowed: false
|
1284
1356
|
end
|
1285
1357
|
|
1286
1358
|
context "with script context" do
|
@@ -1296,6 +1368,7 @@ describe Qiita::Markdown::Processor do
|
|
1296
1368
|
include_examples "data-attributes", allowed: true
|
1297
1369
|
include_examples "class attribute", allowed: true
|
1298
1370
|
include_examples "background-color", allowed: true
|
1371
|
+
include_examples "override codepen attributes", allowed: true
|
1299
1372
|
end
|
1300
1373
|
|
1301
1374
|
context "with strict context" do
|
@@ -1311,6 +1384,7 @@ describe Qiita::Markdown::Processor do
|
|
1311
1384
|
include_examples "data-attributes", allowed: false
|
1312
1385
|
include_examples "class attribute", allowed: false
|
1313
1386
|
include_examples "background-color", allowed: false
|
1387
|
+
include_examples "override codepen attributes", allowed: false
|
1314
1388
|
end
|
1315
1389
|
|
1316
1390
|
context "with script and strict context" do
|
@@ -1326,6 +1400,7 @@ describe Qiita::Markdown::Processor do
|
|
1326
1400
|
include_examples "data-attributes", allowed: false
|
1327
1401
|
include_examples "class attribute", allowed: false
|
1328
1402
|
include_examples "background-color", allowed: false
|
1403
|
+
include_examples "override codepen attributes", allowed: false
|
1329
1404
|
end
|
1330
1405
|
end
|
1331
1406
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qiita-markdown
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.22.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryo Nakamura
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-11-
|
11
|
+
date: 2017-11-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: gemoji
|
@@ -256,6 +256,7 @@ files:
|
|
256
256
|
- lib/qiita-markdown.rb
|
257
257
|
- lib/qiita/markdown.rb
|
258
258
|
- lib/qiita/markdown/base_processor.rb
|
259
|
+
- lib/qiita/markdown/code_pen.rb
|
259
260
|
- lib/qiita/markdown/filters/checkbox.rb
|
260
261
|
- lib/qiita/markdown/filters/code_block.rb
|
261
262
|
- lib/qiita/markdown/filters/emoji.rb
|
@@ -277,6 +278,9 @@ files:
|
|
277
278
|
- lib/qiita/markdown/greenmat/html_toc_renderer.rb
|
278
279
|
- lib/qiita/markdown/processor.rb
|
279
280
|
- lib/qiita/markdown/summary_processor.rb
|
281
|
+
- lib/qiita/markdown/transformers/filter_attributes.rb
|
282
|
+
- lib/qiita/markdown/transformers/filter_script.rb
|
283
|
+
- lib/qiita/markdown/transformers/strip_invalid_node.rb
|
280
284
|
- lib/qiita/markdown/version.rb
|
281
285
|
- qiita-markdown.gemspec
|
282
286
|
- spec/qiita/markdown/filters/greenmat_spec.rb
|