haml 5.2.2 → 6.0.0.beta.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.
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