slim_lint_standard 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +21 -0
- data/bin/slim-lint-standard +7 -0
- data/config/default.yml +109 -0
- data/lib/slim_lint/atom.rb +129 -0
- data/lib/slim_lint/capture_map.rb +19 -0
- data/lib/slim_lint/cli.rb +167 -0
- data/lib/slim_lint/configuration.rb +111 -0
- data/lib/slim_lint/configuration_loader.rb +86 -0
- data/lib/slim_lint/constants.rb +10 -0
- data/lib/slim_lint/document.rb +78 -0
- data/lib/slim_lint/engine.rb +41 -0
- data/lib/slim_lint/exceptions.rb +20 -0
- data/lib/slim_lint/file_finder.rb +88 -0
- data/lib/slim_lint/filter.rb +126 -0
- data/lib/slim_lint/filters/attribute_processor.rb +46 -0
- data/lib/slim_lint/filters/auto_indenter.rb +39 -0
- data/lib/slim_lint/filters/control_processor.rb +46 -0
- data/lib/slim_lint/filters/do_inserter.rb +39 -0
- data/lib/slim_lint/filters/end_inserter.rb +74 -0
- data/lib/slim_lint/filters/interpolation.rb +73 -0
- data/lib/slim_lint/filters/multi_flattener.rb +32 -0
- data/lib/slim_lint/filters/splat_processor.rb +20 -0
- data/lib/slim_lint/filters/static_merger.rb +47 -0
- data/lib/slim_lint/lint.rb +70 -0
- data/lib/slim_lint/linter/avoid_multiline_expressions.rb +41 -0
- data/lib/slim_lint/linter/comment_control_statement.rb +26 -0
- data/lib/slim_lint/linter/consecutive_control_statements.rb +26 -0
- data/lib/slim_lint/linter/control_statement_spacing.rb +32 -0
- data/lib/slim_lint/linter/dynamic_output_spacing.rb +77 -0
- data/lib/slim_lint/linter/embedded_engines.rb +18 -0
- data/lib/slim_lint/linter/empty_control_statement.rb +15 -0
- data/lib/slim_lint/linter/empty_lines.rb +24 -0
- data/lib/slim_lint/linter/file_length.rb +18 -0
- data/lib/slim_lint/linter/line_length.rb +18 -0
- data/lib/slim_lint/linter/redundant_div.rb +21 -0
- data/lib/slim_lint/linter/rubocop.rb +131 -0
- data/lib/slim_lint/linter/standard.rb +69 -0
- data/lib/slim_lint/linter/tab.rb +20 -0
- data/lib/slim_lint/linter/tag_case.rb +15 -0
- data/lib/slim_lint/linter/trailing_blank_lines.rb +19 -0
- data/lib/slim_lint/linter/trailing_whitespace.rb +17 -0
- data/lib/slim_lint/linter.rb +93 -0
- data/lib/slim_lint/linter_registry.rb +37 -0
- data/lib/slim_lint/linter_selector.rb +87 -0
- data/lib/slim_lint/logger.rb +103 -0
- data/lib/slim_lint/matcher/anything.rb +11 -0
- data/lib/slim_lint/matcher/base.rb +21 -0
- data/lib/slim_lint/matcher/capture.rb +32 -0
- data/lib/slim_lint/matcher/nothing.rb +13 -0
- data/lib/slim_lint/options.rb +110 -0
- data/lib/slim_lint/parser.rb +584 -0
- data/lib/slim_lint/rake_task.rb +125 -0
- data/lib/slim_lint/report.rb +25 -0
- data/lib/slim_lint/reporter/checkstyle_reporter.rb +42 -0
- data/lib/slim_lint/reporter/default_reporter.rb +40 -0
- data/lib/slim_lint/reporter/emacs_reporter.rb +40 -0
- data/lib/slim_lint/reporter/json_reporter.rb +50 -0
- data/lib/slim_lint/reporter.rb +44 -0
- data/lib/slim_lint/ruby_extract_engine.rb +30 -0
- data/lib/slim_lint/ruby_extractor.rb +175 -0
- data/lib/slim_lint/ruby_parser.rb +32 -0
- data/lib/slim_lint/runner.rb +82 -0
- data/lib/slim_lint/sexp.rb +134 -0
- data/lib/slim_lint/sexp_visitor.rb +150 -0
- data/lib/slim_lint/source_location.rb +45 -0
- data/lib/slim_lint/utils.rb +84 -0
- data/lib/slim_lint/version.rb +6 -0
- data/lib/slim_lint.rb +55 -0
- metadata +218 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Represents a parsed Slim document and its associated metadata.
|
5
|
+
class Document
|
6
|
+
FRONTMATTER_RE = /
|
7
|
+
# From the start of the string
|
8
|
+
\A
|
9
|
+
# First-capture match --- followed by optional whitespace up
|
10
|
+
# to a newline then 0 or more chars followed by an optional newline.
|
11
|
+
# This matches the --- and the contents of the frontmatter
|
12
|
+
(---\s*\n.*?\n?)
|
13
|
+
# From the start of the line
|
14
|
+
^
|
15
|
+
# Second capture match --- or ... followed by optional whitespace
|
16
|
+
# and newline. This matches the closing --- for the frontmatter.
|
17
|
+
(---|\.\.\.)\s*$\n?
|
18
|
+
/mx
|
19
|
+
|
20
|
+
# @return [SlimLint::Configuration] Configuration used to parse template
|
21
|
+
attr_reader :config
|
22
|
+
|
23
|
+
# @return [String] Slim template file path
|
24
|
+
attr_reader :file
|
25
|
+
|
26
|
+
# @return [SlimLint::Sexp] Sexpression representing the parsed document
|
27
|
+
attr_reader :sexp
|
28
|
+
|
29
|
+
# @return [String] original source code
|
30
|
+
attr_reader :source
|
31
|
+
|
32
|
+
# @return [Array<String>] original source code as an array of lines
|
33
|
+
attr_reader :source_lines
|
34
|
+
|
35
|
+
# Parses the specified Slim code into a {Document}.
|
36
|
+
#
|
37
|
+
# @param source [String] Slim code to parse
|
38
|
+
# @param options [Hash]
|
39
|
+
# @option options :file [String] file name of document that was parsed
|
40
|
+
# @raise [Slim::Parser::Error] if there was a problem parsing the document
|
41
|
+
def initialize(source, options)
|
42
|
+
@config = options[:config]
|
43
|
+
@file = options.fetch(:file, nil)
|
44
|
+
|
45
|
+
process_source(source)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# @param source [String] Slim code to parse
|
51
|
+
# @raise [SlimLint::Exceptions::ParseError] if there was a problem parsing the document
|
52
|
+
def process_source(source)
|
53
|
+
@source = process_encoding(source)
|
54
|
+
@source = strip_frontmatter(source)
|
55
|
+
@source_lines = @source.split("\n")
|
56
|
+
|
57
|
+
engine = SlimLint::Engine.new(file: @file)
|
58
|
+
@sexp = engine.parse(@source)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Ensure the string's encoding is valid.
|
62
|
+
#
|
63
|
+
# @param source [String]
|
64
|
+
# @return [String] source encoded in a valid encoding
|
65
|
+
def process_encoding(source)
|
66
|
+
::Temple::Filters::Encoding.new.call(source)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Removes YAML frontmatter
|
70
|
+
def strip_frontmatter(source)
|
71
|
+
if config["skip_frontmatter"] && source =~ FRONTMATTER_RE
|
72
|
+
source = $POSTMATCH
|
73
|
+
end
|
74
|
+
|
75
|
+
source
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Temple engine used to generate a {Sexp} parse tree for use by linters.
|
5
|
+
#
|
6
|
+
# We omit a lot of the filters that are in {Slim::Engine} because they result
|
7
|
+
# in information potentially being removed from the parse tree (since some
|
8
|
+
# Sexp abstractions are optimized/removed or otherwise transformed). In order
|
9
|
+
# for linters to be useful, they need to operate on the original parse tree.
|
10
|
+
#
|
11
|
+
# The other key task this engine accomplishes is converting the Array-based
|
12
|
+
# S-expressions into {SlimLint::Sexp} objects, which have a number of helper
|
13
|
+
# methods that makes working with them easier. It also annotates these
|
14
|
+
# {SlimLint::Sexp} objects with line numbers so it's easy to cross reference
|
15
|
+
# with the original source code.
|
16
|
+
class Engine < Temple::Engine
|
17
|
+
filter :Encoding
|
18
|
+
filter :RemoveBOM
|
19
|
+
|
20
|
+
# Parse into S-expression using Slim parser
|
21
|
+
use SlimLint::Parser
|
22
|
+
|
23
|
+
# Parses the given source code into a Sexp.
|
24
|
+
#
|
25
|
+
# @param source [String] source code to parse
|
26
|
+
# @return [SlimLint::Sexp] parsed Sexp
|
27
|
+
def parse(source)
|
28
|
+
call(source)
|
29
|
+
rescue ::Slim::Parser::SyntaxError => e
|
30
|
+
# Convert to our own exception type to isolate from upstream changes
|
31
|
+
error = SlimLint::Exceptions::ParseError.new(
|
32
|
+
e.error,
|
33
|
+
e.file,
|
34
|
+
e.line,
|
35
|
+
e.lineno,
|
36
|
+
e.column
|
37
|
+
)
|
38
|
+
raise error
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Collection of exceptions that can be raised by the application.
|
4
|
+
module SlimLint::Exceptions
|
5
|
+
# Raised when a {Configuration} could not be loaded from a file.
|
6
|
+
class ConfigurationError < StandardError; end
|
7
|
+
|
8
|
+
# Raised when invalid/incompatible command line options are provided.
|
9
|
+
class InvalidCLIOption < StandardError; end
|
10
|
+
|
11
|
+
# Raised when an invalid file path is specified
|
12
|
+
class InvalidFilePath < StandardError; end
|
13
|
+
|
14
|
+
# Raised when the Slim parser is unable to parse a template.
|
15
|
+
class ParseError < ::Slim::Parser::SyntaxError; end
|
16
|
+
|
17
|
+
# Raised when attempting to execute `Runner` with options that would result in
|
18
|
+
# no linters being enabled.
|
19
|
+
class NoLintersError < StandardError; end
|
20
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "find"
|
4
|
+
|
5
|
+
module SlimLint
|
6
|
+
# Finds Slim files that should be linted given a specified list of paths, glob
|
7
|
+
# patterns, and configuration.
|
8
|
+
class FileFinder
|
9
|
+
# List of extensions of files to include under a directory when a directory
|
10
|
+
# is specified instead of a file.
|
11
|
+
VALID_EXTENSIONS = %w[.slim].freeze
|
12
|
+
|
13
|
+
# Create a file finder using the specified configuration.
|
14
|
+
#
|
15
|
+
# @param config [SlimLint::Configuration]
|
16
|
+
def initialize(config)
|
17
|
+
@config = config
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return list of files to lint given the specified set of paths and glob
|
21
|
+
# patterns.
|
22
|
+
# @param patterns [Array<String>]
|
23
|
+
# @param excluded_patterns [Array<String>]
|
24
|
+
# @raise [SlimLint::Exceptions::InvalidFilePath]
|
25
|
+
# @return [Array<String>] list of actual files
|
26
|
+
def find(patterns, excluded_patterns)
|
27
|
+
excluded_patterns = excluded_patterns.map { |pattern| normalize_path(pattern) }
|
28
|
+
|
29
|
+
extract_files_from(patterns).reject do |file|
|
30
|
+
SlimLint::Utils.any_glob_matches?(excluded_patterns, file)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Extract the list of matching files given the list of glob patterns, file
|
37
|
+
# paths, and directories.
|
38
|
+
#
|
39
|
+
# @param patterns [Array<String>]
|
40
|
+
# @return [Array<String>]
|
41
|
+
def extract_files_from(patterns)
|
42
|
+
files = []
|
43
|
+
|
44
|
+
patterns.each do |pattern|
|
45
|
+
if File.file?(pattern)
|
46
|
+
files << pattern
|
47
|
+
else
|
48
|
+
begin
|
49
|
+
::Find.find(pattern) do |file|
|
50
|
+
files << file if slim_file?(file)
|
51
|
+
end
|
52
|
+
rescue ::Errno::ENOENT
|
53
|
+
# File didn't exist; it might be a file glob pattern
|
54
|
+
matches = ::Dir.glob(pattern)
|
55
|
+
if matches.any?
|
56
|
+
files += matches
|
57
|
+
else
|
58
|
+
# One of the paths specified does not exist; raise a more
|
59
|
+
# descriptive exception so we know which one
|
60
|
+
raise SlimLint::Exceptions::InvalidFilePath,
|
61
|
+
"File path '#{pattern}' does not exist"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
files.uniq.sort.map { |file| normalize_path(file) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Trim "./" from the front of relative paths.
|
71
|
+
#
|
72
|
+
# @param path [String]
|
73
|
+
# @return [String]
|
74
|
+
def normalize_path(path)
|
75
|
+
path.start_with?(".#{File::SEPARATOR}") ? path[2..] : path
|
76
|
+
end
|
77
|
+
|
78
|
+
# Whether the given file should be treated as a Slim file.
|
79
|
+
#
|
80
|
+
# @param file [String]
|
81
|
+
# @return [Boolean]
|
82
|
+
def slim_file?(file)
|
83
|
+
return false unless ::FileTest.file?(file)
|
84
|
+
|
85
|
+
VALID_EXTENSIONS.include?(::File.extname(file))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module SlimLint
|
2
|
+
# Alternative implementation of Slim::Filter that operates without
|
3
|
+
# destroying the Sexp position data.
|
4
|
+
class Filter < Temple::HTML::Filter
|
5
|
+
module Overrides
|
6
|
+
def on_multi(*exps)
|
7
|
+
exps.each.with_index(1) { |exp, i| @self[i] = compile(exp) }
|
8
|
+
@self
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_escape(flag, content)
|
12
|
+
@self[2] = compile(content)
|
13
|
+
@self
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_html_attrs(*attrs)
|
17
|
+
attrs.each.with_index(2) { |attr, i| @self[i] = compile(attr) }
|
18
|
+
@self
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_html_attr(name, content)
|
22
|
+
@self[3] = compile(content)
|
23
|
+
@self
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_html_comment(content)
|
27
|
+
@self[2] = compile(content)
|
28
|
+
@self
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_html_condcomment(condition, content)
|
32
|
+
@self[3] = compile(content)
|
33
|
+
@self
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_html_js(content)
|
37
|
+
@self[2] = compile(content)
|
38
|
+
@self
|
39
|
+
end
|
40
|
+
|
41
|
+
def on_html_tag(name, attrs, content = nil)
|
42
|
+
@self[3] = compile(attrs)
|
43
|
+
@self[4] = compile(content) if content
|
44
|
+
@self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Pass-through handler
|
48
|
+
def on_slim_text(type, content)
|
49
|
+
@self[3] = compile(content)
|
50
|
+
@self
|
51
|
+
end
|
52
|
+
|
53
|
+
# Pass-through handler
|
54
|
+
def on_slim_embedded(type, content, attrs)
|
55
|
+
@self[3] = compile(content)
|
56
|
+
@self
|
57
|
+
end
|
58
|
+
|
59
|
+
# Pass-through handler
|
60
|
+
def on_slim_control(code, content)
|
61
|
+
@self[3] = compile(content)
|
62
|
+
@self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Pass-through handler
|
66
|
+
def on_slim_output(escape, code, content)
|
67
|
+
@self[4] = compile(content)
|
68
|
+
@self
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def dispatcher(exp)
|
74
|
+
@self_stack ||= []
|
75
|
+
@key_stack ||= []
|
76
|
+
@self_stack << @self
|
77
|
+
@self = exp
|
78
|
+
|
79
|
+
exp.size.downto(1) do |depth|
|
80
|
+
available_methods = dispatched_methods_by_depth[depth]
|
81
|
+
next unless available_methods
|
82
|
+
|
83
|
+
slice = exp.take(depth)
|
84
|
+
next unless slice.all? { |x| x.is_a?(Atom) && x.value.is_a?(Symbol) }
|
85
|
+
|
86
|
+
name = "on_#{slice.join("_")}"
|
87
|
+
if available_methods.include?(name)
|
88
|
+
@key_stack << @key
|
89
|
+
@key = slice
|
90
|
+
return send(name, *exp.drop(depth))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
exp
|
95
|
+
ensure
|
96
|
+
@self = @self_stack.pop
|
97
|
+
@key = @key_stack.pop
|
98
|
+
end
|
99
|
+
|
100
|
+
def dispatched_methods_by_depth
|
101
|
+
@dispatched_methods_by_depth ||= dispatched_methods.group_by { |x| x.count("_") }
|
102
|
+
end
|
103
|
+
|
104
|
+
def empty_exp?(exp)
|
105
|
+
case exp[0].value
|
106
|
+
when :multi
|
107
|
+
exp[1..].all? { |e| empty_exp?(e) }
|
108
|
+
else
|
109
|
+
false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Compares two [line, column] position pairs, and returns true if position
|
114
|
+
# `a` comes before position `b`.
|
115
|
+
#
|
116
|
+
# @param a [Array(Int, Int)] Position `a`
|
117
|
+
# @param b [Array(Int, Int)] Position `b`
|
118
|
+
# @return Does position `a` occur before position `b`?
|
119
|
+
def later_pos?(a, b)
|
120
|
+
a[0] < b[0] || (a[0] == b[0] && a[1] < b[1])
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
include Overrides
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
module Filters
|
5
|
+
# A dumbed-down version of {Slim::CodeAttributes} which doesn't introduce any
|
6
|
+
# temporary variables or other cruft.
|
7
|
+
class AttributeProcessor < Filter
|
8
|
+
define_options :merge_attrs
|
9
|
+
|
10
|
+
# Handle attributes expression `[:html, :attrs, *attrs]`
|
11
|
+
#
|
12
|
+
# @param attrs [Array]
|
13
|
+
# @return [Array]
|
14
|
+
def on_html_attrs(*attrs)
|
15
|
+
@self.delete_at(1)
|
16
|
+
expr = on_multi(*attrs)
|
17
|
+
expr[0].value = :multi
|
18
|
+
expr
|
19
|
+
end
|
20
|
+
|
21
|
+
# # Handle attribute expression `[:html, :attr, name, value]`
|
22
|
+
# #
|
23
|
+
# # @param name [String] name of the attribute
|
24
|
+
# # @param value [Array] Sexp representing the value
|
25
|
+
# def on_html_attr(name, value)
|
26
|
+
# if value[0] == :slim && value[1] == :attrvalue
|
27
|
+
# code = value[3]
|
28
|
+
# [:code, code]
|
29
|
+
# else
|
30
|
+
# @attr = name
|
31
|
+
# super
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
|
35
|
+
def on_slim_attrvalue(_escape, code)
|
36
|
+
return code if code[0] == :multi
|
37
|
+
@self.start = code.start
|
38
|
+
@self.finish = code.finish
|
39
|
+
@self[0].value = :code
|
40
|
+
@self.delete_at(2)
|
41
|
+
@self.delete_at(1)
|
42
|
+
@self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SlimLint
|
2
|
+
module Filters
|
3
|
+
# This filter annotates the sexp with indentation guidance, so that we can
|
4
|
+
# generate Ruby code with reasonable indentation semantics.
|
5
|
+
class AutoIndenter < Filter
|
6
|
+
BLOCK_REGEX = /(\A(if|unless|else|elsif|when|begin|rescue|ensure|case)\b)|\bdo\s*(\|[^|]*\|\s*)?\Z/
|
7
|
+
|
8
|
+
# Handle control expression `[:slim, :control, code, content]`
|
9
|
+
#
|
10
|
+
# @param [String] code Ruby code
|
11
|
+
# @param [Array] content Temple expression
|
12
|
+
# @return [Array] Compiled temple expression
|
13
|
+
def on_slim_control(code, content)
|
14
|
+
@self[3] = compile(content)
|
15
|
+
if code.last.last.value =~ BLOCK_REGEX && content[0].value == :multi
|
16
|
+
@self[3].insert(1, Sexp.new(:slim_lint, :indent, start: content.start, finish: content.start))
|
17
|
+
@self[3].insert(-1, Sexp.new(:slim_lint, :outdent, start: content.finish, finish: content.finish))
|
18
|
+
end
|
19
|
+
|
20
|
+
@self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Handle output expression `[:slim, :control, escape, code, content]`
|
24
|
+
#
|
25
|
+
# @param [String] code Ruby code
|
26
|
+
# @param [Array] content Temple expression
|
27
|
+
# @return [Array] Compiled temple expression
|
28
|
+
def on_slim_output(escape, code, content)
|
29
|
+
@self[4] = compile(content)
|
30
|
+
if code.last.last.value =~ BLOCK_REGEX && content[0].value == :multi
|
31
|
+
@self[4].insert(1, Sexp.new(:slim_lint, :indent, start: content.start, finish: content.start))
|
32
|
+
@self[4].insert(-1, Sexp.new(:slim_lint, :outdent, start: content.finish, finish: content.finish))
|
33
|
+
end
|
34
|
+
|
35
|
+
@self
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
module Filters
|
5
|
+
# A dumbed-down version of {Slim::Controls} which doesn't introduce temporary
|
6
|
+
# variables and other cruft (which in the context of extracting Ruby code,
|
7
|
+
# results in a lot of weird cops reported by RuboCop).
|
8
|
+
class ControlProcessor < Filter
|
9
|
+
BLOCK_RE = /\A(if|unless)\b|\bdo\s*(\|[^|]*\|)?\s*$/
|
10
|
+
|
11
|
+
# Handle output expression `[:slim, :output, escape, code, content]`
|
12
|
+
#
|
13
|
+
# @param _escape [Boolean]
|
14
|
+
# @param code [Sexp]
|
15
|
+
# @param content [Sexp]
|
16
|
+
# @return [Sexp]
|
17
|
+
def on_slim_output(_escape, code, content)
|
18
|
+
_, lines = code
|
19
|
+
|
20
|
+
code.start = @self.start
|
21
|
+
code.finish = @self.finish
|
22
|
+
code << compile(content)
|
23
|
+
|
24
|
+
if lines.last[BLOCK_RE]
|
25
|
+
code << Sexp.new(Atom.new(:code, pos: code.finish), "end", start: code.finish, finish: code.finish)
|
26
|
+
end
|
27
|
+
|
28
|
+
Sexp.new(
|
29
|
+
Atom.new(:dynamic, pos: code.start),
|
30
|
+
code,
|
31
|
+
start: code.start,
|
32
|
+
finish: code.finish
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Handle text expression `[:slim, :text, type, content]`
|
37
|
+
#
|
38
|
+
# @param _type [Symbol]
|
39
|
+
# @param content [Sexp]
|
40
|
+
# @return [Sexp]
|
41
|
+
def on_slim_text(_type, content)
|
42
|
+
compile(content)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SlimLint
|
2
|
+
module Filters
|
3
|
+
# In Slim you don't need the do keyword sometimes. This
|
4
|
+
# filter adds the missing keyword.
|
5
|
+
#
|
6
|
+
# - 10.times
|
7
|
+
# | Hello
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class DoInserter < Filter
|
11
|
+
BLOCK_REGEX = /(\A(if|unless|else|elsif|when|begin|rescue|ensure|case)\b)|\bdo\s*(\|[^|]*\|\s*)?\Z/
|
12
|
+
|
13
|
+
# Handle control expression `[:slim, :control, code, content]`
|
14
|
+
#
|
15
|
+
# @param [Sexp] code Ruby code
|
16
|
+
# @param [Sexp] content Temple expression
|
17
|
+
# @return [Sexp] Compiled temple expression
|
18
|
+
def on_slim_control(code, content)
|
19
|
+
_, lines = code
|
20
|
+
lines.last.value.concat(" do") unless lines.last.value =~ BLOCK_REGEX || empty_exp?(content)
|
21
|
+
@self[3] = compile(content)
|
22
|
+
@self
|
23
|
+
end
|
24
|
+
|
25
|
+
# Handle output expression `[:slim, :output, escape, code, content]`
|
26
|
+
#
|
27
|
+
# @param [Boolean] escape Escape html
|
28
|
+
# @param [Sexp] code Ruby code
|
29
|
+
# @param [Sexp] content Temple expression
|
30
|
+
# @return [Sexp] Compiled temple expression
|
31
|
+
def on_slim_output(escape, code, content)
|
32
|
+
_, lines = code
|
33
|
+
lines.last.value.concat(" do") unless lines.last.value =~ BLOCK_REGEX || empty_exp?(content)
|
34
|
+
@self[4] = compile(content)
|
35
|
+
@self
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module SlimLint
|
2
|
+
module Filters
|
3
|
+
# In Slim you don't need to close any blocks:
|
4
|
+
#
|
5
|
+
# - if Slim.awesome?
|
6
|
+
# | But of course it is!
|
7
|
+
#
|
8
|
+
# However, the parser is not smart enough (and that's a good thing) to
|
9
|
+
# automatically insert end's where they are needed. Luckily, this filter
|
10
|
+
# does *exactly* that (and it does it well!)
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class EndInserter < Filter
|
14
|
+
IF_RE = /\A(if|begin|unless|else|elsif|when|rescue|ensure)\b|\bdo\s*(\|[^|]*\|)?\s*$/
|
15
|
+
ELSE_RE = /\A(else|elsif|when|rescue|ensure)\b/
|
16
|
+
END_RE = /\Aend\b/
|
17
|
+
|
18
|
+
# Handle multi expression `[:multi, *exps]`
|
19
|
+
#
|
20
|
+
# @return [Sexp] Corrected Temple expression with ends inserted
|
21
|
+
def on_multi(*exps)
|
22
|
+
@self.clear
|
23
|
+
@self.concat(@key)
|
24
|
+
|
25
|
+
# This variable is true if the previous line was
|
26
|
+
# (1) a control code and (2) contained indented content.
|
27
|
+
prev_indent = false
|
28
|
+
|
29
|
+
exps.each do |exp|
|
30
|
+
if control?(exp)
|
31
|
+
code_frags = exp[2].last
|
32
|
+
statement = code_frags.last.value
|
33
|
+
raise(Temple::FilterError, "Explicit end statements are forbidden") if END_RE.match?(statement)
|
34
|
+
|
35
|
+
# Two control code in a row. If this one is *not*
|
36
|
+
# an else block, we should close the previous one.
|
37
|
+
if prev_indent && statement !~ ELSE_RE
|
38
|
+
@self << Sexp.new(:code, "end", start: prev_indent.start, finish: prev_indent.start)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Indent if the control code starts a block.
|
42
|
+
prev_indent = (statement =~ IF_RE) && exp
|
43
|
+
elsif prev_indent
|
44
|
+
# This is *not* a control code, so we should close the previous one.
|
45
|
+
# Ignores newlines because they will be inserted after each line.
|
46
|
+
@self << Sexp.new(:code, "end", start: prev_indent.start, finish: prev_indent.start)
|
47
|
+
prev_indent = false
|
48
|
+
end
|
49
|
+
|
50
|
+
@self << compile(exp)
|
51
|
+
end
|
52
|
+
|
53
|
+
# The last line can be a control code too.
|
54
|
+
if prev_indent
|
55
|
+
@self << Sexp.new(:code, "end", start: prev_indent.start, finish: prev_indent.start)
|
56
|
+
end
|
57
|
+
|
58
|
+
@self
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Checks if an expression is a Slim control code
|
64
|
+
def control?(exp)
|
65
|
+
exp[0].value == :slim && exp[1].value == :control
|
66
|
+
end
|
67
|
+
|
68
|
+
# Checks if an expression is Slim embedded code
|
69
|
+
def embedded?(exp)
|
70
|
+
exp[0].value == :slim && exp[1].value == :embedded
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module SlimLint
|
2
|
+
module Filters
|
3
|
+
# Alternative implementation of Slim::Interpolation that operates without
|
4
|
+
# destroying the Sexp position data.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
class Interpolation < Filter
|
8
|
+
# Handle interpolate expression `[:slim, :interpolate, string]`
|
9
|
+
#
|
10
|
+
# @param [String] string Static interpolate
|
11
|
+
# @return [Array] Compiled temple expression
|
12
|
+
def on_slim_interpolate(string)
|
13
|
+
# Interpolate variables in text (#{variable}).
|
14
|
+
# Split the text into multiple dynamic and static parts.
|
15
|
+
block = Sexp.new(:multi, start: @self.start, finish: @self.finish)
|
16
|
+
line, column = string.start
|
17
|
+
string = string.to_s
|
18
|
+
loop do
|
19
|
+
case string
|
20
|
+
when /\A\\#\{/
|
21
|
+
# Escaped interpolation
|
22
|
+
block << Sexp.new(:static, '#{', start: [line, column], finish: [line, (column += 2)])
|
23
|
+
string = $'
|
24
|
+
when /\A#\{((?>[^{}]|(\{(?>[^{}]|\g<1>)*\}))*)\}/
|
25
|
+
# Interpolation
|
26
|
+
_, string, code = $&, $', $1
|
27
|
+
escape = code !~ /\A\{.*\}\Z/
|
28
|
+
|
29
|
+
column += 2
|
30
|
+
unless escape
|
31
|
+
code = code[1..-2]
|
32
|
+
column += 1
|
33
|
+
end
|
34
|
+
|
35
|
+
start = [line, column]
|
36
|
+
finish = [line, column + code.size]
|
37
|
+
|
38
|
+
block << Sexp.new(
|
39
|
+
:slim,
|
40
|
+
:output,
|
41
|
+
escape,
|
42
|
+
Sexp.new(
|
43
|
+
:multi,
|
44
|
+
Sexp.new(:interpolated, code, start: start, finish: finish),
|
45
|
+
start: start,
|
46
|
+
finish: finish
|
47
|
+
),
|
48
|
+
Sexp.new(:multi, start: start, finish: finish),
|
49
|
+
start: start,
|
50
|
+
finish: finish
|
51
|
+
)
|
52
|
+
|
53
|
+
column += code.size + 1
|
54
|
+
column += 1 unless escape
|
55
|
+
when /\A([#\\]?[^#\\]*([#\\][^\\{#][^#\\]*)*)/
|
56
|
+
# Static text
|
57
|
+
text, string = $&, $'
|
58
|
+
text_lines = text.count("\n")
|
59
|
+
|
60
|
+
block << Sexp.new(:static, text, start: [line, column], finish: [(line + text_lines), (text_lines == 0 ? column + text.size : 1)])
|
61
|
+
|
62
|
+
line += text_lines
|
63
|
+
column = (text_lines == 0 ? column + text.size : 1)
|
64
|
+
end
|
65
|
+
|
66
|
+
break if string.empty?
|
67
|
+
end
|
68
|
+
|
69
|
+
block
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|