hamlit 1.7.2 → 2.0.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 (283) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -3
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +25 -37
  5. data/CHANGELOG.md +18 -0
  6. data/Gemfile +16 -0
  7. data/LICENSE.txt +23 -2
  8. data/README.md +106 -48
  9. data/REFERENCE.md +222 -0
  10. data/Rakefile +77 -19
  11. data/benchmark/boolean_attribute.haml +6 -0
  12. data/benchmark/class_attribute.haml +5 -0
  13. data/benchmark/common_attribute.haml +3 -0
  14. data/benchmark/data_attribute.haml +4 -0
  15. data/benchmark/dynamic_attributes/boolean_attribute.haml +4 -0
  16. data/benchmark/dynamic_attributes/class_attribute.haml +4 -0
  17. data/benchmark/dynamic_attributes/common_attribute.haml +2 -0
  18. data/benchmark/dynamic_attributes/data_attribute.haml +2 -0
  19. data/benchmark/dynamic_attributes/id_attribute.haml +2 -0
  20. data/benchmark/etc/attribute_builder.haml +5 -0
  21. data/benchmark/etc/real_sample.haml +888 -0
  22. data/benchmark/etc/real_sample.rb +11 -0
  23. data/benchmark/etc/static_analyzer.haml +1 -0
  24. data/benchmark/etc/tags.haml +3 -0
  25. data/benchmark/ext/build_data.rb +15 -0
  26. data/benchmark/ext/build_id.rb +13 -0
  27. data/benchmark/id_attribute.haml +3 -0
  28. data/benchmark/plain.haml +4 -0
  29. data/benchmark/script.haml +4 -0
  30. data/benchmark/slim/LICENSE +21 -0
  31. data/{benchmarks → benchmark/slim}/context.rb +2 -4
  32. data/benchmark/slim/run-benchmarks.rb +94 -0
  33. data/{benchmarks → benchmark/slim}/view.erb +3 -3
  34. data/{benchmarks → benchmark/slim}/view.haml +0 -0
  35. data/{benchmarks/view.escaped.slim → benchmark/slim/view.slim} +1 -1
  36. data/benchmark/string_interpolation.haml +2 -0
  37. data/benchmark/utils/benchmark_ips_extension.rb +43 -0
  38. data/bin/bench +85 -0
  39. data/bin/clone +14 -0
  40. data/bin/console +11 -0
  41. data/bin/lineprof +48 -0
  42. data/bin/ruby +3 -0
  43. data/bin/setup +7 -0
  44. data/bin/stackprof +27 -0
  45. data/{test → bin/test} +6 -10
  46. data/{bin → exe}/hamlit +0 -0
  47. data/ext/hamlit/extconf.rb +14 -0
  48. data/ext/hamlit/hamlit.c +512 -0
  49. data/ext/hamlit/houdini/.gitignore +3 -0
  50. data/ext/hamlit/houdini/COPYING +7 -0
  51. data/ext/hamlit/houdini/Makefile +79 -0
  52. data/ext/hamlit/houdini/README.md +59 -0
  53. data/ext/hamlit/houdini/buffer.c +249 -0
  54. data/ext/hamlit/houdini/buffer.h +113 -0
  55. data/ext/hamlit/houdini/houdini.h +46 -0
  56. data/ext/hamlit/houdini/houdini_href_e.c +115 -0
  57. data/ext/hamlit/houdini/houdini_html_e.c +90 -0
  58. data/ext/hamlit/houdini/houdini_html_u.c +122 -0
  59. data/ext/hamlit/houdini/houdini_js_e.c +90 -0
  60. data/ext/hamlit/houdini/houdini_js_u.c +60 -0
  61. data/ext/hamlit/houdini/houdini_uri_e.c +107 -0
  62. data/ext/hamlit/houdini/houdini_uri_u.c +68 -0
  63. data/ext/hamlit/houdini/houdini_xml_e.c +136 -0
  64. data/ext/hamlit/houdini/html_unescape.gperf +258 -0
  65. data/ext/hamlit/houdini/html_unescape.h +754 -0
  66. data/ext/hamlit/houdini/tools/build_table.py +13 -0
  67. data/ext/hamlit/houdini/tools/build_tables.c +51 -0
  68. data/ext/hamlit/houdini/tools/wikipedia_table.txt +2025 -0
  69. data/hamlit.gemspec +30 -31
  70. data/lib/hamlit.rb +3 -1
  71. data/lib/hamlit/attribute_builder.rb +12 -0
  72. data/lib/hamlit/cli.rb +44 -43
  73. data/lib/hamlit/compiler.rb +92 -16
  74. data/lib/hamlit/compiler/attribute_compiler.rb +148 -0
  75. data/lib/hamlit/compiler/children_compiler.rb +111 -0
  76. data/lib/hamlit/compiler/comment_compiler.rb +36 -0
  77. data/lib/hamlit/compiler/doctype_compiler.rb +45 -0
  78. data/lib/hamlit/compiler/script_compiler.rb +97 -0
  79. data/lib/hamlit/compiler/silent_script_compiler.rb +24 -0
  80. data/lib/hamlit/compiler/tag_compiler.rb +69 -0
  81. data/lib/hamlit/engine.rb +12 -7
  82. data/lib/hamlit/error.rb +14 -0
  83. data/lib/hamlit/escapable.rb +12 -0
  84. data/lib/hamlit/filters.rb +65 -0
  85. data/lib/hamlit/filters/base.rb +4 -62
  86. data/lib/hamlit/filters/coffee.rb +9 -7
  87. data/lib/hamlit/filters/css.rb +25 -8
  88. data/lib/hamlit/filters/erb.rb +4 -6
  89. data/lib/hamlit/filters/escaped.rb +11 -9
  90. data/lib/hamlit/filters/javascript.rb +25 -8
  91. data/lib/hamlit/filters/less.rb +9 -7
  92. data/lib/hamlit/filters/markdown.rb +5 -6
  93. data/lib/hamlit/filters/plain.rb +11 -15
  94. data/lib/hamlit/filters/preserve.rb +15 -5
  95. data/lib/hamlit/filters/ruby.rb +3 -5
  96. data/lib/hamlit/filters/sass.rb +9 -7
  97. data/lib/hamlit/filters/scss.rb +9 -7
  98. data/lib/hamlit/filters/text_base.rb +24 -0
  99. data/lib/hamlit/filters/tilt_base.rb +47 -0
  100. data/lib/hamlit/hash_parser.rb +107 -0
  101. data/lib/hamlit/html.rb +9 -6
  102. data/lib/hamlit/identity.rb +12 -0
  103. data/lib/hamlit/object_ref.rb +29 -0
  104. data/lib/hamlit/parser.rb +25 -142
  105. data/lib/hamlit/parser/MIT-LICENSE +20 -0
  106. data/lib/hamlit/parser/README.md +28 -0
  107. data/lib/hamlit/parser/haml_buffer.rb +348 -0
  108. data/lib/hamlit/parser/haml_compiler.rb +553 -0
  109. data/lib/hamlit/parser/haml_error.rb +61 -0
  110. data/lib/hamlit/parser/haml_helpers.rb +727 -0
  111. data/lib/hamlit/parser/haml_options.rb +286 -0
  112. data/lib/hamlit/parser/haml_parser.rb +801 -0
  113. data/lib/hamlit/parser/haml_util.rb +283 -0
  114. data/lib/hamlit/parser/haml_xss_mods.rb +109 -0
  115. data/lib/hamlit/{helpers.rb → rails_helpers.rb} +2 -7
  116. data/lib/hamlit/rails_template.rb +30 -0
  117. data/lib/hamlit/railtie.rb +1 -12
  118. data/lib/hamlit/ruby_expression.rb +31 -0
  119. data/lib/hamlit/static_analyzer.rb +49 -0
  120. data/lib/hamlit/string_interpolation.rb +69 -0
  121. data/lib/hamlit/template.rb +8 -0
  122. data/lib/hamlit/utils.rb +9 -0
  123. data/lib/hamlit/version.rb +1 -1
  124. metadata +116 -324
  125. data/.rspec +0 -2
  126. data/benchmarks/benchmark.rb +0 -110
  127. data/benchmarks/view.slim +0 -17
  128. data/doc/README.md +0 -19
  129. data/doc/engine/indent.md +0 -48
  130. data/doc/engine/new_attribute.md +0 -77
  131. data/doc/engine/old_attributes.md +0 -198
  132. data/doc/engine/silent_script.md +0 -97
  133. data/doc/engine/tag.md +0 -48
  134. data/doc/engine/text.md +0 -64
  135. data/doc/faml/README.md +0 -16
  136. data/doc/faml/engine/indent.md +0 -48
  137. data/doc/faml/engine/old_attributes.md +0 -111
  138. data/doc/faml/engine/silent_script.md +0 -97
  139. data/doc/faml/engine/text.md +0 -59
  140. data/doc/faml/filters/erb.md +0 -24
  141. data/doc/faml/filters/javascript.md +0 -27
  142. data/doc/faml/filters/less.md +0 -57
  143. data/doc/faml/filters/plain.md +0 -25
  144. data/doc/filters/erb.md +0 -31
  145. data/doc/filters/javascript.md +0 -83
  146. data/doc/filters/less.md +0 -57
  147. data/doc/filters/markdown.md +0 -31
  148. data/doc/filters/plain.md +0 -25
  149. data/doc/haml/README.md +0 -15
  150. data/doc/haml/engine/new_attribute.md +0 -77
  151. data/doc/haml/engine/old_attributes.md +0 -142
  152. data/doc/haml/engine/tag.md +0 -48
  153. data/doc/haml/engine/text.md +0 -29
  154. data/doc/haml/filters/erb.md +0 -26
  155. data/doc/haml/filters/javascript.md +0 -76
  156. data/doc/haml/filters/markdown.md +0 -31
  157. data/lib/hamlit/attribute.rb +0 -78
  158. data/lib/hamlit/compilers/attributes.rb +0 -108
  159. data/lib/hamlit/compilers/comment.rb +0 -13
  160. data/lib/hamlit/compilers/doctype.rb +0 -39
  161. data/lib/hamlit/compilers/filter.rb +0 -53
  162. data/lib/hamlit/compilers/new_attribute.rb +0 -115
  163. data/lib/hamlit/compilers/old_attribute.rb +0 -241
  164. data/lib/hamlit/compilers/runtime_attribute.rb +0 -58
  165. data/lib/hamlit/compilers/script.rb +0 -31
  166. data/lib/hamlit/compilers/strip.rb +0 -19
  167. data/lib/hamlit/compilers/text.rb +0 -111
  168. data/lib/hamlit/concerns/attribute_builder.rb +0 -22
  169. data/lib/hamlit/concerns/balanceable.rb +0 -68
  170. data/lib/hamlit/concerns/deprecation.rb +0 -20
  171. data/lib/hamlit/concerns/error.rb +0 -31
  172. data/lib/hamlit/concerns/escapable.rb +0 -17
  173. data/lib/hamlit/concerns/included.rb +0 -28
  174. data/lib/hamlit/concerns/indentable.rb +0 -117
  175. data/lib/hamlit/concerns/lexable.rb +0 -32
  176. data/lib/hamlit/concerns/line_reader.rb +0 -62
  177. data/lib/hamlit/concerns/registerable.rb +0 -24
  178. data/lib/hamlit/concerns/string_interpolation.rb +0 -48
  179. data/lib/hamlit/concerns/whitespace.rb +0 -91
  180. data/lib/hamlit/filters/tilt.rb +0 -41
  181. data/lib/hamlit/parsers/attribute.rb +0 -71
  182. data/lib/hamlit/parsers/comment.rb +0 -30
  183. data/lib/hamlit/parsers/doctype.rb +0 -18
  184. data/lib/hamlit/parsers/filter.rb +0 -18
  185. data/lib/hamlit/parsers/multiline.rb +0 -58
  186. data/lib/hamlit/parsers/script.rb +0 -126
  187. data/lib/hamlit/parsers/tag.rb +0 -83
  188. data/lib/hamlit/parsers/text.rb +0 -28
  189. data/lib/hamlit/temple.rb +0 -9
  190. data/release +0 -6
  191. data/spec/Rakefile +0 -72
  192. data/spec/hamlit/engine/comment_spec.rb +0 -56
  193. data/spec/hamlit/engine/doctype_spec.rb +0 -19
  194. data/spec/hamlit/engine/error_spec.rb +0 -135
  195. data/spec/hamlit/engine/indent_spec.rb +0 -42
  196. data/spec/hamlit/engine/multiline_spec.rb +0 -44
  197. data/spec/hamlit/engine/new_attribute_spec.rb +0 -110
  198. data/spec/hamlit/engine/old_attributes_spec.rb +0 -404
  199. data/spec/hamlit/engine/script_spec.rb +0 -116
  200. data/spec/hamlit/engine/silent_script_spec.rb +0 -213
  201. data/spec/hamlit/engine/tag_spec.rb +0 -295
  202. data/spec/hamlit/engine/text_spec.rb +0 -239
  203. data/spec/hamlit/engine_spec.rb +0 -58
  204. data/spec/hamlit/filters/coffee_spec.rb +0 -60
  205. data/spec/hamlit/filters/css_spec.rb +0 -33
  206. data/spec/hamlit/filters/erb_spec.rb +0 -16
  207. data/spec/hamlit/filters/javascript_spec.rb +0 -82
  208. data/spec/hamlit/filters/less_spec.rb +0 -37
  209. data/spec/hamlit/filters/markdown_spec.rb +0 -30
  210. data/spec/hamlit/filters/plain_spec.rb +0 -15
  211. data/spec/hamlit/filters/ruby_spec.rb +0 -24
  212. data/spec/hamlit/filters/sass_spec.rb +0 -33
  213. data/spec/hamlit/filters/scss_spec.rb +0 -37
  214. data/spec/hamlit/haml_spec.rb +0 -910
  215. data/spec/rails/.gitignore +0 -18
  216. data/spec/rails/.rspec +0 -2
  217. data/spec/rails/Gemfile +0 -19
  218. data/spec/rails/README.rdoc +0 -28
  219. data/spec/rails/Rakefile +0 -6
  220. data/spec/rails/app/assets/images/.keep +0 -0
  221. data/spec/rails/app/assets/javascripts/application.js +0 -15
  222. data/spec/rails/app/assets/stylesheets/application.css +0 -15
  223. data/spec/rails/app/controllers/application_controller.rb +0 -8
  224. data/spec/rails/app/controllers/concerns/.keep +0 -0
  225. data/spec/rails/app/controllers/users_controller.rb +0 -23
  226. data/spec/rails/app/helpers/application_helper.rb +0 -2
  227. data/spec/rails/app/mailers/.keep +0 -0
  228. data/spec/rails/app/models/.keep +0 -0
  229. data/spec/rails/app/models/concerns/.keep +0 -0
  230. data/spec/rails/app/views/application/index.html.haml +0 -18
  231. data/spec/rails/app/views/layouts/application.html.haml +0 -12
  232. data/spec/rails/app/views/users/capture.html.haml +0 -5
  233. data/spec/rails/app/views/users/capture_haml.html.haml +0 -5
  234. data/spec/rails/app/views/users/form.html.haml +0 -2
  235. data/spec/rails/app/views/users/helpers.html.haml +0 -10
  236. data/spec/rails/app/views/users/index.html.haml +0 -9
  237. data/spec/rails/app/views/users/inline.html.haml +0 -6
  238. data/spec/rails/app/views/users/old_attributes.html.haml +0 -5
  239. data/spec/rails/app/views/users/safe_buffer.html.haml +0 -4
  240. data/spec/rails/app/views/users/whitespace.html.haml +0 -4
  241. data/spec/rails/bin/bundle +0 -3
  242. data/spec/rails/bin/rails +0 -8
  243. data/spec/rails/bin/rake +0 -8
  244. data/spec/rails/bin/setup +0 -29
  245. data/spec/rails/bin/spring +0 -15
  246. data/spec/rails/config.ru +0 -4
  247. data/spec/rails/config/application.rb +0 -34
  248. data/spec/rails/config/boot.rb +0 -3
  249. data/spec/rails/config/database.yml +0 -25
  250. data/spec/rails/config/environment.rb +0 -5
  251. data/spec/rails/config/environments/development.rb +0 -41
  252. data/spec/rails/config/environments/production.rb +0 -79
  253. data/spec/rails/config/environments/test.rb +0 -42
  254. data/spec/rails/config/initializers/assets.rb +0 -11
  255. data/spec/rails/config/initializers/backtrace_silencers.rb +0 -7
  256. data/spec/rails/config/initializers/cookies_serializer.rb +0 -3
  257. data/spec/rails/config/initializers/filter_parameter_logging.rb +0 -4
  258. data/spec/rails/config/initializers/inflections.rb +0 -16
  259. data/spec/rails/config/initializers/mime_types.rb +0 -4
  260. data/spec/rails/config/initializers/session_store.rb +0 -3
  261. data/spec/rails/config/initializers/wrap_parameters.rb +0 -14
  262. data/spec/rails/config/locales/en.yml +0 -24
  263. data/spec/rails/config/routes.rb +0 -16
  264. data/spec/rails/config/secrets.yml +0 -22
  265. data/spec/rails/db/schema.rb +0 -16
  266. data/spec/rails/db/seeds.rb +0 -7
  267. data/spec/rails/lib/assets/.keep +0 -0
  268. data/spec/rails/lib/tasks/.keep +0 -0
  269. data/spec/rails/log/.keep +0 -0
  270. data/spec/rails/public/404.html +0 -67
  271. data/spec/rails/public/422.html +0 -67
  272. data/spec/rails/public/500.html +0 -66
  273. data/spec/rails/public/favicon.ico +0 -0
  274. data/spec/rails/public/robots.txt +0 -5
  275. data/spec/rails/spec/hamlit_spec.rb +0 -123
  276. data/spec/rails/spec/rails_helper.rb +0 -56
  277. data/spec/rails/spec/spec_helper.rb +0 -91
  278. data/spec/rails/vendor/assets/javascripts/.keep +0 -0
  279. data/spec/rails/vendor/assets/stylesheets/.keep +0 -0
  280. data/spec/spec_helper.rb +0 -36
  281. data/spec/spec_helper/document_generator.rb +0 -93
  282. data/spec/spec_helper/render_helper.rb +0 -120
  283. data/spec/spec_helper/test_case.rb +0 -55
