slim_lint_standard 0.0.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.
- 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
|