haml 5.2.2 → 6.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +13 -9
  4. data/.gitignore +16 -16
  5. data/CHANGELOG.md +13 -3
  6. data/Gemfile +18 -11
  7. data/MIT-LICENSE +1 -1
  8. data/README.md +13 -19
  9. data/Rakefile +95 -93
  10. data/bin/bench +66 -0
  11. data/bin/console +11 -0
  12. data/bin/ruby +3 -0
  13. data/bin/setup +7 -0
  14. data/bin/stackprof +27 -0
  15. data/bin/test +24 -0
  16. data/exe/haml +6 -0
  17. data/ext/haml/extconf.rb +10 -0
  18. data/ext/haml/haml.c +537 -0
  19. data/ext/haml/hescape.c +108 -0
  20. data/ext/haml/hescape.h +20 -0
  21. data/haml.gemspec +39 -37
  22. data/lib/haml/ambles.rb +20 -0
  23. data/lib/haml/attribute_builder.rb +135 -179
  24. data/lib/haml/attribute_compiler.rb +85 -194
  25. data/lib/haml/attribute_parser.rb +86 -126
  26. data/lib/haml/cli.rb +154 -0
  27. data/lib/haml/compiler/children_compiler.rb +126 -0
  28. data/lib/haml/compiler/comment_compiler.rb +39 -0
  29. data/lib/haml/compiler/doctype_compiler.rb +46 -0
  30. data/lib/haml/compiler/script_compiler.rb +116 -0
  31. data/lib/haml/compiler/silent_script_compiler.rb +24 -0
  32. data/lib/haml/compiler/tag_compiler.rb +76 -0
  33. data/lib/haml/compiler.rb +63 -296
  34. data/lib/haml/dynamic_merger.rb +67 -0
  35. data/lib/haml/engine.rb +42 -227
  36. data/lib/haml/error.rb +3 -52
  37. data/lib/haml/escapable.rb +6 -70
  38. data/lib/haml/filters/base.rb +12 -0
  39. data/lib/haml/filters/cdata.rb +20 -0
  40. data/lib/haml/filters/coffee.rb +17 -0
  41. data/lib/haml/filters/css.rb +33 -0
  42. data/lib/haml/filters/erb.rb +10 -0
  43. data/lib/haml/filters/escaped.rb +22 -0
  44. data/lib/haml/filters/javascript.rb +33 -0
  45. data/lib/haml/filters/less.rb +20 -0
  46. data/lib/haml/filters/markdown.rb +11 -0
  47. data/lib/haml/filters/plain.rb +29 -0
  48. data/lib/haml/filters/preserve.rb +22 -0
  49. data/lib/haml/filters/ruby.rb +10 -0
  50. data/lib/haml/filters/sass.rb +15 -0
  51. data/lib/haml/filters/scss.rb +15 -0
  52. data/lib/haml/filters/text_base.rb +25 -0
  53. data/lib/haml/filters/tilt_base.rb +49 -0
  54. data/lib/haml/filters.rb +54 -378
  55. data/lib/haml/force_escapable.rb +29 -0
  56. data/lib/haml/haml_error.rb +66 -0
  57. data/lib/haml/helpers.rb +3 -697
  58. data/lib/haml/html.rb +22 -0
  59. data/lib/haml/identity.rb +13 -0
  60. data/lib/haml/object_ref.rb +30 -0
  61. data/lib/haml/parser.rb +179 -49
  62. data/lib/haml/rails_helpers.rb +51 -0
  63. data/lib/haml/rails_template.rb +55 -0
  64. data/lib/haml/railtie.rb +7 -45
  65. data/lib/haml/ruby_expression.rb +32 -0
  66. data/lib/haml/string_splitter.rb +20 -0
  67. data/lib/haml/template.rb +15 -34
  68. data/lib/haml/temple_line_counter.rb +2 -1
  69. data/lib/haml/util.rb +17 -15
  70. data/lib/haml/version.rb +1 -2
  71. data/lib/haml.rb +8 -20
  72. metadata +211 -57
  73. data/.gitmodules +0 -3
  74. data/.yardopts +0 -22
  75. data/TODO +0 -24
  76. data/benchmark.rb +0 -70
  77. data/bin/haml +0 -9
  78. data/lib/haml/.gitattributes +0 -1
  79. data/lib/haml/buffer.rb +0 -182
  80. data/lib/haml/exec.rb +0 -347
  81. data/lib/haml/generator.rb +0 -42
  82. data/lib/haml/helpers/action_view_extensions.rb +0 -60
  83. data/lib/haml/helpers/action_view_mods.rb +0 -132
  84. data/lib/haml/helpers/action_view_xss_mods.rb +0 -60
  85. data/lib/haml/helpers/safe_erubi_template.rb +0 -20
  86. data/lib/haml/helpers/safe_erubis_template.rb +0 -33
  87. data/lib/haml/helpers/xss_mods.rb +0 -114
  88. data/lib/haml/options.rb +0 -273
  89. data/lib/haml/plugin.rb +0 -54
  90. data/lib/haml/sass_rails_filter.rb +0 -47
  91. data/lib/haml/template/options.rb +0 -27
  92. data/lib/haml/temple_engine.rb +0 -124
  93. data/yard/default/.gitignore +0 -1
  94. data/yard/default/fulldoc/html/css/common.sass +0 -15
  95. data/yard/default/layout/html/footer.erb +0 -12