@@ -0,0 +1,286 @@
1
+ require 'hamlit/parser/haml_parser'
2
+ require 'hamlit/parser/haml_compiler'
3
+ require 'hamlit/parser/haml_error'
4
+
5
+ module Hamlit
6
+ # This class encapsulates all of the configuration options that Haml
7
+ # understands. Please see the {file:REFERENCE.md#options Haml Reference} to
8
+ # learn how to set the options.
9
+ class HamlOptions
10
+
11
+ @defaults = {
12
+ :attr_wrapper => "'",
13
+ :autoclose => %w(area base basefont br col command embed frame
14
+ hr img input isindex keygen link menuitem meta
15
+ param source track wbr),
16
+ :encoding => "UTF-8",
17
+ :escape_attrs => true,
18
+ :escape_html => false,
19
+ :filename => '(haml)',
20
+ :format => :html5,
21
+ :hyphenate_data_attrs => true,
22
+ :line => 1,
23
+ :mime_type => 'text/html',
24
+ :preserve => %w(textarea pre code),
25
+ :remove_whitespace => false,
26
+ :suppress_eval => false,
27
+ :ugly => false,
28
+ :cdata => false,
29
+ :parser_class => ::Hamlit::HamlParser,
30
+ :compiler_class => ::Hamlit::HamlCompiler,
31
+ :trace => false
32
+ }
33
+
34
+ @valid_formats = [:html4, :html5, :xhtml]
35
+
36
+ @buffer_option_keys = [:autoclose, :preserve, :attr_wrapper, :ugly, :format,
37
+ :encoding, :escape_html, :escape_attrs, :hyphenate_data_attrs, :cdata]
38
+
39
+ # The default option values.
40
+ # @return Hash
41
+ def self.defaults
42
+ @defaults
43
+ end
44
+
45
+ # An array of valid values for the `:format` option.
46
+ # @return Array
47
+ def self.valid_formats
48
+ @valid_formats
49
+ end
50
+
51
+ # An array of keys that will be used to provide a hash of options to
52
+ # {Haml::Buffer}.
53
+ # @return Hash
54
+ def self.buffer_option_keys
55
+ @buffer_option_keys
56
+ end
57
+
58
+ # The character that should wrap element attributes. This defaults to `'`
59
+ # (an apostrophe). Characters of this type within the attributes will be
60
+ # escaped (e.g. by replacing them with `'`) if the character is an
61
+ # apostrophe or a quotation mark.
62
+ attr_reader :attr_wrapper
63
+
64
+ # A list of tag names that should be automatically self-closed if they have
65
+ # no content. This can also contain regular expressions that match tag names
66
+ # (or any object which responds to `#===`). Defaults to `['meta', 'img',
67
+ # 'link', 'br', 'hr', 'input', 'area', 'param', 'col', 'base']`.
68
+ attr_accessor :autoclose
69
+
70
+ # The encoding to use for the HTML output.
71
+ # This can be a string or an `Encoding` Object. Note that Haml **does not**
72
+ # automatically re-encode Ruby values; any strings coming from outside the
73
+ # application should be converted before being passed into the Haml
74
+ # template. Defaults to `Encoding.default_internal`; if that's not set,
75
+ # defaults to the encoding of the Haml template; if that's `US-ASCII`,
76
+ # defaults to `"UTF-8"`.
77
+ attr_reader :encoding
78
+
79
+ # Sets whether or not to escape HTML-sensitive characters in attributes. If
80
+ # this is true, all HTML-sensitive characters in attributes are escaped. If
81
+ # it's set to false, no HTML-sensitive characters in attributes are escaped.
82
+ # If it's set to `:once`, existing HTML escape sequences are preserved, but
83
+ # other HTML-sensitive characters are escaped.
84
+ #
85
+ # Defaults to `true`.
86
+ attr_accessor :escape_attrs
87
+
88
+ # Sets whether or not to escape HTML-sensitive characters in script. If this
89
+ # is true, `=` behaves like {file:REFERENCE.md#escaping_html `&=`};
90
+ # otherwise, it behaves like {file:REFERENCE.md#unescaping_html `!=`}. Note
91
+ # that if this is set, `!=` should be used for yielding to subtemplates and
92
+ # rendering partials. See also {file:REFERENCE.md#escaping_html Escaping HTML} and
93
+ # {file:REFERENCE.md#unescaping_html Unescaping HTML}.
94
+ #
95
+ # Defaults to false.
96
+ attr_accessor :escape_html
97
+
98
+ # The name of the Haml file being parsed.
99
+ # This is only used as information when exceptions are raised. This is
100
+ # automatically assigned when working through ActionView, so it's really
101
+ # only useful for the user to assign when dealing with Haml programatically.
102
+ attr_accessor :filename
103
+
104
+ # If set to `true`, Haml will convert underscores to hyphens in all
105
+ # {file:REFERENCE.md#html5_custom_data_attributes Custom Data Attributes} As
106
+ # of Haml 4.0, this defaults to `true`.
107
+ attr_accessor :hyphenate_data_attrs
108
+
109
+ # The line offset of the Haml template being parsed. This is useful for
110
+ # inline templates, similar to the last argument to `Kernel#eval`.
111
+ attr_accessor :line
112
+
113
+ # Determines the output format. The default is `:html5`. The other options
114
+ # are `:html4` and `:xhtml`. If the output is set to XHTML, then Haml
115
+ # automatically generates self-closing tags and wraps the output of the
116
+ # Javascript and CSS-like filters inside CDATA. When the output is set to
117
+ # `:html5` or `:html4`, XML prologs are ignored. In all cases, an appropriate
118
+ # doctype is generated from `!!!`.
119
+ #
120
+ # If the mime_type of the template being rendered is `text/xml` then a
121
+ # format of `:xhtml` will be used even if the global output format is set to
122
+ # `:html4` or `:html5`.
123
+ attr :format
124
+
125
+ # The mime type that the rendered document will be served with. If this is
126
+ # set to `text/xml` then the format will be overridden to `:xhtml` even if
127
+ # it has set to `:html4` or `:html5`.
128
+ attr_accessor :mime_type
129
+
130
+ # A list of tag names that should automatically have their newlines
131
+ # preserved using the {Haml::Helpers#preserve} helper. This means that any
132
+ # content given on the same line as the tag will be preserved. For example,
133
+ # `%textarea= "Foo\nBar"` compiles to `<textarea>Foo&#x000A;Bar</textarea>`.
134
+ # Defaults to `['textarea', 'pre']`. See also
135
+ # {file:REFERENCE.md#whitespace_preservation Whitespace Preservation}.
136
+ attr_accessor :preserve
137
+
138
+ # If set to `true`, all tags are treated as if both
139
+ # {file:REFERENCE.md#whitespace_removal__and_ whitespace removal} options
140
+ # were present. Use with caution as this may cause whitespace-related
141
+ # formatting errors.
142
+ #
143
+ # Defaults to `false`.
144
+ attr_reader :remove_whitespace
145
+
146
+ # Whether or not attribute hashes and Ruby scripts designated by `=` or `~`
147
+ # should be evaluated. If this is `true`, said scripts are rendered as empty
148
+ # strings.
149
+ #
150
+ # Defaults to `false`.
151
+ attr_accessor :suppress_eval
152
+
153
+ # If set to `true`, Haml makes no attempt to properly indent or format the
154
+ # HTML output. This significantly improves rendering performance but makes
155
+ # viewing the source unpleasant.
156
+ #
157
+ # Defaults to `true` in Rails production mode, and `false` everywhere else.
158
+ attr_accessor :ugly
159
+
160
+ # Whether to include CDATA sections around javascript and css blocks when
161
+ # using the `:javascript` or `:css` filters.
162
+ #
163
+ # This option also affects the `:sass`, `:scss`, `:less` and `:coffeescript`
164
+ # filters.
165
+ #
166
+ # Defaults to `false` for html, `true` for xhtml. Cannot be changed when using
167
+ # xhtml.
168
+ attr_accessor :cdata
169
+
170
+ # The parser class to use. Defaults to Haml::Parser.
171
+ attr_accessor :parser_class
172
+
173
+ # The compiler class to use. Defaults to Haml::Compiler.
174
+ attr_accessor :compiler_class
175
+
176
+ # Enable template tracing. If true, it will add a 'data-trace' attribute to
177
+ # each tag generated by Haml. The value of the attribute will be the
178
+ # source template name and the line number from which the tag was generated,
179
+ # separated by a colon. On Rails applications, the path given will be a
180
+ # relative path as from the views directory. On non-Rails applications,
181
+ # the path will be the full path.
182
+ attr_accessor :trace
183
+
184
+ def initialize(values = {}, &block)
185
+ defaults.each {|k, v| instance_variable_set :"@#{k}", v}
186
+ values.each {|k, v| send("#{k}=", v) if defaults.has_key?(k) && !v.nil?}
187
+ yield if block_given?
188
+ end
189
+
190
+ # Retrieve an option value.
191
+ # @param key The value to retrieve.
192
+ def [](key)
193
+ send key
194
+ end
195
+
196
+ # Set an option value.
197
+ # @param key The key to set.
198
+ # @param value The value to set for the key.
199
+ def []=(key, value)
200
+ send "#{key}=", value
201
+ end
202
+
203
+ [:escape_attrs, :hyphenate_data_attrs, :remove_whitespace, :suppress_eval,
204
+ :ugly].each do |method|
205
+ class_eval(<<-END)
206
+ def #{method}?
207
+ !! @#{method}
208
+ end
209
+ END
210
+ end
211
+
212
+ # @return [Boolean] Whether or not the format is XHTML.
213
+ def xhtml?
214
+ not html?
215
+ end
216
+
217
+ # @return [Boolean] Whether or not the format is any flavor of HTML.
218
+ def html?
219
+ html4? or html5?
220
+ end
221
+
222
+ # @return [Boolean] Whether or not the format is HTML4.
223
+ def html4?
224
+ format == :html4
225
+ end
226
+
227
+ # @return [Boolean] Whether or not the format is HTML5.
228
+ def html5?
229
+ format == :html5
230
+ end
231
+
232
+ def attr_wrapper=(value)
233
+ @attr_wrapper = value || self.class.defaults[:attr_wrapper]
234
+ end
235
+
236
+ # Undef :format to suppress warning. It's defined above with the `:attr`
237
+ # macro in order to make it appear in Yard's list of instance attributes.
238
+ undef :format
239
+ def format
240
+ mime_type == "text/xml" ? :xhtml : @format
241
+ end
242
+
243
+ def format=(value)
244
+ unless self.class.valid_formats.include?(value)
245
+ raise ::Hamlit::HamlError, "Invalid output format #{value.inspect}"
246
+ end
247
+ @format = value
248
+ end
249
+
250
+ undef :cdata
251
+ def cdata
252
+ xhtml? || @cdata
253
+ end
254
+
255
+ def remove_whitespace=(value)
256
+ @ugly = true if value
257
+ @remove_whitespace = value
258
+ end
259
+
260
+ def encoding=(value)
261
+ return unless value
262
+ @encoding = value.is_a?(Encoding) ? value.name : value.to_s
263
+ @encoding = "UTF-8" if @encoding.upcase == "US-ASCII"
264
+ end
265
+
266
+ # Returns a subset of options: those that {Haml::Buffer} cares about.
267
+ # All of the values here are such that when `#inspect` is called on the hash,
268
+ # it can be `Kernel#eval`ed to get the same result back.
269
+ #
270
+ # See {file:REFERENCE.md#options the Haml options documentation}.
271
+ #
272
+ # @return [{Symbol => Object}] The options hash
273
+ def for_buffer
274
+ self.class.buffer_option_keys.inject({}) do |hash, key|
275
+ hash[key] = send(key)
276
+ hash
277
+ end
278
+ end
279
+
280
+ private
281
+
282
+ def defaults
283
+ self.class.defaults
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,801 @@
1
+ require 'strscan'
2
+ require 'hamlit/parser/haml_util'
3
+ require 'hamlit/parser/haml_buffer'
4
+ require 'hamlit/parser/haml_error'
5
+
6
+ module Hamlit
7
+ class HamlParser
8
+ include ::Hamlit::HamlUtil
9
+
10
+ attr_reader :root
11
+
12
+ # Designates an XHTML/XML element.
13
+ ELEMENT = ?%
14
+
15
+ # Designates a `<div>` element with the given class.
16
+ DIV_CLASS = ?.
17
+
18
+ # Designates a `<div>` element with the given id.
19
+ DIV_ID = ?#
20
+
21
+ # Designates an XHTML/XML comment.
22
+ COMMENT = ?/
23
+
24
+ # Designates an XHTML doctype or script that is never HTML-escaped.
25
+ DOCTYPE = ?!
26
+
27
+ # Designates script, the result of which is output.
28
+ SCRIPT = ?=
29
+
30
+ # Designates script that is always HTML-escaped.
31
+ SANITIZE = ?&
32
+
33
+ # Designates script, the result of which is flattened and output.
34
+ FLAT_SCRIPT = ?~
35
+
36
+ # Designates script which is run but not output.
37
+ SILENT_SCRIPT = ?-
38
+
39
+ # When following SILENT_SCRIPT, designates a comment that is not output.
40
+ SILENT_COMMENT = ?#
41
+
42
+ # Designates a non-parsed line.
43
+ ESCAPE = ?\\
44
+
45
+ # Designates a block of filtered text.
46
+ FILTER = ?:
47
+
48
+ # Designates a non-parsed line. Not actually a character.
49
+ PLAIN_TEXT = -1
50
+
51
+ # Keeps track of the ASCII values of the characters that begin a
52
+ # specially-interpreted line.
53
+ SPECIAL_CHARACTERS = [
54
+ ELEMENT,
55
+ DIV_CLASS,
56
+ DIV_ID,
57
+ COMMENT,
58
+ DOCTYPE,
59
+ SCRIPT,
60
+ SANITIZE,
61
+ FLAT_SCRIPT,
62
+ SILENT_SCRIPT,
63
+ ESCAPE,
64
+ FILTER
65
+ ]
66
+
67
+ # The value of the character that designates that a line is part
68
+ # of a multiline string.
69
+ MULTILINE_CHAR_VALUE = ?|
70
+
71
+ # Regex to check for blocks with spaces around arguments. Not to be confused
72
+ # with multiline script.
73
+ # For example:
74
+ # foo.each do | bar |
75
+ # = bar
76
+ #
77
+ BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
78
+
79
+ MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
80
+ START_BLOCK_KEYWORDS = %w[if begin case unless]
81
+ # Try to parse assignments to block starters as best as possible
82
+ START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
83
+ BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
84
+
85
+ # The Regex that matches a Doctype command.
86
+ DOCTYPE_REGEX = /(\d(?:\.\d)?)?\s*([a-z]*)\s*([^ ]+)?/i
87
+
88
+ # The Regex that matches a literal string or symbol value
89
+ LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?!\\|\#\{|\#@|\#\$|\2).|\\.)*\2/
90
+
91
+ ID_KEY = 'id'.freeze
92
+ CLASS_KEY = 'class'.freeze
93
+
94
+ def initialize(template, options)
95
+ @options = options
96
+ # Record the indent levels of "if" statements to validate the subsequent
97
+ # elsif and else statements are indented at the appropriate level.
98
+ @script_level_stack = []
99
+ @template_index = 0
100
+ @template_tabs = 0
101
+
102
+ match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
103
+ # discard the last match which is always blank
104
+ match.pop
105
+ @template = match.each_with_index.map do |(full, whitespace, text), index|
106
+ Line.new(whitespace, text.rstrip, full, index, self, false)
107
+ end
108
+ # Append special end-of-document marker
109
+ @template << Line.new(nil, '-#', '-#', @template.size, self, true)
110
+ end
111
+
112
+ def parse
113
+ @root = @parent = ParseNode.new(:root)
114
+ @flat = false
115
+ @filter_buffer = nil
116
+ @indentation = nil
117
+ @line = next_line
118
+
119
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:indenting_at_start), @line.index) if @line.tabs != 0
120
+
121
+ loop do
122
+ next_line
123
+
124
+ process_indent(@line) unless @line.text.empty?
125
+
126
+ if flat?
127
+ text = @line.full.dup
128
+ text = "" unless text.gsub!(/^#{@flat_spaces}/, '')
129
+ @filter_buffer << "#{text}\n"
130
+ @line = @next_line
131
+ next
132
+ end
133
+
134
+ @tab_up = nil
135
+ process_line(@line) unless @line.text.empty?
136
+ if block_opened? || @tab_up
137
+ @template_tabs += 1
138
+ @parent = @parent.children.last
139
+ end
140
+
141
+ if !flat? && @next_line.tabs - @line.tabs > 1
142
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
143
+ end
144
+
145
+ @line = @next_line
146
+ end
147
+ # Close all the open tags
148
+ close until @parent.type == :root
149
+ @root
150
+ rescue ::Hamlit::HamlError => e
151
+ e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
152
+ raise
153
+ end
154
+
155
+ def compute_tabs(line)
156
+ return 0 if line.text.empty? || !line.whitespace
157
+
158
+ if @indentation.nil?
159
+ @indentation = line.whitespace
160
+
161
+ if @indentation.include?(?\s) && @indentation.include?(?\t)
162
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:cant_use_tabs_and_spaces), line.index)
163
+ end
164
+
165
+ @flat_spaces = @indentation * (@template_tabs+1) if flat?
166
+ return 1
167
+ end
168
+
169
+ tabs = line.whitespace.length / @indentation.length
170
+ return tabs if line.whitespace == @indentation * tabs
171
+ return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
172
+
173
+ message = ::Hamlit::HamlError.message(:inconsistent_indentation,
174
+ human_indentation(line.whitespace),
175
+ human_indentation(@indentation)
176
+ )
177
+ raise ::Hamlit::HamlSyntaxError.new(message, line.index)
178
+ end
179
+
180
+ private
181
+
182
+ # @private
183
+ class Line < Struct.new(:whitespace, :text, :full, :index, :parser, :eod)
184
+ alias_method :eod?, :eod
185
+
186
+ # @private
187
+ def tabs
188
+ @tabs ||= parser.compute_tabs(self)
189
+ end
190
+
191
+ def strip!(from)
192
+ self.text = text[from..-1]
193
+ self.text.lstrip!
194
+ self
195
+ end
196
+ end
197
+
198
+ # @private
199
+ class ParseNode < Struct.new(:type, :line, :value, :parent, :children)
200
+ def initialize(*args)
201
+ super
202
+ self.children ||= []
203
+ end
204
+
205
+ def inspect
206
+ %Q[(#{type} #{value.inspect}#{children.each_with_object('') {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})]
207
+ end
208
+ end
209
+
210
+ # Processes and deals with lowering indentation.
211
+ def process_indent(line)
212
+ return unless line.tabs <= @template_tabs && @template_tabs > 0
213
+
214
+ to_close = @template_tabs - line.tabs
215
+ to_close.times {|i| close unless to_close - 1 - i == 0 && continuation_script?(line.text)}
216
+ end
217
+
218
+ def continuation_script?(text)
219
+ text[0] == SILENT_SCRIPT && mid_block_keyword?(text)
220
+ end
221
+
222
+ def mid_block_keyword?(text)
223
+ MID_BLOCK_KEYWORDS.include?(block_keyword(text))
224
+ end
225
+
226
+ # Processes a single line of Haml.
227
+ #
228
+ # This method doesn't return anything; it simply processes the line and
229
+ # adds the appropriate code to `@precompiled`.
230
+ def process_line(line)
231
+ case line.text[0]
232
+ when DIV_CLASS; push div(line)
233
+ when DIV_ID
234
+ return push plain(line) if %w[{ @ $].include?(line.text[1])
235
+ push div(line)
236
+ when ELEMENT; push tag(line)
237
+ when COMMENT; push comment(line.text[1..-1].lstrip)
238
+ when SANITIZE
239
+ return push plain(line.strip!(3), :escape_html) if line.text[1, 2] == '=='
240
+ return push script(line.strip!(2), :escape_html) if line.text[1] == SCRIPT
241
+ return push flat_script(line.strip!(2), :escape_html) if line.text[1] == FLAT_SCRIPT
242
+ return push plain(line.strip!(1), :escape_html) if line.text[1] == ?\s || line.text[1..2] == '#{'
243
+ push plain(line)
244
+ when SCRIPT
245
+ return push plain(line.strip!(2)) if line.text[1] == SCRIPT
246
+ line.text = line.text[1..-1]
247
+ push script(line)
248
+ when FLAT_SCRIPT; push flat_script(line.strip!(1))
249
+ when SILENT_SCRIPT
250
+ return push haml_comment(line.text[2..-1]) if line.text[1] == SILENT_COMMENT
251
+ push silent_script(line)
252
+ when FILTER; push filter(line.text[1..-1].downcase)
253
+ when DOCTYPE
254
+ return push doctype(line.text) if line.text[0, 3] == '!!!'
255
+ return push plain(line.strip!(3), false) if line.text[1, 2] == '=='
256
+ return push script(line.strip!(2), false) if line.text[1] == SCRIPT
257
+ return push flat_script(line.strip!(2), false) if line.text[1] == FLAT_SCRIPT
258
+ return push plain(line.strip!(1), false) if line.text[1] == ?\s || line.text[1..2] == '#{'
259
+ push plain(line)
260
+ when ESCAPE
261
+ line.text = line.text[1..-1]
262
+ push plain(line)
263
+ else; push plain(line)
264
+ end
265
+ end
266
+
267
+ def block_keyword(text)
268
+ return unless keyword = text.scan(BLOCK_KEYWORD_REGEX)[0]
269
+ keyword[0] || keyword[1]
270
+ end
271
+
272
+ def push(node)
273
+ @parent.children << node
274
+ node.parent = @parent
275
+ end
276
+
277
+ def plain(line, escape_html = nil)
278
+ if block_opened?
279
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_plain), @next_line.index)
280
+ end
281
+
282
+ unless contains_interpolation?(line.text)
283
+ return ParseNode.new(:plain, line.index + 1, :text => line.text)
284
+ end
285
+
286
+ escape_html = @options.escape_html if escape_html.nil?
287
+ line.text = ::Hamlit::HamlUtil.unescape_interpolation(line.text)
288
+ script(line, false).tap { |n| n.value[:escape_interpolation] = true if escape_html }
289
+ end
290
+
291
+ def script(line, escape_html = nil, preserve = false)
292
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, '=')) if line.text.empty?
293
+ line = handle_ruby_multiline(line)
294
+ escape_html = @options.escape_html if escape_html.nil?
295
+
296
+ keyword = block_keyword(line.text)
297
+ check_push_script_stack(keyword)
298
+
299
+ ParseNode.new(:script, line.index + 1, :text => line.text, :escape_html => escape_html,
300
+ :preserve => preserve, :keyword => keyword)
301
+ end
302
+
303
+ def flat_script(line, escape_html = nil)
304
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, '~')) if line.text.empty?
305
+ script(line, escape_html, :preserve)
306
+ end
307
+
308
+ def silent_script(line)
309
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
310
+
311
+ line = handle_ruby_multiline(line)
312
+ keyword = block_keyword(line.text)
313
+
314
+ check_push_script_stack(keyword)
315
+
316
+ if ["else", "elsif", "when"].include?(keyword)
317
+ if @script_level_stack.empty?
318
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:missing_if, keyword), @line.index)
319
+ end
320
+
321
+ if keyword == 'when' and !@script_level_stack.last[2]
322
+ if @script_level_stack.last[1] + 1 == @line.tabs
323
+ @script_level_stack.last[1] += 1
324
+ end
325
+ @script_level_stack.last[2] = true
326
+ end
327
+
328
+ if @script_level_stack.last[1] != @line.tabs
329
+ message = ::Hamlit::HamlError.message(:bad_script_indent, keyword, @script_level_stack.last[1], @line.tabs)
330
+ raise ::Hamlit::HamlSyntaxError.new(message, @line.index)
331
+ end
332
+ end
333
+
334
+ ParseNode.new(:silent_script, @line.index + 1,
335
+ :text => line.text[1..-1], :keyword => keyword)
336
+ end
337
+
338
+ def check_push_script_stack(keyword)
339
+ if ["if", "case", "unless"].include?(keyword)
340
+ # @script_level_stack contents are arrays of form
341
+ # [:keyword, stack_level, other_info]
342
+ @script_level_stack.push([keyword.to_sym, @line.tabs])
343
+ @script_level_stack.last << false if keyword == 'case'
344
+ @tab_up = true
345
+ end
346
+ end
347
+
348
+ def haml_comment(text)
349
+ if filter_opened?
350
+ @flat = true
351
+ @filter_buffer = String.new
352
+ @filter_buffer << "#{text}\n" unless text.empty?
353
+ text = @filter_buffer
354
+ # If we don't know the indentation by now, it'll be set in Line#tabs
355
+ @flat_spaces = @indentation * (@template_tabs+1) if @indentation
356
+ end
357
+
358
+ ParseNode.new(:haml_comment, @line.index + 1, :text => text)
359
+ end
360
+
361
+ def tag(line)
362
+ tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
363
+ nuke_inner_whitespace, action, value, last_line = parse_tag(line.text)
364
+
365
+ preserve_tag = @options.preserve.include?(tag_name)
366
+ nuke_inner_whitespace ||= preserve_tag
367
+ preserve_tag = false if @options.ugly
368
+ escape_html = (action == '&' || (action != '!' && @options.escape_html))
369
+
370
+ case action
371
+ when '/'; self_closing = true
372
+ when '~'; parse = preserve_script = true
373
+ when '='
374
+ parse = true
375
+ if value[0] == ?=
376
+ value = ::Hamlit::HamlUtil.unescape_interpolation(value[1..-1].strip)
377
+ escape_interpolation = true if escape_html
378
+ escape_html = false
379
+ end
380
+ when '&', '!'
381
+ if value[0] == ?= || value[0] == ?~
382
+ parse = true
383
+ preserve_script = (value[0] == ?~)
384
+ if value[1] == ?=
385
+ value = ::Hamlit::HamlUtil.unescape_interpolation(value[2..-1].strip)
386
+ escape_interpolation = true if escape_html
387
+ escape_html = false
388
+ else
389
+ value = value[1..-1].strip
390
+ end
391
+ elsif contains_interpolation?(value)
392
+ value = ::Hamlit::HamlUtil.unescape_interpolation(value)
393
+ escape_interpolation = true if escape_html
394
+ parse = true
395
+ escape_html = false
396
+ end
397
+ else
398
+ if contains_interpolation?(value)
399
+ value = ::Hamlit::HamlUtil.unescape_interpolation(value)
400
+ escape_interpolation = true if escape_html
401
+ parse = true
402
+ escape_html = false
403
+ end
404
+ end
405
+
406
+ attributes = ::Hamlit::HamlParser.parse_class_and_id(attributes)
407
+ attributes_list = []
408
+
409
+ if attributes_hashes[:new]
410
+ static_attributes, attributes_hash = attributes_hashes[:new]
411
+ ::Hamlit::HamlBuffer.merge_attrs(attributes, static_attributes) if static_attributes
412
+ attributes_list << attributes_hash
413
+ end
414
+
415
+ if attributes_hashes[:old]
416
+ static_attributes = parse_static_hash(attributes_hashes[:old])
417
+ ::Hamlit::HamlBuffer.merge_attrs(attributes, static_attributes) if static_attributes
418
+ attributes_list << attributes_hashes[:old] unless static_attributes || @options.suppress_eval
419
+ end
420
+
421
+ attributes_list.compact!
422
+
423
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
424
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
425
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
426
+
427
+ if block_opened? && !value.empty? && !is_ruby_multiline?(value)
428
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_line, tag_name), @next_line.index)
429
+ end
430
+
431
+ self_closing ||= !!(!block_opened? && value.empty? && @options.autoclose.any? {|t| t === tag_name})
432
+ value = nil if value.empty? && (block_opened? || self_closing)
433
+ line.text = value
434
+ line = handle_ruby_multiline(line) if parse
435
+
436
+ ParseNode.new(:tag, line.index + 1, :name => tag_name, :attributes => attributes,
437
+ :attributes_hashes => attributes_list, :self_closing => self_closing,
438
+ :nuke_inner_whitespace => nuke_inner_whitespace,
439
+ :nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
440
+ :escape_html => escape_html, :preserve_tag => preserve_tag,
441
+ :preserve_script => preserve_script, :parse => parse, :value => line.text,
442
+ :escape_interpolation => escape_interpolation)
443
+ end
444
+
445
+ # Renders a line that creates an XHTML tag and has an implicit div because of
446
+ # `.` or `#`.
447
+ def div(line)
448
+ line.text = "%div#{line.text}"
449
+ tag(line)
450
+ end
451
+
452
+ # Renders an XHTML comment.
453
+ def comment(text)
454
+ if text[0..1] == '!['
455
+ revealed = true
456
+ text = text[1..-1]
457
+ else
458
+ revealed = false
459
+ end
460
+
461
+ conditional, text = balance(text, ?[, ?]) if text[0] == ?[
462
+ text.strip!
463
+
464
+ if contains_interpolation?(text)
465
+ parse = true
466
+ text = slow_unescape_interpolation(text)
467
+ else
468
+ parse = false
469
+ end
470
+
471
+ if block_opened? && !text.empty?
472
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_content), @next_line.index)
473
+ end
474
+
475
+ ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
476
+ end
477
+
478
+ # Renders an XHTML doctype or XML shebang.
479
+ def doctype(text)
480
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_header), @next_line.index) if block_opened?
481
+ version, type, encoding = text[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
482
+ ParseNode.new(:doctype, @line.index + 1, :version => version, :type => type, :encoding => encoding)
483
+ end
484
+
485
+ def filter(name)
486
+ raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
487
+
488
+ if filter_opened?
489
+ @flat = true
490
+ @filter_buffer = String.new
491
+ # If we don't know the indentation by now, it'll be set in Line#tabs
492
+ @flat_spaces = @indentation * (@template_tabs+1) if @indentation
493
+ end
494
+
495
+ ParseNode.new(:filter, @line.index + 1, :name => name, :text => @filter_buffer)
496
+ end
497
+
498
+ def close
499
+ node, @parent = @parent, @parent.parent
500
+ @template_tabs -= 1
501
+ send("close_#{node.type}", node) if respond_to?("close_#{node.type}", :include_private)
502
+ end
503
+
504
+ def close_filter(_)
505
+ close_flat_section
506
+ end
507
+
508
+ def close_haml_comment(_)
509
+ close_flat_section
510
+ end
511
+
512
+ def close_flat_section
513
+ @flat = false
514
+ @flat_spaces = nil
515
+ @filter_buffer = nil
516
+ end
517
+
518
+ def close_silent_script(node)
519
+ @script_level_stack.pop if ["if", "case", "unless"].include? node.value[:keyword]
520
+
521
+ # Post-process case statements to normalize the nesting of "when" clauses
522
+ return unless node.value[:keyword] == "case"
523
+ return unless first = node.children.first
524
+ return unless first.type == :silent_script && first.value[:keyword] == "when"
525
+ return if first.children.empty?
526
+ # If the case node has a "when" child with children, it's the
527
+ # only child. Then we want to put everything nested beneath it
528
+ # beneath the case itself (just like "if").
529
+ node.children = [first, *first.children]
530
+ first.children = []
531
+ end
532
+
533
+ alias :close_script :close_silent_script
534
+
535
+ # This is a class method so it can be accessed from {Haml::Helpers}.
536
+ #
537
+ # Iterates through the classes and ids supplied through `.`
538
+ # and `#` syntax, and returns a hash with them as attributes,
539
+ # that can then be merged with another attributes hash.
540
+ def self.parse_class_and_id(list)
541
+ attributes = {}
542
+ return attributes if list.empty?
543
+
544
+ list.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, property|
545
+ case type
546
+ when '.'
547
+ if attributes[CLASS_KEY]
548
+ attributes[CLASS_KEY] += " "
549
+ else
550
+ attributes[CLASS_KEY] = ""
551
+ end
552
+ attributes[CLASS_KEY] += property
553
+ when '#'; attributes[ID_KEY] = property
554
+ end
555
+ end
556
+ attributes
557
+ end
558
+
559
+ def parse_static_hash(text)
560
+ attributes = {}
561
+ return attributes if text.empty?
562
+
563
+ scanner = StringScanner.new(text)
564
+ scanner.scan(/\s+/)
565
+ until scanner.eos?
566
+ return unless key = scanner.scan(LITERAL_VALUE_REGEX)
567
+ return unless scanner.scan(/\s*=>\s*/)
568
+ return unless value = scanner.scan(LITERAL_VALUE_REGEX)
569
+ return unless scanner.scan(/\s*(?:,|$)\s*/)
570
+ attributes[eval(key).to_s] = eval(value).to_s
571
+ end
572
+ attributes
573
+ end
574
+
575
+ # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
576
+ def parse_tag(text)
577
+ match = text.scan(/%([-:\w]+)([-:\w.#]*)(.+)?/)[0]
578
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:invalid_tag, text)) unless match
579
+
580
+ tag_name, attributes, rest = match
581
+
582
+ if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
583
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_element))
584
+ end
585
+
586
+ new_attributes_hash = old_attributes_hash = last_line = nil
587
+ object_ref = :nil
588
+ attributes_hashes = {}
589
+ while rest && !rest.empty?
590
+ case rest[0]
591
+ when ?{
592
+ break if old_attributes_hash
593
+ old_attributes_hash, rest, last_line = parse_old_attributes(rest)
594
+ attributes_hashes[:old] = old_attributes_hash
595
+ when ?(
596
+ break if new_attributes_hash
597
+ new_attributes_hash, rest, last_line = parse_new_attributes(rest)
598
+ attributes_hashes[:new] = new_attributes_hash
599
+ when ?[
600
+ break unless object_ref == :nil
601
+ object_ref, rest = balance(rest, ?[, ?])
602
+ else; break
603
+ end
604
+ end
605
+
606
+ if rest && !rest.empty?
607
+ nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
608
+ if nuke_whitespace
609
+ nuke_outer_whitespace = nuke_whitespace.include? '>'
610
+ nuke_inner_whitespace = nuke_whitespace.include? '<'
611
+ end
612
+ end
613
+
614
+ if @options.remove_whitespace
615
+ nuke_outer_whitespace = true
616
+ nuke_inner_whitespace = true
617
+ end
618
+
619
+ if value.nil?
620
+ value = ''
621
+ else
622
+ value.strip!
623
+ end
624
+ [tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
625
+ nuke_inner_whitespace, action, value, last_line || @line.index + 1]
626
+ end
627
+
628
+ def parse_old_attributes(text)
629
+ text = text.dup
630
+ last_line = @line.index + 1
631
+
632
+ begin
633
+ attributes_hash, rest = balance(text, ?{, ?})
634
+ rescue ::Hamlit::HamlSyntaxError => e
635
+ if text.strip[-1] == ?, && e.message == ::Hamlit::HamlError.message(:unbalanced_brackets)
636
+ text << "\n#{@next_line.text}"
637
+ last_line += 1
638
+ next_line
639
+ retry
640
+ end
641
+
642
+ raise e
643
+ end
644
+
645
+ attributes_hash = attributes_hash[1...-1] if attributes_hash
646
+ return attributes_hash, rest, last_line
647
+ end
648
+
649
+ def parse_new_attributes(text)
650
+ scanner = StringScanner.new(text)
651
+ last_line = @line.index + 1
652
+ attributes = {}
653
+
654
+ scanner.scan(/\(\s*/)
655
+ loop do
656
+ name, value = parse_new_attribute(scanner)
657
+ break if name.nil?
658
+
659
+ if name == false
660
+ scanned = ::Hamlit::HamlUtil.balance(text, ?(, ?))
661
+ text = scanned ? scanned.first : text
662
+ raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:invalid_attribute_list, text.inspect), last_line - 1)
663
+ end
664
+ attributes[name] = value
665
+ scanner.scan(/\s*/)
666
+
667
+ if scanner.eos?
668
+ text << " #{@next_line.text}"
669
+ last_line += 1
670
+ next_line
671
+ scanner.scan(/\s*/)
672
+ end
673
+ end
674
+
675
+ static_attributes = {}
676
+ dynamic_attributes = "{"
677
+ attributes.each do |name, (type, val)|
678
+ if type == :static
679
+ static_attributes[name] = val
680
+ else
681
+ dynamic_attributes << "#{inspect_obj(name)} => #{val},"
682
+ end
683
+ end
684
+ dynamic_attributes << "}"
685
+ dynamic_attributes = nil if dynamic_attributes == "{}"
686
+
687
+ return [static_attributes, dynamic_attributes], scanner.rest, last_line
688
+ end
689
+
690
+ def parse_new_attribute(scanner)
691
+ unless name = scanner.scan(/[-:\w]+/)
692
+ return if scanner.scan(/\)/)
693
+ return false
694
+ end
695
+
696
+ scanner.scan(/\s*/)
697
+ return name, [:static, true] unless scanner.scan(/=/) #/end
698
+
699
+ scanner.scan(/\s*/)
700
+ unless quote = scanner.scan(/["']/)
701
+ return false unless var = scanner.scan(/(@@?|\$)?\w+/)
702
+ return name, [:dynamic, var]
703
+ end
704
+
705
+ re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
706
+ content = []
707
+ loop do
708
+ return false unless scanner.scan(re)
709
+ content << [:str, scanner[1].gsub(/\\(.)/, '\1')]
710
+ break if scanner[2] == quote
711
+ content << [:ruby, balance(scanner, ?{, ?}, 1).first[0...-1]]
712
+ end
713
+
714
+ return name, [:static, content.first[1]] if content.size == 1
715
+ return name, [:dynamic,
716
+ %!"#{content.each_with_object('') {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
717
+ end
718
+
719
+ def next_line
720
+ line = @template.shift || raise(StopIteration)
721
+
722
+ # `flat?' here is a little outdated,
723
+ # so we have to manually check if either the previous or current line
724
+ # closes the flat block, as well as whether a new block is opened.
725
+ line_defined = instance_variable_defined?(:@line)
726
+ @line.tabs if line_defined
727
+ unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
728
+ (line_defined && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
729
+ return next_line if line.text.empty?
730
+
731
+ handle_multiline(line)
732
+ end
733
+
734
+ @next_line = line
735
+ end
736
+
737
+ def closes_flat?(line)
738
+ line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
739
+ end
740
+
741
+ def handle_multiline(line)
742
+ return unless is_multiline?(line.text)
743
+ line.text.slice!(-1)
744
+ loop do
745
+ new_line = @template.first
746
+ break if new_line.eod?
747
+ next @template.shift if new_line.text.strip.empty?
748
+ break unless is_multiline?(new_line.text.strip)
749
+ line.text << new_line.text.strip[0...-1]
750
+ @template.shift
751
+ end
752
+ end
753
+
754
+ # Checks whether or not `line` is in a multiline sequence.
755
+ def is_multiline?(text)
756
+ text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && text !~ BLOCK_WITH_SPACES
757
+ end
758
+
759
+ def handle_ruby_multiline(line)
760
+ line.text.rstrip!
761
+ return line unless is_ruby_multiline?(line.text)
762
+ begin
763
+ # Use already fetched @next_line in the first loop. Otherwise, fetch next
764
+ new_line = new_line.nil? ? @next_line : @template.shift
765
+ break if new_line.eod?
766
+ next if new_line.text.empty?
767
+ line.text << " #{new_line.text.rstrip}"
768
+ end while is_ruby_multiline?(new_line.text)
769
+ next_line
770
+ line
771
+ end
772
+
773
+ # `text' is a Ruby multiline block if it:
774
+ # - ends with a comma
775
+ # - but not "?," which is a character literal
776
+ # (however, "x?," is a method call and not a literal)
777
+ # - and not "?\," which is a character literal
778
+ def is_ruby_multiline?(text)
779
+ text && text.length > 1 && text[-1] == ?, &&
780
+ !((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
781
+ end
782
+
783
+ def balance(*args)
784
+ ::Hamlit::HamlUtil.balance(*args) or raise(::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:unbalanced_brackets)))
785
+ end
786
+
787
+ def block_opened?
788
+ @next_line.tabs > @line.tabs
789
+ end
790
+
791
+ # Same semantics as block_opened?, except that block_opened? uses Line#tabs,
792
+ # which doesn't interact well with filter lines
793
+ def filter_opened?
794
+ @next_line.full =~ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/)
795
+ end
796
+
797
+ def flat?
798
+ @flat
799
+ end
800
+ end
801
+ end