erb_lint 0.0.37 → 0.1.1
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/exe/erblint +1 -1
- data/lib/erb_lint/all.rb +26 -0
- data/lib/erb_lint/cli.rb +73 -24
- data/lib/erb_lint/corrector.rb +1 -1
- data/lib/erb_lint/linter.rb +5 -5
- data/lib/erb_lint/linter_config.rb +3 -3
- data/lib/erb_lint/linter_registry.rb +2 -2
- data/lib/erb_lint/linters/allowed_script_type.rb +7 -7
- data/lib/erb_lint/linters/closing_erb_tag_indent.rb +2 -2
- data/lib/erb_lint/linters/deprecated_classes.rb +7 -7
- data/lib/erb_lint/linters/erb_safety.rb +2 -2
- data/lib/erb_lint/linters/extra_newline.rb +1 -1
- data/lib/erb_lint/linters/final_newline.rb +2 -2
- data/lib/erb_lint/linters/hard_coded_string.rb +36 -16
- data/lib/erb_lint/linters/no_javascript_tag_helper.rb +8 -8
- data/lib/erb_lint/linters/partial_instance_variable.rb +23 -0
- data/lib/erb_lint/linters/require_input_autocomplete.rb +121 -0
- data/lib/erb_lint/linters/require_script_nonce.rb +92 -0
- data/lib/erb_lint/linters/right_trim.rb +1 -1
- data/lib/erb_lint/linters/rubocop.rb +11 -11
- data/lib/erb_lint/linters/rubocop_text.rb +1 -1
- data/lib/erb_lint/linters/self_closing_tag.rb +5 -7
- data/lib/erb_lint/linters/space_around_erb_tag.rb +5 -5
- data/lib/erb_lint/linters/space_in_html_tag.rb +6 -6
- data/lib/erb_lint/linters/space_indentation.rb +1 -1
- data/lib/erb_lint/linters/trailing_whitespace.rb +1 -1
- data/lib/erb_lint/offense.rb +7 -4
- data/lib/erb_lint/reporter.rb +2 -2
- data/lib/erb_lint/reporters/compact_reporter.rb +9 -3
- data/lib/erb_lint/reporters/json_reporter.rb +72 -0
- data/lib/erb_lint/reporters/multiline_reporter.rb +1 -1
- data/lib/erb_lint/runner.rb +1 -1
- data/lib/erb_lint/runner_config.rb +8 -7
- data/lib/erb_lint/runner_config_resolver.rb +4 -4
- data/lib/erb_lint/stats.rb +9 -6
- data/lib/erb_lint/utils/block_map.rb +2 -2
- data/lib/erb_lint/utils/offset_corrector.rb +1 -1
- data/lib/erb_lint/utils/ruby_to_erb.rb +5 -5
- data/lib/erb_lint/utils/severity_levels.rb +16 -0
- data/lib/erb_lint/version.rb +1 -1
- data/lib/erb_lint.rb +1 -24
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b98507494f8af2a33e0e6fcbf28c48a2d91c78f42273a3cc6c1206578f3a55b0
|
4
|
+
data.tar.gz: c090a633a9d041a6d4d71bc292b1525b3a74dccd61311e0224bc1cbb0d8fea33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2ac77d88b7be36010d69c072e3ee72c370edefa1fc3b68135593b5b494da8b0f9eac79e9b5408d10155a007d94948ae6a4b3b279de2541be66f0576abc964a8
|
7
|
+
data.tar.gz: f0be6a75c5060b49a8b7c5e5a5d19adb3222fb580a11c2e2e488e5b83ada00db0c4daee25e99ba5538057c749f5937b80d25a96bda1ed967e6d1baeddf787965
|
data/exe/erblint
CHANGED
data/lib/erb_lint/all.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
|
5
|
+
require "erb_lint"
|
6
|
+
require "erb_lint/corrector"
|
7
|
+
require "erb_lint/file_loader"
|
8
|
+
require "erb_lint/linter_config"
|
9
|
+
require "erb_lint/linter_registry"
|
10
|
+
require "erb_lint/linter"
|
11
|
+
require "erb_lint/offense"
|
12
|
+
require "erb_lint/processed_source"
|
13
|
+
require "erb_lint/runner_config"
|
14
|
+
require "erb_lint/runner"
|
15
|
+
require "erb_lint/stats"
|
16
|
+
require "erb_lint/reporter"
|
17
|
+
|
18
|
+
# Load linters
|
19
|
+
Dir[File.expand_path("linters/**/*.rb", __dir__)].each do |file|
|
20
|
+
require file
|
21
|
+
end
|
22
|
+
|
23
|
+
# Load reporters
|
24
|
+
Dir[File.expand_path("reporters/**/*.rb", __dir__)].each do |file|
|
25
|
+
require file
|
26
|
+
end
|
data/lib/erb_lint/cli.rb
CHANGED
@@ -1,16 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
3
|
+
require "erb_lint/all"
|
4
|
+
require "active_support"
|
5
|
+
require "active_support/inflector"
|
6
|
+
require "optparse"
|
7
|
+
require "psych"
|
8
|
+
require "yaml"
|
9
|
+
require "rainbow"
|
10
|
+
require "erb_lint/utils/severity_levels"
|
10
11
|
|
11
12
|
module ERBLint
|
12
13
|
class CLI
|
13
|
-
|
14
|
+
include Utils::SeverityLevels
|
15
|
+
|
16
|
+
DEFAULT_CONFIG_FILENAME = ".erb-lint.yml"
|
14
17
|
DEFAULT_LINT_ALL_GLOB = "**/*.html{+*,}.erb"
|
15
18
|
|
16
19
|
class ExitWithFailure < RuntimeError; end
|
@@ -27,7 +30,7 @@ module ERBLint
|
|
27
30
|
def run(args = ARGV)
|
28
31
|
dupped_args = args.dup
|
29
32
|
load_options(dupped_args)
|
30
|
-
@files = dupped_args
|
33
|
+
@files = @options[:stdin] || dupped_args
|
31
34
|
|
32
35
|
load_config
|
33
36
|
|
@@ -40,10 +43,11 @@ module ERBLint
|
|
40
43
|
ensure_files_exist(lint_files)
|
41
44
|
|
42
45
|
if enabled_linter_classes.empty?
|
43
|
-
failure!(
|
46
|
+
failure!("no linter available with current configuration")
|
44
47
|
end
|
45
48
|
|
46
49
|
@options[:format] ||= :multiline
|
50
|
+
@options[:fail_level] ||= severity_level_for_name(:refactor)
|
47
51
|
@stats.files = lint_files.size
|
48
52
|
@stats.linters = enabled_linter_classes.size
|
49
53
|
|
@@ -51,14 +55,15 @@ module ERBLint
|
|
51
55
|
reporter.preview
|
52
56
|
|
53
57
|
runner = ERBLint::Runner.new(file_loader, @config)
|
58
|
+
file_content = nil
|
54
59
|
|
55
60
|
lint_files.each do |filename|
|
56
61
|
runner.clear_offenses
|
57
62
|
begin
|
58
|
-
run_with_corrections(runner, filename)
|
63
|
+
file_content = run_with_corrections(runner, filename)
|
59
64
|
rescue => e
|
60
65
|
@stats.exceptions += 1
|
61
|
-
puts "Exception
|
66
|
+
puts "Exception occurred when processing: #{relative_filename(filename)}"
|
62
67
|
puts "If this file cannot be processed by erb-lint, "\
|
63
68
|
"you can exclude it in your configuration file."
|
64
69
|
puts e.message
|
@@ -69,6 +74,12 @@ module ERBLint
|
|
69
74
|
|
70
75
|
reporter.show
|
71
76
|
|
77
|
+
if stdin? && autocorrect?
|
78
|
+
# When running from stdin, we only lint a single file
|
79
|
+
puts "================ #{lint_files.first} ==================\n"
|
80
|
+
puts file_content
|
81
|
+
end
|
82
|
+
|
72
83
|
@stats.found == 0 && @stats.exceptions == 0
|
73
84
|
rescue OptionParser::InvalidOption, OptionParser::InvalidArgument, ExitWithFailure => e
|
74
85
|
warn(Rainbow(e.message).red)
|
@@ -88,7 +99,7 @@ module ERBLint
|
|
88
99
|
end
|
89
100
|
|
90
101
|
def run_with_corrections(runner, filename)
|
91
|
-
file_content =
|
102
|
+
file_content = read_content(filename)
|
92
103
|
|
93
104
|
7.times do
|
94
105
|
processed_source = ERBLint::ProcessedSource.new(filename, file_content)
|
@@ -101,8 +112,11 @@ module ERBLint
|
|
101
112
|
|
102
113
|
@stats.corrected += corrector.corrections.size
|
103
114
|
|
104
|
-
|
105
|
-
|
115
|
+
# Don't overwrite the file if the input comes from stdin
|
116
|
+
unless stdin?
|
117
|
+
File.open(filename, "wb") do |file|
|
118
|
+
file.write(corrector.corrected_content)
|
119
|
+
end
|
106
120
|
end
|
107
121
|
|
108
122
|
file_content = corrector.corrected_content
|
@@ -111,14 +125,27 @@ module ERBLint
|
|
111
125
|
offenses_filename = relative_filename(filename)
|
112
126
|
offenses = runner.offenses || []
|
113
127
|
|
114
|
-
@stats.found
|
128
|
+
@stats.ignored, @stats.found = offenses.partition do |offense|
|
129
|
+
severity_level_for_name(offense.severity) < @options[:fail_level]
|
130
|
+
end.map(&:size)
|
131
|
+
.zip([@stats.ignored, @stats.found])
|
132
|
+
.map(&:sum)
|
133
|
+
|
115
134
|
@stats.processed_files[offenses_filename] ||= []
|
116
135
|
@stats.processed_files[offenses_filename] |= offenses
|
136
|
+
|
137
|
+
file_content
|
138
|
+
end
|
139
|
+
|
140
|
+
def read_content(filename)
|
141
|
+
return File.read(filename, encoding: Encoding::UTF_8) unless stdin?
|
142
|
+
|
143
|
+
$stdin.binmode.read.force_encoding(Encoding::UTF_8)
|
117
144
|
end
|
118
145
|
|
119
146
|
def correct(processed_source, offenses)
|
120
147
|
corrector = ERBLint::Corrector.new(processed_source, offenses)
|
121
|
-
failure!(corrector.diagnostics.join(
|
148
|
+
failure!(corrector.diagnostics.join(", ")) if corrector.diagnostics.any?
|
122
149
|
corrector
|
123
150
|
end
|
124
151
|
|
@@ -156,7 +183,7 @@ module ERBLint
|
|
156
183
|
else
|
157
184
|
@files
|
158
185
|
.map { |f| Dir.exist?(f) ? Dir[File.join(f, glob)] : f }
|
159
|
-
.map { |f| f.include?(
|
186
|
+
.map { |f| f.include?("*") ? Dir[f] : f }
|
160
187
|
.flatten
|
161
188
|
.map { |f| File.expand_path(f, Dir.pwd) }
|
162
189
|
.select { |filename| !excluded?(filename) }
|
@@ -169,7 +196,8 @@ module ERBLint
|
|
169
196
|
|
170
197
|
def excluded?(filename)
|
171
198
|
@config.global_exclude.any? do |path|
|
172
|
-
File.
|
199
|
+
expanded_path = File.expand_path(path, Dir.pwd)
|
200
|
+
File.fnmatch?(expanded_path, filename)
|
173
201
|
end
|
174
202
|
end
|
175
203
|
|
@@ -212,14 +240,14 @@ module ERBLint
|
|
212
240
|
end
|
213
241
|
|
214
242
|
def relative_filename(filename)
|
215
|
-
filename.sub("#{File.expand_path(
|
243
|
+
filename.sub("#{File.expand_path(".", Dir.pwd)}/", "")
|
216
244
|
end
|
217
245
|
|
218
246
|
def runner_config_override
|
219
247
|
RunnerConfig.new(
|
220
248
|
linters: {}.tap do |linters|
|
221
249
|
ERBLint::LinterRegistry.linters.map do |klass|
|
222
|
-
linters[klass.simple_name] = {
|
250
|
+
linters[klass.simple_name] = { "enabled" => enabled_linter_classes.include?(klass) }
|
223
251
|
end
|
224
252
|
end
|
225
253
|
)
|
@@ -255,19 +283,36 @@ module ERBLint
|
|
255
283
|
end
|
256
284
|
|
257
285
|
opts.on("--enable-linters LINTER[,LINTER,...]", Array,
|
258
|
-
"Only use specified linter", "Known linters are: #{known_linter_names.join(
|
286
|
+
"Only use specified linter", "Known linters are: #{known_linter_names.join(", ")}") do |linters|
|
259
287
|
linters.each do |linter|
|
260
288
|
unless known_linter_names.include?(linter)
|
261
|
-
failure!("#{linter}: not a valid linter name (#{known_linter_names.join(
|
289
|
+
failure!("#{linter}: not a valid linter name (#{known_linter_names.join(", ")})")
|
262
290
|
end
|
263
291
|
end
|
264
292
|
@options[:enabled_linters] = linters
|
265
293
|
end
|
266
294
|
|
295
|
+
opts.on("--fail-level SEVERITY", "Minimum severity for exit with error code") do |level|
|
296
|
+
parsed_severity = SEVERITY_CODE_TABLE[level.upcase.to_sym] || (SEVERITY_NAMES & [level.downcase]).first
|
297
|
+
|
298
|
+
if parsed_severity.nil?
|
299
|
+
failure!("#{level}: not a valid failure level (#{SEVERITY_NAMES.join(", ")})")
|
300
|
+
end
|
301
|
+
@options[:fail_level] = severity_level_for_name(parsed_severity)
|
302
|
+
end
|
303
|
+
|
267
304
|
opts.on("-a", "--autocorrect", "Correct offenses automatically if possible (default: false)") do |config|
|
268
305
|
@options[:autocorrect] = config
|
269
306
|
end
|
270
307
|
|
308
|
+
opts.on(
|
309
|
+
"-sFILE",
|
310
|
+
"--stdin FILE",
|
311
|
+
"Pipe source from STDIN. Takes the path to be used to check which rules to apply."
|
312
|
+
) do |file|
|
313
|
+
@options[:stdin] = [file]
|
314
|
+
end
|
315
|
+
|
271
316
|
opts.on_tail("-h", "--help", "Show this message") do
|
272
317
|
success!(opts)
|
273
318
|
end
|
@@ -280,12 +325,16 @@ module ERBLint
|
|
280
325
|
|
281
326
|
def format_options_help
|
282
327
|
"Report offenses in the given format: "\
|
283
|
-
"(#{Reporter.available_formats.join(
|
328
|
+
"(#{Reporter.available_formats.join(", ")}) (default: multiline)"
|
284
329
|
end
|
285
330
|
|
286
331
|
def invalid_format_error_message(given_format)
|
287
332
|
formats = Reporter.available_formats.map { |format| " - #{format}\n" }
|
288
333
|
"#{given_format}: is not a valid format. Available formats:\n#{formats.join}"
|
289
334
|
end
|
335
|
+
|
336
|
+
def stdin?
|
337
|
+
@options[:stdin].present?
|
338
|
+
end
|
290
339
|
end
|
291
340
|
end
|
data/lib/erb_lint/corrector.rb
CHANGED
data/lib/erb_lint/linter.rb
CHANGED
@@ -15,9 +15,9 @@ module ERBLint
|
|
15
15
|
# `ERBLint::Linters::Compass::Bar.simple_name` #=> "Compass::Bar"
|
16
16
|
def inherited(linter)
|
17
17
|
super
|
18
|
-
linter.simple_name = if linter.name.start_with?(
|
19
|
-
name_parts = linter.name.split(
|
20
|
-
name_parts[2..-1].join(
|
18
|
+
linter.simple_name = if linter.name.start_with?("ERBLint::Linters::")
|
19
|
+
name_parts = linter.name.split("::")
|
20
|
+
name_parts[2..-1].join("::")
|
21
21
|
else
|
22
22
|
linter.name
|
23
23
|
end
|
@@ -53,8 +53,8 @@ module ERBLint
|
|
53
53
|
raise NotImplementedError, "must implement ##{__method__}"
|
54
54
|
end
|
55
55
|
|
56
|
-
def add_offense(source_range, message, context = nil)
|
57
|
-
@offenses << Offense.new(self, source_range, message, context)
|
56
|
+
def add_offense(source_range, message, context = nil, severity = nil)
|
57
|
+
@offenses << Offense.new(self, source_range, message, context, severity)
|
58
58
|
end
|
59
59
|
|
60
60
|
def clear_offenses
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "active_support"
|
4
|
+
require "smart_properties"
|
5
5
|
|
6
6
|
module ERBLint
|
7
7
|
class LinterConfig
|
@@ -27,7 +27,7 @@ module ERBLint
|
|
27
27
|
allowed_keys = self.class.properties.keys.map(&:to_s)
|
28
28
|
given_keys = config.keys
|
29
29
|
if (extra_keys = given_keys - allowed_keys).any?
|
30
|
-
raise Error, "Given key is not allowed: #{extra_keys.join(
|
30
|
+
raise Error, "Given key is not allowed: #{extra_keys.join(", ")}"
|
31
31
|
end
|
32
32
|
super(config)
|
33
33
|
rescue SmartProperties::InitializationError => e
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module ERBLint
|
4
4
|
# Stores all linters available to the application.
|
5
5
|
module LinterRegistry
|
6
|
-
CUSTOM_LINTERS_DIR =
|
6
|
+
CUSTOM_LINTERS_DIR = ".erb-linters"
|
7
7
|
@loaded_linters = []
|
8
8
|
|
9
9
|
class << self
|
@@ -27,7 +27,7 @@ module ERBLint
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def load_custom_linters(directory = CUSTOM_LINTERS_DIR)
|
30
|
-
ruby_files = Dir.glob(File.expand_path(File.join(directory,
|
30
|
+
ruby_files = Dir.glob(File.expand_path(File.join(directory, "**", "*.rb")))
|
31
31
|
ruby_files.each { |file| require file }
|
32
32
|
end
|
33
33
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "better_html"
|
4
|
+
require "better_html/tree/tag"
|
5
5
|
|
6
6
|
module ERBLint
|
7
7
|
module Linters
|
@@ -13,7 +13,7 @@ module ERBLint
|
|
13
13
|
|
14
14
|
class ConfigSchema < LinterConfig
|
15
15
|
property :allowed_types, accepts: array_of?(String),
|
16
|
-
default: -> { [
|
16
|
+
default: -> { ["text/javascript"] }
|
17
17
|
property :allow_blank, accepts: [true, false], default: true, reader: :allow_blank?
|
18
18
|
property :disallow_inline_scripts, accepts: [true, false], default: false, reader: :disallow_inline_scripts?
|
19
19
|
end
|
@@ -24,7 +24,7 @@ module ERBLint
|
|
24
24
|
parser.nodes_with_type(:tag).each do |tag_node|
|
25
25
|
tag = BetterHtml::Tree::Tag.from_node(tag_node)
|
26
26
|
next if tag.closing?
|
27
|
-
next unless tag.name ==
|
27
|
+
next unless tag.name == "script"
|
28
28
|
|
29
29
|
if @config.disallow_inline_scripts?
|
30
30
|
name_node = tag_node.to_a[1]
|
@@ -36,7 +36,7 @@ module ERBLint
|
|
36
36
|
next
|
37
37
|
end
|
38
38
|
|
39
|
-
type_attribute = tag.attributes[
|
39
|
+
type_attribute = tag.attributes["type"]
|
40
40
|
type_present = type_attribute.present? && type_attribute.value_node.present?
|
41
41
|
|
42
42
|
if !type_present && !@config.allow_blank?
|
@@ -50,8 +50,8 @@ module ERBLint
|
|
50
50
|
add_offense(
|
51
51
|
type_attribute.loc,
|
52
52
|
"Avoid using #{type_attribute.value.inspect} as type for `<script>` tag. "\
|
53
|
-
"Must be one of: #{@config.allowed_types.join(
|
54
|
-
"#{
|
53
|
+
"Must be one of: #{@config.allowed_types.join(", ")}"\
|
54
|
+
"#{" (or no type attribute)" if @config.allow_blank?}."
|
55
55
|
)
|
56
56
|
end
|
57
57
|
end
|
@@ -25,7 +25,7 @@ module ERBLint
|
|
25
25
|
add_offense(
|
26
26
|
code_node.loc.end.adjust(begin_pos: -end_spaces.size),
|
27
27
|
"Remove newline before `%>` to match start of tag.",
|
28
|
-
|
28
|
+
" "
|
29
29
|
)
|
30
30
|
elsif start_with_newline && !end_with_newline
|
31
31
|
add_offense(
|
@@ -39,7 +39,7 @@ module ERBLint
|
|
39
39
|
add_offense(
|
40
40
|
code_node.loc.end.adjust(begin_pos: -current_indent.size),
|
41
41
|
"Indent `%>` on column #{erb_node.loc.column} to match start of tag.",
|
42
|
-
|
42
|
+
" " * erb_node.loc.column
|
43
43
|
)
|
44
44
|
end
|
45
45
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "better_html"
|
4
|
+
require "better_html/parser"
|
5
5
|
|
6
6
|
module ERBLint
|
7
7
|
module Linters
|
@@ -11,7 +11,7 @@ module ERBLint
|
|
11
11
|
|
12
12
|
class RuleSet
|
13
13
|
include SmartProperties
|
14
|
-
property :suggestion, accepts: String, default:
|
14
|
+
property :suggestion, accepts: String, default: ""
|
15
15
|
property :deprecated, accepts: LinterConfig.array_of?(String), default: -> { [] }
|
16
16
|
end
|
17
17
|
|
@@ -57,9 +57,9 @@ module ERBLint
|
|
57
57
|
def class_name_with_loc(processed_source)
|
58
58
|
Enumerator.new do |yielder|
|
59
59
|
tags(processed_source).each do |tag|
|
60
|
-
class_value = tag.attributes[
|
60
|
+
class_value = tag.attributes["class"]&.value
|
61
61
|
next unless class_value
|
62
|
-
class_value.split(
|
62
|
+
class_value.split(" ").each do |class_name|
|
63
63
|
yielder.yield(class_name, tag.loc)
|
64
64
|
end
|
65
65
|
end
|
@@ -69,7 +69,7 @@ module ERBLint
|
|
69
69
|
def text_tags_content(processed_source)
|
70
70
|
Enumerator.new do |yielder|
|
71
71
|
script_tags(processed_source)
|
72
|
-
.select { |tag| tag.attributes[
|
72
|
+
.select { |tag| tag.attributes["type"]&.value == "text/html" }
|
73
73
|
.each do |tag|
|
74
74
|
index = processed_source.ast.to_a.find_index(tag.node)
|
75
75
|
next_node = processed_source.ast.to_a[index + 1]
|
@@ -80,7 +80,7 @@ module ERBLint
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def script_tags(processed_source)
|
83
|
-
tags(processed_source).select { |tag| tag.name ==
|
83
|
+
tags(processed_source).select { |tag| tag.name == "script" }
|
84
84
|
end
|
85
85
|
|
86
86
|
def tags(processed_source)
|