data/lib/haml/cli.rb ADDED
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+ require 'haml'
3
+ require 'thor'
4
+
5
+ module Haml
6
+ class CLI < Thor
7
+ class_option :escape_html, type: :boolean, default: true
8
+ class_option :escape_attrs, type: :boolean, default: true
9
+
10
+ desc 'render HAML', 'Render haml template'
11
+ option :load_path, type: :string, aliases: %w[-I]
12
+ option :require, type: :string, aliases: %w[-r]
13
+ def render(file)
14
+ process_load_options
15
+ code = generate_code(file)
16
+ puts eval(code, binding, file)
17
+ end
18
+
19
+ desc 'compile HAML', 'Show compile result'
20
+ option :actionview, type: :boolean, default: false, aliases: %w[-a]
21
+ option :color, type: :boolean, default: true
22
+ option :check, type: :boolean, default: false, aliases: %w[-c]
23
+ def compile(file)
24
+ code = generate_code(file)
25
+ if options[:check]
26
+ if error = validate_ruby(code, file)
27
+ abort error.message.split("\n").first
28
+ end
29
+ puts "Syntax OK"
30
+ return
31
+ end
32
+ puts_code(code, color: options[:color])
33
+ end
34
+
35
+ desc 'temple HAML', 'Show temple intermediate expression'
36
+ option :color, type: :boolean, default: true
37
+ def temple(file)
38
+ pp_object(generate_temple(file), color: options[:color])
39
+ end
40
+
41
+ desc 'parse HAML', 'Show parse result'
42
+ option :color, type: :boolean, default: true
43
+ def parse(file)
44
+ pp_object(generate_ast(file), color: options[:color])
45
+ end
46
+
47
+ desc 'version', 'Show the used haml version'
48
+ def version
49
+ puts Haml::VERSION
50
+ end
51
+
52
+ private
53
+
54
+ def process_load_options
55
+ if options[:load_path]
56
+ options[:load_path].split(':').each do |dir|
57
+ $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
58
+ end
59
+ end
60
+
61
+ if options[:require]
62
+ require options[:require]
63
+ end
64
+ end
65
+
66
+ def read_file(file)
67
+ if file == '-'
68
+ STDIN.read
69
+ else
70
+ File.read(file)
71
+ end
72
+ end
73
+
74
+ def generate_code(file)
75
+ template = read_file(file)
76
+ if options[:actionview]
77
+ require 'action_view'
78
+ require 'action_view/base'
79
+ require 'haml/rails_template'
80
+ handler = Haml::RailsTemplate.new
81
+ template = ActionView::Template.new(template, 'inline template', handler, { locals: [] })
82
+ code = handler.call(template)
83
+ <<-end_src
84
+ def _inline_template___2144273726781623612_70327218547300(local_assigns, output_buffer)
85
+ _old_virtual_path, @virtual_path = @virtual_path, nil;_old_output_buffer = @output_buffer;;#{code}
86
+ ensure
87
+ @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
88
+ end
89
+ end_src
90
+ else
91
+ Haml::Engine.new(engine_options).call(template)
92
+ end
93
+ end
94
+
95
+ def generate_ast(file)
96
+ template = read_file(file)
97
+ Haml::Parser.new(engine_options).call(template)
98
+ end
99
+
100
+ def generate_temple(file)
101
+ ast = generate_ast(file)
102
+ Haml::Compiler.new(engine_options).call(ast)
103
+ end
104
+
105
+ def engine_options
106
+ Haml::Engine.options.to_h.merge(
107
+ escape_attrs: options[:escape_attrs],
108
+ escape_html: options[:escape_html],
109
+ )
110
+ end
111
+
112
+ # Flexible default_task, compatible with haml's CLI
113
+ def method_missing(*args)
114
+ return super(*args) if args.length > 1
115
+ render(args.first.to_s)
116
+ end
117
+
118
+ def puts_code(code, color: true)
119
+ begin
120
+ require 'irb/color'
121
+ rescue LoadError
122
+ color = false
123
+ end
124
+ if color
125
+ puts IRB::Color.colorize_code(code)
126
+ else
127
+ puts code
128
+ end
129
+ end
130
+
131
+ # Enable colored pretty printing only for development environment.
132
+ def pp_object(arg, color: true)
133
+ begin
134
+ require 'irb/color_printer'
135
+ rescue LoadError
136
+ color = false
137
+ end
138
+ if color
139
+ IRB::ColorPrinter.pp(arg)
140
+ else
141
+ require 'pp'
142
+ pp(arg)
143
+ end
144
+ end
145
+
146
+ def validate_ruby(code, file)
147
+ begin
148
+ eval("BEGIN {return nil}; #{code}", binding, file)
149
+ rescue ::SyntaxError # Not to be confused with Haml::SyntaxError
150
+ $!
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+ require 'haml/temple_line_counter'
3
+
4
+ module Haml
5
+ class Compiler
6
+ class ChildrenCompiler
7
+ def initialize
8
+ @lineno = 1
9
+ end
10
+
11
+ def compile(node, &block)
12
+ temple = [:multi]
13
+ return temple if node.children.empty?
14
+
15
+ temple << :whitespace if prepend_whitespace?(node)
16
+ node.children.each do |n|
17
+ rstrip_whitespace!(temple) if nuke_prev_whitespace?(n)
18
+ insert_newlines!(temple, n)
19
+ temple << moving_lineno(n) { block.call(n) }
20
+ temple << :whitespace if insert_whitespace?(n)
21
+ end
22
+ rstrip_whitespace!(temple) if nuke_inner_whitespace?(node)
23
+ confirm_whitespace(temple)
24
+ end
25
+
26
+ private
27
+
28
+ def insert_newlines!(temple, node)
29
+ (node.line - @lineno).times do
30
+ temple << [:newline]
31
+ end
32
+
33
+ @lineno = node.line
34
+ end
35
+
36
+ def moving_lineno(node, &block)
37
+ # before: As they may have children, we need to increment lineno before compilation.
38
+ case node.type
39
+ when :script, :silent_script
40
+ @lineno += 1
41
+ when :tag
42
+ [node.value[:dynamic_attributes].new, node.value[:dynamic_attributes].old].compact.each do |attribute_hash|
43
+ @lineno += attribute_hash.count("\n")
44
+ end
45
+ @lineno += 1 if node.children.empty? && node.value[:parse]
46
+ end
47
+
48
+ temple = block.call # compile
49
+
50
+ # after: filter may not have children, and for some dynamic filters we can't predict the number of lines.
51
+ case node.type
52
+ when :filter
53
+ @lineno += TempleLineCounter.count_lines(temple)
54
+ end
55
+
56
+ temple
57
+ end
58
+
59
+ def confirm_whitespace(temple)
60
+ temple.map do |exp|
61
+ case exp
62
+ when :whitespace
63
+ [:static, "\n"]
64
+ else
65
+ exp
66
+ end
67
+ end
68
+ end
69
+
70
+ def prepend_whitespace?(node)
71
+ return false unless %i[comment tag].include?(node.type)
72
+ !nuke_inner_whitespace?(node)
73
+ end
74
+
75
+ def nuke_inner_whitespace?(node)
76
+ case
77
+ when node.type == :tag
78
+ node.value[:nuke_inner_whitespace]
79
+ when node.parent.nil?
80
+ false
81
+ else
82
+ nuke_inner_whitespace?(node.parent)
83
+ end
84
+ end
85
+
86
+ def nuke_prev_whitespace?(node)
87
+ case node.type
88
+ when :tag
89
+ node.value[:nuke_outer_whitespace]
90
+ when :silent_script
91
+ !node.children.empty? && nuke_prev_whitespace?(node.children.first)
92
+ else
93
+ false
94
+ end
95
+ end
96
+
97
+ def nuke_outer_whitespace?(node)
98
+ return false if node.type != :tag
99
+ node.value[:nuke_outer_whitespace]
100
+ end
101
+
102
+ def rstrip_whitespace!(temple)
103
+ if temple[-1] == :whitespace
104
+ temple.delete_at(-1)
105
+ end
106
+ end
107
+
108
+ def insert_whitespace?(node)
109
+ return false if nuke_outer_whitespace?(node)
110
+
111
+ case node.type
112
+ when :doctype
113
+ node.value[:type] != 'xml'
114
+ when :comment, :plain, :tag
115
+ true
116
+ when :script
117
+ node.children.empty? && !nuke_inner_whitespace?(node)
118
+ when :filter
119
+ !%w[ruby].include?(node.value[:name])
120
+ else
121
+ false
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ class Compiler
4
+ class CommentCompiler
5
+ def compile(node, &block)
6
+ if node.value[:conditional]
7
+ compile_conditional_comment(node, &block)
8
+ else
9
+ compile_html_comment(node, &block)
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def compile_html_comment(node, &block)
16
+ if node.children.empty?
17
+ [:html, :comment, [:static, " #{node.value[:text]} "]]
18
+ else
19
+ [:html, :comment, yield(node)]
20
+ end
21
+ end
22
+
23
+ def compile_conditional_comment(node, &block)
24
+ condition = node.value[:conditional]
25
+ if node.value[:conditional] =~ /\A\[(\[*[^\[\]]+\]*)\]/
26
+ condition = $1
27
+ end
28
+
29
+ content =
30
+ if node.children.empty?
31
+ [:static, " #{node.value[:text]} "]
32
+ else
33
+ yield(node)
34
+ end
35
+ [:html, :condcomment, condition, content, node.value[:revealed]]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ class Compiler
4
+ class DoctypeCompiler
5
+ def initialize(options = {})
6
+ @format = options[:format]
7
+ end
8
+
9
+ def compile(node)
10
+ case node.value[:type]
11
+ when 'xml'
12
+ xml_doctype
13
+ when ''
14
+ html_doctype(node)
15
+ else
16
+ [:html, :doctype, node.value[:type]]
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def html_doctype(node)
23
+ version = node.value[:version] || :transitional
24
+ case @format
25
+ when :xhtml
26
+ [:html, :doctype, version]
27
+ when :html4
28
+ [:html, :doctype, :transitional]
29
+ when :html5
30
+ [:html, :doctype, :html]
31
+ else
32
+ [:html, :doctype, @format]
33
+ end
34
+ end
35
+
36
+ def xml_doctype
37
+ case @format
38
+ when :xhtml
39
+ [:static, "<?xml version='1.0' encoding='utf-8' ?>\n"]
40
+ else
41
+ [:multi]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+ require 'temple/static_analyzer'
3
+ require 'haml/ruby_expression'
4
+ require 'haml/string_splitter'
5
+
6
+ module Haml
7
+ class Compiler
8
+ class ScriptCompiler
9
+ def self.find_and_preserve(input, tags)
10
+ tags = tags.map { |tag| Regexp.escape(tag) }.join('|')
11
+ re = /<(#{tags})([^>]*)>(.*?)(<\/\1>)/im
12
+ input.to_s.gsub(re) do |s|
13
+ s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible
14
+ "<#{$1}#{$2}>#{Haml::Helpers.preserve($3)}</#{$1}>"
15
+ end
16
+ end
17
+
18
+ def initialize(identity, options)
19
+ @identity = identity
20
+ @disable_capture = options[:disable_capture]
21
+ end
22
+
23
+ def compile(node, &block)
24
+ unless Ripper.respond_to?(:lex) # No Ripper.lex in truffleruby
25
+ return dynamic_compile(node, &block)
26
+ end
27
+
28
+ no_children = node.children.empty?
29
+ case
30
+ when no_children && node.value[:escape_interpolation]
31
+ compile_interpolated_plain(node)
32
+ when no_children && RubyExpression.string_literal?(node.value[:text])
33
+ delegate_optimization(node)
34
+ when no_children && Temple::StaticAnalyzer.static?(node.value[:text])
35
+ static_compile(node)
36
+ else
37
+ dynamic_compile(node, &block)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # String-interpolated plain text must be compiled with this method
44
+ # because we have to escape only interpolated values.
45
+ def compile_interpolated_plain(node)
46
+ temple = [:multi]
47
+ StringSplitter.compile(node.value[:text]).each do |type, value|
48
+ case type
49
+ when :static
50
+ temple << [:static, value]
51
+ when :dynamic
52
+ temple << [:escape, node.value[:escape_interpolation], [:dynamic, value]]
53
+ end
54
+ end
55
+ temple << [:newline]
56
+ end
57
+
58
+ # :dynamic is optimized in other filter: StringSplitter
59
+ def delegate_optimization(node)
60
+ [:multi,
61
+ [:escape, node.value[:escape_html], [:dynamic, node.value[:text]]],
62
+ [:newline],
63
+ ]
64
+ end
65
+
66
+ def static_compile(node)
67
+ str = eval(node.value[:text]).to_s
68
+ if node.value[:escape_html]
69
+ str = Haml::Util.escape_html(str)
70
+ elsif node.value[:preserve]
71
+ str = ScriptCompiler.find_and_preserve(str, %w(textarea pre code))
72
+ end
73
+ [:multi, [:static, str], [:newline]]
74
+ end
75
+
76
+ def dynamic_compile(node, &block)
77
+ var = @identity.generate
78
+ temple = compile_script_assign(var, node, &block)
79
+ temple << compile_script_result(var, node)
80
+ end
81
+
82
+ def compile_script_assign(var, node, &block)
83
+ if node.children.empty?
84
+ [:multi,
85
+ [:code, "#{var} = (#{node.value[:text]}"],
86
+ [:newline],
87
+ [:code, ')'],
88
+ ]
89
+ else
90
+ [:multi,
91
+ [:block, "#{var} = #{node.value[:text]}",
92
+ [:multi, [:newline], @disable_capture ? yield(node) : [:capture, Temple::Utils.unique_name, yield(node)]]
93
+ ],
94
+ ]
95
+ end
96
+ end
97
+
98
+ def compile_script_result(result, node)
99
+ if !node.value[:escape_html] && node.value[:preserve]
100
+ result = find_and_preserve(result)
101
+ else
102
+ result = "(#{result}).to_s"
103
+ end
104
+ [:escape, node.value[:escape_html], [:dynamic, result]]
105
+ end
106
+
107
+ def find_and_preserve(code)
108
+ %Q[::Haml::Compiler::ScriptCompiler.find_and_preserve(#{code}, %w(textarea pre code))]
109
+ end
110
+
111
+ def escape_html(temple)
112
+ [:escape, true, temple]
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ class Compiler
4
+ class SilentScriptCompiler
5
+ def compile(node, &block)
6
+ if node.children.empty?
7
+ [:multi, [:code, node.value[:text]], [:newline]]
8
+ else
9
+ compile_with_children(node, &block)
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def compile_with_children(node, &block)
16
+ [:multi,
17
+ [:block, node.value[:text],
18
+ [:multi, [:newline], yield(node)],
19
+ ],
20
+ ]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+ require 'haml/util'
3
+ require 'haml/attribute_compiler'
4
+ require 'haml/string_splitter'
5
+
6
+ module Haml
7
+ class Compiler
8
+ class TagCompiler
9
+ def initialize(identity, options)
10
+ @autoclose = options[:autoclose]
11
+ @identity = identity
12
+ @attribute_compiler = AttributeCompiler.new(identity, options)
13
+ end
14
+
15
+ def compile(node, &block)
16
+ attrs = @attribute_compiler.compile(node)
17
+ contents = compile_contents(node, &block)
18
+ [:html, :tag, node.value[:name], attrs, contents]
19
+ end
20
+
21
+ private
22
+
23
+ def compile_contents(node, &block)
24
+ case
25
+ when !node.children.empty?
26
+ yield(node)
27
+ when node.value[:value].nil? && self_closing?(node)
28
+ nil
29
+ when node.value[:parse]
30
+ return compile_interpolated_plain(node) if node.value[:escape_interpolation]
31
+ if Ripper.respond_to?(:lex) # No Ripper.lex in truffleruby
32
+ return delegate_optimization(node) if RubyExpression.string_literal?(node.value[:value])
33
+ return delegate_optimization(node) if Temple::StaticAnalyzer.static?(node.value[:value])
34
+ end
35
+
36
+ var = @identity.generate
37
+ [:multi,
38
+ [:code, "#{var} = (#{node.value[:value]}"],
39
+ [:newline],
40
+ [:code, ')'],
41
+ [:escape, node.value[:escape_html], [:dynamic, var]]
42
+ ]
43
+ else
44
+ [:static, node.value[:value]]
45
+ end
46
+ end
47
+
48
+ # :dynamic is optimized in other filters: StringSplitter or StaticAnalyzer
49
+ def delegate_optimization(node)
50
+ [:multi,
51
+ [:escape, node.value[:escape_html], [:dynamic, node.value[:value]]],
52
+ [:newline],
53
+ ]
54
+ end
55
+
56
+ # We should handle interpolation here to escape only interpolated values.
57
+ def compile_interpolated_plain(node)
58
+ temple = [:multi]
59
+ StringSplitter.compile(node.value[:value]).each do |type, value|
60
+ case type
61
+ when :static
62
+ temple << [:static, value]
63
+ when :dynamic
64
+ temple << [:escape, node.value[:escape_interpolation], [:dynamic, value]]
65
+ end
66
+ end
67
+ temple << [:newline]
68
+ end
69
+
70
+ def self_closing?(node)
71
+ return true if @autoclose && @autoclose.include?(node.value[:name])
72
+ node.value[:self_closing]
73
+ end
74
+ end
75
+ end
76
+ end