Import ruby-hamlit_2.7.5.orig.tar.gz
authorBalasankar C <balasankarc@autistici.org>
Sun, 13 Nov 2016 06:48:46 +0000 (06:48 +0000)
committerBalasankar C <balasankarc@autistici.org>
Sun, 13 Nov 2016 06:48:46 +0000 (06:48 +0000)
[dgit import orig ruby-hamlit_2.7.5.orig.tar.gz]

281 files changed:
.gitignore [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
CHANGELOG.md [new file with mode: 0644]
Gemfile [new file with mode: 0644]
LICENSE.txt [new file with mode: 0644]
README.md [new file with mode: 0644]
REFERENCE.md [new file with mode: 0644]
Rakefile [new file with mode: 0644]
benchmark/boolean_attribute.haml [new file with mode: 0644]
benchmark/class_attribute.haml [new file with mode: 0644]
benchmark/common_attribute.haml [new file with mode: 0644]
benchmark/data_attribute.haml [new file with mode: 0644]
benchmark/dynamic_attributes/boolean_attribute.haml [new file with mode: 0644]
benchmark/dynamic_attributes/class_attribute.haml [new file with mode: 0644]
benchmark/dynamic_attributes/common_attribute.haml [new file with mode: 0644]
benchmark/dynamic_attributes/data_attribute.haml [new file with mode: 0644]
benchmark/dynamic_attributes/id_attribute.haml [new file with mode: 0644]
benchmark/dynamic_boolean_attribute.haml [new file with mode: 0644]
benchmark/etc/attribute_builder.haml [new file with mode: 0644]
benchmark/etc/real_sample.haml [new file with mode: 0644]
benchmark/etc/real_sample.rb [new file with mode: 0644]
benchmark/etc/static_analyzer.haml [new file with mode: 0644]
benchmark/etc/string_interpolation.haml [new file with mode: 0644]
benchmark/etc/tags.haml [new file with mode: 0644]
benchmark/ext/build_data.rb [new file with mode: 0755]
benchmark/ext/build_id.rb [new file with mode: 0755]
benchmark/id_attribute.haml [new file with mode: 0644]
benchmark/plain.haml [new file with mode: 0644]
benchmark/script.haml [new file with mode: 0644]
benchmark/slim/LICENSE [new file with mode: 0644]
benchmark/slim/context.rb [new file with mode: 0644]
benchmark/slim/run-benchmarks.rb [new file with mode: 0644]
benchmark/slim/view.erb [new file with mode: 0644]
benchmark/slim/view.haml [new file with mode: 0644]
benchmark/slim/view.slim [new file with mode: 0644]
benchmark/utils/benchmark_ips_extension.rb [new file with mode: 0644]
bin/bench [new file with mode: 0755]
bin/console [new file with mode: 0755]
bin/lineprof [new file with mode: 0755]
bin/ruby [new file with mode: 0755]
bin/setup [new file with mode: 0755]
bin/stackprof [new file with mode: 0755]
bin/test [new file with mode: 0755]
exe/hamlit [new file with mode: 0755]
ext/hamlit/extconf.rb [new file with mode: 0644]
ext/hamlit/hamlit.c [new file with mode: 0644]
ext/hamlit/hescape.c [new file with mode: 0644]
ext/hamlit/hescape.h [new file with mode: 0644]
hamlit.gemspec [new file with mode: 0644]
lib/hamlit.rb [new file with mode: 0644]
lib/hamlit/attribute_builder.rb [new file with mode: 0644]
lib/hamlit/attribute_compiler.rb [new file with mode: 0644]
lib/hamlit/attribute_parser.rb [new file with mode: 0644]
lib/hamlit/cli.rb [new file with mode: 0644]
lib/hamlit/compiler.rb [new file with mode: 0644]
lib/hamlit/compiler/children_compiler.rb [new file with mode: 0644]
lib/hamlit/compiler/comment_compiler.rb [new file with mode: 0644]
lib/hamlit/compiler/doctype_compiler.rb [new file with mode: 0644]
lib/hamlit/compiler/script_compiler.rb [new file with mode: 0644]
lib/hamlit/compiler/silent_script_compiler.rb [new file with mode: 0644]
lib/hamlit/compiler/tag_compiler.rb [new file with mode: 0644]
lib/hamlit/engine.rb [new file with mode: 0644]
lib/hamlit/error.rb [new file with mode: 0644]
lib/hamlit/escapable.rb [new file with mode: 0644]
lib/hamlit/filters.rb [new file with mode: 0644]
lib/hamlit/filters/base.rb [new file with mode: 0644]
lib/hamlit/filters/cdata.rb [new file with mode: 0644]
lib/hamlit/filters/coffee.rb [new file with mode: 0644]
lib/hamlit/filters/css.rb [new file with mode: 0644]
lib/hamlit/filters/erb.rb [new file with mode: 0644]
lib/hamlit/filters/escaped.rb [new file with mode: 0644]
lib/hamlit/filters/javascript.rb [new file with mode: 0644]
lib/hamlit/filters/less.rb [new file with mode: 0644]
lib/hamlit/filters/markdown.rb [new file with mode: 0644]
lib/hamlit/filters/plain.rb [new file with mode: 0644]
lib/hamlit/filters/preserve.rb [new file with mode: 0644]
lib/hamlit/filters/ruby.rb [new file with mode: 0644]
lib/hamlit/filters/sass.rb [new file with mode: 0644]
lib/hamlit/filters/scss.rb [new file with mode: 0644]
lib/hamlit/filters/text_base.rb [new file with mode: 0644]
lib/hamlit/filters/tilt_base.rb [new file with mode: 0644]
lib/hamlit/force_escapable.rb [new file with mode: 0644]
lib/hamlit/helpers.rb [new file with mode: 0644]
lib/hamlit/html.rb [new file with mode: 0644]
lib/hamlit/identity.rb [new file with mode: 0644]
lib/hamlit/object_ref.rb [new file with mode: 0644]
lib/hamlit/parser.rb [new file with mode: 0644]
lib/hamlit/parser/MIT-LICENSE [new file with mode: 0644]
lib/hamlit/parser/README.md [new file with mode: 0644]
lib/hamlit/parser/haml_buffer.rb [new file with mode: 0644]
lib/hamlit/parser/haml_compiler.rb [new file with mode: 0644]
lib/hamlit/parser/haml_error.rb [new file with mode: 0644]
lib/hamlit/parser/haml_helpers.rb [new file with mode: 0644]
lib/hamlit/parser/haml_options.rb [new file with mode: 0644]
lib/hamlit/parser/haml_parser.rb [new file with mode: 0644]
lib/hamlit/parser/haml_util.rb [new file with mode: 0644]
lib/hamlit/parser/haml_xss_mods.rb [new file with mode: 0644]
lib/hamlit/rails_helpers.rb [new file with mode: 0644]
lib/hamlit/rails_template.rb [new file with mode: 0644]
lib/hamlit/railtie.rb [new file with mode: 0644]
lib/hamlit/ruby_expression.rb [new file with mode: 0644]
lib/hamlit/static_analyzer.rb [new file with mode: 0644]
lib/hamlit/string_splitter.rb [new file with mode: 0644]
lib/hamlit/template.rb [new file with mode: 0644]
lib/hamlit/utils.rb [new file with mode: 0644]
lib/hamlit/version.rb [new file with mode: 0644]
sample/rails/.gitignore [new file with mode: 0644]
sample/rails/Gemfile [new file with mode: 0644]
sample/rails/Gemfile.lock [new file with mode: 0644]
sample/rails/README.rdoc [new file with mode: 0644]
sample/rails/Rakefile [new file with mode: 0644]
sample/rails/app/assets/images/.keep [new file with mode: 0644]
sample/rails/app/assets/javascripts/application.js [new file with mode: 0644]
sample/rails/app/assets/javascripts/users.coffee [new file with mode: 0644]
sample/rails/app/assets/stylesheets/application.css [new file with mode: 0644]
sample/rails/app/assets/stylesheets/scaffolds.scss [new file with mode: 0644]
sample/rails/app/assets/stylesheets/users.scss [new file with mode: 0644]
sample/rails/app/controllers/application_controller.rb [new file with mode: 0644]
sample/rails/app/controllers/concerns/.keep [new file with mode: 0644]
sample/rails/app/controllers/users_controller.rb [new file with mode: 0644]
sample/rails/app/helpers/application_helper.rb [new file with mode: 0644]
sample/rails/app/helpers/users_helper.rb [new file with mode: 0644]
sample/rails/app/mailers/.keep [new file with mode: 0644]
sample/rails/app/models/.keep [new file with mode: 0644]
sample/rails/app/models/concerns/.keep [new file with mode: 0644]
sample/rails/app/models/user.rb [new file with mode: 0644]
sample/rails/app/views/layouts/application.html.haml [new file with mode: 0644]
sample/rails/app/views/users/_form.html.haml [new file with mode: 0644]
sample/rails/app/views/users/edit.html.haml [new file with mode: 0644]
sample/rails/app/views/users/index.html.haml [new file with mode: 0644]
sample/rails/app/views/users/new.html.haml [new file with mode: 0644]
sample/rails/app/views/users/show.html.haml [new file with mode: 0644]
sample/rails/app/views/users/test.html.haml [new file with mode: 0644]
sample/rails/bin/bundle [new file with mode: 0755]
sample/rails/bin/rails [new file with mode: 0755]
sample/rails/bin/rake [new file with mode: 0755]
sample/rails/bin/setup [new file with mode: 0755]
sample/rails/config.ru [new file with mode: 0644]
sample/rails/config/application.rb [new file with mode: 0644]
sample/rails/config/boot.rb [new file with mode: 0644]
sample/rails/config/database.yml [new file with mode: 0644]
sample/rails/config/environment.rb [new file with mode: 0644]
sample/rails/config/environments/development.rb [new file with mode: 0644]
sample/rails/config/environments/production.rb [new file with mode: 0644]
sample/rails/config/environments/test.rb [new file with mode: 0644]
sample/rails/config/initializers/assets.rb [new file with mode: 0644]
sample/rails/config/initializers/backtrace_silencers.rb [new file with mode: 0644]
sample/rails/config/initializers/cookies_serializer.rb [new file with mode: 0644]
sample/rails/config/initializers/filter_parameter_logging.rb [new file with mode: 0644]
sample/rails/config/initializers/inflections.rb [new file with mode: 0644]
sample/rails/config/initializers/mime_types.rb [new file with mode: 0644]
sample/rails/config/initializers/session_store.rb [new file with mode: 0644]
sample/rails/config/initializers/wrap_parameters.rb [new file with mode: 0644]
sample/rails/config/locales/en.yml [new file with mode: 0644]
sample/rails/config/routes.rb [new file with mode: 0644]
sample/rails/config/secrets.yml [new file with mode: 0644]
sample/rails/db/migrate/20151119022746_create_users.rb [new file with mode: 0644]
sample/rails/db/schema.rb [new file with mode: 0644]
sample/rails/db/seeds.rb [new file with mode: 0644]
sample/rails/lib/assets/.keep [new file with mode: 0644]
sample/rails/lib/tasks/.keep [new file with mode: 0644]
sample/rails/log/.keep [new file with mode: 0644]
sample/rails/public/404.html [new file with mode: 0644]
sample/rails/public/422.html [new file with mode: 0644]
sample/rails/public/500.html [new file with mode: 0644]
sample/rails/public/favicon.ico [new file with mode: 0644]
sample/rails/public/robots.txt [new file with mode: 0644]
sample/rails/vendor/assets/javascripts/.keep [new file with mode: 0644]
sample/rails/vendor/assets/stylesheets/.keep [new file with mode: 0644]
sample/sinatra/Gemfile [new file with mode: 0644]
sample/sinatra/Gemfile.lock [new file with mode: 0644]
sample/sinatra/app.rb [new file with mode: 0644]
sample/sinatra/views/center.haml [new file with mode: 0644]
sample/sinatra/views/index.haml [new file with mode: 0644]
sample/sinatra/views/layout.haml [new file with mode: 0644]
test/haml/MIT-LICENSE [new file with mode: 0644]
test/haml/README.md [new file with mode: 0644]
test/haml/engine_test.rb [new file with mode: 0644]
test/haml/erb/_av_partial_1.erb [new file with mode: 0644]
test/haml/erb/_av_partial_2.erb [new file with mode: 0644]
test/haml/erb/action_view.erb [new file with mode: 0644]
test/haml/erb/standard.erb [new file with mode: 0644]
test/haml/filters_test.rb [new file with mode: 0644]
test/haml/gemfiles/.bundle/config [new file with mode: 0644]
test/haml/gemfiles/Gemfile.rails-4.0.x [new file with mode: 0644]
test/haml/gemfiles/Gemfile.rails-4.1.x [new file with mode: 0644]
test/haml/gemfiles/Gemfile.rails-4.2.x [new file with mode: 0644]
test/haml/haml-spec/LICENSE [new file with mode: 0644]
test/haml/haml-spec/README.md [new file with mode: 0644]
test/haml/haml-spec/Rakefile [new file with mode: 0644]
test/haml/haml-spec/tests.yml [new file with mode: 0644]
test/haml/haml-spec/ugly_test.rb [new file with mode: 0644]
test/haml/helper_test.rb [new file with mode: 0644]
test/haml/markaby/standard.mab [new file with mode: 0644]
test/haml/mocks/article.rb [new file with mode: 0644]
test/haml/results/content_for_layout.xhtml [new file with mode: 0644]
test/haml/results/eval_suppressed.xhtml [new file with mode: 0644]
test/haml/results/helpers.xhtml [new file with mode: 0644]
test/haml/results/helpful.xhtml [new file with mode: 0644]
test/haml/results/just_stuff.xhtml [new file with mode: 0644]
test/haml/results/list.xhtml [new file with mode: 0644]
test/haml/results/nuke_inner_whitespace.xhtml [new file with mode: 0644]
test/haml/results/nuke_outer_whitespace.xhtml [new file with mode: 0644]
test/haml/results/original_engine.xhtml [new file with mode: 0644]
test/haml/results/partial_layout.xhtml [new file with mode: 0644]
test/haml/results/partial_layout_erb.xhtml [new file with mode: 0644]
test/haml/results/partials.xhtml [new file with mode: 0644]
test/haml/results/render_layout.xhtml [new file with mode: 0644]
test/haml/results/silent_script.xhtml [new file with mode: 0644]
test/haml/results/standard.xhtml [new file with mode: 0644]
test/haml/results/tag_parsing.xhtml [new file with mode: 0644]
test/haml/results/very_basic.xhtml [new file with mode: 0644]
test/haml/results/whitespace_handling.xhtml [new file with mode: 0644]
test/haml/template_test.rb [new file with mode: 0644]
test/haml/templates/_av_partial_1.haml [new file with mode: 0644]
test/haml/templates/_av_partial_1_ugly.haml [new file with mode: 0644]
test/haml/templates/_av_partial_2.haml [new file with mode: 0644]
test/haml/templates/_av_partial_2_ugly.haml [new file with mode: 0644]
test/haml/templates/_layout.erb [new file with mode: 0644]
test/haml/templates/_layout_for_partial.haml [new file with mode: 0644]
test/haml/templates/_partial.haml [new file with mode: 0644]
test/haml/templates/_text_area.haml [new file with mode: 0644]
test/haml/templates/_text_area_helper.html.haml [new file with mode: 0644]
test/haml/templates/action_view.haml [new file with mode: 0644]
test/haml/templates/action_view_ugly.haml [new file with mode: 0644]
test/haml/templates/breakage.haml [new file with mode: 0644]
test/haml/templates/content_for_layout.haml [new file with mode: 0644]
test/haml/templates/eval_suppressed.haml [new file with mode: 0644]
test/haml/templates/helpers.haml [new file with mode: 0644]
test/haml/templates/helpful.haml [new file with mode: 0644]
test/haml/templates/just_stuff.haml [new file with mode: 0644]
test/haml/templates/list.haml [new file with mode: 0644]
test/haml/templates/nuke_inner_whitespace.haml [new file with mode: 0644]
test/haml/templates/nuke_outer_whitespace.haml [new file with mode: 0644]
test/haml/templates/original_engine.haml [new file with mode: 0644]
test/haml/templates/partial_layout.haml [new file with mode: 0644]
test/haml/templates/partial_layout_erb.erb [new file with mode: 0644]
test/haml/templates/partialize.haml [new file with mode: 0644]
test/haml/templates/partials.haml [new file with mode: 0644]
test/haml/templates/render_layout.haml [new file with mode: 0644]
test/haml/templates/silent_script.haml [new file with mode: 0644]
test/haml/templates/standard.haml [new file with mode: 0644]
test/haml/templates/standard_ugly.haml [new file with mode: 0644]
test/haml/templates/tag_parsing.haml [new file with mode: 0644]
test/haml/templates/very_basic.haml [new file with mode: 0644]
test/haml/templates/whitespace_handling.haml [new file with mode: 0644]
test/haml/templates/with_bom.haml [new file with mode: 0644]
test/hamlit/attribute_parser_test.rb [new file with mode: 0644]
test/hamlit/cli_test.rb [new file with mode: 0644]
test/hamlit/engine/attributes_test.rb [new file with mode: 0644]
test/hamlit/engine/comment_test.rb [new file with mode: 0644]
test/hamlit/engine/doctype_test.rb [new file with mode: 0644]
test/hamlit/engine/indent_test.rb [new file with mode: 0644]
test/hamlit/engine/multiline_test.rb [new file with mode: 0644]
test/hamlit/engine/new_attribute_test.rb [new file with mode: 0644]
test/hamlit/engine/old_attribute_test.rb [new file with mode: 0644]
test/hamlit/engine/script_test.rb [new file with mode: 0644]
test/hamlit/engine/silent_script_test.rb [new file with mode: 0644]
test/hamlit/engine/tag_test.rb [new file with mode: 0644]
test/hamlit/engine/text_test.rb [new file with mode: 0644]
test/hamlit/engine/whitespace_test.rb [new file with mode: 0644]
test/hamlit/error_test.rb [new file with mode: 0644]
test/hamlit/filters/cdata_test.rb [new file with mode: 0644]
test/hamlit/filters/coffee_test.rb [new file with mode: 0644]
test/hamlit/filters/css_test.rb [new file with mode: 0644]
test/hamlit/filters/erb_test.rb [new file with mode: 0644]
test/hamlit/filters/javascript_test.rb [new file with mode: 0644]
test/hamlit/filters/markdown_test.rb [new file with mode: 0644]
test/hamlit/filters/plain_test.rb [new file with mode: 0644]
test/hamlit/filters/ruby_test.rb [new file with mode: 0644]
test/hamlit/filters/sass_test.rb [new file with mode: 0644]
test/hamlit/filters/scss_test.rb [new file with mode: 0644]
test/hamlit/helpers_test.rb [new file with mode: 0644]
test/hamlit/line_number_test.rb [new file with mode: 0644]
test/hamlit/optimization_test.rb [new file with mode: 0644]
test/hamlit/rails_template_test.rb [new file with mode: 0644]
test/hamlit/ruby_expression_test.rb [new file with mode: 0644]
test/hamlit/static_analyzer_test.rb [new file with mode: 0644]
test/hamlit/string_splitter_test.rb [new file with mode: 0644]
test/hamlit/template_test.rb [new file with mode: 0644]
test/test_helper.rb [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..a54487a
--- /dev/null
@@ -0,0 +1,16 @@
+/.bundle/
+/.yardoc
+/Gemfile.lock
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+.sass-cache
+.ruby-version
+*.bundle
+*.so
+*.o
+*.a
+*.swp
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..b041bfa
--- /dev/null
@@ -0,0 +1,41 @@
+language: ruby
+cache: bundler
+branches:
+  only:
+    - master
+script:
+  - "bundle exec rake $TASK"
+matrix:
+  include:
+    - rvm: 2.1.10
+      env: TASK=test
+    - rvm: 2.2.5
+      env: TASK=test
+    - rvm: 2.3.1
+      env: TASK=test
+    - rvm: ruby-head
+      env: TASK=test
+    - rvm: 2.3.1
+      env: TASK=bench TEMPLATE=benchmark/boolean_attribute.haml,benchmark/class_attribute.haml,benchmark/id_attribute.haml,benchmark/data_attribute.haml,benchmark/common_attribute.haml
+    - rvm: 2.3.1
+      env: TASK=bench TEMPLATE=benchmark/dynamic_attributes/boolean_attribute.haml,benchmark/dynamic_attributes/class_attribute.haml,benchmark/dynamic_attributes/id_attribute.haml,benchmark/dynamic_attributes/data_attribute.haml,benchmark/dynamic_attributes/common_attribute.haml
+    - rvm: 2.3.1
+      env: TASK=bench SLIM_BENCH=1
+    - rvm: 2.3.1
+      env: TASK=bench TEMPLATE=benchmark/etc/attribute_builder.haml
+    - rvm: 2.3.1
+      env: TASK=bench TEMPLATE=benchmark/etc/static_analyzer.haml
+    - rvm: 2.3.1
+      env: TASK=bench TEMPLATE=benchmark/etc/string_interpolation.haml
+    - rvm: 2.3.1
+      env: TASK=bench TEMPLATE=test/haml/templates/standard.haml COMPILE=1
+  allow_failures:
+    - rvm: ruby-head
+      env: TASK=test
+    - env: TASK=bench TEMPLATE=benchmark/boolean_attribute.haml,benchmark/class_attribute.haml,benchmark/id_attribute.haml,benchmark/data_attribute.haml,benchmark/common_attribute.haml
+    - env: TASK=bench TEMPLATE=benchmark/dynamic_attributes/boolean_attribute.haml,benchmark/dynamic_attributes/class_attribute.haml,benchmark/dynamic_attributes/id_attribute.haml,benchmark/dynamic_attributes/data_attribute.haml,benchmark/dynamic_attributes/common_attribute.haml
+    - env: TASK=bench SLIM_BENCH=1
+    - env: TASK=bench TEMPLATE=benchmark/etc/attribute_builder.haml
+    - env: TASK=bench TEMPLATE=benchmark/etc/static_analyzer.haml
+    - env: TASK=bench TEMPLATE=benchmark/etc/string_interpolation.haml
+    - env: TASK=bench TEMPLATE=test/haml/templates/standard.haml COMPILE=1
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644 (file)
index 0000000..bb94755
--- /dev/null
@@ -0,0 +1,576 @@
+# Change Log
+
+All notable changes to this project will be documented in this file. This
+project adheres to [Semantic Versioning](http://semver.org/). This change log is based upon
+[keep-a-changelog](https://github.com/olivierlacan/keep-a-changelog).
+
+## [2.7.5](https://github.com/k0kubun/hamlit/compare/v2.7.4...v2.7.5) - 2016-10-15
+
+### Fixed
+
+- Resurrect `Hamlit::RailsTemplate.set_options` dropped in v2.7.4 unexpectedly.
+
+## [2.7.4](https://github.com/k0kubun/hamlit/compare/v2.7.3...v2.7.4) - 2016-10-15 [YANKED]
+
+### Fixed
+
+- Compile template as xhtml when ActionView regards template as text/xml
+  [#92](https://github.com/k0kubun/hamlit/issues/92). *Thank to @shmargum*
+
+## [2.7.3](https://github.com/k0kubun/hamlit/compare/v2.7.2...v2.7.3) - 2016-10-12
+
+### Fixed
+
+- Regard download as an boolean attribute
+  [#91](https://github.com/k0kubun/hamlit/pull/91). *Thank to @pushcx*
+
+## [2.7.2](https://github.com/k0kubun/hamlit/compare/v2.7.1...v2.7.2) - 2016-09-19
+
+### Fixed
+
+- Fix engine option warning
+  [#90](https://github.com/k0kubun/hamlit/issues/90). *Thank to @kikonen*
+
+## [2.7.1](https://github.com/k0kubun/hamlit/compare/v2.7.0...v2.7.1) - 2016-09-19
+
+### Fixed
+
+- Fix Rails handler to use `ActionView::OutputBuffer` instead of `ActionView::SafeBuffer` to justify encoding
+  [#89](https://github.com/k0kubun/hamlit/pull/89). *Thanks to @akelmanson*
+
+## [2.7.0](https://github.com/k0kubun/hamlit/compare/v2.6.2...v2.7.0) - 2016-08-31
+
+### Changed
+
+- Don't escape interpolated content in plain filter
+  [#87](https://github.com/k0kubun/hamlit/pull/87). *Thanks to @shmargum*
+
+## [2.6.2](https://github.com/k0kubun/hamlit/compare/v2.6.1...v2.6.2) - 2016-08-27
+
+### Added
+
+- Add cdata filter
+  [#84](https://github.com/k0kubun/hamlit/issues/84). *Thanks to @shmargum*
+- Minimize string allocation on template comipilation using `# frozen_string_literal: true`
+
+## [2.6.1](https://github.com/k0kubun/hamlit/compare/v2.6.0...v2.6.1) - 2016-08-18
+
+### Fixed
+
+- For Rails, escape attributes even if it's html\_safe
+  - This is the same fix as Rails for [CVE-2016-6316](https://groups.google.com/forum/#!topic/ruby-security-ann/8B2iV2tPRSE)
+
+## [2.6.0](https://github.com/k0kubun/hamlit/compare/v2.5.0...v2.6.0) - 2016-08-14
+
+### Changed
+
+- Stop using [houdini](https://github.com/vmg/houdini) and rewrite HTML escape function to resolve building or packaging problems [#82](https://github.com/k0kubun/hamlit/pull/82).
+  - No behavior is changed
+
+## [2.5.0](https://github.com/k0kubun/hamlit/compare/v2.4.2...v2.5.0) - 2016-06-04
+
+### Changed
+
+- Don't escape the result of `preserve` helper in Rails
+
+## [2.4.2](https://github.com/k0kubun/hamlit/compare/v2.4.1...v2.4.2) - 2016-06-04
+
+### Fixed
+
+- Regard cygwin and bccwin as Windows environment too
+
+## [2.4.1](https://github.com/k0kubun/hamlit/compare/v2.4.0...v2.4.1) - 2016-06-03
+
+### Fixed
+
+- Fix C extension builder to work with Ruby 2.3 on Windows
+  [#69](https://github.com/k0kubun/hamlit/issues/69). *Thanks to @francesco-loreti*
+
+## [2.4.0](https://github.com/k0kubun/hamlit/compare/v2.3.1...v2.4.0) - 2016-05-13
+
+### Added
+
+- Add `Hamlit::Helpers.preserve` method for Tilt templates
+
+## [2.3.1](https://github.com/k0kubun/hamlit/compare/v2.3.0...v2.3.1) - 2016-05-09
+
+### Fixed
+
+- Specify Ruby version dependency on gemspec
+  [#67](https://github.com/k0kubun/hamlit/issues/67). *Thanks to @grosser*
+
+## [2.3.0](https://github.com/k0kubun/hamlit/compare/v2.2.3...v2.3.0) - 2016-04-24
+
+### Added
+
+- Add `Hamlit::Filters.remove_filter` method
+  [#66](https://github.com/k0kubun/hamlit/issues/66). *Thanks to @connorshea*
+
+### Changed
+
+- `:coffeescript` filter's internal class name is changed from `Coffee` to `CoffeeScript`
+
+## [2.2.3](https://github.com/k0kubun/hamlit/compare/v2.2.2...v2.2.3) - 2016-03-10
+
+### Added
+
+- Add `hamlit version` subcommand
+  [#60](https://github.com/k0kubun/hamlit/pull/60). *Thanks to @timoschilling*
+
+### Fixed
+
+- Fix load path for CLI
+  [#61](https://github.com/k0kubun/hamlit/pull/61). *Thanks to @timoschilling*
+
+## [2.2.2](https://github.com/k0kubun/hamlit/compare/v2.2.1...v2.2.2) - 2016-02-21
+
+### Added
+
+- Optimize performance of plain filter
+
+### Fixed
+
+- Escape only interpolated text for plain filter
+  [#58](https://github.com/k0kubun/hamlit/issues/58). *Thanks to @shaneog*
+
+## [2.2.1](https://github.com/k0kubun/hamlit/compare/v2.2.0...v2.2.1) - 2016-02-06
+
+### Added
+
+- Support Windows
+  [#54](https://github.com/k0kubun/hamlit/issues/54). *Thanks to @francesco-loreti*
+
+## [2.2.0](https://github.com/k0kubun/hamlit/compare/v2.1.2...v2.2.0) - 2015-12-24
+
+### Added
+
+- Optimize inline script inside a tag
+- Optimize string interpolation recursively
+
+## [2.1.2](https://github.com/k0kubun/hamlit/compare/v2.1.1...v2.1.2) - 2015-12-16
+
+### Fixed
+
+- Fix rendering failure for static integer
+  [#50](https://github.com/k0kubun/hamlit/pull/50). *Thanks to @yatmsu*
+
+## [2.1.1](https://github.com/k0kubun/hamlit/compare/v2.1.0...v2.1.1) - 2015-12-15
+
+### Fixed
+
+- Use faster HTML-escape method for compiling
+- Show proper line number for unbalanced brackets error
+
+## [2.1.0](https://github.com/k0kubun/hamlit/compare/v2.0.2...v2.1.0) - 2015-12-14
+
+### Added
+
+- `-I` and `-r` options are added to `hamlit render` command
+  [#37](https://github.com/k0kubun/hamlit/issues/37). *Thanks to @jhurliman*
+
+### Changed
+
+- Dropped obsolete `escape_utils` gem dependency
+  [#48](https://github.com/k0kubun/hamlit/pull/48). *Thanks to @eagletmt*
+
+### Fixed
+
+- Accept NUL character in attribute keys
+  [#49](https://github.com/k0kubun/hamlit/pull/49). *Thanks to @eagletmt*
+
+## [2.0.2](https://github.com/k0kubun/hamlit/compare/v2.0.1...v2.0.2) - 2015-12-12
+
+### Fixed
+- Fix a crash in compiling with CLI
+  [#46](https://github.com/k0kubun/hamlit/pull/46). *Thanks to @walf443*
+- Use default engine options properly in CLI commands
+
+## [2.0.1](https://github.com/k0kubun/hamlit/compare/v2.0.0...v2.0.1) - 2015-11-30
+
+### Fixed
+- Fix build failure of native extension
+
+## [2.0.0](https://github.com/k0kubun/hamlit/compare/v1.7.2...v2.0.0) - 2015-11-30 [YANKED]
+### Added
+- Support object reference
+
+### Changed
+- Full scratch of internal implementation
+  - Rendering is strongly optimized
+     - Static analyzer is introduced
+     - Built with C extension for runtime rendering
+     - Optimized compilation for 5 types of attributes
+  - Compilation became faster too
+  - Many rendering incompatibilities are resolved
+- [**breaking**] Replaced parser with original Haml's one
+  - Incompatible parsing error will never happen, but we can no longer parse
+    attributes with Ripper
+- [**breaking**] Unified behavior for both static and dynamic attributes, see
+  [5 types of attributes](REFERENCE.md#5-types-of-attributes)
+  - Though inconsistent behavior is removed, we can no longer rely on
+    completely-Haml-compatible behavior of static attributes and pass haml-spec
+- [**breaking**] Added :escape\_attrs option
+  - You should specify HTML-escaping availability for script and attrs
+    separately.
+
+## [1.7.2](https://github.com/k0kubun/hamlit/compare/v1.7.1...v1.7.2) - 2015-07-22
+
+### Fixed
+- Bugfix about parsing a content of tag
+  - This was introduced in v1.6.6.
+
+## [1.7.1](https://github.com/k0kubun/hamlit/compare/v1.7.0...v1.7.1) - 2015-07-21
+
+### Fixed
+- Don't escape a block content of some helpers
+  [#35](https://github.com/k0kubun/hamlit/issues/35). *Thanks to @felixbuenemann*
+
+## [1.7.0](https://github.com/k0kubun/hamlit/compare/v1.6.7...v1.7.0) - 2015-07-09
+
+### Added
+- Support Ruby 2.2.0 hash syntax
+  - like `{ "hyphened-key": "value" }`
+
+## [1.6.7](https://github.com/k0kubun/hamlit/compare/v1.6.6...v1.6.7) - 2015-06-27
+
+### Fixed
+- Remove unused variables and avoid shadowing
+  - To suppress warnings in application using `rspec --warnings`
+
+## [1.6.6](https://github.com/k0kubun/hamlit/compare/v1.6.5...v1.6.6) - 2015-06-24
+
+### Added
+- Allow hyphenated HTML-style attributes
+  [pull #29](https://github.com/k0kubun/hamlit/pull/29). *thanks to @babelfish*
+
+## [1.6.5](https://github.com/k0kubun/hamlit/compare/v1.6.4...v1.6.5) - 2015-06-13
+
+### Fixed
+- Don't duplicate element class and attribute class
+- Raise an error for an empty tag name
+
+## [1.6.4](https://github.com/k0kubun/hamlit/compare/v1.6.3...v1.6.4) - 2015-06-13
+
+### Changed
+- Show human-friendly error messages
+
+### Fixed
+- Fix line number of runtime syntax error
+- Increase the number of checked cases for illegal nesting.
+  *Thanks to @eagletmt*
+
+## [1.6.3](https://github.com/k0kubun/hamlit/compare/v1.6.2...v1.6.3) - 2015-06-13
+
+### Fixed
+- Fix ! and & parsing inside a tag
+  [#27](https://github.com/k0kubun/hamlit/issues/27#issuecomment-111593458).
+  *Thanks to @leesmith*
+
+## [1.6.2](https://github.com/k0kubun/hamlit/compare/v1.6.1...v1.6.2) - 2015-06-11
+
+### Fixed
+- Reject a content for self-closing tags
+- Reject nesing within self-closing tags
+
+## [1.6.1](https://github.com/k0kubun/hamlit/compare/v1.6.0...v1.6.1) - 2015-06-11
+
+### Fixed
+- Parse N-space indentation
+  [#26](https://github.com/k0kubun/hamlit/issues/26). *Thanks to @eagletmt*
+
+## [1.6.0](https://github.com/k0kubun/hamlit/compare/v1.5.9...v1.6.0) - 2015-06-11
+
+### Fixed
+- Fix line number of compiled code for new attributes
+- Render HTML entities normally for plain text
+  [#27](https://github.com/k0kubun/hamlit/issues/27). *Thanks to @jeffblake*
+
+## [1.5.9](https://github.com/k0kubun/hamlit/compare/v1.5.8...v1.5.9) - 2015-06-08
+
+### Fixed
+- Reject silent script after a tag
+
+## [1.5.8](https://github.com/k0kubun/hamlit/compare/v1.5.7...v1.5.8) - 2015-06-08
+
+### Fixed
+- Fix parsing inline script for != and &=
+
+## [1.5.7](https://github.com/k0kubun/hamlit/compare/v1.5.6...v1.5.7) - 2015-06-08
+
+### Fixed
+- Fix the behavior for multi-line script
+
+## [1.5.6](https://github.com/k0kubun/hamlit/compare/v1.5.5...v1.5.6) - 2015-06-07
+
+### Added
+- Raise error for unbalanced brackets
+
+### Changed
+- Don't render newline after block script
+
+## [1.5.5](https://github.com/k0kubun/hamlit/compare/v1.5.4...v1.5.5) - 2015-06-07
+
+### Added
+- Support &, &== operator
+
+### Changed
+- Depend on v0.7.6 of temple for refactoring
+
+### Fixed
+- Fix a trivial diff of rendering multiline operator
+
+## [1.5.4](https://github.com/k0kubun/hamlit/compare/v1.5.3...v1.5.4) - 2015-06-07
+
+### Changed
+- Recursively remove whitespace inside a tag
+
+### Fixed
+- Fix ! operator immediately before whitespace
+
+## [1.5.3](https://github.com/k0kubun/hamlit/compare/v1.5.2...v1.5.3) - 2015-06-06
+
+### Added
+- Support !, !=, !==, &= and ~ as inline operators
+
+## [1.5.2](https://github.com/k0kubun/hamlit/compare/v1.5.1...v1.5.2) - 2015-06-06
+
+### Changed
+- Disable html escaping in CSS and JavaScript filter
+
+## [1.5.1](https://github.com/k0kubun/hamlit/compare/v1.5.0...v1.5.1) - 2015-06-05
+
+### Changed
+- Remove outer whitespace in the block
+
+## [1.5.0](https://github.com/k0kubun/hamlit/compare/v1.4.7...v1.5.0) - 2015-06-03
+
+### Changed
+- Remake implementation of outer whitespace removal
+
+## [1.4.7](https://github.com/k0kubun/hamlit/compare/v1.4.6...v1.4.7) - 2015-06-03
+
+### Changed
+- Sort static old attributes by name
+
+### Fixed
+- Bugfix for old array attributes with class element
+
+## [1.4.6](https://github.com/k0kubun/hamlit/compare/v1.4.5...v1.4.6) - 2015-06-03
+
+### Added
+- Support `!==`, `==` operator
+
+### Fixed
+- Avoid regarding spaced block as multiline
+
+## [1.4.5](https://github.com/k0kubun/hamlit/compare/v1.4.4...v1.4.5) - 2015-06-02
+
+### Fixed
+- Support Ruby 2.0 and 2.1 for v1.4.4
+
+## [1.4.4](https://github.com/k0kubun/hamlit/compare/v1.4.3...v1.4.4) - 2015-06-02 [YANKED]
+
+### Fixed
+- Fix old attribute parser to be more flexible
+  - Accept multiple hashes as old attributes
+  - Accept old attributes with hash and literal
+
+## [1.4.3](https://github.com/k0kubun/hamlit/compare/v1.4.2...v1.4.3) - 2015-06-02
+
+### Changed
+- Allow `when` to have multiple candidates
+- Allow `rescue` to specify an error variable
+
+## [1.4.2](https://github.com/k0kubun/hamlit/compare/v1.4.1...v1.4.2) - 2015-05-31
+
+### Added
+- Support `!` operator
+  - It disables html escaping for interpolated text
+
+## [1.4.1](https://github.com/k0kubun/hamlit/compare/v1.4.0...v1.4.1) - 2015-05-31
+
+### Fixed
+- Fix code mistake in 1.4.0
+
+## [1.4.0](https://github.com/k0kubun/hamlit/compare/v1.3.2...v1.4.0) - 2015-05-31 [YANKED]
+
+### Added
+- Escape interpolated string in plain text
+
+## [1.3.2](https://github.com/k0kubun/hamlit/compare/v1.3.1...v1.3.2) - 2015-05-30
+
+- Render `begin`, `rescue` and `ensure`
+
+## [1.3.1](https://github.com/k0kubun/hamlit/compare/v1.3.0...v1.3.1) - 2015-05-30
+
+### Fixed
+- Bugfix about a backslash-only comment
+- Don't strip a plain text
+
+## [1.3.0](https://github.com/k0kubun/hamlit/compare/v1.2.1...v1.3.0) - 2015-05-16
+
+### Added
+- Resurrect escape\_html option
+  [#25](https://github.com/k0kubun/hamlit/issues/25).
+  *Thanks to @resistorsoftware*
+  - Still enabled by default
+  - This has been dropped since v0.6.0
+
+## [1.2.1](https://github.com/k0kubun/hamlit/compare/v1.2.0...v1.2.1) - 2015-05-15
+
+### Fixed
+- Fix the list of boolean attributes
+  [#24](https://github.com/k0kubun/hamlit/issues/24). *Thanks to @jeffblake*
+
+## [1.2.0](https://github.com/k0kubun/hamlit/compare/v1.1.1...v1.2.0) - 2015-05-06
+
+Added
+- Support `succeed`, `precede` and `surround`
+  [#22](https://github.com/k0kubun/hamlit/issues/22). *Thanks to @sneakernets*
+
+## [1.1.1](https://github.com/k0kubun/hamlit/compare/v1.1.0...v1.1.1) - 2015-05-06
+
+### Fixed
+- Bugfix of rendering array attributes
+
+## [1.1.0](https://github.com/k0kubun/hamlit/compare/v1.0.0...v1.1.0) - 2015-05-06
+
+### Fixed
+- Join id and class attributes
+  [#23](https://github.com/k0kubun/hamlit/issues/23).
+  *Thanks to @felixbuenemann*
+
+## [1.0.0](https://github.com/k0kubun/hamlit/compare/v0.6.2...v1.0.0) - 2015-04-12
+
+### Added
+- Use escape\_utils gem for faster escape\_html
+
+## [0.6.2](https://github.com/k0kubun/hamlit/compare/v0.6.1...v0.6.2) - 2015-04-12
+
+### Fixed
+- Don't render falsy attributes
+  [#2](https://github.com/k0kubun/hamlit/issues/2). *Thanks to @eagletmt*
+
+## [0.6.1](https://github.com/k0kubun/hamlit/compare/v0.6.0...v0.6.1) - 2015-04-12
+
+### Fixed
+- Bugfix of line numbers for better error backtrace
+  [pull #19](https://github.com/k0kubun/hamlit/pull/19)
+
+## [0.6.0](https://github.com/k0kubun/hamlit/compare/v0.5.3...v0.6.0) - 2015-04-12
+
+### Added
+- Automatically escape html in all situations
+  [pull #18](https://github.com/k0kubun/hamlit/pull/18)
+
+## [0.5.3](https://github.com/k0kubun/hamlit/compare/v0.5.2...v0.5.3) - 2015-04-12
+
+### Fixed
+- Bugfix for syntax error in data attribute hash
+  [#17](https://github.com/k0kubun/hamlit/issues/17).  *Thanks to @eagletmt*
+
+## [0.5.2](https://github.com/k0kubun/hamlit/compare/v0.5.1...v0.5.2) - 2015-04-12
+
+### Fixed
+- Bugfix for silent script without block
+  [#16](https://github.com/k0kubun/hamlit/issues/16).  *Thanks to @eagletmt*
+
+## [0.5.1](https://github.com/k0kubun/hamlit/compare/v0.5.0...v0.5.1) - 2015-04-12
+
+### Fixed
+- Bugfix about duplicated id and class
+  [#4](https://github.com/k0kubun/hamlit/issues/4).  *Thanks to @os0x*
+
+## [0.5.0](https://github.com/k0kubun/hamlit/compare/v0.4.3...v0.5.0) - 2015-04-12
+
+### Fixed
+- Escape special characters in attribute values
+  [#10](https://github.com/k0kubun/hamlit/issues/10). *Thanks to @mono0x,
+  @eagletmt*
+
+## [0.4.3](https://github.com/k0kubun/hamlit/compare/v0.4.2...v0.4.3) - 2015-04-12
+
+### Fixed
+- Allow empty else statement [#14](https://github.com/k0kubun/hamlit/issues/14).
+  *Thanks to @jeffblake*
+- Accept comment-only script [#13](https://github.com/k0kubun/hamlit/issues/13).
+  *Thanks to @jeffblake*
+
+## [0.4.2](https://github.com/k0kubun/hamlit/compare/v0.4.1...v0.4.2) - 2015-04-05
+
+### Fixed
+- Bugfix about parsing nested attributes
+  [#12](https://github.com/k0kubun/hamlit/issues/12). *Thanks to @creasty*
+
+## [0.4.1](https://github.com/k0kubun/hamlit/compare/v0.4.0...v0.4.1) - 2015-04-05
+
+### Removed
+- Automatic escape html is sintara, consult `README.md`.
+
+### Fixed
+- Escape haml operators by backslash
+  [#11](https://github.com/k0kubun/hamlit/issues/11).  *Thanks to @mono0x*
+
+## [0.4.0](https://github.com/k0kubun/hamlit/compare/v0.3.4...v0.4.0) - 2015-04-05 [YANKED]
+
+### Added
+- Automatically escape html in sinatra
+
+## [0.3.4](https://github.com/k0kubun/hamlit/compare/v0.3.3...v0.3.4) - 2015-04-02
+
+### Fixed
+- Allow tab indentation [#9](https://github.com/k0kubun/hamlit/issues/9).
+  *Thanks to @tdtds*
+
+## [0.3.3](https://github.com/k0kubun/hamlit/compare/v0.3.2...v0.3.3) - 2015-04-01
+
+### Fixed
+- Accept multi byte parsing [#8](https://github.com/k0kubun/hamlit/issues/8).
+  *Thanks to @machu*
+
+## [0.3.2](https://github.com/k0kubun/hamlit/compare/v0.3.1...v0.3.2) - 2015-03-31
+
+### Fixed
+- Bugfix for compiling old attributes [#7](https://github.com/k0kubun/hamlit/issues/7).
+  *Thanks to @creasty*
+
+## [0.3.1](https://github.com/k0kubun/hamlit/compare/v0.3.0...v0.3.1) - 2015-03-31
+
+### Fixed
+- Hyphenate data attributes [#5](https://github.com/k0kubun/hamlit/issues/5).
+  *Thanks to @os0x*
+
+## [0.3.0](https://github.com/k0kubun/hamlit/compare/v0.2.0...v0.3.0) - 2015-03-31
+
+### Added
+- Specify a version in dependency of temple
+
+## [0.2.0](https://github.com/k0kubun/hamlit/compare/v0.1.3...v0.2.0) - 2015-03-30
+
+### Added
+- Allow comments in script [#3](https://github.com/k0kubun/hamlit/issues/3).
+  *Thanks to @eagletmt*
+
+## [0.1.3](https://github.com/k0kubun/hamlit/compare/v0.1.2...v0.1.3) - 2015-03-30
+
+### Fixed
+- Bugfix for [#1](https://github.com/k0kubun/hamlit/issues/1) attribute nesting
+  on runtime. *Thanks to @eagletmt*
+
+## [0.1.2](https://github.com/k0kubun/hamlit/compare/v0.1.1...v0.1.2) - 2015-03-30
+
+### Fixed
+- Ignore false or nil values in attributes
+  - Partial fix for [#2](https://github.com/k0kubun/hamlit/issues/2).
+    *Thanks to @eagletmt*
+
+## [0.1.1](https://github.com/k0kubun/hamlit/compare/v0.1.0...v0.1.1) - 2015-03-30
+
+### Removed
+- Drop obsolete `--ugly` option for CLI
+  - Currently pretty mode is not implemented #2
+
+## [0.1.0](https://github.com/k0kubun/hamlit/compare/9cf8216...v0.1.0) - 2015-03-30
+
+- Initial release
+  - Passing haml-spec with ugly mode
diff --git a/Gemfile b/Gemfile
new file mode 100644 (file)
index 0000000..865dd88
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,14 @@
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in hamlit.gemspec
+gemspec
+
+gem 'benchmark-ips', '2.3.0'
+gem 'minitest-line'
+gem 'pry-byebug'
+
+if RUBY_PLATFORM !~ /mswin|mingw|bccwin|wince/
+  gem 'faml'
+  gem 'lineprof'
+  gem 'stackprof'
+end
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644 (file)
index 0000000..fe29eae
--- /dev/null
@@ -0,0 +1,44 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Takashi Kokubun
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+lib/hamlit/parser/*.rb and test/haml/* are:
+
+Copyright (c) 2006-2009 Hampton Catlin and Natalie Weizenbaum
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..25da210
--- /dev/null
+++ b/README.md
@@ -0,0 +1,152 @@
+# Hamlit
+
+[![Gem Version](https://badge.fury.io/rb/hamlit.svg)](http://badge.fury.io/rb/hamlit)
+[![Build Status](https://travis-ci.org/k0kubun/hamlit.svg?branch=master)](https://travis-ci.org/k0kubun/hamlit)
+
+Hamlit is a high performance [Haml](https://github.com/haml/haml) implementation.
+
+## Introduction
+
+### What is Hamlit?
+Hamlit is another implementation of [Haml](https://github.com/haml/haml).
+With some [limitations](REFERENCE.md#limitations) by design for performance,
+Hamlit is **8.54x times faster** than original haml gem in [this benchmark](benchmark/slim/run-benchmarks.rb),
+which is an HTML-escaped version of [slim-template/slim's one](https://github.com/slim-template/slim/blob/v3.0.6/benchmarks/run-benchmarks.rb) for fairness. ([Result on Travis](https://travis-ci.org/k0kubun/hamlit/jobs/93928561))
+
+![Hamlit Benchmark](https://i.gyazo.com/ac9eb910c7261cc0181bd7427c860d79.png)
+
+```
+       hamlit v2.0.1:   122622.3 i/s
+         faml v0.7.0:    94239.1 i/s - 1.30x slower
+         slim v3.0.6:    89143.0 i/s - 1.38x slower
+       erubis v2.7.0:    65047.8 i/s - 1.89x slower
+  haml v5.0.0.beta.2:    14363.6 i/s - 8.54x slower
+```
+
+### Why is Hamlit faster?
+
+#### Less string concatenation by design
+As written in [limitations](REFERENCE.md#limitations), Hamlit drops some not-so-important features which require
+works on runtime. With the optimized language design, we can reduce the string concatenation
+to build attributes.
+
+#### Temple optimizers
+Hamlit is built with [Temple](https://github.com/judofyr/temple), which is a framework to build
+template engines and also used in Slim. By using the framework and its optimizers, Hamlit can
+reduce string allocation and concatenation easily.
+
+#### Static analyzer
+Hamlit analyzes Ruby expressions with Ripper and render it on compilation if the expression
+is static. And Hamlit can also compile string literal with string interpolation to reduce
+string allocation and concatenation on runtime.
+
+#### C extension to build attributes
+While Hamlit has static analyzer and static attributes are rendered on compilation,
+dynamic attributes must be rendered on runtime. So Hamlit optimizes rendering on runtime
+with C extension.
+
+## Usage
+
+Hamlit currently supports Ruby 2.1 and higher. See [REFERENCE.md](REFERENCE.md) for detail features of Hamlit.
+
+### Rails
+
+Add this line to your application's Gemfile or just replace `gem "haml"` with `gem "hamlit"`.
+It enables rendering by Hamlit for \*.haml automatically.
+
+```rb
+gem 'hamlit'
+```
+
+If you want to use view generator, consider using [hamlit-rails](https://github.com/mfung/hamlit-rails).
+
+### Sinatra
+
+Replace `gem "haml"` with `gem "hamlit"` in Gemfile, and require "hamlit".
+See [sample/sinatra](sample/sinatra) for working sample.
+
+While Haml disables `escape_html` option by default, Hamlit enables it for security.
+If you want to disable it, please write:
+
+```rb
+set :haml, { escape_html: false }
+```
+
+
+## Command line interface
+
+You can see compiled code or rendering result with "hamlit" command.
+
+```bash
+$ gem install hamlit
+$ hamlit --help
+Commands:
+  hamlit compile HAML    # Show compile result
+  hamlit help [COMMAND]  # Describe available commands or one specific command
+  hamlit parse HAML      # Show parse result
+  hamlit render HAML     # Render haml template
+  hamlit temple HAML     # Show temple intermediate expression
+
+$ cat in.haml
+- user_id = 123
+%a{ href: "/users/#{user_id}" }
+
+# Show compiled code
+$ hamlit compile in.haml
+_buf = [];  user_id = 123;
+; _buf << ("<a href='/users/".freeze); _buf << (::Hamlit::Utils.escape_html((user_id))); _buf << ("'></a>\n".freeze); _buf = _buf.join
+
+# Render html
+$ hamlit render in.haml
+<a href='/users/123'></a>
+```
+
+## Contributing
+
+### Test latest version
+
+```rb
+# Gemfile
+gem 'hamlit', github: 'k0kubun/hamlit', submodules: true
+```
+
+### Development
+
+Contributions are welcomed. It'd be good to see
+[Temple's EXPRESSIONS.md](https://github.com/judofyr/temple/blob/v0.7.6/EXPRESSIONS.md)
+to learn Temple which is a template engine framework used in Hamlit.
+
+```bash
+$ git clone --recursive https://github.com/k0kubun/hamlit
+$ cd hamlit
+$ bundle install
+
+# Run all tests
+$ bundle exec rake test
+
+# Run one test
+$ bundle exec ruby -Ilib:test -rtest_helper test/hamlit/line_number_test.rb -l 12
+
+# Show compiling/rendering result of some template
+$ bundle exec exe/hamlit compile in.haml
+$ bundle exec exe/hamlit render in.haml
+
+# Use rails app to debug Hamlit
+$ cd sample/rails
+$ bundle install
+$ bundle exec rails s
+```
+
+### Reporting an issue
+
+Please report an issue with following information:
+
+- Full error backtrace
+- Haml template
+- Ruby version
+- Hamlit version
+- Rails/Sinatra version
+
+## License
+
+Copyright (c) 2015 Takashi Kokubun
diff --git a/REFERENCE.md b/REFERENCE.md
new file mode 100644 (file)
index 0000000..12748c0
--- /dev/null
@@ -0,0 +1,223 @@
+# Hamlit
+
+Basically Hamlit is the same as Haml.
+See [Haml's tutorial](http://haml.info/tutorial.html) if you are not familiar with Haml's syntax.
+
+[REFERENCE - Haml Documentation](http://haml.info/docs/yardoc/file.REFERENCE.html)
+
+## Supported features
+
+See [Haml's reference](http://haml.info/docs/yardoc/file.REFERENCE.html)
+for full features in original implementation.
+
+- [x] Using Haml
+  - [x] Rails XSS Protection
+  - [x] Ruby Module
+  - [x] Options
+  - [ ] Encodings
+- [x] Plain Text
+  - [x] Escaping: \
+- [x] HTML Elements
+  - [x] Element Name: %
+  - [x] Attributes: `
+     - [x] :class and :id Attributes
+     - [x] HTML-style Attributes: ()
+     - [x] Ruby 1.9-style Hashes
+     - [ ] Attribute Methods
+     - [x] Boolean Attributes
+     - [x] HTML5 Custom Data Attributes
+  - [x] Class and ID: . and #
+     - Implicit Div Elements
+  - [x] Empty (void) Tags: /
+  - [x] Whitespace Removal: > and <
+  - [x] Object Reference: []
+- [x] Doctype: !!!
+- [x] Comments
+  - [x] HTML Comments: /
+     - [x] Conditional Comments: /[]
+  - [x] Haml Comments: -#
+- [x] Ruby Evaluation
+  - [x] Inserting Ruby: =
+  - [x] Running Ruby: -
+     - [x] Ruby Blocks
+  - [x] Whitespace Preservation: ~
+  - [x] Ruby Interpolation: #{}
+  - [x] Escaping HTML: &=
+  - [x] Unescaping HTML: !=
+- [x] Filters
+  - [x] :cdata
+  - [x] :coffee
+  - [x] :css
+  - [x] :erb
+  - [x] :escaped
+  - [x] :javascript
+  - [x] :less
+  - [x] :markdown
+  - [ ] :maruku
+  - [x] :plain
+  - [x] :preserve
+  - [x] :ruby
+  - [x] :sass
+  - [x] :scss
+  - [ ] :textile
+  - [ ] Custom Filters
+- [ ] Helper Methods
+  - [x] preserve
+  - [x] surround
+  - [x] precede
+  - [x] succeed
+- [x] Multiline: |
+- [x] Whitespace Preservation
+- [ ] Helpers
+
+
+## Limitations
+
+### No pretty mode
+Haml has :pretty mode and :ugly mode. :pretty mode is used on development and indented beautifully.
+On production environment, :ugly mode is used and Hamlit currently supports only this mode.
+
+So you'll see difference rendering result on development environment, but it'll be the same on production.
+
+### No Haml buffer
+Hamlit uses `Array` as buffer for performance. So you can't touch Haml::Buffer from template when using Hamlit.
+
+### Haml helpers are still in development
+At the same time, because some methods in `Haml::Helpers` require `Haml::Buffer`, they are not supported now.
+But some helpers are supported on Rails. Some of not-implemented methods are planned to be supported.
+
+### Limited attributes hyphenation
+In Haml, `%a{ foo: { bar: 'baz' } }` is rendered as `<a foo-bar='baz'></a>`, whatever foo is.
+In Hamlit, this feature is supported only for data attribute. Hamlit renders `%a{ data: { foo: 'bar' } }`
+as `<a data-foo='bar'></a>` because it's data attribute. This design allows us to reduce work on runtime
+and the idea is originally in [Faml](https://github.com/eagletmt/faml).
+
+### Limited boolean attributes
+In Haml, `%a{ foo: false }` is rendered as `<a></a>`, whatever `foo` is.
+In Hamlit, this feature is supported for only boolean attributes, which are defined by
+http://www.w3.org/TR/xhtml1/guidelines.html or https://html.spec.whatwg.org/.
+The list is the same as `ActionView::Helpers::TagHelper::BOOLEAN_ATTRIBUTES`.
+In addition, data-\* is also regarded as boolean.
+
+Since `foo` is not boolean attribute, `%a{ foo: false }` is rendered as `<a foo='false'></a>`
+This is the same behavior as Rails helpers. Also for `%a{ foo: nil }`,
+Hamlit does not remove non-boolean attributes and render `<a foo=''></a>`
+(`foo` is not removed). This design allows us to reduce string concatenation and
+is the only difference between Faml and Hamlit.
+
+## 5 Types of Attributes
+
+Haml has 3 types of attributes: id, class and others.
+In addition, Hamlit treats data and boolean attributes specially.
+So there are 5 types of attributes in Hamlit.
+
+### id attribute
+Almost the same behavior as Haml, except no hyphenation and boolean support.
+Arrays are flattened, falsey values are removed (but attribute itself is not removed)
+and merging multiple ids results in concatenation by "\_".
+
+```rb
+# Input
+#foo{ id: 'bar' }
+%div{ id: %w[foo bar] }
+%div{ id: ['foo', false, ['bar', nil]] }
+%div{ id: false }
+
+# Output
+<div id='foo_bar'></span>
+<div id='foo_bar'></span>
+<div id='foo_bar'></span>
+<div id=''></span>
+```
+
+### class attribute
+Almost the same behavior as Haml, except no hyphenation and boolean support.
+Arrays are flattened, falsey values are removed (but attribute itself is not removed)
+and merging multiple classes results in unique alphabetical sort.
+
+```rb
+# Input
+.d.a(class='b c'){ class: 'c a' }
+%div{ class: 'd c b a' }
+%div{ class: ['d', nil, 'c', [false, 'b', 'a']] }
+%div{ class: false }
+
+# Output
+<div class='a b c d'></div>
+<div class='d c b a'></div>
+<div class='d c b a'></div>
+<div class=''></div>
+```
+
+### data attribute
+Completely compatible with Haml, hyphenation and boolean are supported.
+
+```rb
+# Input
+%div{ data: { disabled: true } }
+%div{ data: { foo: 'bar' } }
+
+# Output
+<div data-disabled></div>
+<div data-foo='bar'></div>
+```
+
+### boolean attributes
+No hyphenation but complete boolean support.
+
+```rb
+# Input
+%div{ disabled: 'foo' }
+%div{ disabled: true }
+%div{ disabled: false }
+
+# Output
+<div disabled='foo'></div>
+<div disabled></div>
+<div></div>
+```
+
+List of boolean attributes is:
+
+```
+disabled readonly multiple checked autobuffer autoplay controls loop selected hidden scoped async
+defer reversed ismap seamless muted required autofocus novalidate formnovalidate open pubdate
+itemscope allowfullscreen default inert sortable truespeed typemustmatch
+```
+
+"data-\*" is also regarded as boolean.
+
+### other attributes
+No hyphenation and boolean support. `false` is rendered as "false" (like Rails helpers).
+
+```rb
+# Input
+%input{ value: true }
+%input{ value: false }
+
+# Output
+<input value='true'>
+<input value='false'>
+```
+
+## Engine options
+
+| Option | Default | Feature |
+|:-------|:--------|:--------|
+| escape\_html | true | HTML-escape for Ruby script and interpolation. This is false in Haml. |
+| escape\_attrs | true | HTML-escape for Html attributes. |
+| format | :html | You can set :xhtml to change boolean attribute's format. |
+| attr\_quote | `'` | You can change attribute's wrapper to `"` or something. |
+
+### Set options for Rails
+
+```rb
+# config/initializers/hamlit.rb or somewhere
+Hamlit::RailsTemplate.set_options attr_quote: '"'
+```
+
+### Set options for Sinatra
+
+```rb
+set :haml, { attr_quote: '"' }
+```
diff --git a/Rakefile b/Rakefile
new file mode 100644 (file)
index 0000000..7208072
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,106 @@
+require 'bundler/setup'
+require 'bundler/gem_tasks'
+
+#
+# Prepend DevKit into compilation phase
+#
+if Gem.win_platform?
+  desc 'Activates DevKit'
+  task :devkit do
+    begin
+      require 'devkit'
+    rescue LoadError
+      abort 'Failed to load DevKit required for compilation'
+    end
+  end
+  task compile: :devkit
+end
+
+require 'rake/testtask'
+require 'rake/extensiontask'
+
+Dir['benchmark/*.rake'].each { |b| import(b) }
+
+namespace :haml do
+  Rake::TestTask.new do |t|
+    t.libs << 'lib' << 'test'
+    files = Dir['test/haml/*_test.rb']
+    files << 'test/haml/haml-spec/*_test.rb'
+    t.ruby_opts = %w[-rtest_helper]
+    t.test_files = files
+    t.verbose = true
+  end
+end
+
+namespace :hamlit do
+  Rake::TestTask.new do |t|
+    t.libs << 'lib' << 'test'
+    t.ruby_opts = %w[-rtest_helper]
+    t.test_files = Dir['test/hamlit/**/*_test.rb']
+    t.verbose = true
+  end
+end
+
+namespace :test do
+  Rake::TestTask.new(:all) do |t|
+    t.libs << 'lib' << 'test'
+    files = Dir['test/hamlit/**/*_test.rb']
+    files += Dir['test/haml/*_test.rb']
+    files << 'test/haml/haml-spec/*_test.rb'
+    t.ruby_opts = %w[-rtest_helper]
+    t.test_files = files
+    t.verbose = true
+  end
+
+  Rake::TestTask.new(:spec) do |t|
+    t.libs << 'lib' << 'test'
+    t.ruby_opts = %w[-rtest_helper]
+    t.test_files = %w[test/haml/haml-spec/ugly_test.rb test/haml/haml-spec/pretty_test.rb]
+    t.verbose = true
+  end
+
+  Rake::TestTask.new(:engine) do |t|
+    t.libs << 'lib' << 'test'
+    t.ruby_opts = %w[-rtest_helper]
+    t.test_files = %w[test/haml/engine_test.rb]
+    t.verbose = true
+  end
+
+  Rake::TestTask.new(:filters) do |t|
+    t.libs << 'lib' << 'test'
+    t.ruby_opts = %w[-rtest_helper]
+    t.test_files = %w[test/haml/filters_test.rb]
+    t.verbose = true
+  end
+
+  Rake::TestTask.new(:helper) do |t|
+    t.libs << 'lib' << 'test'
+    t.ruby_opts = %w[-rtest_helper]
+    t.test_files = %w[test/haml/helper_test.rb]
+    t.verbose = true
+  end
+
+  Rake::TestTask.new(:template) do |t|
+    t.libs << 'lib' << 'test'
+    t.ruby_opts = %w[-rtest_helper]
+    t.test_files = %w[test/haml/template_test.rb]
+    t.verbose = true
+  end
+end
+
+Rake::ExtensionTask.new(:hamlit) do |ext|
+  ext.lib_dir = 'lib/hamlit'
+end
+
+desc 'bench task for CI'
+task bench: :compile do
+  if ENV['SLIM_BENCH'] == '1'
+    cmd = %w[bundle exec ruby benchmark/slim/run-benchmarks.rb]
+  else
+    cmd = ['bin/bench', 'bench', ('-c' if ENV['COMPILE'] == '1'), *ENV['TEMPLATE'].split(',')].compact
+  end
+  exit system(*cmd)
+end
+
+task default: %w[compile hamlit:test]
+task test: %w[compile test:all]
diff --git a/benchmark/boolean_attribute.haml b/benchmark/boolean_attribute.haml
new file mode 100644 (file)
index 0000000..82587a6
--- /dev/null
@@ -0,0 +1,6 @@
+%input{ disabled: false }
+%input{ disabled: true }
+- disabled = false
+%input{ disabled: disabled }
+- disabled = true
+%input{ disabled: disabled }
diff --git a/benchmark/class_attribute.haml b/benchmark/class_attribute.haml
new file mode 100644 (file)
index 0000000..2faaf7e
--- /dev/null
@@ -0,0 +1,5 @@
+.book{ class: 'content active' }
+.book(class='content active')
+
+- klass = %w[content active]
+.book{ class: klass }
diff --git a/benchmark/common_attribute.haml b/benchmark/common_attribute.haml
new file mode 100644 (file)
index 0000000..7516da9
--- /dev/null
@@ -0,0 +1,3 @@
+%a{ href: '&"\'<>' }
+- href = '&"\'<>'
+%a{ href: href }
diff --git a/benchmark/data_attribute.haml b/benchmark/data_attribute.haml
new file mode 100644 (file)
index 0000000..b2ba20c
--- /dev/null
@@ -0,0 +1,4 @@
+%div{ data: { disabled: false } }
+%div{ data: { disabled: true } }
+- hash = { 'user' => { id: 1234, name: 'k0kubun' }, book_id: 5432 }
+%div{ data: hash } data
diff --git a/benchmark/dynamic_attributes/boolean_attribute.haml b/benchmark/dynamic_attributes/boolean_attribute.haml
new file mode 100644 (file)
index 0000000..f619d53
--- /dev/null
@@ -0,0 +1,4 @@
+- hash = { disabled: false }
+%input{ hash }
+- hash = { disabled: true }
+%input{ hash }
diff --git a/benchmark/dynamic_attributes/class_attribute.haml b/benchmark/dynamic_attributes/class_attribute.haml
new file mode 100644 (file)
index 0000000..3c750de
--- /dev/null
@@ -0,0 +1,4 @@
+- hash = { class: %w[content active] }
+.book{ hash }
+- arr = %w[foo bar]
+.book(class=arr){ hash }
diff --git a/benchmark/dynamic_attributes/common_attribute.haml b/benchmark/dynamic_attributes/common_attribute.haml
new file mode 100644 (file)
index 0000000..9c3e19f
--- /dev/null
@@ -0,0 +1,2 @@
+- hash = { href: '&"\'<>' }
+%a{ hash }
diff --git a/benchmark/dynamic_attributes/data_attribute.haml b/benchmark/dynamic_attributes/data_attribute.haml
new file mode 100644 (file)
index 0000000..a53d89b
--- /dev/null
@@ -0,0 +1,2 @@
+- hash = { data: { 'user' => { id: 1234, name: 'k0kubun' }, book_id: 5432 } }
+%div{ hash } data
diff --git a/benchmark/dynamic_attributes/id_attribute.haml b/benchmark/dynamic_attributes/id_attribute.haml
new file mode 100644 (file)
index 0000000..2969893
--- /dev/null
@@ -0,0 +1,2 @@
+- hash = { id: %w[content active] }
+#book{ hash }
diff --git a/benchmark/dynamic_boolean_attribute.haml b/benchmark/dynamic_boolean_attribute.haml
new file mode 100644 (file)
index 0000000..e8b9c90
--- /dev/null
@@ -0,0 +1,4 @@
+- disabled = false
+%input{ disabled: disabled }
+- disabled = true
+%input{ disabled: disabled }
diff --git a/benchmark/etc/attribute_builder.haml b/benchmark/etc/attribute_builder.haml
new file mode 100644 (file)
index 0000000..7c607e1
--- /dev/null
@@ -0,0 +1,5 @@
+- h = { 'user' => { id: 1234, name: 'eagletmt' }, book_id: 5432 }
+- c = %w[content active]
+
+%span.book{data: h, class: c}
+  Book
diff --git a/benchmark/etc/real_sample.haml b/benchmark/etc/real_sample.haml
new file mode 100644 (file)
index 0000000..1119690
--- /dev/null
@@ -0,0 +1,888 @@
+#id-1
+  = render partial: 'test'
+
+  %ul#id-2.class-1.class-2
+
+  %section#id-3
+    .class-3 string-1
+    .class-4
+      .class-5 string-2
+      %pre.class-6(readonly="readonly" style='width:1px')
+        :preserve
+          .class-7 string-3
+
+      .class-8 string-4
+      %pre.class-9(readonly="readonly" style='width:2px')
+        :preserve
+          .class-10 string-5
+      %p
+        Hello world
+
+      .class-12 string-6
+      %pre.class-13(readonly="readonly" style='width:3px')
+        :preserve
+          .class-14 string-7
+      %p
+        Hello world
+
+  %section#id-4
+    .class-17 string-8
+    .class-18
+      .class-19 string-9
+      %pre.class-20(readonly="readonly" style='width:4px')
+        :preserve
+          .class-21 string-10
+
+      .class-22 string-11
+      %pre.class-23(readonly="readonly" style='width:5px')
+        :preserve
+          .class-24 string-12
+
+      .class-25.class-26 Hello world
+      %pre.class-27(readonly="readonly" style='width:6px')
+        :preserve
+          .class-28.class-29 Hello world
+
+  %section#id-5
+    .class-30 string-13
+    .class-31 string-14
+    .class-32
+      %pre.class-33(readonly="readonly" style='width:7px')
+        :preserve
+          .class-34 string-15
+
+  %section#id-6
+    .class-35 string-16
+    %ul.class-36.class-37
+      %li
+        = link_to 'link', '#'
+      %li
+        = link_to 'link', '#', class: 'klass'
+      %li
+        = link_to 'link', '#', class: 'klass'
+      %li
+        = link_to 'link', '#', class: 'klass'
+      %li
+        = link_to 'link', '#', class: 'klass'
+    .class-38
+      %p text-17
+      %p text-18
+      %pre.class-41(readonly="readonly" style='width:8px')
+        :preserve
+          %ul.class-42.class-43
+            %li
+              = link_to 'link', '#'
+            %li
+              = link_to 'link', '#', class: 'klass'
+            %li
+              = link_to 'link', '#', class: 'klass'
+            %li
+              = link_to 'link', '#', class: 'klass'
+            %li
+              = link_to 'link', '#', class: 'klass'
+
+  %section#id-7
+    .class-44 string-19
+    %ul.class-45.class-46
+      %li#id-8
+        = link_to 'link', '#', class: 'klass1 klass2'
+        .class-47.class-48.class-49
+          Hello world
+    .class-50
+      %pre.class-51(readonly="readonly" style='width:9px')
+        :preserve
+          %ul.class-52.class-53
+            %li#id-10
+              = link_to 'link',
+                '#id-11',
+                class: 'klass1 klass2'
+              .class-54.class-55.class-56
+                Hello world
+
+  %section#id-12
+    .class-57 string-20
+    %ul.class-58.class-59
+      %li
+        = link_to 'link', '#'
+        .class-60 string-21
+        .class-61 string-22
+      %li
+        = link_to 'link', '#'
+        .class-62 string-23
+        .class-63 string-24
+    .class-64
+      %pre.class-65(readonly="readonly" style='width:10px')
+        :preserve
+          %ul.class-66.class-67
+            %li
+              = link_to 'link', '#'
+              .class-68 string-25
+              .class-69 string-26
+            %li
+              = link_to 'link', '#'
+              .class-70 string-27
+              .class-71 string-28
+
+  %section#id-13
+    .class-72 string-29
+    %ul.class-73.class-74
+      %li
+        = link_to 'link', '#'
+        .class-75 string-30
+        .class-76 string-31
+      %li
+        = link_to 'link', '#'
+        = image_tag 'https://google.com/favicon.ico', class: 'klass1'
+        .class-78 string-32
+      %li
+        = link_to 'link', '#'
+        = image_tag 'https://google.com/favicon.ico', class: 'klass1'
+        .class-80
+          .class-81 string-33
+          .class-82 string-34
+      %li
+        = link_to 'link', '#'
+        = image_tag 'https://google.com/favicon.ico', class: 'klass1'
+        .class-84
+          .class-85 string-35
+          .class-86 string-36
+      %li
+        = link_to 'link', '#'
+        = image_tag 'https://google.com/favicon.ico', class: 'klass1'
+        .class-88 string-37
+        .class-89 string-38
+    .class-90
+      %pre.class-91(readonly="readonly" style='width:11px')
+        :preserve
+          %ul.class-92.class-93
+            %li
+              = link_to 'link', '#'
+              .class-94 string-39
+              .class-95 string-40
+            %li
+              = link_to 'link', '#'
+              = image_tag class: 'klass1'
+              .class-96 string-41
+            %li
+              = link_to 'link', '#'
+              = image_tag class: 'klass1'
+              .class-97
+                .class-98 string-42
+                .class-99 string-43
+            %li
+              = link_to 'link', '#'
+              = image_tag class: 'klass1'
+              .class-100
+                .class-101 string-44
+                .class-102 string-45
+            %li
+              = link_to 'link', '#'
+              = image_tag class: 'klass1'
+              .class-103 string-46
+              .class-104 string-47
+
+  %section#id-14
+    .class-105 string-48
+    %ul.class-106.class-107.class-108
+      %li
+        = link_to 'link', '#'
+      %li
+        = link_to 'link', '#'
+      %li
+        = link_to 'link', '#'
+      %li
+        = link_to 'link', '#'
+
+    .class-109
+      %pre.class-110(readonly="readonly" style='width:12px')
+        :preserve
+          %ul.class-111.class-112.class-113
+            %li
+              = link_to 'link', '#'
+            %li
+              = link_to 'link', '#'
+
+  %section#id-15
+    .class-114 string-49
+    %ul.class-115.class-116.class-117
+      %li
+        = link_to 'link', '#', class: 'klass'
+        = image_tag 'https://github.com/favicon.ico', class: 'klass'
+        .class-119 string-50
+      %li
+        = link_to 'link', '#', class: 'klass'
+        = image_tag 'https://github.com/favicon.ico', class: 'klass'
+        .class-121 string-51
+      %li
+        = link_to 'link', '#', class: 'klass'
+        = image_tag 'https://github.com/favicon.ico', class: 'klass'
+        .class-123 string-52
+      %li
+        = link_to 'link', '#', class: 'klass'
+        = image_tag 'https://github.com/favicon.ico', class: 'klass'
+        .class-125 string-53
+
+    .class-126
+      %pre.class-127(readonly="readonly" style='width:13px')
+        :preserve
+          %ul.class-128.class-129.class-130
+            %li
+              = link_to 'link', '#'
+              = image_tag clsss: 'klass'
+              .class-131 string-54
+            %li
+              = link_to 'link', '#'
+              = image_tag clsss: 'klass'
+              .class-132 string-55
+
+  %section#id-16
+    .class-133 string-56
+    %ul.class-134.class-135
+      %li= link_to 'link', '#'
+      %li= link_to 'link', '#'
+      %li= link_to 'link', '#'
+    .class-136
+      %pre.class-137(readonly="readonly" style='width:14px')
+        :preserve
+          %ul.class-138.class-139
+            %li= link_to 'link', '#'
+            %li= link_to 'link', '#'
+            %li= link_to 'link', '#'
+
+  %section#id-17
+    .class-140 string-57
+    .class-141
+      %ul.class-142
+        %li
+          = image_tag 'https://github.com/favicon.ico'
+        %li
+          = image_tag 'https://github.com/favicon.ico'
+        %li
+          = image_tag 'https://github.com/favicon.ico'
+
+
+      %pre.class-146(readonly="readonly" style='width:15px')
+        :preserve
+          %ul.class-147
+            %li
+              = image_tag ''
+            %li
+              = image_tag ''
+            %li
+              = image_tag ''
+
+  %section#id-18
+    .class-148 string-58
+    .class-149
+      .class-150
+        .class-151.class-152
+          = image_tag 'https://github.com/favicon.ico'
+        .class-154.class-155
+          .class-156-title string-59
+          Hello world
+
+      %pre.class-157(readonly="readonly" style='width:16px')
+        :preserve
+          .class-158
+            .class-159.class-160
+              Hello world
+            .class-161.class-162
+              Hello world
+
+      %p text-60
+
+  %section#id-19
+    .class-164 string-61
+    .class-165
+      .class-166
+        .class-167
+          = image_tag 'https://github.com/favicon.ico'
+        .class-169
+          = image_tag 'https://github.com/favicon.ico'
+        .class-171
+          = image_tag 'https://github.com/favicon.ico'
+
+        .class-173
+          .class-174-title string-62
+          str
+
+      %pre.class-175(readonly="readonly" style='width:17px')
+        :preserve
+          .class-176
+            .class-177
+              = image_tag ''
+            .class-178
+              = image_tag ''
+            .class-179
+              = image_tag ''
+            .class-180
+              content
+      %p text-63
+      %p text-64
+
+
+  %section#id-20
+    .class-182 string-65
+    .class-183
+      %ul.class-184.class-185
+        %li.class-186.class-187
+          %span.class-188 str
+        %li.class-189
+          = link_to 'link', '#', class: 'klass'
+    .class-190
+      %pre.class-191(readonly="readonly" style='width:18px')
+        :preserve
+          .class-192
+            %ul.class-193.class-194
+              %li.class-195.class-196
+                %span.class-197 str
+              %li.class-198
+                = link_to 'link', '#', class: 'klass'
+
+  %section#id-21
+    .class-199 string-66
+    .class-200
+      %ul.class-201
+        %li.class-202.class-203
+          %span.class-204 str
+        %li.class-205
+          = link_to 'link', '#', class: 'klass'
+        %li.class-206
+          = link_to 'link', '#', class: 'klass'
+    .class-207
+      %pre.class-208(readonly="readonly" style='width:19px')
+        :preserve
+          .class-209
+            %ul.class-210
+              %li.class-211.class-212
+                %span.class-213 str
+              %li.class-214
+                = link_to 'link', '#', class: 'klass'
+              %li.class-215
+                = link_to 'link', '#', class: 'klass'
+
+  %section#id-22
+    .class-216 string-67
+    %ul.class-217
+      %li.class-218
+        = link_to 'link', '#'
+      %li
+        = link_to 'link', '#'
+      %li
+        = link_to 'link', '#'
+    .class-219
+      %pre.class-220(readonly="readonly" style='width:20px')
+        :preserve
+          %ul.class-221
+            %li.class-222
+              = link_to 'link', '#'
+            %li
+              = link_to 'link', '#'
+            %li
+              = link_to 'link', '#'
+
+      %p text-68
+
+  %section#id-23
+    .class-223 string-69
+    %ul.class-224
+      %li
+        = link_to 'link', '#'
+      %li
+        = link_to 'link', '#'
+      %li
+        = link_to 'link', '#'
+    .class-225
+      %pre.class-226(readonly="readonly" style='width:21px')
+        :preserve
+          %ul.class-227
+            %li
+              = link_to 'link', '#'
+            %li
+              = link_to 'link', '#'
+            %li
+              = link_to 'link', '#'
+
+  %section#id-24
+    .class-228 string-70
+    .class-229
+      %a(href="#" class="button") Hello world
+      %p text-71
+      %pre.class-230(readonly="readonly" style='width:22px')
+        :preserve
+          = link_to 'link', '#', class: 'klass'
+
+      %a(href="#" class="button min") Hello world
+      %pre.class-231(readonly="readonly" style='width:23px')
+        :preserve
+          = link_to 'link', '#', class: 'klass'
+
+  %section#id-25
+    .class-232 string-72
+    .class-233
+      %a(href="#" class="klass") Hello world
+      %p text-73
+      %pre.class-234(readonly="readonly" style='width:24px')
+        :preserve
+          = link_to 'link', '#',
+            class: 'klass'
+
+      %a(href="#" class="klass") Hello world
+      %pre.class-235(readonly="readonly" style='width:25px')
+        :preserve
+          = link_to 'link', '#',
+            class: 'klass'
+
+      %a(href="#" class="klass") Hello world
+      %pre.class-236(readonly="readonly" style='width:26px')
+        :preserve
+          = link_to 'link', '#',
+            class: 'klass'
+
+  %section#id-26
+    .class-237 string-74
+    .class-238
+      %a(href="#" class="klass") Hello world
+      %p text-75
+      %pre.class-239(readonly="readonly" style='width:27px')
+        :preserve
+          = link_to 'link', '#',
+          class: 'klass'
+      %a(href="#" class="klass") Hello world
+      %pre.class-240(readonly="readonly" style='width:28px')
+        :preserve
+          = link_to 'link', '#',
+          class: 'klass'
+      %a(href="#" class="klass") Hello world
+      %pre.class-241(readonly="readonly" style='width:29px')
+        :preserve
+          = link_to 'link', '#',
+          class: 'klass'
+
+  %section#id-27
+    .class-242 string-76
+    .class-243
+      %a(href="#" class="klass") Hello world
+      %p text-77
+      %pre.class-244(readonly="readonly" style='width:30px')
+        :preserve
+          = link_to 'link', '#',
+          class: 'klass'
+      %a(href="#" class="klass") Hello world
+      %pre.class-245(readonly="readonly" style='width:31px')
+        :preserve
+          = link_to 'link', '#',
+          class: 'klass'
+      %a(href="#" class="klass") Hello world
+      %pre.class-246(readonly="readonly" style='width:32px')
+        :preserve
+          = link_to 'link', '#',
+          class: 'klass'
+
+  %section#id-28
+    .class-247 string-78
+    %a(href="#" class="klass")
+      str
+      %span.class-248
+        str
+        %b text-79
+        str
+    .class-249
+      %pre.class-250(readonly="readonly" style='width:33px')
+        :preserve
+          = link_to 'link', '#'
+          %span.class-251
+            str
+            %b text-80
+            str
+
+  %section#id-29
+    .class-252 string-81
+    %label.class-253{for: 'f1_c1'}
+      %input{type: 'checkbox', id: 'f1_c1', checked: 'checked'}
+      str
+    %label.class-254{for: 'f1_c2'}
+      %input{type: 'checkbox', id: 'f1_c2'}
+      str
+    .class-255
+      %pre.class-256(readonly="readonly" style='width:34px')
+        :preserve
+          %label.class-257{for: 'f1_c1'}
+            %input{type: 'checkbox', id: 'f1_c1', checked: 'checked'}
+            str
+          %label.class-258{for: 'f1_c2'}
+            %input{type: 'checkbox', id: 'f1_c2'}
+            str
+
+    %label.class-259{for: 'f1_r1'}
+      %input{type: 'radio', name: 'form1', id: 'f1_r1', checked: 'checked'}
+      str
+    %label.class-260{for: 'f1_r2'}
+      %input{type: 'radio', name: 'form1', id: 'f1_r2'}
+      str
+    .class-261
+      %pre.class-262(readonly="readonly" style='width:35px')
+        :preserve
+          %label.class-263{for: 'f1_r1'}
+            %input{type: 'radio', name: 'form1', id: 'f1_r1', checked: 'checked'}
+            str
+          %label.class-264{for: 'f1_r2'}
+            %input{type: 'radio', name: 'form1', id: 'f1_r2'}
+            str
+
+  %section#id-30
+    .class-265 string-82
+    %ul.class-266.class-267
+      %li
+        %label.class-268{for: 'f2_c1'}
+          %input{type: 'checkbox', id: 'f2_c1', checked: 'checked'}
+          str
+      %li
+        %label.class-269{for: 'f2_c2'}
+          %input{type: 'checkbox', id: 'f2_c2'}
+          str
+    .class-270
+      %pre.class-271(readonly="readonly" style='width:36px')
+        :preserve
+          %ul.class-272.class-273
+            %li
+              %label.class-274{for: 'f2_c1'}
+                %input{type: 'checkbox', id: 'f2_c1', checked: 'checked'}
+                str
+            %li
+              %label.class-275{for: 'f2_c2'}
+                %input{type: 'checkbox', id: 'f2_c2'}
+                str
+
+    %ul.class-276.class-277
+      %li
+        %label.class-278{for: 'f2_r1'}
+          %input{type: 'radio', name: 'form2', id: 'f2_r1', checked: 'checked'}
+          str
+      %li
+        %label.class-279{for: 'f2_r2'}
+          %input{type: 'radio', name: 'form2', id: 'f2_r2'}
+          str
+    .class-280
+      %pre.class-281(readonly="readonly" style='width:37px')
+        :preserve
+          %ul.class-282.class-283
+            %li
+              %label.class-284{for: 'f2_r1'}
+                %input{type: 'radio', name: 'form2', id: 'f2_r1', checked: 'checked'}
+                str
+            %li
+              %label.class-285{for: 'f2_r2'}
+                %input{type: 'radio', name: 'form2', id: 'f2_r2'}
+                str
+
+  %section#id-31
+    .class-286 string-83
+    .class-287
+      %ul.class-288
+        %li text-84
+        %li text-85
+      %pre.class-289(readonly="readonly" style='width:38px')
+        :preserve
+          %ul.class-290
+            %li text-86
+            %li text-87
+
+      %ul.class-291.class-292
+        %li text-88
+        %li text-89
+      %pre.class-293(readonly="readonly" style='width:39px')
+        :preserve
+          %ul.class-294.class-295
+            %li text-90
+            %li text-91
+
+      %ul.class-296.class-297
+        %li text-92
+        %li text-93
+      %pre.class-298(readonly="readonly" style='width:40px')
+        :preserve
+          %ul.class-299.class-300
+            %li text-94
+            %li text-95
+
+      %ul.class-301.class-302
+        %li text-96
+        %li text-97
+      %pre.class-303(readonly="readonly" style='width:41px')
+        :preserve
+          %ul.class-304.class-305
+            %li text-98
+            %li text-99
+
+  %section#id-32
+    .class-306 string-100
+    .class-307
+      = image_tag '#'
+      = image_tag '#'
+      %pre.class-312(readonly="readonly" style='width:42px')
+        :preserve
+          = image_tag '#'
+          = image_tag '#'
+
+  %section#id-33
+    .class-315 string-101
+    .class-316
+      = image_tag '#'
+      %span.class-317 str
+      %pre.class-318(readonly="readonly" style='width:43px')
+        :preserve
+          = image_tag '#'
+          %span.class-319 str
+
+  %section#id-34
+    .class-320 string-102
+    .class-321
+      %a(href="#" class="klass")
+      %pre.class-322(readonly="readonly" style='width:44px')
+        :preserve
+          = link_to '', '#', class: 'klass'
+
+  %section#id-35
+    .class-323 string-103
+    .class-324
+      %a(href="#" class="klass")
+      %pre.class-325(readonly="readonly" style='width:45px')
+        :preserve
+          = link_to '', '#', class: 'klass'
+
+  %section#id-36
+    .class-326 string-104
+    .class-327
+      .class-328
+        %a(rel="prev" href="#")
+        %a(rel="next" href="#")
+      %pre.class-329(readonly="readonly" style='width:46px')
+        :preserve
+          .class-330
+            = link_to '', '#', rel: 'klass'
+            = link_to '', '#', rel: 'klass'
+
+  %section#id-37
+    .class-331 string-105
+    .class-332
+      .class-333
+        .class-334
+          %strong text-106
+          %span text-107
+        .class-335{ style: "width: 50%;" }
+
+      %pre.class-336{ readonly: "readonly", style: "height: 120px" }
+        :preserve
+          .class-337
+            .class-338
+              %strong text-108
+              %span text-109
+            .class-339{ style: "width: 50%;" }
+
+      .class-340.class-341
+        .class-342
+          %strong text-110
+          %span text-111
+        .class-343{ style: "width: 50%;" }
+
+      %pre.class-344{ readonly: "readonly", style: "height: 120px" }
+        :preserve
+          .class-345.class-346
+            .class-347
+              %strong text-112
+              %span text-113
+            .class-348{ style: "width: 50%;" }
+
+  %section#id-38
+    .class-349 string-114
+    .class-350
+      = render '#'
+      = render '#'
+      %pre.class-351(readonly="readonly" style='width:47px')
+        :preserve
+          = render '#'
+          = render '#'
+
+      %p text-115
+      %p text-116
+
+  %section#id-39
+    .class-353 string-117
+    .class-354
+      = link_to 'link', '#', class: 'klass1 klass2', :'data-foo_bar' => 'foo!!'
+    .class-355
+      .class-356 string-118
+      %pre.class-357(readonly="readonly" style='width:48px')
+        :preserve
+          = link_to 'link', '#',
+            class: 'klass1 klass2',
+            :'data-foo_bar' => 'foo!!'
+      .class-358 string-119
+      %pre.class-359(readonly="readonly" style='width:49px')
+        :preserve
+          foo.bar('Hoge')
+
+  %section#id-40
+    .class-361 string-120
+    .class-362
+      = link_to 'link', '#', class: 'klass1 klass2 klass3'
+    .class-363
+      .class-364 string-121
+      %pre.class-365(readonly="readonly" style='width:50px')
+        :preserve
+          = link_to 'link', '#',
+            class: 'klass1 klass2 klass3'
+
+      .class-366 string-122
+      %pre.class-367(readonly="readonly" style='width:51px')
+        :preserve
+          #id-43.class-368.class-369
+            .class-370
+              .class-371 string-123
+              %a.class-372{href: "#"}
+              str
+
+      %p text-124
+      %p text-125
+
+      .class-373 string-126
+      %pre.class-374(readonly="readonly" style='width:52px')
+        :preserve
+          // hello
+          $(window).bind('click', function(event) {
+          });
+
+          // hello
+          $('#id-44').bind('click', function(event) {
+          });
+
+          // world
+          $('#id-45').bind('click', function(event) {
+          });
+
+      %p text-127
+
+  %section#id-46
+    .class-378 string-128
+    .class-379
+      %ul.class-380
+        %li.class-381
+          str1
+        %li.class-382
+          str2
+        %li.class-383
+          str3
+    :javascript
+      $('.class-384').foo({bar: '.class-386'});
+    :css
+      .class-387 {
+        min-height: 13px;
+      }
+      .class-388 {
+        height: 1px;
+        background: #000;
+        padding: 1px;
+        text-align: center;
+      }
+      .class-390 {
+        background: #000;
+      }
+      .class-392 {
+        background: #000;
+      }
+
+    .class-394
+      %pre.class-395(readonly="readonly" style='width:53px')
+        :preserve
+          .class-396
+            %ul.class-397
+              %li.class-398 str1
+              %li.class-399 str2
+              %li.class-400 str3
+          :javascript
+            $('.class-401').bar({foo: '.class-403'});
+
+    %ul.class-404.class-405
+      %li= link_to 'link', '#'
+      %li= link_to 'link', '#'
+      %li= link_to 'link', '#'
+
+    .class-406
+      %ul.class-407
+        %li#id-52A.class-408 str1
+        %li#id-53B.class-409 str2
+        %li#id-54C.class-410 str3
+
+    :javascript
+      $('.class-411').click({foo: '.class-413 > li > a'});
+
+    :css
+      .class-414 {
+        height: 1px;
+        background: #000;
+        padding: 1px;
+        text-align: center;
+      }
+      .class-416 {
+        background: #000;
+      }
+      .class-418 {
+        background: #000;
+      }
+
+    .class-420
+      %pre.class-421(readonly="readonly" style='width:54px')
+        :preserve
+          %ul.class-422.class-423
+            %li= link_to 'link', '#'
+            %li= link_to 'link', '#'
+            %li= link_to 'link', '#'
+
+          .class-424
+            %ul.class-425
+              %li#id-60A.class-426 str1
+              %li#id-61B.class-427 str2
+              %li#id-62C.class-428 str3
+
+          :javascript
+            $('.class-429').bind({links: '.klass'});
+
+  %section#id-63
+    .class-432 string-136
+    .class-433
+      .class-434 string-137
+      %pre.class-435(readonly="readonly" style='width:55px')
+        :preserve
+          #id-64
+            -# hello
+
+            .class-436
+              -# world
+
+            %span.class-437
+
+            #id-65
+              -# hey
+      .class-438 string-138
+      %pre.class-439(readonly="readonly" style='width:56px')
+        :preserve
+          // hello
+          $(document).bind('click', function(event) {
+          });
+
+          // world
+          $(document).bind('click', function(event) {
+          });
+
+#id-66XXX.class-442.class-443
+  .class-444
+    .class-445 string-139
+
+    %a.class-446{href: "#"}
+    str
+
+:javascript
+  (function ($) {
+    $(".foo").removeClass("bar");
+  })(jQuery);
diff --git a/benchmark/etc/real_sample.rb b/benchmark/etc/real_sample.rb
new file mode 100644 (file)
index 0000000..5cefec7
--- /dev/null
@@ -0,0 +1,11 @@
+def render(*)
+  '<div class="render"></div>'
+end
+
+def link_to(a, b, *c)
+  "<a href='" << b << ">".freeze << a << '</div>'.freeze
+end
+
+def image_tag(*)
+  '<img src="https://github.com/favicon.ico" />'
+end
diff --git a/benchmark/etc/static_analyzer.haml b/benchmark/etc/static_analyzer.haml
new file mode 100644 (file)
index 0000000..210a551
--- /dev/null
@@ -0,0 +1 @@
+#foo.bar{ data: { 'user' => { id: 1234, name: 'k0kubun' }, book_id: 5432 } }
diff --git a/benchmark/etc/string_interpolation.haml b/benchmark/etc/string_interpolation.haml
new file mode 100644 (file)
index 0000000..6ccfe11
--- /dev/null
@@ -0,0 +1,2 @@
+- id = 12347
+%a{ href: "https://example.com/users/#{id}" }= "id: #{id}"
diff --git a/benchmark/etc/tags.haml b/benchmark/etc/tags.haml
new file mode 100644 (file)
index 0000000..49c139d
--- /dev/null
@@ -0,0 +1,3 @@
+%span hello
+%div
+  world
diff --git a/benchmark/ext/build_data.rb b/benchmark/ext/build_data.rb
new file mode 100755 (executable)
index 0000000..5d47242
--- /dev/null
@@ -0,0 +1,17 @@
+#!/usr/bin/env ruby
+
+require 'bundler/setup'
+require 'hamlit'
+require 'faml'
+require 'benchmark/ips'
+require_relative '../utils/benchmark_ips_extension'
+
+h = { 'user' => { id: 1234, name: 'k0kubun' }, book_id: 5432 }
+
+Benchmark.ips do |x|
+  quote = "'"
+  faml_options = { data: h }
+  x.report("Faml::AB.build")    { Faml::AttributeBuilder.build(quote, true, nil, faml_options) }
+  x.report("Hamlit.build_data") { Hamlit::AttributeBuilder.build_data(true, quote, h) }
+  x.compare!
+end
diff --git a/benchmark/ext/build_id.rb b/benchmark/ext/build_id.rb
new file mode 100755 (executable)
index 0000000..0551c74
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env ruby
+
+require 'bundler/setup'
+require 'hamlit'
+require 'faml'
+require 'benchmark/ips'
+require_relative '../utils/benchmark_ips_extension'
+
+Benchmark.ips do |x|
+  x.report("Faml::AB.build")      { Faml::AttributeBuilder.build("'", true, nil, {:id=>"book"},  id: %w[content active]) }
+  x.report("Hamlit::AB.build_id") { Hamlit::AttributeBuilder.build_id(true, "book", %w[content active]) }
+  x.compare!
+end
diff --git a/benchmark/id_attribute.haml b/benchmark/id_attribute.haml
new file mode 100644 (file)
index 0000000..af6acf6
--- /dev/null
@@ -0,0 +1,3 @@
+#book{ id: 'content active' }
+- id = %w[content active]
+#book{ id: id }
diff --git a/benchmark/plain.haml b/benchmark/plain.haml
new file mode 100644 (file)
index 0000000..4c7cdc3
--- /dev/null
@@ -0,0 +1,4 @@
+- hello = 'world'
+%span aaa#{hello}bbb
+%span
+  aaa#{hello}bbb
diff --git a/benchmark/script.haml b/benchmark/script.haml
new file mode 100644 (file)
index 0000000..f318d7b
--- /dev/null
@@ -0,0 +1,4 @@
+- dynamic = 'dynamic'
+= "#{ dynamic } script"
+= "#{ 'static'} script"
+= ['&', '"', "'", '<', '>']
diff --git a/benchmark/slim/LICENSE b/benchmark/slim/LICENSE
new file mode 100644 (file)
index 0000000..6af6518
--- /dev/null
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2010 - 2015 Slim Team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/benchmark/slim/context.rb b/benchmark/slim/context.rb
new file mode 100644 (file)
index 0000000..4d71e34
--- /dev/null
@@ -0,0 +1,11 @@
+class Context
+  def header
+    'Colors'
+  end
+
+  def item
+    [ { name: 'red',   current: true,  url: '#red'   },
+      { name: 'green', current: false, url: '#green' },
+      { name: 'blue',  current: false, url: '#blue'  } ]
+  end
+end
diff --git a/benchmark/slim/run-benchmarks.rb b/benchmark/slim/run-benchmarks.rb
new file mode 100644 (file)
index 0000000..b46950d
--- /dev/null
@@ -0,0 +1,94 @@
+#!/usr/bin/env ruby
+
+=begin
+The MIT License
+
+Copyright (c) 2010 - 2015 Slim Team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+=end
+
+#
+# Original: https://github.com/slim-template/slim/blob/v3.0.6/benchmarks/run-benchmarks.rb
+#
+# SlimBenchmarks with following modifications:
+#   1. Skipping slow engines, tilt and parsing benches.
+#   2. All Ruby script and attributes are escaped for fairness.
+#   3. Faml and Hamlit are added.
+#
+
+$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'), File.dirname(__FILE__))
+
+require 'slim'
+require 'context'
+
+require 'benchmark/ips'
+require 'tilt'
+require 'erubis'
+require 'erb'
+require 'haml'
+require 'faml'
+require 'hamlit'
+
+class SlimBenchmarks
+  def initialize(slow)
+    @benches    = []
+
+    @erb_code  = File.read(File.dirname(__FILE__) + '/view.erb')
+    @haml_code = File.read(File.dirname(__FILE__) + '/view.haml')
+    @slim_code = File.read(File.dirname(__FILE__) + '/view.slim')
+
+    init_compiled_benches
+  end
+
+  def init_compiled_benches
+    haml_ugly   = Haml::Engine.new(@haml_code, format: :html5, ugly: true, escape_html: true)
+
+    context  = Context.new
+
+    haml_ugly.def_method(context, :run_haml_ugly)
+    context.instance_eval %{
+      def run_erubis; #{Erubis::Eruby.new(@erb_code).src}; end
+      def run_slim_ugly; #{Slim::Engine.new.call @slim_code}; end
+      def run_faml; #{Faml::Engine.new.call @haml_code}; end
+      def run_hamlit; #{Hamlit::Engine.new.call @haml_code}; end
+    }
+
+    bench("erubis v#{Erubis::VERSION}") { context.run_erubis }
+    bench("slim v#{Slim::VERSION}")     { context.run_slim_ugly }
+    bench("haml v#{Haml::VERSION}")     { context.run_haml_ugly }
+    bench("faml v#{Faml::VERSION}")     { context.run_faml }
+    bench("hamlit v#{Hamlit::VERSION}") { context.run_hamlit }
+  end
+
+  def run
+    Benchmark.ips do |x|
+      @benches.each do |name, block|
+        x.report(name.to_s, &block)
+      end
+      x.compare!
+    end
+  end
+
+  def bench(name, &block)
+    @benches.push([name, block])
+  end
+end
+
+SlimBenchmarks.new(ENV['slow']).run
diff --git a/benchmark/slim/view.erb b/benchmark/slim/view.erb
new file mode 100644 (file)
index 0000000..3ffc829
--- /dev/null
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+
+<html>
+  <head>
+    <title>Simple Benchmark</title>
+  </head>
+  <body>
+    <h1><%== header %></h1>
+    <% unless item.empty? %>
+      <ul>
+      <% for i in item %>
+        <% if i[:current] %>
+          <li><strong><%== i[:name] %></strong></li>
+        <% else %>
+          <li><a href="<%== i[:url] %>"><%== i[:name] %></a></li>
+        <% end %>
+      <% end %>
+      </ul>
+    <% else %>
+      <p>The list is empty.</p>
+    <% end %>
+  </body>
+</html>
diff --git a/benchmark/slim/view.haml b/benchmark/slim/view.haml
new file mode 100644 (file)
index 0000000..2e37c85
--- /dev/null
@@ -0,0 +1,18 @@
+!!! html
+
+%html
+  %head
+    %title Simple Benchmark
+  %body
+    %h1= header
+    - unless item.empty?
+      %ul
+        - for i in item
+          - if i[:current]
+            %li
+              %strong= i[:name]
+          - else
+            %li
+              %a{:href => i[:url]}= i[:name]
+    - else
+      %p The list is empty.
diff --git a/benchmark/slim/view.slim b/benchmark/slim/view.slim
new file mode 100644 (file)
index 0000000..9853bc7
--- /dev/null
@@ -0,0 +1,17 @@
+doctype html
+html
+  head
+    title Simple Benchmark
+  body
+    h1 = header
+    - unless item.empty?
+      ul
+        - for i in item
+          - if i[:current]
+            li
+              strong = i[:name]
+          - else
+            li
+              a href=i[:url] = i[:name]
+    - else
+      p The list is empty.
diff --git a/benchmark/utils/benchmark_ips_extension.rb b/benchmark/utils/benchmark_ips_extension.rb
new file mode 100644 (file)
index 0000000..a8fbe1e
--- /dev/null
@@ -0,0 +1,43 @@
+# Monkey patch to show milliseconds
+module Benchmark
+  module IPS
+    class Report
+      module EntryExtension
+        def body
+          return super if Benchmark::IPS.options[:format] != :human
+
+          left = "%s i/s (%1.3fms)" % [Helpers.scale(ips), (1000.0 / ips)]
+          iters = Helpers.scale(@iterations)
+
+          if @show_total_time
+            left.ljust(20) + (" - %s in %10.6fs" % [iters, runtime])
+          else
+            left.ljust(20) + (" - %s" % iters)
+          end
+        end
+      end
+      Entry.prepend(EntryExtension)
+    end
+  end
+
+  module CompareExtension
+    def compare(*reports)
+      return if reports.size < 2
+
+      sorted = reports.sort_by(&:ips).reverse
+      best = sorted.shift
+      $stdout.puts "\nComparison:"
+      $stdout.printf "%20s: %10.1f i/s (%1.3fms)\n", best.label, best.ips, (1000.0 / best.ips)
+
+      sorted.each do |report|
+        name = report.label.to_s
+
+        x = (best.ips.to_f / report.ips.to_f)
+        $stdout.printf "%20s: %10.1f i/s (%1.3fms) - %.2fx slower\n", name, report.ips, (1000.0 / report.ips), x
+      end
+
+      $stdout.puts
+    end
+  end
+  extend CompareExtension
+end
diff --git a/bin/bench b/bin/bench
new file mode 100755 (executable)
index 0000000..686225f
--- /dev/null
+++ b/bin/bench
@@ -0,0 +1,77 @@
+#!/usr/bin/env ruby
+
+require 'bundler/setup'
+require 'hamlit'
+require 'faml'
+require 'thor'
+require 'benchmark/ips'
+require_relative '../benchmark/utils/benchmark_ips_extension'
+
+class Bench < Thor
+  class_option :show_template, type: :boolean, aliases: ['-t']
+
+  desc 'bench HAML', 'Benchmark haml template'
+  option :compile, type: :boolean, aliases: ['-c']
+  option :show_code, type: :boolean, aliases: ['-s']
+  def bench(*files)
+    files.each { |file| render(file) }
+    files.each { |file| compile(file) if options[:compile] }
+    files.each { |file| code(file) if options[:show_code] }
+  end
+
+  desc 'compile HAML', 'Benchmark compilation'
+  def compile(file)
+    puts "#{?= * 49}\n Compilation: #{file}\n#{?= * 49}"
+    haml = File.read(file)
+
+    Benchmark.ips do |x|
+      x.report("haml v#{Haml::VERSION}") { Haml::Engine.new(haml, escape_html: true, escape_attrs: true, ugly: true).precompiled }
+      x.report("faml v#{Faml::VERSION}") { Faml::Engine.new.call(haml) }
+      x.report("hamlit v#{Hamlit::VERSION}") { Hamlit::Engine.new.call(haml) }
+      x.compare!
+    end
+  end
+
+  desc 'render HAML', 'Benchmark rendering'
+  def render(file)
+    puts "#{?= * 49}\n Rendering: #{file}\n#{?= * 49}"
+    haml = File.read(file)
+    puts haml + "\n" if options[:show_template]
+    object = Object.new
+    ruby_file = file.gsub(/\.haml\z/, '.rb')
+    if File.exist?(ruby_file)
+      object.instance_eval(File.read(ruby_file))
+    end
+
+    Haml::Engine.new(haml, escape_html: true, escape_attrs: true, ugly: true).def_method(object, :haml)
+    object.instance_eval "def faml; #{Faml::Engine.new.call(haml)}; end"
+    object.instance_eval "def hamlit; #{Hamlit::Engine.new.call(haml)}; end"
+
+    Benchmark.ips do |x|
+      x.report("haml v#{Haml::VERSION}") { object.haml }
+      x.report("faml v#{Faml::VERSION}") { object.faml }
+      x.report("hamlit v#{Hamlit::VERSION}") { object.hamlit }
+      x.compare!
+    end
+  end
+
+  desc 'code HAML', 'Show compiled code'
+  def code(file)
+    haml = File.read(file)
+    puts "#{?= * 49}\n Haml Source: #{file}\n#{?= * 49}"
+    puts Haml::Engine.new(haml, escape_html: true, escape_attrs: true, ugly: true).precompiled
+    puts "\n#{?= * 49}\n Faml Source: #{file}\n#{?= * 49}"
+    puts Faml::Engine.new.call(haml)
+    puts "\n#{?= * 49}\n Hamlit Source: #{file}\n#{?= * 49}"
+    puts Hamlit::Engine.new.call(haml)
+  end
+
+  private
+
+  def method_missing(*args)
+    return super if args.length > 1
+    render(args.first.to_s)
+  end
+end
+
+Bench.start
diff --git a/bin/console b/bin/console
new file mode 100755 (executable)
index 0000000..9249aa5
--- /dev/null
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+
+require 'bundler/setup'
+require 'hamlit'
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+# (If you use this, don't forget to add pry to your Gemfile!)
+require 'pry'
+Pry.start
diff --git a/bin/lineprof b/bin/lineprof
new file mode 100755 (executable)
index 0000000..e127f0b
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env ruby
+
+require 'bundler/setup'
+require 'hamlit'
+require 'lineprof'
+require 'tempfile'
+require 'thor'
+
+class CLI < Thor
+  desc 'render HAML', 'Benchmark render'
+  def render(file)
+    haml = File.read(file)
+    compiled = Hamlit::Engine.new.call(haml)
+    code = [
+      'require "lineprof"',
+      'require "hamlit"',
+
+      'Lineprof.profile(/./) do',
+        '100.times do',
+          compiled,
+        'end',
+      'end',
+    ].join("\n")
+
+    file = Tempfile.create('compiled')
+    file.write(code)
+    file.close
+
+    system("bundle exec ruby #{file.path}")
+  end
+
+  desc 'compile HAML', 'Benchmark compile'
+  def compile(file)
+    haml = File.read(file)
+    Lineprof.profile(/./) do
+      100.times { Hamlit::Engine.new.call(haml) }
+    end
+  end
+
+  private
+
+  def method_missing(*args)
+    return super if args.length > 1
+    render(args.first.to_s)
+  end
+end
+
+CLI.start
diff --git a/bin/ruby b/bin/ruby
new file mode 100755 (executable)
index 0000000..4e72e3f
--- /dev/null
+++ b/bin/ruby
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+bundle exec ruby -Ilib:test -rtest_helper $@
diff --git a/bin/setup b/bin/setup
new file mode 100755 (executable)
index 0000000..b65ed50
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -euo pipefail
+IFS=$'\n\t'
+
+bundle install
+
+# Do any other automated setup that you need to do here
diff --git a/bin/stackprof b/bin/stackprof
new file mode 100755 (executable)
index 0000000..76078d4
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+
+require 'bundler/setup'
+require 'hamlit'
+require 'stackprof'
+
+def open_flamegraph(report)
+  temp = `mktemp /tmp/stackflame-XXXXXXXX`.strip
+  data_path = "#{temp}.js"
+  system("mv #{temp} #{data_path}")
+
+  File.open(data_path, 'w') do |f|
+    report.print_flamegraph(f)
+  end
+
+  viewer_path = File.join(`bundle show stackprof`.strip, 'lib/stackprof/flamegraph/viewer.html')
+  url = "file://#{viewer_path}?data=#{data_path}"
+  system(%Q[osascript -e 'open location "#{url}"'])
+end
+
+haml = File.read(ARGV.first)
+StackProf.start(mode: :wall, interval: 1, raw: false)
+Hamlit::Engine.new.call(haml)
+StackProf.stop
+
+report = StackProf::Report.new(StackProf.results)
+report.print_text(false)
diff --git a/bin/test b/bin/test
new file mode 100755 (executable)
index 0000000..6d353b0
--- /dev/null
+++ b/bin/test
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+VERSIONS=(
+  2.1.10
+  2.2.5
+  2.3.1
+)
+
+set -e
+trap 'echo "${VERSIONS[2]}" > .ruby-version' 0
+
+function test_with() {
+  version=$1
+  rbenv local $version
+  if ! bundle check > /dev/null; then
+    bundle install
+  fi
+  ruby -v
+  bundle exec rake test
+}
+
+for version in ${VERSIONS[@]}; do
+  test_with $version
+done
diff --git a/exe/hamlit b/exe/hamlit
new file mode 100755 (executable)
index 0000000..0978976
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+require 'hamlit/cli'
+
+Hamlit::CLI.start(ARGV)
diff --git a/ext/hamlit/extconf.rb b/ext/hamlit/extconf.rb
new file mode 100644 (file)
index 0000000..182716d
--- /dev/null
@@ -0,0 +1,10 @@
+require 'mkmf'
+
+$CFLAGS << ' -Wall -Wextra'
+
+$srcs = %w[
+  hamlit.c
+  hescape.c
+]
+
+create_makefile('hamlit/hamlit')
diff --git a/ext/hamlit/hamlit.c b/ext/hamlit/hamlit.c
new file mode 100644 (file)
index 0000000..bd7793b
--- /dev/null
@@ -0,0 +1,513 @@
+#include <ruby.h>
+#include <ruby/encoding.h>
+#include "hescape.h"
+#include "string.h"
+
+VALUE mAttributeBuilder, mObjectRef;
+static ID id_flatten, id_keys, id_parse, id_prepend, id_tr, id_uniq_bang;
+static ID id_data, id_equal, id_hyphen, id_space, id_underscore;
+static ID id_boolean_attributes, id_xhtml;
+
+static VALUE str_data()        { return rb_const_get(mAttributeBuilder, id_data); }
+static VALUE str_equal()       { return rb_const_get(mAttributeBuilder, id_equal); }
+static VALUE str_hyphen()      { return rb_const_get(mAttributeBuilder, id_hyphen); }
+static VALUE str_space()       { return rb_const_get(mAttributeBuilder, id_space); }
+static VALUE str_underscore()  { return rb_const_get(mAttributeBuilder, id_underscore); }
+
+static void
+delete_falsey_values(VALUE values)
+{
+  VALUE value;
+  long i;
+
+  for (i = RARRAY_LEN(values) - 1; 0 <= i; i--) {
+    value = rb_ary_entry(values, i);
+    if (!RTEST(value)) {
+      rb_ary_delete_at(values, i);
+    }
+  }
+}
+
+static int
+str_eq(VALUE str, const char *cstr, long n)
+{
+  return RSTRING_LEN(str) == n && memcmp(RSTRING_PTR(str), cstr, n) == 0;
+}
+
+static VALUE
+to_s(VALUE value)
+{
+  return rb_convert_type(value, T_STRING, "String", "to_s");
+}
+
+static VALUE
+hyphenate(VALUE str)
+{
+  long i;
+
+  if (OBJ_FROZEN(str)) str = rb_str_dup(str);
+
+  for (i = 0; i < RSTRING_LEN(str); i++) {
+    if (RSTRING_PTR(str)[i] == '_') {
+      rb_str_update(str, i, 1, str_hyphen());
+    }
+  }
+  return str;
+}
+
+static VALUE
+escape_html(VALUE str)
+{
+  char *buf;
+  unsigned int size;
+  Check_Type(str, T_STRING);
+
+  size = hesc_escape_html(&buf, RSTRING_PTR(str), RSTRING_LEN(str));
+  if (size > RSTRING_LEN(str)) {
+    str = rb_enc_str_new(buf, size, rb_utf8_encoding());
+    free((void *)buf);
+  }
+
+  return str;
+}
+
+static VALUE
+escape_attribute(VALUE escape_attrs, VALUE str)
+{
+  if (RTEST(escape_attrs)) {
+    return escape_html(str);
+  } else {
+    return str;
+  }
+}
+
+static VALUE
+rb_escape_html(RB_UNUSED_VAR(VALUE self), VALUE value)
+{
+  return escape_html(to_s(value));
+}
+
+static VALUE
+hamlit_build_id(VALUE escape_attrs, VALUE values)
+{
+  VALUE attr_value;
+
+  values = rb_funcall(values, id_flatten, 0);
+  delete_falsey_values(values);
+
+  attr_value = rb_ary_join(values, str_underscore());
+  return escape_attribute(escape_attrs, attr_value);
+}
+
+static VALUE
+hamlit_build_single_class(VALUE escape_attrs, VALUE value)
+{
+  switch (TYPE(value)) {
+    case T_STRING:
+      break;
+    case T_ARRAY:
+      value = rb_funcall(value, id_flatten, 0);
+      delete_falsey_values(value);
+      value = rb_ary_join(value, str_space());
+      break;
+    default:
+      if (RTEST(value)) {
+        value = to_s(value);
+      } else {
+        return rb_str_new_cstr("");
+      }
+      break;
+  }
+  return escape_attribute(escape_attrs, value);
+}
+
+static VALUE
+hamlit_build_multi_class(VALUE escape_attrs, VALUE values)
+{
+  long i, j;
+  VALUE value, buf;
+
+  buf = rb_ary_new2(RARRAY_LEN(values));
+
+  for (i = 0; i < RARRAY_LEN(values); i++) {
+    value = rb_ary_entry(values, i);
+    switch (TYPE(value)) {
+      case T_STRING:
+        rb_ary_concat(buf, rb_str_split(value, " "));
+        break;
+      case T_ARRAY:
+        value = rb_funcall(value, id_flatten, 0);
+        delete_falsey_values(value);
+        for (j = 0; j < RARRAY_LEN(value); j++) {
+          rb_ary_push(buf, to_s(rb_ary_entry(value, j)));
+        }
+        break;
+      default:
+        if (RTEST(value)) {
+          rb_ary_push(buf, to_s(value));
+        }
+        break;
+    }
+  }
+
+  rb_ary_sort_bang(buf);
+  rb_funcall(buf, id_uniq_bang, 0);
+
+  return escape_attribute(escape_attrs, rb_ary_join(buf, str_space()));
+}
+
+static VALUE
+hamlit_build_class(VALUE escape_attrs, VALUE array)
+{
+  if (RARRAY_LEN(array) == 1) {
+    return hamlit_build_single_class(escape_attrs, rb_ary_entry(array, 0));
+  } else {
+    return hamlit_build_multi_class(escape_attrs, array);
+  }
+}
+
+static int
+merge_data_attrs_i(VALUE key, VALUE value, VALUE merged)
+{
+  if (NIL_P(key)) {
+    rb_hash_aset(merged, str_data(), value);
+  } else {
+    key = rb_str_concat(rb_str_new_cstr("data-"), to_s(key));
+    rb_hash_aset(merged, key, value);
+  }
+  return ST_CONTINUE;
+}
+
+static VALUE
+merge_data_attrs(VALUE values)
+{
+  long i;
+  VALUE value, merged = rb_hash_new();
+
+  for (i = 0; i < RARRAY_LEN(values); i++) {
+    value = rb_ary_entry(values, i);
+    switch (TYPE(value)) {
+      case T_HASH:
+        rb_hash_foreach(value, merge_data_attrs_i, merged);
+        break;
+      default:
+        rb_hash_aset(merged, str_data(), value);
+        break;
+    }
+  }
+  return merged;
+}
+
+struct flatten_data_attrs_i2_arg {
+  VALUE flattened;
+  VALUE key;
+};
+
+static int
+flatten_data_attrs_i2(VALUE k, VALUE v, VALUE ptr)
+{
+  VALUE key;
+  struct flatten_data_attrs_i2_arg *arg = (struct flatten_data_attrs_i2_arg *)ptr;
+
+  if (!RTEST(v)) return ST_CONTINUE;
+
+  if (k == Qnil) {
+    rb_hash_aset(arg->flattened, arg->key, v);
+  } else {
+    key = rb_str_dup(arg->key);
+    rb_str_cat(key, "-", 1);
+    rb_str_concat(key, to_s(k));
+
+    rb_hash_aset(arg->flattened, key, v);
+  }
+  return ST_CONTINUE;
+}
+
+static VALUE flatten_data_attrs(VALUE attrs);
+
+static int
+flatten_data_attrs_i(VALUE key, VALUE value, VALUE flattened)
+{
+  struct flatten_data_attrs_i2_arg arg;
+  key = hyphenate(to_s(key));
+
+  switch (TYPE(value)) {
+    case T_HASH:
+      value = flatten_data_attrs(value);
+      arg.key       = key;
+      arg.flattened = flattened;
+      rb_hash_foreach(value, flatten_data_attrs_i2, (VALUE)(&arg));
+      break;
+    default:
+      if (RTEST(value)) rb_hash_aset(flattened, key, value);
+      break;
+  }
+  return ST_CONTINUE;
+}
+
+static VALUE
+flatten_data_attrs(VALUE attrs)
+{
+  VALUE flattened = rb_hash_new();
+  rb_hash_foreach(attrs, flatten_data_attrs_i, flattened);
+
+  return flattened;
+}
+
+static VALUE
+hamlit_build_data(VALUE escape_attrs, VALUE quote, VALUE values)
+{
+  long i;
+  VALUE attrs, buf, keys, key, value;
+
+  attrs = merge_data_attrs(values);
+  attrs = flatten_data_attrs(attrs);
+  keys  = rb_ary_sort_bang(rb_funcall(attrs, id_keys, 0));
+  buf   = rb_str_new("", 0);
+
+  for (i = 0; i < RARRAY_LEN(keys); i++) {
+    key   = rb_ary_entry(keys, i);
+    value = rb_hash_aref(attrs, key);
+
+    switch (value) {
+      case Qtrue:
+        rb_str_concat(buf, str_space());
+        rb_str_concat(buf, key);
+        break;
+      case Qnil:
+        break; // noop
+      case Qfalse:
+        break; // noop
+      default:
+        rb_str_concat(buf, str_space());
+        rb_str_concat(buf, key);
+        rb_str_concat(buf, str_equal());
+        rb_str_concat(buf, quote);
+        rb_str_concat(buf, escape_attribute(escape_attrs, to_s(value)));
+        rb_str_concat(buf, quote);
+        break;
+    }
+  }
+
+  return buf;
+}
+
+static VALUE
+parse_object_ref(VALUE object_ref)
+{
+  return rb_funcall(mObjectRef, id_parse, 1, object_ref);
+}
+
+static int
+merge_all_attrs_i(VALUE key, VALUE value, VALUE merged)
+{
+  VALUE array;
+
+  key = to_s(key);
+  if (str_eq(key, "id", 2) || str_eq(key, "class", 5) || str_eq(key, "data", 4)) {
+    array = rb_hash_aref(merged, key);
+    if (NIL_P(array)) {
+      array = rb_ary_new2(1);
+      rb_hash_aset(merged, key, array);
+    }
+    rb_ary_push(array, value);
+  } else {
+    rb_hash_aset(merged, key, value);
+  }
+  return ST_CONTINUE;
+}
+
+static VALUE
+merge_all_attrs(VALUE hashes)
+{
+  long i;
+  VALUE hash, merged = rb_hash_new();
+
+  for (i = 0; i < RARRAY_LEN(hashes); i++) {
+    hash = rb_ary_entry(hashes, i);
+    rb_hash_foreach(hash, merge_all_attrs_i, merged);
+  }
+  return merged;
+}
+
+int
+is_boolean_attribute(VALUE key)
+{
+  VALUE boolean_attributes;
+  if (str_eq(rb_str_substr(key, 0, 5), "data-", 5)) return 1;
+
+  boolean_attributes = rb_const_get(mAttributeBuilder, id_boolean_attributes);
+  return RTEST(rb_ary_includes(boolean_attributes, key));
+}
+
+void
+hamlit_build_for_id(VALUE escape_attrs, VALUE quote, VALUE buf, VALUE values)
+{
+  rb_str_cat(buf, " id=", 4);
+  rb_str_concat(buf, quote);
+  rb_str_concat(buf, hamlit_build_id(escape_attrs, values));
+  rb_str_concat(buf, quote);
+}
+
+void
+hamlit_build_for_class(VALUE escape_attrs, VALUE quote, VALUE buf, VALUE values)
+{
+  rb_str_cat(buf, " class=", 7);
+  rb_str_concat(buf, quote);
+  rb_str_concat(buf, hamlit_build_class(escape_attrs, values));
+  rb_str_concat(buf, quote);
+}
+
+void
+hamlit_build_for_data(VALUE escape_attrs, VALUE quote, VALUE buf, VALUE values)
+{
+  rb_str_concat(buf, hamlit_build_data(escape_attrs, quote, values));
+}
+
+void
+hamlit_build_for_others(VALUE escape_attrs, VALUE quote, VALUE buf, VALUE key, VALUE value)
+{
+  rb_str_cat(buf, " ", 1);
+  rb_str_concat(buf, key);
+  rb_str_cat(buf, "=", 1);
+  rb_str_concat(buf, quote);
+  rb_str_concat(buf, escape_attribute(escape_attrs, to_s(value)));
+  rb_str_concat(buf, quote);
+}
+
+void
+hamlit_build_for_boolean(VALUE escape_attrs, VALUE quote, VALUE format, VALUE buf, VALUE key, VALUE value)
+{
+  switch (value) {
+    case Qtrue:
+      rb_str_cat(buf, " ", 1);
+      rb_str_concat(buf, key);
+      if ((TYPE(format) == T_SYMBOL || TYPE(format) == T_STRING) && rb_to_id(format) == id_xhtml) {
+        rb_str_cat(buf, "=", 1);
+        rb_str_concat(buf, quote);
+        rb_str_concat(buf, key);
+        rb_str_concat(buf, quote);
+      }
+      break;
+    case Qfalse:
+      break; // noop
+    case Qnil:
+      break; // noop
+    default:
+      hamlit_build_for_others(escape_attrs, quote, buf, key, value);
+      break;
+  }
+}
+
+static VALUE
+hamlit_build(VALUE escape_attrs, VALUE quote, VALUE format, VALUE object_ref, VALUE hashes)
+{
+  long i;
+  VALUE attrs, buf, key, keys, value;
+
+  if (!NIL_P(object_ref)) rb_ary_push(hashes, parse_object_ref(object_ref));
+  attrs = merge_all_attrs(hashes);
+  buf   = rb_str_new("", 0);
+  keys  = rb_ary_sort_bang(rb_funcall(attrs, id_keys, 0));
+
+  for (i = 0; i < RARRAY_LEN(keys); i++) {
+    key   = rb_ary_entry(keys, i);
+    value = rb_hash_aref(attrs, key);
+    if (str_eq(key, "id", 2)) {
+      hamlit_build_for_id(escape_attrs, quote, buf, value);
+    } else if (str_eq(key, "class", 5)) {
+      hamlit_build_for_class(escape_attrs, quote, buf, value);
+    } else if (str_eq(key, "data", 4)) {
+      hamlit_build_for_data(escape_attrs, quote, buf, value);
+    } else if (is_boolean_attribute(key)) {
+      hamlit_build_for_boolean(escape_attrs, quote, format, buf, key, value);
+    } else {
+      hamlit_build_for_others(escape_attrs, quote, buf, key, value);
+    }
+  }
+
+  return buf;
+}
+
+static VALUE
+rb_hamlit_build_id(int argc, VALUE *argv, RB_UNUSED_VAR(VALUE self))
+{
+  VALUE array;
+
+  rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
+  rb_scan_args(argc - 1, argv + 1, "*", &array);
+
+  return hamlit_build_id(argv[0], array);
+}
+
+static VALUE
+rb_hamlit_build_class(int argc, VALUE *argv, RB_UNUSED_VAR(VALUE self))
+{
+  VALUE array;
+
+  rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
+  rb_scan_args(argc - 1, argv + 1, "*", &array);
+
+  return hamlit_build_class(argv[0], array);
+}
+
+static VALUE
+rb_hamlit_build_data(int argc, VALUE *argv, RB_UNUSED_VAR(VALUE self))
+{
+  VALUE array;
+
+  rb_check_arity(argc, 2, UNLIMITED_ARGUMENTS);
+  rb_scan_args(argc - 2, argv + 2, "*", &array);
+
+  return hamlit_build_data(argv[0], argv[1], array);
+}
+
+static VALUE
+rb_hamlit_build(int argc, VALUE *argv, RB_UNUSED_VAR(VALUE self))
+{
+  VALUE array;
+
+  rb_check_arity(argc, 4, UNLIMITED_ARGUMENTS);
+  rb_scan_args(argc - 4, argv + 4, "*", &array);
+
+  return hamlit_build(argv[0], argv[1], argv[2], argv[3], array);
+}
+
+void
+Init_hamlit(void)
+{
+  VALUE mHamlit, mUtils;
+
+  mHamlit           = rb_define_module("Hamlit");
+  mObjectRef        = rb_define_module_under(mHamlit, "ObjectRef");
+  mUtils            = rb_define_module_under(mHamlit, "Utils");
+  mAttributeBuilder = rb_define_module_under(mHamlit, "AttributeBuilder");
+
+  rb_define_singleton_method(mUtils, "escape_html", rb_escape_html, 1);
+  rb_define_singleton_method(mAttributeBuilder, "build", rb_hamlit_build, -1);
+  rb_define_singleton_method(mAttributeBuilder, "build_id", rb_hamlit_build_id, -1);
+  rb_define_singleton_method(mAttributeBuilder, "build_class", rb_hamlit_build_class, -1);
+  rb_define_singleton_method(mAttributeBuilder, "build_data", rb_hamlit_build_data, -1);
+
+  id_flatten   = rb_intern("flatten");
+  id_keys      = rb_intern("keys");
+  id_parse     = rb_intern("parse");
+  id_prepend   = rb_intern("prepend");
+  id_tr        = rb_intern("tr");
+  id_uniq_bang = rb_intern("uniq!");
+
+  id_data       = rb_intern("DATA");
+  id_equal      = rb_intern("EQUAL");
+  id_hyphen     = rb_intern("HYPHEN");
+  id_space      = rb_intern("SPACE");
+  id_underscore = rb_intern("UNDERSCORE");
+
+  id_boolean_attributes = rb_intern("BOOLEAN_ATTRIBUTES");
+  id_xhtml = rb_intern("xhtml");
+
+  rb_const_set(mAttributeBuilder, id_data,       rb_obj_freeze(rb_str_new_cstr("data")));
+  rb_const_set(mAttributeBuilder, id_equal,      rb_obj_freeze(rb_str_new_cstr("=")));
+  rb_const_set(mAttributeBuilder, id_hyphen,     rb_obj_freeze(rb_str_new_cstr("-")));
+  rb_const_set(mAttributeBuilder, id_space,      rb_obj_freeze(rb_str_new_cstr(" ")));
+  rb_const_set(mAttributeBuilder, id_underscore, rb_obj_freeze(rb_str_new_cstr("_")));
+}
diff --git a/ext/hamlit/hescape.c b/ext/hamlit/hescape.c
new file mode 100644 (file)
index 0000000..15010ac
--- /dev/null
@@ -0,0 +1,108 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include "hescape.h"
+
+static const char *ESCAPED_STRING[] = {
+  "",
+  "&quot;",
+  "&amp;",
+  "&#39;",
+  "&lt;",
+  "&gt;",
+};
+
+// This is strlen(ESCAPED_STRING[x]) optimized specially.
+// Mapping: 1 => 6, 2 => 5, 3 => 5, 4 => 4, 5 => 4
+#define ESC_LEN(x) ((13 - x) / 2)
+
+/*
+ * Given ASCII-compatible character, return index of ESCAPED_STRING.
+ *
+ * " (34) => 1 (&quot;)
+ * & (38) => 2 (&amp;)
+ * ' (39) => 3 (&#39;)
+ * < (60) => 4 (&lt;)
+ * > (62) => 5 (&gt;)
+ */
+static const char HTML_ESCAPE_TABLE[] = {
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 5, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static char*
+ensure_allocated(char *buf, size_t size, size_t *asize)
+{
+  size_t new_size;
+
+  if (size < *asize)
+    return buf;
+
+  if (*asize == 0) {
+    new_size = size;
+  } else {
+    new_size = *asize;
+  }
+
+  // Increase buffer size by 1.5x if realloced multiple times.
+  while (new_size < size)
+    new_size = (new_size << 1) - (new_size >> 1);
+
+  // Round allocation up to multiple of 8.
+  new_size = (new_size + 7) & ~7;
+
+  *asize = new_size;
+  return realloc(buf, new_size);
+}
+
+size_t
+hesc_escape_html(char **dest, const char *buf, size_t size)
+{
+  size_t asize = 0, esc_i, esize = 0, i = 0, rbuf_end = 0;
+  const char *esc;
+  char *rbuf = NULL;
+
+  while (i < size) {
+    // Loop here to skip non-escaped characters fast.
+    while (i < size && (esc_i = HTML_ESCAPE_TABLE[(unsigned char)buf[i]]) == 0)
+      i++;
+
+    if (i < size && esc_i) {
+      esc = ESCAPED_STRING[esc_i];
+      rbuf = ensure_allocated(rbuf, sizeof(char) * (size + esize + ESC_LEN(esc_i) + 1), &asize);
+
+      // Copy pending characters and escaped string.
+      memmove(rbuf + rbuf_end, buf + (rbuf_end - esize), i - (rbuf_end - esize));
+      memmove(rbuf + i + esize, esc, ESC_LEN(esc_i));
+      rbuf_end = i + esize + ESC_LEN(esc_i);
+      esize += ESC_LEN(esc_i) - 1;
+    }
+    i++;
+  }
+
+  if (rbuf_end == 0) {
+    // Return given buf and size if there are no escaped characters.
+    *dest = (char *)buf;
+    return size;
+  } else {
+    // Copy pending characters including NULL character.
+    memmove(rbuf + rbuf_end, buf + (rbuf_end - esize), (size + 1) - (rbuf_end - esize));
+
+    *dest = rbuf;
+    return size + esize;
+  }
+}
diff --git a/ext/hamlit/hescape.h b/ext/hamlit/hescape.h
new file mode 100644 (file)
index 0000000..df18f4b
--- /dev/null
@@ -0,0 +1,20 @@
+#ifndef HESCAPE_H
+#define HESCAPE_H
+
+#include <sys/types.h>
+
+/*
+ * Replace characters according to the following rules.
+ * Note that this function can handle only ASCII-compatible string.
+ *
+ * " => &quot;
+ * & => &amp;
+ * ' => &#39;
+ * < => &lt;
+ * > => &gt;
+ *
+ * @return size of dest. If it's larger than len, dest is required to be freed.
+ */
+extern size_t hesc_escape_html(char **dest, const char *src, size_t size);
+
+#endif
diff --git a/hamlit.gemspec b/hamlit.gemspec
new file mode 100644 (file)
index 0000000..5ac63cb
--- /dev/null
@@ -0,0 +1,41 @@
+# coding: utf-8
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+require 'hamlit/version'
+
+Gem::Specification.new do |spec|
+  spec.name          = 'hamlit'
+  spec.version       = Hamlit::VERSION
+  spec.authors       = ['Takashi Kokubun']
+  spec.email         = ['takashikkbn@gmail.com']
+
+  spec.summary       = %q{High Performance Haml Implementation}
+  spec.description   = %q{High Performance Haml Implementation}
+  spec.homepage      = 'https://github.com/k0kubun/hamlit'
+  spec.license       = 'MIT'
+
+  spec.files         = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|sample)/}) }
+  spec.bindir        = 'exe'
+  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+  spec.extensions    = ['ext/hamlit/extconf.rb']
+  spec.require_paths = ['lib']
+  spec.required_ruby_version = '>= 2.1.0'
+
+  spec.add_dependency 'temple', '~> 0.7.6'
+  spec.add_dependency 'thor'
+  spec.add_dependency 'tilt'
+
+  spec.add_development_dependency 'bundler'
+  spec.add_development_dependency 'coffee-script'
+  spec.add_development_dependency 'erubis'
+  spec.add_development_dependency 'haml', '4.1.0.beta.1'
+  spec.add_development_dependency 'less'
+  spec.add_development_dependency 'minitest-reporters', '~> 1.1'
+  spec.add_development_dependency 'rails', '>= 4.0.0'
+  spec.add_development_dependency 'rake', '~> 10.0'
+  spec.add_development_dependency 'rake-compiler'
+  spec.add_development_dependency 'redcarpet'
+  spec.add_development_dependency 'sass'
+  spec.add_development_dependency 'slim'
+  spec.add_development_dependency 'unindent'
+end
diff --git a/lib/hamlit.rb b/lib/hamlit.rb
new file mode 100644 (file)
index 0000000..bcfa264
--- /dev/null
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+require 'hamlit/engine'
+require 'hamlit/error'
+require 'hamlit/version'
+require 'hamlit/template'
+
+begin
+  require 'rails'
+  require 'hamlit/railtie'
+rescue LoadError
+end
diff --git a/lib/hamlit/attribute_builder.rb b/lib/hamlit/attribute_builder.rb
new file mode 100644 (file)
index 0000000..68764f4
--- /dev/null
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+require 'hamlit/hamlit'
+require 'hamlit/object_ref'
+require 'hamlit/utils'
+
+module Hamlit::AttributeBuilder
+  BOOLEAN_ATTRIBUTES = %w[disabled readonly multiple checked autobuffer
+                       autoplay controls loop selected hidden scoped async
+                       defer reversed ismap seamless muted required
+                       autofocus novalidate formnovalidate open pubdate
+                       itemscope allowfullscreen default inert sortable
+                       truespeed typemustmatch download].freeze
+end
diff --git a/lib/hamlit/attribute_compiler.rb b/lib/hamlit/attribute_compiler.rb
new file mode 100644 (file)
index 0000000..6247a8a
--- /dev/null
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+require 'hamlit/attribute_builder'
+require 'hamlit/attribute_parser'
+require 'hamlit/ruby_expression'
+require 'hamlit/static_analyzer'
+
+module Hamlit
+  class AttributeCompiler
+    def initialize(identity, options)
+      @identity = identity
+      @quote  = options[:attr_quote]
+      @format = options[:format]
+      @escape_attrs = options[:escape_attrs]
+    end
+
+    def compile(node)
+      hashes = []
+      return runtime_compile(node) if node.value[:object_ref] != :nil
+      node.value[:attributes_hashes].each do |attribute_str|
+        hash = AttributeParser.parse(attribute_str)
+        return runtime_compile(node) unless hash
+        hashes << hash
+      end
+      static_compile(node.value[:attributes], hashes)
+    end
+
+    private
+
+    def runtime_compile(node)
+      attrs = node.value[:attributes_hashes]
+      attrs.unshift(node.value[:attributes].inspect) if node.value[:attributes] != {}
+
+      args = [@escape_attrs, @quote, @format].map(&:inspect).push(node.value[:object_ref]) + attrs
+      [:html, :attrs, [:dynamic, "::Hamlit::AttributeBuilder.build(#{args.join(', ')})"]]
+    end
+
+    def static_compile(static_hash, dynamic_hashes)
+      temple = [:html, :attrs]
+      keys = [*static_hash.keys, *dynamic_hashes.map(&:keys).flatten].uniq.sort
+      keys.each do |key|
+        values = [[:static, static_hash[key]], *dynamic_hashes.map { |h| [:dynamic, h[key]] }]
+        values.select! { |_, exp| exp != nil }
+
+        case key
+        when 'id'
+          compile_id!(temple, key, values)
+        when 'class'
+          compile_class!(temple, key, values)
+        when 'data'
+          compile_data!(temple, key, values)
+        when *AttributeBuilder::BOOLEAN_ATTRIBUTES, /\Adata-/
+          compile_boolean!(temple, key, values)
+        else
+          compile_common!(temple, key, values)
+        end
+      end
+      temple
+    end
+
+    def compile_id!(temple, key, values)
+      build_code = attribute_builder(:id, values)
+      if values.all? { |type, exp| type == :static || StaticAnalyzer.static?(exp) }
+        temple << [:html, :attr, key, [:static, eval(build_code).to_s]]
+      else
+        temple << [:html, :attr, key, [:dynamic, build_code]]
+      end
+    end
+
+    def compile_class!(temple, key, values)
+      build_code = attribute_builder(:class, values)
+      if values.all? { |type, exp| type == :static || StaticAnalyzer.static?(exp) }
+        temple << [:html, :attr, key, [:static, eval(build_code).to_s]]
+      else
+        temple << [:html, :attr, key, [:dynamic, build_code]]
+      end
+    end
+
+    def compile_data!(temple, key, values)
+      args = [@escape_attrs.inspect, @quote.inspect, values.map { |v| literal_for(v) }]
+      build_code = "::Hamlit::AttributeBuilder.build_data(#{args.join(', ')})"
+
+      if values.all? { |type, exp| type == :static || StaticAnalyzer.static?(exp) }
+        temple << [:static, eval(build_code).to_s]
+      else
+        temple << [:dynamic, build_code]
+      end
+    end
+
+    def compile_boolean!(temple, key, values)
+      exp = literal_for(values.last)
+
+      if StaticAnalyzer.static?(exp)
+        value = eval(exp)
+        case value
+        when true then temple << [:html, :attr, key, @format == :xhtml ? [:static, key] : [:multi]]
+        when false, nil
+        else temple << [:html, :attr, key, [:fescape, @escape_attrs, [:static, value.to_s]]]
+        end
+      else
+        var = @identity.generate
+        temple << [
+          :case, "(#{var} = (#{exp}))",
+          ['true', [:html, :attr, key, @format == :xhtml ? [:static, key] : [:multi]]],
+          ['false, nil', [:multi]],
+          [:else, [:multi, [:static, " #{key}=#{@quote}"], [:fescape, @escape_attrs, [:dynamic, var]], [:static, @quote]]],
+        ]
+      end
+    end
+
+    def compile_common!(temple, key, values)
+      temple << [:html, :attr, key, [:fescape, @escape_attrs, values.last]]
+    end
+
+    def attribute_builder(type, values)
+      args = [@escape_attrs.inspect, *values.map { |v| literal_for(v) }]
+      "::Hamlit::AttributeBuilder.build_#{type}(#{args.join(', ')})"
+    end
+
+    def literal_for(value)
+      type, exp = value
+      type == :static ? exp.inspect : exp
+    end
+  end
+end
diff --git a/lib/hamlit/attribute_parser.rb b/lib/hamlit/attribute_parser.rb
new file mode 100644 (file)
index 0000000..ec8fba4
--- /dev/null
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+require 'hamlit/ruby_expression'
+
+module Hamlit
+  class AttributeParser
+    class ParseSkip < StandardError
+    end
+
+    def self.parse(text)
+      self.new.parse(text)
+    end
+
+    def parse(text)
+      exp = wrap_bracket(text)
+      return if RubyExpression.syntax_error?(exp)
+
+      hash = {}
+      tokens = Ripper.lex(exp)[1..-2] || []
+      each_attr(tokens) do |attr_tokens|
+        key = parse_key!(attr_tokens)
+        hash[key] = attr_tokens.map(&:last).join.strip
+      end
+      hash
+    rescue ParseSkip
+      nil
+    end
+
+    private
+
+    def wrap_bracket(text)
+      text = text.strip
+      return text if text[0] == '{'
+      "{#{text}}"
+    end
+
+    def parse_key!(tokens)
+      _, type, str = tokens.shift
+      case type
+      when :on_sp
+        parse_key!(tokens)
+      when :on_label
+        str.tr(':', '')
+      when :on_symbeg
+        _, _, key = tokens.shift
+        assert_type!(tokens.shift, :on_tstring_end) if str != ':'
+        skip_until_hash_rocket!(tokens)
+        key
+      when :on_tstring_beg
+        _, _, key = tokens.shift
+        next_token = tokens.shift
+        unless next_token[1] == :on_label_end
+          assert_type!(next_token, :on_tstring_end)
+          skip_until_hash_rocket!(tokens)
+        end
+        key
+      else
+        raise ParseSkip
+      end
+    end
+
+    def assert_type!(token, type)
+      raise ParseSkip if token[1] != type
+    end
+
+    def skip_until_hash_rocket!(tokens)
+      until tokens.empty?
+        _, type, str = tokens.shift
+        break if type == :on_op && str == '=>'
+      end
+    end
+
+    def each_attr(tokens)
+      attr_tokens = []
+      array_open  = 0
+      brace_open  = 0
+      paren_open  = 0
+
+      tokens.each do |token|
+        (row, col), type, str = token
+        case type
+        when :on_comma
+          if array_open == 0 && brace_open == 0 && paren_open == 0
+            yield(attr_tokens)
+            attr_tokens = []
+            next
+          end
+        when :on_lbracket
+          array_open += 1
+        when :on_rbracket
+          array_open -= 1
+        when :on_lbrace
+          brace_open += 1
+        when :on_rbrace
+          brace_open -= 1
+        when :on_lparen
+          paren_open += 1
+        when :on_rparen
+          paren_open -= 1
+        when :on_sp
+          next if attr_tokens.empty?
+        end
+
+        attr_tokens << token
+      end
+      yield(attr_tokens) unless attr_tokens.empty?
+    end
+  end
+end
diff --git a/lib/hamlit/cli.rb b/lib/hamlit/cli.rb
new file mode 100644 (file)
index 0000000..d8d9909
--- /dev/null
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+require 'hamlit'
+require 'thor'
+
+module Hamlit
+  class CLI < Thor
+    class_option :escape_html, type: :boolean, default: true
+    class_option :escape_attrs, type: :boolean, default: true
+
+    desc 'render HAML', 'Render haml template'
+    option :load_path, type: :string, aliases: %w[-I]
+    option :require, type: :string, aliases: %w[-r]
+    def render(file)
+      process_load_options
+      code = generate_code(file)
+      puts eval(code)
+    end
+
+    desc 'compile HAML', 'Show compile result'
+    option :actionview, type: :boolean, default: false, aliases: %w[-a]
+    def compile(file)
+      print_code generate_code(file)
+    end
+
+    desc 'temple HAML', 'Show temple intermediate expression'
+    def temple(file)
+      pp generate_temple(file)
+    end
+
+    desc 'parse HAML', 'Show parse result'
+    def parse(file)
+      pp generate_ast(file)
+    end
+
+    desc 'version', 'Show the used hamlit version'
+    def version
+      puts Hamlit::VERSION
+    end
+
+    private
+
+    def process_load_options
+      if options[:load_path]
+        options[:load_path].split(':').each do |dir|
+          $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
+        end
+      end
+
+      if options[:require]
+        require options[:require]
+      end
+    end
+
+    def generate_code(file)
+      template = File.read(file)
+      if options[:actionview]
+        require 'action_view'
+        require 'action_view/base'
+        require 'hamlit/rails_template'
+        handler = Hamlit::RailsTemplate.new
+        template = ActionView::Template.new(template, 'inline template', handler, { locals: [] })
+        code = handler.call(template)
+        source = <<-end_src
+          def _inline_template___2144273726781623612_70327218547300(local_assigns, output_buffer)
+            _old_virtual_path, @virtual_path = @virtual_path, nil;_old_output_buffer = @output_buffer;;#{code}
+          ensure
+            @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
+          end
+        end_src
+      else
+        Hamlit::Engine.new(engine_options).call(template)
+      end
+    end
+
+    def generate_ast(file)
+      template = File.read(file)
+      Hamlit::Parser.new(engine_options).call(template)
+    end
+
+    def generate_temple(file)
+      ast = generate_ast(file)
+      Hamlit::Compiler.new(engine_options).call(ast)
+    end
+
+    def engine_options
+      Hamlit::Engine.options.to_h.merge(
+        escape_attrs: options[:escape_attrs],
+        escape_html:  options[:escape_html],
+      )
+    end
+
+    # Flexible default_task, compatible with haml's CLI
+    def method_missing(*args)
+      return super(*args) if args.length > 1
+      render(args.first.to_s)
+    end
+
+    def print_code(code)
+      require 'pry'
+      puts Pry.Code(code).highlighted
+    rescue LoadError
+      puts code
+    end
+
+    # Enable colored pretty printing only for development environment.
+    def pp(arg)
+      require 'pry'
+      Pry::ColorPrinter.pp(arg)
+    rescue LoadError
+      require 'pp'
+      super(arg)
+    end
+  end
+end
diff --git a/lib/hamlit/compiler.rb b/lib/hamlit/compiler.rb
new file mode 100644 (file)
index 0000000..4f2584f
--- /dev/null
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+require 'hamlit/compiler/children_compiler'
+require 'hamlit/compiler/comment_compiler'
+require 'hamlit/compiler/doctype_compiler'
+require 'hamlit/compiler/script_compiler'
+require 'hamlit/compiler/silent_script_compiler'
+require 'hamlit/compiler/tag_compiler'
+require 'hamlit/filters'
+require 'hamlit/identity'
+
+module Hamlit
+  class Compiler
+    def initialize(options = {})
+      identity                = Identity.new
+      @children_compiler      = ChildrenCompiler.new
+      @comment_compiler       = CommentCompiler.new
+      @doctype_compiler       = DoctypeCompiler.new(options)
+      @filter_compiler        = Filters.new(options)
+      @script_compiler        = ScriptCompiler.new(identity)
+      @silent_script_compiler = SilentScriptCompiler.new
+      @tag_compiler           = TagCompiler.new(identity, options)
+    end
+
+    def call(ast)
+      return runtime_error(ast) if ast.is_a?(HamlError)
+      compile(ast)
+    rescue Error => e
+      runtime_error(e)
+    end
+
+    private
+
+    def compile(node)
+      case node.type
+      when :root
+        compile_children(node)
+      when :comment
+        compile_comment(node)
+      when :doctype
+        compile_doctype(node)
+      when :filter
+        compile_filter(node)
+      when :plain
+        compile_plain(node)
+      when :script
+        compile_script(node)
+      when :silent_script
+        compile_silent_script(node)
+      when :tag
+        compile_tag(node)
+      when :haml_comment
+        [:multi]
+      else
+        raise InternalError.new("Unexpected node type: #{node.type}")
+      end
+    end
+
+    def compile_children(node)
+      @children_compiler.compile(node) { |n| compile(n) }
+    end
+
+    def compile_comment(node)
+      @comment_compiler.compile(node) { |n| compile_children(n) }
+    end
+
+    def compile_doctype(node)
+      @doctype_compiler.compile(node)
+    end
+
+    def compile_filter(node)
+      @filter_compiler.compile(node)
+    end
+
+    def compile_plain(node)
+      [:static, node.value[:text]]
+    end
+
+    def compile_script(node)
+      @script_compiler.compile(node) { |n| compile_children(n) }
+    end
+
+    def compile_silent_script(node)
+      @silent_script_compiler.compile(node) { |n| compile_children(n) }
+    end
+
+    def compile_tag(node)
+      @tag_compiler.compile(node) { |n| compile_children(n) }
+    end
+
+    def runtime_error(error)
+      [:multi].tap do |temple|
+        error.line.times { temple << [:newline] } if error.line
+        temple << [:code, %Q[raise #{error.class}.new(%q[#{error.message}], #{error.line.inspect})]]
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/compiler/children_compiler.rb b/lib/hamlit/compiler/children_compiler.rb
new file mode 100644 (file)
index 0000000..507beae
--- /dev/null
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+module Hamlit
+  class Compiler
+    class ChildrenCompiler
+      def initialize
+        @lineno = 1
+      end
+
+      def compile(node, &block)
+        temple = [:multi]
+        return temple if node.children.empty?
+
+        temple << :whitespace if prepend_whitespace?(node)
+        node.children.each do |n|
+          rstrip_whitespace!(temple) if nuke_prev_whitespace?(n)
+          insert_newlines!(temple, n)
+          temple << yield(n)
+          temple << :whitespace if insert_whitespace?(n)
+        end
+        rstrip_whitespace!(temple) if nuke_inner_whitespace?(node)
+        confirm_whitespace(temple)
+      end
+
+      private
+
+      def insert_newlines!(temple, node)
+        (node.line - @lineno).times do
+          temple << [:newline]
+        end
+        @lineno = node.line
+
+        case node.type
+        when :script, :silent_script
+          @lineno += 1
+        when :filter
+          @lineno += (node.value[:text] || '').split("\n").size
+        when :tag
+          node.value[:attributes_hashes].each do |attribute_hash|
+            @lineno += attribute_hash.count("\n")
+          end
+          @lineno += 1 if node.children.empty? && node.value[:parse]
+        end
+      end
+
+      def confirm_whitespace(temple)
+        temple.map do |exp|
+          case exp
+          when :whitespace
+            [:static, "\n"]
+          else
+            exp
+          end
+        end
+      end
+
+      def prepend_whitespace?(node)
+        return false unless %i[comment tag].include?(node.type)
+        !nuke_inner_whitespace?(node)
+      end
+
+      def nuke_inner_whitespace?(node)
+        case
+        when node.type == :tag
+          node.value[:nuke_inner_whitespace]
+        when node.parent.nil?
+          false
+        else
+          nuke_inner_whitespace?(node.parent)
+        end
+      end
+
+      def nuke_prev_whitespace?(node)
+        case node.type
+        when :tag
+          node.value[:nuke_outer_whitespace]
+        when :silent_script
+          !node.children.empty? && nuke_prev_whitespace?(node.children.first)
+        else
+          false
+        end
+      end
+
+      def nuke_outer_whitespace?(node)
+        return false if node.type != :tag
+        node.value[:nuke_outer_whitespace]
+      end
+
+      def rstrip_whitespace!(temple)
+        if temple[-1] == :whitespace
+          temple.delete_at(-1)
+        end
+      end
+
+      def insert_whitespace?(node)
+        return false if nuke_outer_whitespace?(node)
+
+        case node.type
+        when :doctype
+          node.value[:type] != 'xml'
+        when :comment, :plain, :tag
+          true
+        when :script
+          node.children.empty? && !nuke_inner_whitespace?(node)
+        when :filter
+          !%w[ruby].include?(node.value[:name])
+        else
+          false
+        end
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/compiler/comment_compiler.rb b/lib/hamlit/compiler/comment_compiler.rb
new file mode 100644 (file)
index 0000000..e51e4bf
--- /dev/null
@@ -0,0 +1,36 @@
+module Hamlit
+  class Compiler
+    class CommentCompiler
+      def compile(node, &block)
+        if node.value[:conditional]
+          compile_conditional_comment(node, &block)
+        else
+          compile_html_comment(node, &block)
+        end
+      end
+
+      private
+
+      def compile_html_comment(node, &block)
+        if node.children.empty?
+          [:html, :comment, [:static, " #{node.value[:text]} "]]
+        else
+          [:html, :comment, yield(node)]
+        end
+      end
+
+      def compile_conditional_comment(node, &block)
+        condition = node.value[:conditional]
+        if node.value[:conditional] =~ /\A\[(\[*[^\[\]]+\]*)\]/
+          condition = $1
+        end
+
+        if node.children.empty?
+          [:html, :condcomment, condition, [:static, " #{node.value[:text]} "]]
+        else
+          [:html, :condcomment, condition, yield(node)]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/compiler/doctype_compiler.rb b/lib/hamlit/compiler/doctype_compiler.rb
new file mode 100644 (file)
index 0000000..5ad4299
--- /dev/null
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+module Hamlit
+  class Compiler
+    class DoctypeCompiler
+      def initialize(options = {})
+        @format = options[:format]
+      end
+
+      def compile(node)
+        case node.value[:type]
+        when 'xml'
+          xml_doctype
+        when ''
+          html_doctype(node)
+        else
+          [:html, :doctype, node.value[:type]]
+        end
+      end
+
+      private
+
+      def html_doctype(node)
+        version = node.value[:version] || :transitional
+        case @format
+        when :xhtml
+          [:html, :doctype, version]
+        when :html4
+          [:html, :doctype, :transitional]
+        when :html5
+          [:html, :doctype, :html]
+        else
+          [:html, :doctype, @format]
+        end
+      end
+
+      def xml_doctype
+        case @format
+        when :xhtml
+          [:static, "<?xml version='1.0' encoding='utf-8' ?>\n"]
+        else
+          [:multi]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/compiler/script_compiler.rb b/lib/hamlit/compiler/script_compiler.rb
new file mode 100644 (file)
index 0000000..ce0fb8a
--- /dev/null
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+require 'hamlit/ruby_expression'
+require 'hamlit/static_analyzer'
+require 'hamlit/string_splitter'
+
+module Hamlit
+  class Compiler
+    class ScriptCompiler
+      def initialize(identity)
+        @identity = identity
+      end
+
+      def compile(node, &block)
+        no_children = node.children.empty?
+        case
+        when no_children && node.value[:escape_interpolation]
+          compile_interpolated_plain(node)
+        when no_children && RubyExpression.string_literal?(node.value[:text])
+          delegate_optimization(node)
+        when no_children && StaticAnalyzer.static?(node.value[:text])
+          static_compile(node)
+        else
+          dynamic_compile(node, &block)
+        end
+      end
+
+      private
+
+      # String-interpolated plain text must be compiled with this method
+      # because we have to escape only interpolated values.
+      def compile_interpolated_plain(node)
+        temple = [:multi]
+        StringSplitter.compile(node.value[:text]).each do |type, value|
+          case type
+          when :static
+            temple << [:static, value]
+          when :dynamic
+            temple << [:escape, node.value[:escape_interpolation], [:dynamic, value]]
+          end
+        end
+        temple << [:newline]
+      end
+
+      # :dynamic is optimized in other filter: StringSplitter
+      def delegate_optimization(node)
+        [:multi,
+         [:escape, node.value[:escape_html], [:dynamic, node.value[:text]]],
+         [:newline],
+        ]
+      end
+
+      def static_compile(node)
+        str = eval(node.value[:text]).to_s
+        if node.value[:escape_html]
+          str = Hamlit::Utils.escape_html(str)
+        elsif node.value[:preserve]
+          str = ::Hamlit::HamlHelpers.find_and_preserve(str, %w(textarea pre code))
+        end
+        [:multi, [:static, str], [:newline]]
+      end
+
+      def dynamic_compile(node, &block)
+        var = @identity.generate
+        temple = compile_script_assign(var, node, &block)
+        temple << compile_script_result(var, node)
+      end
+
+      def compile_script_assign(var, node, &block)
+        if node.children.empty?
+          [:multi,
+           [:code, "#{var} = (#{node.value[:text]}"],
+           [:newline],
+           [:code, ')'],
+          ]
+        else
+          [:multi,
+           [:block, "#{var} = #{node.value[:text]}",
+            [:multi, [:newline], yield(node)],
+           ],
+          ]
+        end
+      end
+
+      def compile_script_result(result, node)
+        if !node.value[:escape_html] && node.value[:preserve]
+          result = find_and_preserve(result)
+        else
+          result = "(#{result}).to_s"
+        end
+        [:escape, node.value[:escape_html], [:dynamic, result]]
+      end
+
+      def find_and_preserve(code)
+        %Q[::Hamlit::HamlHelpers.find_and_preserve(#{code}, %w(textarea pre code))]
+      end
+
+      def escape_html(temple)
+        [:escape, true, temple]
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/compiler/silent_script_compiler.rb b/lib/hamlit/compiler/silent_script_compiler.rb
new file mode 100644 (file)
index 0000000..29bed67
--- /dev/null
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+module Hamlit
+  class Compiler
+    class SilentScriptCompiler
+      def compile(node, &block)
+        if node.children.empty?
+          [:multi, [:code, node.value[:text]], [:newline]]
+        else
+          compile_with_children(node, &block)
+        end
+      end
+
+      private
+
+      def compile_with_children(node, &block)
+        [:multi,
+         [:block, node.value[:text],
+          [:multi, [:newline], yield(node)],
+         ],
+        ]
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/compiler/tag_compiler.rb b/lib/hamlit/compiler/tag_compiler.rb
new file mode 100644 (file)
index 0000000..cd543d4
--- /dev/null
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+require 'hamlit/parser/haml_util'
+require 'hamlit/attribute_compiler'
+require 'hamlit/static_analyzer'
+require 'hamlit/string_splitter'
+
+module Hamlit
+  class Compiler
+    class TagCompiler
+      def initialize(identity, options)
+        @autoclose = options[:autoclose]
+        @identity  = identity
+        @attribute_compiler = AttributeCompiler.new(identity, options)
+      end
+
+      def compile(node, &block)
+        attrs    = @attribute_compiler.compile(node)
+        contents = compile_contents(node, &block)
+        [:html, :tag, node.value[:name], attrs, contents]
+      end
+
+      private
+
+      def compile_contents(node, &block)
+        case
+        when !node.children.empty?
+          yield(node)
+        when node.value[:value].nil? && self_closing?(node)
+          nil
+        when node.value[:parse]
+          return compile_interpolated_plain(node) if node.value[:escape_interpolation]
+          return delegate_optimization(node) if RubyExpression.string_literal?(node.value[:value])
+          return delegate_optimization(node) if StaticAnalyzer.static?(node.value[:value])
+
+          var = @identity.generate
+          [:multi,
+           [:code, "#{var} = (#{node.value[:value]}"],
+           [:newline],
+           [:code, ')'],
+           [:escape, node.value[:escape_html], [:dynamic, var]]
+          ]
+        else
+          [:static, node.value[:value]]
+        end
+      end
+
+      # :dynamic is optimized in other filters: StringSplitter or StaticAnalyzer
+      def delegate_optimization(node)
+        [:multi,
+         [:escape, node.value[:escape_html], [:dynamic, node.value[:value]]],
+         [:newline],
+        ]
+      end
+
+      # We should handle interpolation here to escape only interpolated values.
+      def compile_interpolated_plain(node)
+        temple = [:multi]
+        StringSplitter.compile(node.value[:value]).each do |type, value|
+          case type
+          when :static
+            temple << [:static, value]
+          when :dynamic
+            temple << [:escape, node.value[:escape_interpolation], [:dynamic, value]]
+          end
+        end
+        temple << [:newline]
+      end
+
+      def self_closing?(node)
+        return true if @autoclose && @autoclose.include?(node.value[:name])
+        node.value[:self_closing]
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/engine.rb b/lib/hamlit/engine.rb
new file mode 100644 (file)
index 0000000..e27773a
--- /dev/null
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+require 'temple'
+require 'hamlit/parser'
+require 'hamlit/compiler'
+require 'hamlit/escapable'
+require 'hamlit/force_escapable'
+require 'hamlit/html'
+require 'hamlit/string_splitter'
+require 'hamlit/static_analyzer'
+
+module Hamlit
+  class Engine < Temple::Engine
+    define_options(
+      :buffer_class,
+      generator:    Temple::Generators::ArrayBuffer,
+      format:       :html,
+      attr_quote:   "'",
+      escape_html:  true,
+      escape_attrs: true,
+      autoclose:    %w(area base basefont br col command embed frame
+                       hr img input isindex keygen link menuitem meta
+                       param source track wbr),
+      filename:     "",
+    )
+
+    use Parser
+    use Compiler
+    use HTML
+    use StringSplitter
+    use StaticAnalyzer
+    use Escapable
+    use ForceEscapable
+    filter :ControlFlow
+    filter :MultiFlattener
+    filter :StaticMerger
+    use :Generator, -> { options[:generator] }
+  end
+end
diff --git a/lib/hamlit/error.rb b/lib/hamlit/error.rb
new file mode 100644 (file)
index 0000000..85989f2
--- /dev/null
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+module Hamlit
+  class Error < StandardError
+    attr_reader :line
+
+    def initialize(message = nil, line = nil)
+      super(message)
+      @line = line
+    end
+  end
+
+  class SyntaxError < Error; end
+  class InternalError < Error; end
+  class FilterNotFound < Error; end
+end
diff --git a/lib/hamlit/escapable.rb b/lib/hamlit/escapable.rb
new file mode 100644 (file)
index 0000000..53f71af
--- /dev/null
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+require 'hamlit/utils'
+
+module Hamlit
+  class Escapable < Temple::Filters::Escapable
+    def initialize(opts = {})
+      super
+      @escape_code = options[:escape_code] ||
+        "::Hamlit::Utils.escape_html#{options[:use_html_safe] ? '_safe' : ''}((%s))"
+      @escaper = eval("proc {|v| #{@escape_code % 'v'} }")
+    end
+  end
+end
diff --git a/lib/hamlit/filters.rb b/lib/hamlit/filters.rb
new file mode 100644 (file)
index 0000000..d81f338
--- /dev/null
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+require 'hamlit/filters/base'
+require 'hamlit/filters/text_base'
+require 'hamlit/filters/tilt_base'
+require 'hamlit/filters/coffee'
+require 'hamlit/filters/css'
+require 'hamlit/filters/erb'
+require 'hamlit/filters/escaped'
+require 'hamlit/filters/javascript'
+require 'hamlit/filters/less'
+require 'hamlit/filters/markdown'
+require 'hamlit/filters/plain'
+require 'hamlit/filters/preserve'
+require 'hamlit/filters/ruby'
+require 'hamlit/filters/sass'
+require 'hamlit/filters/scss'
+require 'hamlit/filters/cdata'
+
+module Hamlit
+  class Filters
+    @registered = {}
+
+    class << self
+      attr_reader :registered
+
+      def remove_filter(name)
+        registered.delete(name.to_s.downcase.to_sym)
+        if constants.map(&:to_s).include?(name.to_s)
+          remove_const name.to_sym
+        end
+      end
+
+      private
+
+      def register(name, compiler)
+        registered[name] = compiler
+      end
+    end
+
+    register :coffee,       Coffee
+    register :coffeescript, CoffeeScript
+    register :css,          Css
+    register :erb,          Erb
+    register :escaped,      Escaped
+    register :javascript,   Javascript
+    register :less,         Less
+    register :markdown,     Markdown
+    register :plain,        Plain
+    register :preserve,     Preserve
+    register :ruby,         Ruby
+    register :sass,         Sass
+    register :scss,         Scss
+    register :cdata,        Cdata
+
+    def initialize(options = {})
+      @options = options
+      @compilers = {}
+    end
+
+    def compile(node)
+      node.value[:text] ||= ''
+      find_compiler(node).compile(node)
+    end
+
+    private
+
+    def find_compiler(node)
+      name = node.value[:name].to_sym
+      compiler = Filters.registered[name]
+      raise FilterNotFound.new("FilterCompiler for '#{name}' was not found", node.line.to_i - 1) unless compiler
+
+      @compilers[name] ||= compiler.new(@options)
+    end
+  end
+end
diff --git a/lib/hamlit/filters/base.rb b/lib/hamlit/filters/base.rb
new file mode 100644 (file)
index 0000000..d98d457
--- /dev/null
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+require 'hamlit/parser/haml_util'
+
+module Hamlit
+  class Filters
+    class Base
+      def initialize(options = {})
+        @format = options[:format]
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/cdata.rb b/lib/hamlit/filters/cdata.rb
new file mode 100644 (file)
index 0000000..c90f143
--- /dev/null
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Cdata < TextBase
+      def compile(node)
+        compile_cdata(node)
+      end
+
+      private
+
+      def compile_cdata(node)
+        temple = [:multi]
+        temple << [:static, "<![CDATA[\n"]
+        compile_text!(temple, node, '    ')
+        temple << [:static, "\n]]>"]
+        temple
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/coffee.rb b/lib/hamlit/filters/coffee.rb
new file mode 100644 (file)
index 0000000..9da72d2
--- /dev/null
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Coffee < TiltBase
+      def compile(node)
+        require 'tilt/coffee' if explicit_require?
+        temple = [:multi]
+        temple << [:static, "<script>\n"]
+        temple << compile_with_tilt(node, 'coffee', indent_width: 2)
+        temple << [:static, "</script>"]
+        temple
+      end
+    end
+
+    CoffeeScript = Coffee
+  end
+end
diff --git a/lib/hamlit/filters/css.rb b/lib/hamlit/filters/css.rb
new file mode 100644 (file)
index 0000000..db7769b
--- /dev/null
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Css < TextBase
+      def compile(node)
+        case @format
+        when :xhtml
+          compile_xhtml(node)
+        else
+          compile_html(node)
+        end
+      end
+
+      private
+
+      def compile_html(node)
+        temple = [:multi]
+        temple << [:static, "<style>\n"]
+        compile_text!(temple, node, '  ')
+        temple << [:static, "\n</style>"]
+        temple
+      end
+
+      def compile_xhtml(node)
+        temple = [:multi]
+        temple << [:static, "<style type='text/css'>\n  /*<![CDATA[*/\n"]
+        compile_text!(temple, node, '    ')
+        temple << [:static, "\n  /*]]>*/\n</style>"]
+        temple
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/erb.rb b/lib/hamlit/filters/erb.rb
new file mode 100644 (file)
index 0000000..67d9297
--- /dev/null
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Erb < TiltBase
+      def compile(node)
+        compile_with_tilt(node, 'erb')
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/escaped.rb b/lib/hamlit/filters/escaped.rb
new file mode 100644 (file)
index 0000000..e26c533
--- /dev/null
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Escaped < Base
+      def compile(node)
+        text = node.value[:text].rstrip
+        temple = compile_text(text)
+        [:escape, true, temple]
+      end
+
+      private
+
+      def compile_text(text)
+        if ::Hamlit::HamlUtil.contains_interpolation?(text)
+          [:dynamic, ::Hamlit::HamlUtil.slow_unescape_interpolation(text)]
+        else
+          [:static, text]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/javascript.rb b/lib/hamlit/filters/javascript.rb
new file mode 100644 (file)
index 0000000..57ec162
--- /dev/null
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Javascript < TextBase
+      def compile(node)
+        case @format
+        when :xhtml
+          compile_xhtml(node)
+        else
+          compile_html(node)
+        end
+      end
+
+      private
+
+      def compile_html(node)
+        temple = [:multi]
+        temple << [:static, "<script>\n"]
+        compile_text!(temple, node, '  ')
+        temple << [:static, "\n</script>"]
+        temple
+      end
+
+      def compile_xhtml(node)
+        temple = [:multi]
+        temple << [:static, "<script type='text/javascript'>\n  //<![CDATA[\n"]
+        compile_text!(temple, node, '    ')
+        temple << [:static, "\n  //]]>\n</script>"]
+        temple
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/less.rb b/lib/hamlit/filters/less.rb
new file mode 100644 (file)
index 0000000..0b58d52
--- /dev/null
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+# LESS support is deprecated since it requires therubyracer.gem,
+# which is hard to maintain.
+#
+# It's not supported in Sprockets 3.0+ too.
+# https://github.com/sstephenson/sprockets/pull/547
+module Hamlit
+  class Filters
+    class Less < TiltBase
+      def compile(node)
+        require 'tilt/less' if explicit_require?
+        temple = [:multi]
+        temple << [:static, "<style>\n"]
+        temple << compile_with_tilt(node, 'less', indent_width: 2)
+        temple << [:static, '</style>']
+        temple
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/markdown.rb b/lib/hamlit/filters/markdown.rb
new file mode 100644 (file)
index 0000000..bcab07e
--- /dev/null
@@ -0,0 +1,10 @@
+module Hamlit
+  class Filters
+    class Markdown < TiltBase
+      def compile(node)
+        require 'tilt/redcarpet' if explicit_require?
+        compile_with_tilt(node, 'markdown')
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/plain.rb b/lib/hamlit/filters/plain.rb
new file mode 100644 (file)
index 0000000..9fa3fae
--- /dev/null
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+require 'hamlit/string_splitter'
+
+module Hamlit
+  class Filters
+    class Plain < Base
+      def compile(node)
+        text = node.value[:text]
+        text = text.rstrip unless ::Hamlit::HamlUtil.contains_interpolation?(text) # for compatibility
+        [:multi, *compile_plain(text)]
+      end
+
+      private
+
+      def compile_plain(text)
+        string_literal = ::Hamlit::HamlUtil.unescape_interpolation(text)
+        StringSplitter.compile(string_literal).map do |temple|
+          type, str = temple
+          case type
+          when :dynamic
+            [:escape, false, [:dynamic, str]]
+          else
+            temple
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/preserve.rb b/lib/hamlit/filters/preserve.rb
new file mode 100644 (file)
index 0000000..423d53a
--- /dev/null
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Preserve < Base
+      def compile(node)
+        text = node.value[:text].rstrip + "\n"
+        text = text.gsub("\n", '&#x000A;')
+        compile_text(text)
+      end
+
+      private
+
+      def compile_text(text)
+        if ::Hamlit::HamlUtil.contains_interpolation?(text)
+          [:dynamic, ::Hamlit::HamlUtil.slow_unescape_interpolation(text)]
+        else
+          [:static, text]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/ruby.rb b/lib/hamlit/filters/ruby.rb
new file mode 100644 (file)
index 0000000..e6dccd9
--- /dev/null
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Ruby < Base
+      def compile(node)
+        [:code, node.value[:text]]
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/sass.rb b/lib/hamlit/filters/sass.rb
new file mode 100644 (file)
index 0000000..319e776
--- /dev/null
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Sass < TiltBase
+      def compile(node)
+        require 'tilt/sass' if explicit_require?
+        temple = [:multi]
+        temple << [:static, "<style>\n"]
+        temple << compile_with_tilt(node, 'sass', indent_width: 2)
+        temple << [:static, "</style>"]
+        temple
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/scss.rb b/lib/hamlit/filters/scss.rb
new file mode 100644 (file)
index 0000000..c9b3253
--- /dev/null
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class Scss < TiltBase
+      def compile(node)
+        require 'tilt/sass' if explicit_require?
+        temple = [:multi]
+        temple << [:static, "<style>\n"]
+        temple << compile_with_tilt(node, 'scss', indent_width: 2)
+        temple << [:static, "</style>"]
+        temple
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/text_base.rb b/lib/hamlit/filters/text_base.rb
new file mode 100644 (file)
index 0000000..c269792
--- /dev/null
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+module Hamlit
+  class Filters
+    class TextBase < Base
+      def compile_text!(temple, node, prefix)
+        text = node.value[:text].rstrip.gsub(/^/, prefix)
+        if ::Hamlit::HamlUtil.contains_interpolation?(node.value[:text])
+          # original: Haml::Filters#compile
+          text = ::Hamlit::HamlUtil.slow_unescape_interpolation(text).gsub(/(\\+)n/) do |s|
+            escapes = $1.size
+            next s if escapes % 2 == 0
+            "#{'\\' * (escapes - 1)}\n"
+          end
+          text.prepend("\n")
+          temple << [:dynamic, text]
+        else
+          node.value[:text].split("\n").size.times do
+            temple << [:newline]
+          end
+          temple << [:static, text]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/filters/tilt_base.rb b/lib/hamlit/filters/tilt_base.rb
new file mode 100644 (file)
index 0000000..db0f635
--- /dev/null
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+require 'tilt'
+
+module Hamlit
+  class Filters
+    class TiltBase < Base
+      def self.render(name, source, indent_width: 0)
+        text = ::Tilt["t.#{name}"].new { source }.render
+        return text if indent_width == 0
+        text.gsub!(/^/, ' ' * indent_width)
+      end
+
+      def explicit_require?
+        Gem::Version.new(Tilt::VERSION) >= Gem::Version.new('2.0.0')
+      end
+
+      private
+
+      def compile_with_tilt(node, name, indent_width: 0)
+        if ::Hamlit::HamlUtil.contains_interpolation?(node.value[:text])
+          dynamic_compile(node, name, indent_width: indent_width)
+        else
+          static_compile(node, name, indent_width: indent_width)
+        end
+      end
+
+      def static_compile(node, name, indent_width: 0)
+        temple = [:multi, [:static, TiltBase.render(name, node.value[:text], indent_width: indent_width)]]
+        node.value[:text].split("\n").size.times do
+          temple << [:newline]
+        end
+        temple
+      end
+
+      def dynamic_compile(node, name, indent_width: 0)
+        # original: Haml::Filters#compile
+        text = ::Hamlit::HamlUtil.slow_unescape_interpolation(node.value[:text]).gsub(/(\\+)n/) do |s|
+          escapes = $1.size
+          next s if escapes % 2 == 0
+          "#{'\\' * (escapes - 1)}\n"
+        end
+        text.prepend("\n").sub!(/\n"\z/, '"')
+
+        [:dynamic, "::Hamlit::Filters::TiltBase.render('#{name}', #{text}, indent_width: #{indent_width})"]
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/force_escapable.rb b/lib/hamlit/force_escapable.rb
new file mode 100644 (file)
index 0000000..6b52638
--- /dev/null
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+require 'hamlit/escapable'
+
+module Hamlit
+  # This module allows Temple::Filter to dispatch :fescape on `#compile`.
+  module FescapeDispathcer
+    def on_fescape(flag, exp)
+      [:fescape, flag, compile(exp)]
+    end
+  end
+  ::Temple::Filter.include FescapeDispathcer
+
+  # Unlike Hamlit::Escapable, this escapes value even if it's html_safe.
+  class ForceEscapable < Escapable
+    def initialize(opts = {})
+      super
+      @escape_code = options[:escape_code] || "::Hamlit::Utils.escape_html((%s))"
+      @escaper = eval("proc {|v| #{@escape_code % 'v'} }")
+    end
+
+    alias_method :on_fescape, :on_escape
+
+    # ForceEscapable doesn't touch :escape expression.
+    # This method is not used if it's inserted after Hamlit::Escapable.
+    def on_escape(flag, exp)
+      [:escape, flag, compile(exp)]
+    end
+  end
+end
diff --git a/lib/hamlit/helpers.rb b/lib/hamlit/helpers.rb
new file mode 100644 (file)
index 0000000..48e3367
--- /dev/null
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+module Hamlit
+  module Helpers
+    extend self
+
+    # The same as original Haml::Helpers#preserve without block support.
+    def preserve(input)
+      # https://github.com/haml/haml/blob/4.1.0.beta.1/lib/haml/helpers.rb#L130-L133
+      s = input.to_s.chomp("\n")
+      s.gsub!(/\n/, '&#x000A;')
+      s.delete!("\r")
+      s
+    end
+  end
+end
diff --git a/lib/hamlit/html.rb b/lib/hamlit/html.rb
new file mode 100644 (file)
index 0000000..72ca2c8
--- /dev/null
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+module Hamlit
+  class HTML < Temple::HTML::Fast
+    DEPRECATED_FORMATS = %i[html4 html5].freeze
+
+    def initialize(opts = {})
+      if DEPRECATED_FORMATS.include?(opts[:format])
+        opts = opts.dup
+        opts[:format] = :html
+      end
+      super(opts)
+    end
+  end
+end
diff --git a/lib/hamlit/identity.rb b/lib/hamlit/identity.rb
new file mode 100644 (file)
index 0000000..fabe36d
--- /dev/null
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+module Hamlit
+  class Identity
+    def initialize
+      @unique_id = 0
+    end
+
+    def generate
+      @unique_id += 1
+      "_hamlit_compiler#{@unique_id}"
+    end
+  end
+end
diff --git a/lib/hamlit/object_ref.rb b/lib/hamlit/object_ref.rb
new file mode 100644 (file)
index 0000000..7fca5f4
--- /dev/null
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+module Hamlit
+  module ObjectRef
+    class << self
+      def parse(args)
+        object, prefix = args
+        return {} unless object
+
+        suffix = underscore(object.class)
+        {
+          'class' => [prefix, suffix].compact.join('_'),
+          'id'    => [prefix, suffix, object.id || 'new'].compact.join('_'),
+        }
+      end
+
+      private
+
+      # Haml::Buffer.underscore
+      def underscore(camel_cased_word)
+        word = camel_cased_word.to_s.dup
+        word.gsub!(/::/, '_')
+        word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
+        word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
+        word.tr!('-', '_')
+        word.downcase!
+        word
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/parser.rb b/lib/hamlit/parser.rb
new file mode 100644 (file)
index 0000000..f1f4b01
--- /dev/null
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+# Hamlit::Parser uses original Haml::Parser to generate Haml AST.
+# hamlit/parser/haml_* are modules originally in haml gem.
+
+require 'hamlit/parser/haml_error'
+require 'hamlit/parser/haml_util'
+require 'hamlit/parser/haml_buffer'
+require 'hamlit/parser/haml_compiler'
+require 'hamlit/parser/haml_parser'
+require 'hamlit/parser/haml_helpers'
+require 'hamlit/parser/haml_options'
+
+module Hamlit
+  class Parser
+    AVAILABLE_OPTIONS = %i[
+      autoclose
+      escape_html
+      escape_attrs
+    ].freeze
+
+    def initialize(options = {})
+      @options = HamlOptions.defaults.dup
+      AVAILABLE_OPTIONS.each do |key|
+        @options[key] = options[key]
+      end
+    end
+
+    def call(template)
+      HamlParser.new(template, HamlOptions.new(@options)).parse
+    rescue ::Hamlit::HamlError => e
+      error_with_lineno(e)
+    end
+
+    private
+
+    def error_with_lineno(error)
+      return error if error.line
+
+      trace = error.backtrace.first
+      return error unless trace
+
+      line = trace.match(/\d+\z/).to_s.to_i
+      HamlSyntaxError.new(error.message, line)
+    end
+  end
+end
diff --git a/lib/hamlit/parser/MIT-LICENSE b/lib/hamlit/parser/MIT-LICENSE
new file mode 100644 (file)
index 0000000..758713a
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright (c) 2006-2009 Hampton Catlin and Natalie Weizenbaum
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/hamlit/parser/README.md b/lib/hamlit/parser/README.md
new file mode 100644 (file)
index 0000000..426c539
--- /dev/null
@@ -0,0 +1,28 @@
+# lib/hamlit/parser/haml\_\*.rb
+
+Hamlit::HamlFoo is originally Haml::Foo in haml gem.
+
+## License
+
+lib/hamlit/parser/haml\_\*.rb is:
+
+Copyright (c) 2006-2009 Hampton Catlin and Natalie Weizenbaum
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/hamlit/parser/haml_buffer.rb b/lib/hamlit/parser/haml_buffer.rb
new file mode 100644 (file)
index 0000000..542e95c
--- /dev/null
@@ -0,0 +1,348 @@
+require 'hamlit/parser/haml_helpers'
+require 'hamlit/parser/haml_util'
+require 'hamlit/parser/haml_compiler'
+
+module Hamlit
+  # This class is used only internally. It holds the buffer of HTML that
+  # is eventually output as the resulting document.
+  # It's called from within the precompiled code,
+  # and helps reduce the amount of processing done within `instance_eval`ed code.
+  class HamlBuffer
+    ID_KEY    = 'id'.freeze
+    CLASS_KEY = 'class'.freeze
+    DATA_KEY  = 'data'.freeze
+
+    include ::Hamlit::HamlHelpers
+    include ::Hamlit::HamlUtil
+
+    # The string that holds the compiled HTML. This is aliased as
+    # `_erbout` for compatibility with ERB-specific code.
+    #
+    # @return [String]
+    attr_accessor :buffer
+
+    # The options hash passed in from {Haml::Engine}.
+    #
+    # @return [{String => Object}]
+    # @see Haml::Options#for_buffer
+    attr_accessor :options
+
+    # The {Buffer} for the enclosing Haml document.
+    # This is set for partials and similar sorts of nested templates.
+    # It's `nil` at the top level (see \{#toplevel?}).
+    #
+    # @return [Buffer]
+    attr_accessor :upper
+
+    # nil if there's no capture_haml block running,
+    # and the position at which it's beginning the capture if there is one.
+    #
+    # @return [Fixnum, nil]
+    attr_accessor :capture_position
+
+    # @return [Boolean]
+    # @see #active?
+    attr_writer :active
+
+    # @return [Boolean] Whether or not the format is XHTML
+    def xhtml?
+      not html?
+    end
+
+    # @return [Boolean] Whether or not the format is any flavor of HTML
+    def html?
+      html4? or html5?
+    end
+
+    # @return [Boolean] Whether or not the format is HTML4
+    def html4?
+      @options[:format] == :html4
+    end
+
+    # @return [Boolean] Whether or not the format is HTML5.
+    def html5?
+      @options[:format] == :html5
+    end
+
+    # @return [Boolean] Whether or not this buffer is a top-level template,
+    #   as opposed to a nested partial
+    def toplevel?
+      upper.nil?
+    end
+
+    # Whether or not this buffer is currently being used to render a Haml template.
+    # Returns `false` if a subtemplate is being rendered,
+    # even if it's a subtemplate of this buffer's template.
+    #
+    # @return [Boolean]
+    def active?
+      @active
+    end
+
+    # @return [Fixnum] The current indentation level of the document
+    def tabulation
+      @real_tabs + @tabulation
+    end
+
+    # Sets the current tabulation of the document.
+    #
+    # @param val [Fixnum] The new tabulation
+    def tabulation=(val)
+      val = val - @real_tabs
+      @tabulation = val > -1 ? val : 0
+    end
+
+    # @param upper [Buffer] The parent buffer
+    # @param options [{Symbol => Object}] An options hash.
+    #   See {Haml::Engine#options\_for\_buffer}
+    def initialize(upper = nil, options = {})
+      @active     = true
+      @upper      = upper
+      @options    = options
+      @buffer     = new_encoded_string
+      @tabulation = 0
+
+      # The number of tabs that Engine thinks we should have
+      # @real_tabs + @tabulation is the number of tabs actually output
+      @real_tabs = 0
+    end
+
+    # Appends text to the buffer, properly tabulated.
+    # Also modifies the document's indentation.
+    #
+    # @param text [String] The text to append
+    # @param tab_change [Fixnum] The number of tabs by which to increase
+    #   or decrease the document's indentation
+    # @param dont_tab_up [Boolean] If true, don't indent the first line of `text`
+    def push_text(text, tab_change, dont_tab_up)
+      if @tabulation > 0
+        # Have to push every line in by the extra user set tabulation.
+        # Don't push lines with just whitespace, though,
+        # because that screws up precompiled indentation.
+        text.gsub!(/^(?!\s+$)/m, tabs)
+        text.sub!(tabs, '') if dont_tab_up
+      end
+
+      @real_tabs += tab_change
+      @buffer << text
+    end
+
+    # Modifies the indentation of the document.
+    #
+    # @param tab_change [Fixnum] The number of tabs by which to increase
+    #   or decrease the document's indentation
+    def adjust_tabs(tab_change)
+      @real_tabs += tab_change
+    end
+
+    # the number of arguments here is insane, but passing in an options hash instead of named arguments
+    # causes a significant performance regression
+    def format_script(result, preserve_script, in_tag, preserve_tag, escape_html, nuke_inner_whitespace, interpolated, ugly)
+      result_name = escape_html ? html_escape(result.to_s) : result.to_s
+
+      if ugly
+        result = nuke_inner_whitespace ? result_name.strip : result_name
+        result = preserve(result, preserve_script, preserve_tag)
+        fix_textareas!(result) if toplevel? && result.include?('<textarea')
+        return result
+      end
+
+      # If we're interpolated,
+      # then the custom tabulation is handled in #push_text.
+      # The easiest way to avoid it here is to reset @tabulation.
+      if interpolated
+        old_tabulation = @tabulation
+        @tabulation = 0
+      end
+
+      in_tag_no_nuke = in_tag && !nuke_inner_whitespace
+      preserved_no_nuke = in_tag_no_nuke && preserve_tag
+      tabulation = !preserved_no_nuke && @real_tabs
+
+      result = nuke_inner_whitespace ? result_name.strip : result_name.rstrip
+      result = preserve(result, preserve_script, preserve_tag)
+
+      has_newline = !preserved_no_nuke && result.include?("\n")
+
+      if in_tag_no_nuke && (preserve_tag || !has_newline)
+        @real_tabs -= 1
+        @tabulation = old_tabulation if interpolated
+        return result
+      end
+
+      unless preserved_no_nuke
+        # Precompiled tabulation may be wrong
+        result = "#{tabs}#{result}" if !interpolated && !in_tag && @tabulation > 0
+
+        if has_newline
+          result.gsub! "\n", "\n#{tabs(tabulation)}"
+
+          # Add tabulation if it wasn't precompiled
+          result = "#{tabs(tabulation)}#{result}" if in_tag_no_nuke
+        end
+
+        fix_textareas!(result) if toplevel? && result.include?('<textarea')
+
+        if in_tag_no_nuke
+          result = "\n#{result}\n#{tabs(tabulation-1)}"
+          @real_tabs -= 1
+        end
+        @tabulation = old_tabulation if interpolated
+        result
+      end
+    end
+
+    def attributes(class_id, obj_ref, *attributes_hashes)
+      attributes = class_id
+      attributes_hashes.each do |old|
+        self.class.merge_attrs(attributes, Hash[old.map {|k, v| [k.to_s, v]}])
+      end
+      self.class.merge_attrs(attributes, parse_object_ref(obj_ref)) if obj_ref
+      ::Hamlit::HamlCompiler.build_attributes(
+        html?, @options[:attr_wrapper], @options[:escape_attrs], @options[:hyphenate_data_attrs], attributes)
+    end
+
+    # Remove the whitespace from the right side of the buffer string.
+    # Doesn't do anything if we're at the beginning of a capture_haml block.
+    def rstrip!
+      if capture_position.nil?
+        buffer.rstrip!
+        return
+      end
+
+      buffer << buffer.slice!(capture_position..-1).rstrip
+    end
+
+    # Merges two attribute hashes.
+    # This is the same as `to.merge!(from)`,
+    # except that it merges id, class, and data attributes.
+    #
+    # ids are concatenated with `"_"`,
+    # and classes are concatenated with `" "`.
+    # data hashes are simply merged.
+    #
+    # Destructively modifies both `to` and `from`.
+    #
+    # @param to [{String => String}] The attribute hash to merge into
+    # @param from [{String => #to_s}] The attribute hash to merge from
+    # @return [{String => String}] `to`, after being merged
+    def self.merge_attrs(to, from)
+      from[ID_KEY] = ::Hamlit::HamlCompiler.filter_and_join(from[ID_KEY], '_') if from[ID_KEY]
+      if to[ID_KEY] && from[ID_KEY]
+        to[ID_KEY] << "_#{from.delete(ID_KEY)}"
+      elsif to[ID_KEY] || from[ID_KEY]
+        from[ID_KEY] ||= to[ID_KEY]
+      end
+
+      from[CLASS_KEY] = ::Hamlit::HamlCompiler.filter_and_join(from[CLASS_KEY], ' ') if from[CLASS_KEY]
+      if to[CLASS_KEY] && from[CLASS_KEY]
+        # Make sure we don't duplicate class names
+        from[CLASS_KEY] = (from[CLASS_KEY].to_s.split(' ') | to[CLASS_KEY].split(' ')).sort.join(' ')
+      elsif to[CLASS_KEY] || from[CLASS_KEY]
+        from[CLASS_KEY] ||= to[CLASS_KEY]
+      end
+
+      from.keys.each do |key|
+        next unless from[key].kind_of?(Hash) || to[key].kind_of?(Hash)
+
+        from_data = from.delete(key)
+        # forces to_data & from_data into a hash
+        from_data = { nil => from_data } if from_data && !from_data.is_a?(Hash)
+        to[key] = { nil => to[key] } if to[key] && !to[key].is_a?(Hash)
+
+        if from_data && !to[key]
+          to[key] = from_data
+        elsif from_data && to[key]
+          to[key].merge! from_data
+        end
+      end
+
+      to.merge!(from)
+    end
+
+    private
+
+    def preserve(result, preserve_script, preserve_tag)
+      return ::Hamlit::HamlHelpers.preserve(result) if preserve_tag
+      return ::Hamlit::HamlHelpers.find_and_preserve(result, options[:preserve]) if preserve_script
+      result
+    end
+
+    # Works like #{find_and_preserve}, but allows the first newline after a
+    # preserved opening tag to remain unencoded, and then outdents the content.
+    # This change was motivated primarily by the change in Rails 3.2.3 to emit
+    # a newline after textarea helpers.
+    #
+    # @param input [String] The text to process
+    # @since Haml 4.0.1
+    # @private
+    def fix_textareas!(input)
+      pattern = /<(textarea)([^>]*)>(\n|&#x000A;)(.*?)<\/textarea>/im
+      input.gsub!(pattern) do |s|
+        match = pattern.match(s)
+        content = match[4]
+        if match[3] == '&#x000A;'
+          content.sub!(/\A /, '&#x0020;')
+        else
+          content.sub!(/\A[ ]*/, '')
+        end
+        "<#{match[1]}#{match[2]}>\n#{content}</#{match[1]}>"
+      end
+    end
+
+    def new_encoded_string
+      "".encode(Encoding.find(options[:encoding]))
+    end
+
+    @@tab_cache = {}
+    # Gets `count` tabs. Mostly for internal use.
+    def tabs(count = 0)
+      tabs = [count + @tabulation, 0].max
+      @@tab_cache[tabs] ||= '  ' * tabs
+    end
+
+    # Takes an array of objects and uses the class and id of the first
+    # one to create an attributes hash.
+    # The second object, if present, is used as a prefix,
+    # just like you can do with `dom_id()` and `dom_class()` in Rails
+    def parse_object_ref(ref)
+      prefix = ref[1]
+      ref = ref[0]
+      # Let's make sure the value isn't nil. If it is, return the default Hash.
+      return {} if ref.nil?
+      class_name =
+        if ref.respond_to?(:haml_object_ref)
+          ref.haml_object_ref
+        else
+          underscore(ref.class)
+        end
+      ref_id =
+        if ref.respond_to?(:to_key)
+          key = ref.to_key
+          key.join('_') unless key.nil?
+        else
+          ref.id
+        end
+      id = "#{class_name}_#{ref_id || 'new'}"
+      if prefix
+        class_name = "#{ prefix }_#{ class_name}"
+        id = "#{ prefix }_#{ id }"
+      end
+
+      {ID_KEY => id, CLASS_KEY => class_name}
+    end
+
+    # Changes a word from camel case to underscores.
+    # Based on the method of the same name in Rails' Inflector,
+    # but copied here so it'll run properly without Rails.
+    def underscore(camel_cased_word)
+      word = camel_cased_word.to_s.dup
+      word.gsub!(/::/, '_')
+      word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
+      word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
+      word.tr!('-', '_')
+      word.downcase!
+      word
+    end
+  end
+end
diff --git a/lib/hamlit/parser/haml_compiler.rb b/lib/hamlit/parser/haml_compiler.rb
new file mode 100644 (file)
index 0000000..fbf02ee
--- /dev/null
@@ -0,0 +1,553 @@
+require 'hamlit/parser/haml_util'
+require 'hamlit/parser/haml_parser'
+
+module Hamlit
+  class HamlCompiler
+    include ::Hamlit::HamlUtil
+
+    attr_accessor :options
+
+    def initialize(options)
+      @options     = options
+      @output_tabs = 0
+      @to_merge    = []
+      @precompiled = ''
+      @node        = nil
+    end
+
+    def compile(node)
+      parent, @node = @node, node
+      if node.children.empty?
+        send(:"compile_#{node.type}")
+      else
+        send(:"compile_#{node.type}") {node.children.each {|c| compile c}}
+      end
+    ensure
+      @node = parent
+    end
+
+    # The source code that is evaluated to produce the Haml document.
+    #
+    # This is automatically converted to the correct encoding
+    # (see {file:REFERENCE.md#encodings the `:encoding` option}).
+    #
+    # @return [String]
+    def precompiled
+      encoding = Encoding.find(@options.encoding)
+      return @precompiled.force_encoding(encoding) if encoding == Encoding::ASCII_8BIT
+      return @precompiled.encode(encoding)
+    end
+
+    def precompiled_with_return_value
+      "#{precompiled};#{precompiled_method_return_value}"
+    end
+
+    # Returns the precompiled string with the preamble and postamble.
+    #
+    # Initializes to ActionView::OutputBuffer when available; this is necessary
+    # to avoid ordering issues with partial layouts in Rails. If not available,
+    # initializes to nil.
+    def precompiled_with_ambles(local_names)
+      preamble = <<END.tr!("\n", ';')
+begin
+extend ::Hamlit::HamlHelpers
+_hamlout = @haml_buffer = ::Hamlit::HamlBuffer.new(haml_buffer, #{options.for_buffer.inspect})
+_erbout = _hamlout.buffer
+@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil
+END
+      postamble = <<END.tr!("\n", ';')
+#{precompiled_method_return_value}
+ensure
+@haml_buffer = @haml_buffer.upper if @haml_buffer
+end
+END
+      "#{preamble}#{locals_code(local_names)}#{precompiled}#{postamble}"
+    end
+
+    private
+
+    # Returns the string used as the return value of the precompiled method.
+    # This method exists so it can be monkeypatched to return modified values.
+    def precompiled_method_return_value
+      "_erbout"
+    end
+
+    def locals_code(names)
+      names = names.keys if Hash === names
+
+      names.each_with_object('') do |name, code|
+        # Can't use || because someone might explicitly pass in false with a symbol
+        sym_local = "_haml_locals[#{inspect_obj(name.to_sym)}]"
+        str_local = "_haml_locals[#{inspect_obj(name.to_s)}]"
+        code << "#{name} = #{sym_local}.nil? ? #{str_local} : #{sym_local};"
+      end
+    end
+
+    def compile_root
+      @dont_indent_next_line = @dont_tab_up_next_text = false
+      @output_line = 1
+      yield if block_given?
+      flush_merged_text
+    end
+
+    def compile_plain
+      push_text @node.value[:text]
+    end
+
+    def nuke_inner_whitespace?(node)
+      if node.value && node.value[:nuke_inner_whitespace]
+        true
+      elsif node.parent
+        nuke_inner_whitespace?(node.parent)
+      else
+        false
+      end
+    end
+
+    def compile_script(&block)
+      push_script(@node.value[:text],
+                  :preserve_script       => @node.value[:preserve],
+                  :escape_html           => @node.value[:escape_html],
+                  :nuke_inner_whitespace => nuke_inner_whitespace?(@node),
+                  &block)
+    end
+
+    def compile_silent_script
+      return if @options.suppress_eval
+      push_silent(@node.value[:text])
+      keyword = @node.value[:keyword]
+
+      if block_given?
+        # Store these values because for conditional statements,
+        # we want to restore them for each branch
+        @node.value[:dont_indent_next_line] = @dont_indent_next_line
+        @node.value[:dont_tab_up_next_text] = @dont_tab_up_next_text
+        yield
+        push_silent("end", :can_suppress) unless @node.value[:dont_push_end]
+      elsif keyword == "end"
+        if @node.parent.children.last.equal?(@node)
+          # Since this "end" is ending the block,
+          # we don't need to generate an additional one
+          @node.parent.value[:dont_push_end] = true
+        end
+        # Don't restore dont_* for end because it isn't a conditional branch.
+      elsif ::Hamlit::HamlParser::MID_BLOCK_KEYWORDS.include?(keyword)
+        # Restore dont_* for this conditional branch
+        @dont_indent_next_line = @node.parent.value[:dont_indent_next_line]
+        @dont_tab_up_next_text = @node.parent.value[:dont_tab_up_next_text]
+      end
+    end
+
+    def compile_haml_comment; end
+
+    def compile_tag
+      t = @node.value
+
+      # Get rid of whitespace outside of the tag if we need to
+      rstrip_buffer! if t[:nuke_outer_whitespace]
+
+      dont_indent_next_line =
+        (t[:nuke_outer_whitespace] && !block_given?) ||
+        (t[:nuke_inner_whitespace] && block_given?)
+
+      if @options.suppress_eval
+        object_ref = :nil
+        parse = false
+        value = t[:parse] ? nil : t[:value]
+        attributes_hashes = {}
+        preserve_script = false
+      else
+        object_ref = t[:object_ref]
+        parse = t[:parse]
+        value = t[:value]
+        attributes_hashes = t[:attributes_hashes]
+        preserve_script = t[:preserve_script]
+      end
+
+      if @options[:trace]
+        t[:attributes].merge!({"data-trace" => @options.filename.split('/views').last << ":" << @node.line.to_s})
+      end
+
+      # Check if we can render the tag directly to text and not process it in the buffer
+      if (object_ref == :nil) && attributes_hashes.empty? && !preserve_script
+        tag_closed = !block_given? && !t[:self_closing] && !parse
+
+        open_tag = prerender_tag(t[:name], t[:self_closing], t[:attributes])
+        if tag_closed
+          open_tag << "#{value}</#{t[:name]}>"
+          open_tag << "\n" unless t[:nuke_outer_whitespace]
+        elsif !(parse || t[:nuke_inner_whitespace] ||
+            (t[:self_closing] && t[:nuke_outer_whitespace]))
+          open_tag << "\n"
+        end
+
+        push_merged_text(open_tag,
+          tag_closed || t[:self_closing] || t[:nuke_inner_whitespace] ? 0 : 1,
+          !t[:nuke_outer_whitespace])
+
+        @dont_indent_next_line = dont_indent_next_line
+        return if tag_closed
+      else
+        if attributes_hashes.empty?
+          attributes_hashes = ''
+        elsif attributes_hashes.size == 1
+          attributes_hashes = ", #{attributes_hashes.first}"
+        else
+          attributes_hashes = ", #{attributes_hashes.join(", ")}"
+        end
+
+        push_merged_text "<#{t[:name]}", 0, !t[:nuke_outer_whitespace]
+        push_generated_script(
+          "_hamlout.attributes(#{inspect_obj(t[:attributes])}, #{object_ref}#{attributes_hashes})")
+        concat_merged_text(
+          if t[:self_closing] && @options.xhtml?
+            " />#{"\n" unless t[:nuke_outer_whitespace]}"
+          else
+            ">#{"\n" unless (t[:self_closing] && @options.html?) ? t[:nuke_outer_whitespace] : (!block_given? || t[:preserve_tag] || t[:nuke_inner_whitespace])}"
+          end)
+
+        if value && !parse
+          concat_merged_text("#{value}</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
+        elsif !t[:nuke_inner_whitespace] && !t[:self_closing]
+          @to_merge << [:text, '', 1]
+        end
+
+        @dont_indent_next_line = dont_indent_next_line
+      end
+
+      return if t[:self_closing]
+
+      if value.nil?
+        @output_tabs += 1 unless t[:nuke_inner_whitespace]
+        yield if block_given?
+        @output_tabs -= 1 unless t[:nuke_inner_whitespace]
+        rstrip_buffer! if t[:nuke_inner_whitespace]
+        push_merged_text("</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}",
+          t[:nuke_inner_whitespace] ? 0 : -1, !t[:nuke_inner_whitespace])
+        @dont_indent_next_line = t[:nuke_outer_whitespace]
+        return
+      end
+
+      if parse
+        push_script(value, t.merge(:in_tag => true))
+        concat_merged_text("</#{t[:name]}>#{"\n" unless t[:nuke_outer_whitespace]}")
+      end
+    end
+
+    def compile_comment
+      condition = "#{@node.value[:conditional]}>" if @node.value[:conditional]
+      revealed = @node.value[:revealed]
+
+      open = "<!--#{condition}#{'<!-->' if revealed}"
+
+      close = "#{'<!--' if revealed}#{'<![endif]' if condition}-->"
+
+      unless block_given?
+        push_merged_text("#{open} ")
+
+        if @node.value[:parse]
+          push_script(@node.value[:text], :in_tag => true, :nuke_inner_whitespace => true)
+        else
+          push_merged_text(@node.value[:text], 0, false)
+        end
+
+        push_merged_text(" #{close}\n", 0, false)
+        return
+      end
+
+      push_text(open, 1)
+      @output_tabs += 1
+      yield if block_given?
+      @output_tabs -= 1
+      push_text(close, -1)
+    end
+
+    def compile_doctype
+      doctype = text_for_doctype
+      push_text doctype if doctype
+    end
+
+    def compile_filter
+      unless filter = Filters.defined[@node.value[:name]]
+        name = @node.value[:name]
+        if ["maruku", "textile"].include?(name)
+          raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:install_haml_contrib, name), @node.line - 1)
+        else
+          raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:filter_not_defined, name), @node.line - 1)
+        end
+      end
+      filter.internal_compile(self, @node.value[:text])
+    end
+
+    def text_for_doctype
+      if @node.value[:type] == "xml"
+        return nil if @options.html?
+        wrapper = @options.attr_wrapper
+        return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{@node.value[:encoding] || "utf-8"}#{wrapper} ?>"
+      end
+
+      if @options.html5?
+        '<!DOCTYPE html>'
+      else
+        if @options.xhtml?
+          if @node.value[:version] == "1.1"
+            '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
+          elsif @node.value[:version] == "5"
+            '<!DOCTYPE html>'
+          else
+            case @node.value[:type]
+            when "strict";   '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
+            when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
+            when "mobile";   '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
+            when "rdfa";     '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">'
+            when "basic";    '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
+            else             '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
+            end
+          end
+
+        elsif @options.html4?
+          case @node.value[:type]
+          when "strict";   '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
+          when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
+          else             '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
+          end
+        end
+      end
+    end
+
+    # Evaluates `text` in the context of the scope object, but
+    # does not output the result.
+    def push_silent(text, can_suppress = false)
+      flush_merged_text
+      return if can_suppress && @options.suppress_eval?
+      newline = (text == "end") ? ";" : "\n"
+      @precompiled << "#{resolve_newlines}#{text}#{newline}"
+      @output_line = @output_line + text.count("\n") + newline.count("\n")
+    end
+
+    # Adds `text` to `@buffer` with appropriate tabulation
+    # without parsing it.
+    def push_merged_text(text, tab_change = 0, indent = true)
+      text = !indent || @dont_indent_next_line || @options.ugly ? text : "#{'  ' * @output_tabs}#{text}"
+      @to_merge << [:text, text, tab_change]
+      @dont_indent_next_line = false
+    end
+
+    # Concatenate `text` to `@buffer` without tabulation.
+    def concat_merged_text(text)
+      @to_merge << [:text, text, 0]
+    end
+
+    def push_text(text, tab_change = 0)
+      push_merged_text("#{text}\n", tab_change)
+    end
+
+    def flush_merged_text
+      return if @to_merge.empty?
+
+      mtabs = 0
+      @to_merge.map! do |type, val, tabs|
+        case type
+        when :text
+          mtabs += tabs
+          inspect_obj(val)[1...-1]
+        when :script
+          if mtabs != 0 && !@options.ugly
+            val = "_hamlout.adjust_tabs(#{mtabs}); " + val
+          end
+          mtabs = 0
+          "\#{#{val}}"
+        else
+          raise ::Hamlit::HamlSyntaxError.new("[HAML BUG] Undefined entry in ::Hamlit::HamlCompiler@to_merge.")
+        end
+      end
+      str = @to_merge.join
+
+      unless str.empty?
+        @precompiled <<
+          if @options.ugly
+            "_hamlout.buffer << \"#{str}\";"
+          else
+            "_hamlout.push_text(\"#{str}\", #{mtabs}, #{@dont_tab_up_next_text.inspect});"
+          end
+      end
+      @to_merge = []
+      @dont_tab_up_next_text = false
+    end
+
+    # Causes `text` to be evaluated in the context of
+    # the scope object and the result to be added to `@buffer`.
+    #
+    # If `opts[:preserve_script]` is true, Haml::Helpers#find_and_preserve is run on
+    # the result before it is added to `@buffer`
+    def push_script(text, opts = {})
+      return if @options.suppress_eval?
+
+      args = [:preserve_script, :in_tag, :preserve_tag, :escape_html, :nuke_inner_whitespace]
+      args.map! {|name| !!opts[name]}
+      args << !block_given? << @options.ugly
+
+      no_format = @options.ugly &&
+        !(opts[:preserve_script] || opts[:preserve_tag] || opts[:escape_html])
+
+      # Prerender tabulation unless we're in a tag
+      push_merged_text '' unless opts[:in_tag]
+
+      unless block_given?
+        format_script_method = "_hamlout.format_script((#{text}\n),#{args.join(',')});"
+        push_generated_script(no_format ? "#{text}\n" : format_script_method)
+        concat_merged_text("\n") unless opts[:in_tag] || opts[:nuke_inner_whitespace]
+        return
+      end
+
+      flush_merged_text
+      push_silent "haml_temp = #{text}"
+      yield
+      push_silent('end', :can_suppress) unless @node.value[:dont_push_end]
+      format_script_method = "_hamlout.format_script(haml_temp,#{args.join(',')});"
+      @precompiled << "_hamlout.buffer << #{no_format ? "haml_temp.to_s;" : format_script_method}"
+      concat_merged_text("\n") unless opts[:in_tag] || opts[:nuke_inner_whitespace] || @options.ugly
+    end
+
+    def push_generated_script(text)
+      @to_merge << [:script, resolve_newlines + text]
+      @output_line += text.count("\n")
+    end
+
+    # This is a class method so it can be accessed from Buffer.
+    def self.build_attributes(is_html, attr_wrapper, escape_attrs, hyphenate_data_attrs, attributes = {})
+        # @TODO this is an absolutely ridiculous amount of arguments. At least
+      # some of this needs to be moved into an instance method.
+      quote_escape     = attr_wrapper == '"' ? "&#x0022;" : "&#x0027;"
+      other_quote_char = attr_wrapper == '"' ? "'" : '"'
+      join_char        = hyphenate_data_attrs ? '-' : '_'
+
+      attributes.each do |key, value|
+        if value.is_a?(Hash)
+          data_attributes = attributes.delete(key)
+          data_attributes = flatten_data_attributes(data_attributes, '', join_char)
+          data_attributes = build_data_keys(data_attributes, hyphenate_data_attrs, key)
+          attributes = data_attributes.merge(attributes)
+        end
+      end
+
+      result = attributes.collect do |attr, value|
+        next if value.nil?
+
+        value = filter_and_join(value, ' ') if attr == 'class'
+        value = filter_and_join(value, '_') if attr == 'id'
+
+        if value == true
+          next " #{attr}" if is_html
+          next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
+        elsif value == false
+          next
+        end
+
+        escaped =
+          if escape_attrs == :once
+            ::Hamlit::HamlHelpers.escape_once(value.to_s)
+          elsif escape_attrs
+            ::Hamlit::HamlHelpers.html_escape(value.to_s)
+          else
+            value.to_s
+          end
+        value = ::Hamlit::HamlHelpers.preserve(escaped)
+        if escape_attrs
+          # We want to decide whether or not to escape quotes
+          value.gsub!(/&quot;|&#x0022;/, '"')
+          this_attr_wrapper = attr_wrapper
+          if value.include? attr_wrapper
+            if value.include? other_quote_char
+              value.gsub!(attr_wrapper, quote_escape)
+            else
+              this_attr_wrapper = other_quote_char
+            end
+          end
+        else
+          this_attr_wrapper = attr_wrapper
+        end
+        " #{attr}=#{this_attr_wrapper}#{value}#{this_attr_wrapper}"
+      end
+      result.compact!
+      result.sort!
+      result.join
+    end
+
+    def self.filter_and_join(value, separator)
+      return '' if (value.respond_to?(:empty?) && value.empty?)
+
+      if value.is_a?(Array)
+        value.flatten!
+        value.map! {|item| item ? item.to_s : nil}
+        value.compact!
+        value = value.join(separator)
+      else
+        value = value ? value.to_s : nil
+      end
+      !value.nil? && !value.empty? && value
+    end
+
+    def self.build_data_keys(data_hash, hyphenate, attr_name="data")
+      Hash[data_hash.map do |name, value|
+        if name == nil
+          [attr_name, value]
+        elsif hyphenate
+          ["#{attr_name}-#{name.to_s.tr('_', '-')}", value]
+        else
+          ["#{attr_name}-#{name}", value]
+        end
+      end]
+    end
+
+    def self.flatten_data_attributes(data, key, join_char, seen = [])
+      return {key => data} unless data.is_a?(Hash)
+
+      return {key => nil} if seen.include? data.object_id
+      seen << data.object_id
+
+      data.sort {|x, y| x[0].to_s <=> y[0].to_s}.inject({}) do |hash, (k, v)|
+        joined = key == '' ? k : [key, k].join(join_char)
+        hash.merge! flatten_data_attributes(v, joined, join_char, seen)
+      end
+    end
+
+    def prerender_tag(name, self_close, attributes)
+      attributes_string = ::Hamlit::HamlCompiler.build_attributes(
+        @options.html?, @options.attr_wrapper, @options.escape_attrs, @options.hyphenate_data_attrs, attributes)
+      "<#{name}#{attributes_string}#{self_close && @options.xhtml? ? ' /' : ''}>"
+    end
+
+    def resolve_newlines
+      diff = @node.line - @output_line
+      return "" if diff <= 0
+      @output_line = @node.line
+      "\n" * diff
+    end
+
+    # Get rid of and whitespace at the end of the buffer
+    # or the merged text
+    def rstrip_buffer!(index = -1)
+      last = @to_merge[index]
+      if last.nil?
+        push_silent("_hamlout.rstrip!", false)
+        @dont_tab_up_next_text = true
+        return
+      end
+
+      case last.first
+      when :text
+        last[1].rstrip!
+        if last[1].empty?
+          @to_merge.slice! index
+          rstrip_buffer! index
+        end
+      when :script
+        last[1].gsub!(/\(haml_temp, (.*?)\);$/, '(haml_temp.rstrip, \1);')
+        rstrip_buffer! index - 1
+      else
+        raise ::Hamlit::HamlSyntaxError.new("[HAML BUG] Undefined entry in ::Hamlit::HamlCompiler@to_merge.")
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/parser/haml_error.rb b/lib/hamlit/parser/haml_error.rb
new file mode 100644 (file)
index 0000000..73f507e
--- /dev/null
@@ -0,0 +1,61 @@
+module Hamlit
+  # An exception raised by Haml code.
+  class HamlError < StandardError
+
+    MESSAGES = {
+      :bad_script_indent            => '"%s" is indented at wrong level: expected %d, but was at %d.',
+      :cant_run_filter              => 'Can\'t run "%s" filter; you must require its dependencies first',
+      :cant_use_tabs_and_spaces     => "Indentation can't use both tabs and spaces.",
+      :deeper_indenting             => "The line was indented %d levels deeper than the previous line.",
+      :filter_not_defined           => 'Filter "%s" is not defined.',
+      :gem_install_filter_deps      => '"%s" filter\'s %s dependency missing: try installing it or adding it to your Gemfile',
+      :illegal_element              => "Illegal element: classes and ids must have values.",
+      :illegal_nesting_content      => "Illegal nesting: nesting within a tag that already has content is illegal.",
+      :illegal_nesting_header       => "Illegal nesting: nesting within a header command is illegal.",
+      :illegal_nesting_line         => "Illegal nesting: content can't be both given on the same line as %%%s and nested within it.",
+      :illegal_nesting_plain        => "Illegal nesting: nesting within plain text is illegal.",
+      :illegal_nesting_self_closing => "Illegal nesting: nesting within a self-closing tag is illegal.",
+      :inconsistent_indentation     => "Inconsistent indentation: %s used for indentation, but the rest of the document was indented using %s.",
+      :indenting_at_start           => "Indenting at the beginning of the document is illegal.",
+      :install_haml_contrib         => 'To use the "%s" filter, please install the haml-contrib gem.',
+      :invalid_attribute_list       => 'Invalid attribute list: %s.',
+      :invalid_filter_name          => 'Invalid filter name ":%s".',
+      :invalid_tag                  => 'Invalid tag: "%s".',
+      :missing_if                   => 'Got "%s" with no preceding "if"',
+      :no_ruby_code                 => "There's no Ruby code for %s to evaluate.",
+      :self_closing_content         => "Self-closing tags can't have content.",
+      :unbalanced_brackets          => 'Unbalanced brackets.',
+      :no_end                       => <<-END
+You don't need to use "- end" in Haml. Un-indent to close a block:
+- if foo?
+  %strong Foo!
+- else
+  Not foo.
+%p This line is un-indented, so it isn't part of the "if" block
+END
+    }
+
+    def self.message(key, *args)
+      string = MESSAGES[key] or raise "[HAML BUG] No error messages for #{key}"
+      (args.empty? ? string : string % args).rstrip
+    end
+
+    # The line of the template on which the error occurred.
+    #
+    # @return [Fixnum]
+    attr_reader :line
+
+    # @param message [String] The error message
+    # @param line [Fixnum] See \{#line}
+    def initialize(message = nil, line = nil)
+      super(message)
+      @line = line
+    end
+  end
+
+  # SyntaxError is the type of exception raised when Haml encounters an
+  # ill-formatted document.
+  # It's not particularly interesting,
+  # except in that it's a subclass of {Haml::Error}.
+  class HamlSyntaxError < HamlError; end
+end
diff --git a/lib/hamlit/parser/haml_helpers.rb b/lib/hamlit/parser/haml_helpers.rb
new file mode 100644 (file)
index 0000000..f1a5738
--- /dev/null
@@ -0,0 +1,727 @@
+require 'hamlit/parser/haml_error'
+require 'hamlit/parser/haml_buffer'
+require 'hamlit/parser/haml_options'
+require 'hamlit/parser/haml_compiler'
+require 'hamlit/parser/haml_parser'
+
+module Hamlit
+  # This module contains various helpful methods to make it easier to do various tasks.
+  # {Haml::Helpers} is automatically included in the context
+  # that a Haml template is parsed in, so all these methods are at your
+  # disposal from within the template.
+  module HamlHelpers
+    # An object that raises an error when \{#to\_s} is called.
+    # It's used to raise an error when the return value of a helper is used
+    # when it shouldn't be.
+    class ErrorReturn
+      def initialize(method)
+        @message = <<MESSAGE
+#{method} outputs directly to the Haml template.
+Disregard its return value and use the - operator,
+or use capture_haml to get the value as a String.
+MESSAGE
+      end
+
+      # Raises an error.
+      #
+      # @raise [Haml::Error] The error
+      def to_s
+        raise ::Hamlit::HamlError.new(@message)
+      rescue ::Hamlit::HamlError => e
+        e.backtrace.shift
+
+        # If the ErrorReturn is used directly in the template,
+        # we don't want Haml's stuff to get into the backtrace,
+        # so we get rid of the format_script line.
+        #
+        # We also have to subtract one from the Haml line number
+        # since the value is passed to format_script the line after
+        # it's actually used.
+        if e.backtrace.first =~ /^\(eval\):\d+:in `format_script/
+          e.backtrace.shift
+          e.backtrace.first.gsub!(/^\(haml\):(\d+)/) {|s| "(haml):#{$1.to_i - 1}"}
+        end
+        raise e
+      end
+
+      # @return [String] A human-readable string representation
+      def inspect
+        "::Hamlit::HamlHelpers::ErrorReturn(#{@message.inspect})"
+      end
+    end
+
+    self.extend self
+
+    @@action_view_defined = false
+
+    # @return [Boolean] Whether or not ActionView is loaded
+    def self.action_view?
+      @@action_view_defined
+    end
+
+    # Note: this does **not** need to be called when using Haml helpers
+    # normally in Rails.
+    #
+    # Initializes the current object as though it were in the same context
+    # as a normal ActionView instance using Haml.
+    # This is useful if you want to use the helpers in a context
+    # other than the normal setup with ActionView.
+    # For example:
+    #
+    #     context = Object.new
+    #     class << context
+    #       include Haml::Helpers
+    #     end
+    #     context.init_haml_helpers
+    #     context.haml_tag :p, "Stuff"
+    #
+    def init_haml_helpers
+      @haml_buffer = ::Hamlit::HamlBuffer.new(haml_buffer, ::Hamlit::HamlOptions.new.for_buffer)
+      nil
+    end
+
+    # Runs a block of code in a non-Haml context
+    # (i.e. \{#is\_haml?} will return false).
+    #
+    # This is mainly useful for rendering sub-templates such as partials in a non-Haml language,
+    # particularly where helpers may behave differently when run from Haml.
+    #
+    # Note that this is automatically applied to Rails partials.
+    #
+    # @yield A block which won't register as Haml
+    def non_haml
+      was_active = @haml_buffer.active?
+      @haml_buffer.active = false
+      yield
+    ensure
+      @haml_buffer.active = was_active
+    end
+
+    # Uses \{#preserve} to convert any newlines inside whitespace-sensitive tags
+    # into the HTML entities for endlines.
+    #
+    # @param tags [Array<String>] Tags that should have newlines escaped
+    #
+    # @overload find_and_preserve(input, tags = haml_buffer.options[:preserve])
+    #   Escapes newlines within a string.
+    #
+    #   @param input [String] The string within which to escape newlines
+    # @overload find_and_preserve(tags = haml_buffer.options[:preserve])
+    #   Escapes newlines within a block of Haml code.
+    #
+    #   @yield The block within which to escape newlines
+    def find_and_preserve(input = nil, tags = haml_buffer.options[:preserve], &block)
+      return find_and_preserve(capture_haml(&block), input || tags) if block
+      tags = tags.each_with_object('') do |t, s|
+        s << '|' unless s.empty?
+        s << Regexp.escape(t)
+      end
+      re = /<(#{tags})([^>]*)>(.*?)(<\/\1>)/im
+      input.to_s.gsub(re) do |s|
+        s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible
+        "<#{$1}#{$2}>#{preserve($3)}</#{$1}>"
+      end
+    end
+
+    # Takes any string, finds all the newlines, and converts them to
+    # HTML entities so they'll render correctly in
+    # whitespace-sensitive tags without screwing up the indentation.
+    #
+    # @overload preserve(input)
+    #   Escapes newlines within a string.
+    #
+    #   @param input [String] The string within which to escape all newlines
+    # @overload preserve
+    #   Escapes newlines within a block of Haml code.
+    #
+    #   @yield The block within which to escape newlines
+    def preserve(input = nil, &block)
+      return preserve(capture_haml(&block)) if block
+      s = input.to_s.chomp("\n")
+      s.gsub!(/\n/, '&#x000A;')
+      s.delete!("\r")
+      s
+    end
+    alias_method :flatten, :preserve
+
+    # Takes an `Enumerable` object and a block
+    # and iterates over the enum,
+    # yielding each element to a Haml block
+    # and putting the result into `<li>` elements.
+    # This creates a list of the results of the block.
+    # For example:
+    #
+    #     = list_of([['hello'], ['yall']]) do |i|
+    #       = i[0]
+    #
+    # Produces:
+    #
+    #     <li>hello</li>
+    #     <li>yall</li>
+    #
+    # And:
+    #
+    #     = list_of({:title => 'All the stuff', :description => 'A book about all the stuff.'}) do |key, val|
+    #       %h3= key.humanize
+    #       %p= val
+    #
+    # Produces:
+    #
+    #     <li>
+    #       <h3>Title</h3>
+    #       <p>All the stuff</p>
+    #     </li>
+    #     <li>
+    #       <h3>Description</h3>
+    #       <p>A book about all the stuff.</p>
+    #     </li>
+    #
+    # While:
+    #
+    #     = list_of(["Home", "About", "Contact", "FAQ"], {class: "nav", role: "nav"}) do |item|
+    #       %a{ href="#" }= item
+    #
+    # Produces:
+    #
+    #     <li class='nav' role='nav'>
+    #       <a href='#'>Home</a>
+    #     </li>
+    #     <li class='nav' role='nav'>
+    #       <a href='#'>About</a>
+    #     </li>
+    #     <li class='nav' role='nav'>
+    #       <a href='#'>Contact</a>
+    #     </li>
+    #     <li class='nav' role='nav'>
+    #       <a href='#'>FAQ</a>
+    #     </li>
+    #
+    #  `[[class", "nav"], [role", "nav"]]` could have been used instead of `{class: "nav", role: "nav"}` (or any enumerable collection where each pair of items responds to #to_s)
+    #
+    # @param enum [Enumerable] The list of objects to iterate over
+    # @param [Enumerable<#to_s,#to_s>] opts Each key/value pair will become an attribute pair for each list item element.
+    # @yield [item] A block which contains Haml code that goes within list items
+    # @yieldparam item An element of `enum`
+    def list_of(enum, opts={}, &block)
+      opts_attributes = opts.each_with_object('') {|(k, v), s| s << " #{k}='#{v}'"}
+      enum.each_with_object('') do |i, ret|
+        result = capture_haml(i, &block)
+
+        if result.count("\n") > 1
+          result.gsub!("\n", "\n  ")
+          result = "\n  #{result.strip!}\n"
+        else
+          result.strip!
+        end
+
+        ret << "\n" unless ret.empty?
+        ret << %Q!<li#{opts_attributes}>#{result}</li>!
+      end
+    end
+
+    # Returns a hash containing default assignments for the `xmlns`, `lang`, and `xml:lang`
+    # attributes of the `html` HTML element.
+    # For example,
+    #
+    #     %html{html_attrs}
+    #
+    # becomes
+    #
+    #     <html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en-US' lang='en-US'>
+    #
+    # @param lang [String] The value of `xml:lang` and `lang`
+    # @return [{#to_s => String}] The attribute hash
+    def html_attrs(lang = 'en-US')
+      if haml_buffer.options[:format] == :xhtml
+        {:xmlns => "http://www.w3.org/1999/xhtml", 'xml:lang' => lang, :lang => lang}
+      else
+        {:lang => lang}
+      end
+    end
+
+    # Increments the number of tabs the buffer automatically adds
+    # to the lines of the template.
+    # For example:
+    #
+    #     %h1 foo
+    #     - tab_up
+    #     %p bar
+    #     - tab_down
+    #     %strong baz
+    #
+    # Produces:
+    #
+    #     <h1>foo</h1>
+    #       <p>bar</p>
+    #     <strong>baz</strong>
+    #
+    # @param i [Fixnum] The number of tabs by which to increase the indentation
+    # @see #tab_down
+    def tab_up(i = 1)
+      haml_buffer.tabulation += i
+    end
+
+    # Decrements the number of tabs the buffer automatically adds
+    # to the lines of the template.
+    #
+    # @param i [Fixnum] The number of tabs by which to decrease the indentation
+    # @see #tab_up
+    def tab_down(i = 1)
+      haml_buffer.tabulation -= i
+    end
+
+    # Sets the number of tabs the buffer automatically adds
+    # to the lines of the template,
+    # but only for the duration of the block.
+    # For example:
+    #
+    #     %h1 foo
+    #     - with_tabs(2) do
+    #       %p bar
+    #     %strong baz
+    #
+    # Produces:
+    #
+    #     <h1>foo</h1>
+    #         <p>bar</p>
+    #     <strong>baz</strong>
+    #
+    #
+    # @param i [Fixnum] The number of tabs to use
+    # @yield A block in which the indentation will be `i` spaces
+    def with_tabs(i)
+      old_tabs = haml_buffer.tabulation
+      haml_buffer.tabulation = i
+      yield
+    ensure
+      haml_buffer.tabulation = old_tabs
+    end
+
+    # Surrounds a block of Haml code with strings,
+    # with no whitespace in between.
+    # For example:
+    #
+    #     = surround '(', ')' do
+    #       %a{:href => "food"} chicken
+    #
+    # Produces:
+    #
+    #     (<a href='food'>chicken</a>)
+    #
+    # and
+    #
+    #     = surround '*' do
+    #       %strong angry
+    #
+    # Produces:
+    #
+    #     *<strong>angry</strong>*
+    #
+    # @param front [String] The string to add before the Haml
+    # @param back [String] The string to add after the Haml
+    # @yield A block of Haml to surround
+    def surround(front, back = front, &block)
+      output = capture_haml(&block)
+
+      "#{front}#{output.chomp}#{back}\n"
+    end
+
+    # Prepends a string to the beginning of a Haml block,
+    # with no whitespace between.
+    # For example:
+    #
+    #     = precede '*' do
+    #       %span.small Not really
+    #
+    # Produces:
+    #
+    #     *<span class='small'>Not really</span>
+    #
+    # @param str [String] The string to add before the Haml
+    # @yield A block of Haml to prepend to
+    def precede(str, &block)
+      "#{str}#{capture_haml(&block).chomp}\n"
+    end
+
+    # Appends a string to the end of a Haml block,
+    # with no whitespace between.
+    # For example:
+    #
+    #     click
+    #     = succeed '.' do
+    #       %a{:href=>"thing"} here
+    #
+    # Produces:
+    #
+    #     click
+    #     <a href='thing'>here</a>.
+    #
+    # @param str [String] The string to add after the Haml
+    # @yield A block of Haml to append to
+    def succeed(str, &block)
+      "#{capture_haml(&block).chomp}#{str}\n"
+    end
+
+    # Captures the result of a block of Haml code,
+    # gets rid of the excess indentation,
+    # and returns it as a string.
+    # For example, after the following,
+    #
+    #     .foo
+    #       - foo = capture_haml(13) do |a|
+    #         %p= a
+    #
+    # the local variable `foo` would be assigned to `"<p>13</p>\n"`.
+    #
+    # @param args [Array] Arguments to pass into the block
+    # @yield [args] A block of Haml code that will be converted to a string
+    # @yieldparam args [Array] `args`
+    def capture_haml(*args, &block)
+      buffer = eval('if defined? _hamlout then _hamlout else nil end', block.binding) || haml_buffer
+      with_haml_buffer(buffer) do
+        position = haml_buffer.buffer.length
+
+        haml_buffer.capture_position = position
+        value = block.call(*args)
+
+        captured = haml_buffer.buffer.slice!(position..-1)
+
+        if captured == '' and value != haml_buffer.buffer
+          captured = (value.is_a?(String) ? value : nil)
+        end
+
+        return nil if captured.nil?
+        return (haml_buffer.options[:ugly] ? captured : prettify(captured))
+      end
+    ensure
+      haml_buffer.capture_position = nil
+    end
+
+    # Outputs text directly to the Haml buffer, with the proper indentation.
+    #
+    # @param text [#to_s] The text to output
+    def haml_concat(text = "")
+      haml_internal_concat text
+      ErrorReturn.new("haml_concat")
+    end
+
+    # Internal method to write directly to the buffer with control of
+    # whether the first line should be indented, and if there should be a
+    # final newline.
+    #
+    # Lines added will have the proper indentation. This can be controlled
+    # for the first line.
+    #
+    # Used by #haml_concat and #haml_tag.
+    #
+    # @param text [#to_s] The text to output
+    # @param newline [Boolean] Whether to add a newline after the text
+    # @param indent [Boolean] Whether to add indentation to the first line
+    def haml_internal_concat(text = "", newline = true, indent = true)
+      if haml_buffer.options[:ugly] || haml_buffer.tabulation == 0
+        haml_buffer.buffer << "#{text}#{"\n" if newline}"
+      else
+        haml_buffer.buffer << %[#{haml_indent if indent}#{text.to_s.gsub("\n", "\n#{haml_indent}")}#{"\n" if newline}]
+      end
+    end
+    private :haml_internal_concat
+
+    # Allows writing raw content. `haml_internal_concat_raw` isn't
+    # effected by XSS mods. Used by #haml_tag to write the actual tags.
+    alias :haml_internal_concat_raw :haml_internal_concat
+
+    # @return [String] The indentation string for the current line
+    def haml_indent
+      '  ' * haml_buffer.tabulation
+    end
+
+    # Creates an HTML tag with the given name and optionally text and attributes.
+    # Can take a block that will run between the opening and closing tags.
+    # If the block is a Haml block or outputs text using \{#haml\_concat},
+    # the text will be properly indented.
+    #
+    # `name` can be a string using the standard Haml class/id shorthand
+    # (e.g. "span#foo.bar", "#foo").
+    # Just like standard Haml tags, these class and id values
+    # will be merged with manually-specified attributes.
+    #
+    # `flags` is a list of symbol flags
+    # like those that can be put at the end of a Haml tag
+    # (`:/`, `:<`, and `:>`).
+    # Currently, only `:/` and `:<` are supported.
+    #
+    # `haml_tag` outputs directly to the buffer;
+    # its return value should not be used.
+    # If you need to get the results as a string,
+    # use \{#capture\_haml\}.
+    #
+    # For example,
+    #
+    #     haml_tag :table do
+    #       haml_tag :tr do
+    #         haml_tag 'td.cell' do
+    #           haml_tag :strong, "strong!"
+    #           haml_concat "data"
+    #         end
+    #         haml_tag :td do
+    #           haml_concat "more_data"
+    #         end
+    #       end
+    #     end
+    #
+    # outputs
+    #
+    #     <table>
+    #       <tr>
+    #         <td class='cell'>
+    #           <strong>
+    #             strong!
+    #           </strong>
+    #           data
+    #         </td>
+    #         <td>
+    #           more_data
+    #         </td>
+    #       </tr>
+    #     </table>
+    #
+    # @param name [#to_s] The name of the tag
+    #
+    # @overload haml_tag(name, *rest, attributes = {})
+    #   @yield The block of Haml code within the tag
+    # @overload haml_tag(name, text, *flags, attributes = {})
+    #   @param text [#to_s] The text within the tag
+    #   @param flags [Array<Symbol>] Haml end-of-tag flags
+    def haml_tag(name, *rest, &block)
+      ret = ErrorReturn.new("haml_tag")
+
+      text = rest.shift.to_s unless [Symbol, Hash, NilClass].any? {|t| rest.first.is_a? t}
+      flags = []
+      flags << rest.shift while rest.first.is_a? Symbol
+      attrs = (rest.shift || {})
+      attrs.keys.each {|key| attrs[key.to_s] = attrs.delete(key)} unless attrs.empty?
+      name, attrs = merge_name_and_attributes(name.to_s, attrs)
+
+      attributes = ::Hamlit::HamlCompiler.build_attributes(haml_buffer.html?,
+        haml_buffer.options[:attr_wrapper],
+        haml_buffer.options[:escape_attrs],
+        haml_buffer.options[:hyphenate_data_attrs],
+        attrs)
+
+      if text.nil? && block.nil? && (haml_buffer.options[:autoclose].include?(name) || flags.include?(:/))
+        haml_internal_concat_raw "<#{name}#{attributes}#{' /' if haml_buffer.options[:format] == :xhtml}>"
+        return ret
+      end
+
+      if flags.include?(:/)
+        raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:self_closing_content)) if text
+        raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:illegal_nesting_self_closing)) if block
+      end
+
+      tag = "<#{name}#{attributes}>"
+      end_tag = "</#{name}>"
+      if block.nil?
+        text = text.to_s
+        if text.include?("\n")
+          haml_internal_concat_raw tag
+          tab_up
+          haml_internal_concat text
+          tab_down
+          haml_internal_concat_raw end_tag
+        else
+          haml_internal_concat_raw tag, false
+          haml_internal_concat text, false, false
+          haml_internal_concat_raw end_tag, true, false
+        end
+        return ret
+      end
+
+      if text
+        raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:illegal_nesting_line, name))
+      end
+
+      if flags.include?(:<)
+        haml_internal_concat_raw tag, false
+        haml_internal_concat "#{capture_haml(&block).strip}", false, false
+        haml_internal_concat_raw end_tag, true, false
+        return ret
+      end
+
+      haml_internal_concat_raw tag
+      tab_up
+      block.call
+      tab_down
+      haml_internal_concat_raw end_tag
+
+      ret
+    end
+
+    # Conditionally wrap a block in an element. If `condition` is `true` then
+    # this method renders the tag described by the arguments in `tag` (using
+    # \{#haml_tag}) with the given block inside, otherwise it just renders the block.
+    #
+    # For example,
+    #
+    #     - haml_tag_if important, '.important' do
+    #       %p
+    #         A (possibly) important paragraph.
+    #
+    # will produce
+    #
+    #     <div class='important'>
+    #       <p>
+    #         A (possibly) important paragraph.
+    #       </p>
+    #     </div>
+    #
+    # if `important` is truthy, and just
+    #
+    #     <p>
+    #       A (possibly) important paragraph.
+    #     </p>
+    #
+    # otherwise.
+    #
+    # Like \{#haml_tag}, `haml_tag_if` outputs directly to the buffer and its
+    # return value should not be used. Use \{#capture_haml} if you need to use
+    # its results as a string.
+    #
+    # @param condition The condition to test to determine whether to render
+    #   the enclosing tag
+    # @param tag Definition of the enclosing tag. See \{#haml_tag} for details
+    #   (specifically the form that takes a block)
+    def haml_tag_if(condition, *tag)
+      if condition
+        haml_tag(*tag){ yield }
+      else
+        yield
+      end
+      ErrorReturn.new("haml_tag_if")
+    end
+
+    # Characters that need to be escaped to HTML entities from user input
+    HTML_ESCAPE = { '&' => '&amp;', '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => '&#039;' }
+
+    HTML_ESCAPE_REGEX = /[\"><&]/
+
+    # Returns a copy of `text` with ampersands, angle brackets and quotes
+    # escaped into HTML entities.
+    #
+    # Note that if ActionView is loaded and XSS protection is enabled
+    # (as is the default for Rails 3.0+, and optional for version 2.3.5+),
+    # this won't escape text declared as "safe".
+    #
+    # @param text [String] The string to sanitize
+    # @return [String] The sanitized string
+    def html_escape(text)
+      text = text.to_s
+      text.gsub(HTML_ESCAPE_REGEX, HTML_ESCAPE)
+    end
+
+    HTML_ESCAPE_ONCE_REGEX = /[\"><]|&(?!(?:[a-zA-Z]+|#(?:\d+|[xX][0-9a-fA-F]+));)/
+
+    # Escapes HTML entities in `text`, but without escaping an ampersand
+    # that is already part of an escaped entity.
+    #
+    # @param text [String] The string to sanitize
+    # @return [String] The sanitized string
+    def escape_once(text)
+      text = text.to_s
+      text.gsub(HTML_ESCAPE_ONCE_REGEX, HTML_ESCAPE)
+    end
+
+    # Returns whether or not the current template is a Haml template.
+    #
+    # This function, unlike other {Haml::Helpers} functions,
+    # also works in other `ActionView` templates,
+    # where it will always return false.
+    #
+    # @return [Boolean] Whether or not the current template is a Haml template
+    def is_haml?
+      !@haml_buffer.nil? && @haml_buffer.active?
+    end
+
+    # Returns whether or not `block` is defined directly in a Haml template.
+    #
+    # @param block [Proc] A Ruby block
+    # @return [Boolean] Whether or not `block` is defined directly in a Haml template
+    def block_is_haml?(block)
+      eval('!!defined?(_hamlout)', block.binding)
+    end
+
+    private
+
+    # Parses the tag name used for \{#haml\_tag}
+    # and merges it with the Ruby attributes hash.
+    def merge_name_and_attributes(name, attributes_hash = {})
+      # skip merging if no ids or classes found in name
+      return name, attributes_hash unless name =~ /^(.+?)?([\.#].*)$/
+
+      return $1 || "div", ::Hamlit::HamlBuffer.merge_attrs(
+        ::Hamlit::HamlParser.parse_class_and_id($2), attributes_hash)
+    end
+
+    # Runs a block of code with the given buffer as the currently active buffer.
+    #
+    # @param buffer [Haml::Buffer] The Haml buffer to use temporarily
+    # @yield A block in which the given buffer should be used
+    def with_haml_buffer(buffer)
+      @haml_buffer, old_buffer = buffer, @haml_buffer
+      old_buffer.active, old_was_active = false, old_buffer.active? if old_buffer
+      @haml_buffer.active, was_active = true, @haml_buffer.active?
+      yield
+    ensure
+      @haml_buffer.active = was_active
+      old_buffer.active = old_was_active if old_buffer
+      @haml_buffer = old_buffer
+    end
+
+    # The current {Haml::Buffer} object.
+    #
+    # @return [Haml::Buffer]
+    def haml_buffer
+      @haml_buffer if defined? @haml_buffer
+    end
+
+    # Gives a proc the same local `_hamlout` and `_erbout` variables
+    # that the current template has.
+    #
+    # @param proc [#call] The proc to bind
+    # @return [Proc] A new proc with the new variables bound
+    def haml_bind_proc(&proc)
+      _hamlout = haml_buffer
+      #double assignment is to avoid warnings
+      _erbout = _erbout = _hamlout.buffer
+      proc { |*args| proc.call(*args) }
+    end
+
+    def prettify(text)
+      text = text.split(/^/)
+      text.delete('')
+
+      min_tabs = nil
+      text.each do |line|
+        tabs = line.index(/[^ ]/) || line.length
+        min_tabs ||= tabs
+        min_tabs = min_tabs > tabs ? tabs : min_tabs
+      end
+
+      text.each_with_object('') do |line, str|
+        str << line.slice(min_tabs, line.length)
+      end
+    end
+  end
+end
+
+# @private
+class Object
+  # Haml overrides various `ActionView` helpers,
+  # which call an \{#is\_haml?} method
+  # to determine whether or not the current context object
+  # is a proper Haml context.
+  # Because `ActionView` helpers may be included in non-`ActionView::Base` classes,
+  # it's a good idea to define \{#is\_haml?} for all objects.
+  def is_haml?
+    false
+  end
+end
diff --git a/lib/hamlit/parser/haml_options.rb b/lib/hamlit/parser/haml_options.rb
new file mode 100644 (file)
index 0000000..dbf447d
--- /dev/null
@@ -0,0 +1,286 @@
+require 'hamlit/parser/haml_parser'
+require 'hamlit/parser/haml_compiler'
+require 'hamlit/parser/haml_error'
+
+module Hamlit
+  # This class encapsulates all of the configuration options that Haml
+  # understands. Please see the {file:REFERENCE.md#options Haml Reference} to
+  # learn how to set the options.
+  class HamlOptions
+
+    @defaults = {
+      :attr_wrapper         => "'",
+      :autoclose            => %w(area base basefont br col command embed frame
+                                  hr img input isindex keygen link menuitem meta
+                                  param source track wbr),
+      :encoding             => "UTF-8",
+      :escape_attrs         => true,
+      :escape_html          => false,
+      :filename             => '(haml)',
+      :format               => :html5,
+      :hyphenate_data_attrs => true,
+      :line                 => 1,
+      :mime_type            => 'text/html',
+      :preserve             => %w(textarea pre code),
+      :remove_whitespace    => false,
+      :suppress_eval        => false,
+      :ugly                 => false,
+      :cdata                => false,
+      :parser_class         => ::Hamlit::HamlParser,
+      :compiler_class       => ::Hamlit::HamlCompiler,
+      :trace                => false
+    }
+
+    @valid_formats = [:html4, :html5, :xhtml]
+
+    @buffer_option_keys = [:autoclose, :preserve, :attr_wrapper, :ugly, :format,
+      :encoding, :escape_html, :escape_attrs, :hyphenate_data_attrs, :cdata]
+
+    # The default option values.
+    # @return Hash
+    def self.defaults
+      @defaults
+    end
+
+    # An array of valid values for the `:format` option.
+    # @return Array
+    def self.valid_formats
+      @valid_formats
+    end
+
+    # An array of keys that will be used to provide a hash of options to
+    # {Haml::Buffer}.
+    # @return Hash
+    def self.buffer_option_keys
+      @buffer_option_keys
+    end
+
+    # The character that should wrap element attributes. This defaults to `'`
+    # (an apostrophe). Characters of this type within the attributes will be
+    # escaped (e.g. by replacing them with `&apos;`) if the character is an
+    # apostrophe or a quotation mark.
+    attr_reader :attr_wrapper
+
+    # A list of tag names that should be automatically self-closed if they have
+    # no content. This can also contain regular expressions that match tag names
+    # (or any object which responds to `#===`). Defaults to `['meta', 'img',
+    # 'link', 'br', 'hr', 'input', 'area', 'param', 'col', 'base']`.
+    attr_accessor :autoclose
+
+    # The encoding to use for the HTML output.
+    # This can be a string or an `Encoding` Object. Note that Haml **does not**
+    # automatically re-encode Ruby values; any strings coming from outside the
+    # application should be converted before being passed into the Haml
+    # template. Defaults to `Encoding.default_internal`; if that's not set,
+    # defaults to the encoding of the Haml template; if that's `US-ASCII`,
+    # defaults to `"UTF-8"`.
+    attr_reader :encoding
+
+    # Sets whether or not to escape HTML-sensitive characters in attributes. If
+    # this is true, all HTML-sensitive characters in attributes are escaped. If
+    # it's set to false, no HTML-sensitive characters in attributes are escaped.
+    # If it's set to `:once`, existing HTML escape sequences are preserved, but
+    # other HTML-sensitive characters are escaped.
+    #
+    # Defaults to `true`.
+    attr_accessor :escape_attrs
+
+    # Sets whether or not to escape HTML-sensitive characters in script. If this
+    # is true, `=` behaves like {file:REFERENCE.md#escaping_html `&=`};
+    # otherwise, it behaves like {file:REFERENCE.md#unescaping_html `!=`}. Note
+    # that if this is set, `!=` should be used for yielding to subtemplates and
+    # rendering partials. See also {file:REFERENCE.md#escaping_html Escaping HTML} and
+    # {file:REFERENCE.md#unescaping_html Unescaping HTML}.
+    #
+    # Defaults to false.
+    attr_accessor :escape_html
+
+    # The name of the Haml file being parsed.
+    # This is only used as information when exceptions are raised. This is
+    # automatically assigned when working through ActionView, so it's really
+    # only useful for the user to assign when dealing with Haml programatically.
+    attr_accessor :filename
+
+    # If set to `true`, Haml will convert underscores to hyphens in all
+    # {file:REFERENCE.md#html5_custom_data_attributes Custom Data Attributes} As
+    # of Haml 4.0, this defaults to `true`.
+    attr_accessor :hyphenate_data_attrs
+
+    # The line offset of the Haml template being parsed. This is useful for
+    # inline templates, similar to the last argument to `Kernel#eval`.
+    attr_accessor :line
+
+    # Determines the output format. The default is `:html5`. The other options
+    # are `:html4` and `:xhtml`. If the output is set to XHTML, then Haml
+    # automatically generates self-closing tags and wraps the output of the
+    # Javascript and CSS-like filters inside CDATA. When the output is set to
+    # `:html5` or `:html4`, XML prologs are ignored. In all cases, an appropriate
+    # doctype is generated from `!!!`.
+    #
+    # If the mime_type of the template being rendered is `text/xml` then a
+    # format of `:xhtml` will be used even if the global output format is set to
+    # `:html4` or `:html5`.
+    attr :format
+
+    # The mime type that the rendered document will be served with. If this is
+    # set to `text/xml` then the format will be overridden to `:xhtml` even if
+    # it has set to `:html4` or `:html5`.
+    attr_accessor :mime_type
+
+    # A list of tag names that should automatically have their newlines
+    # preserved using the {Haml::Helpers#preserve} helper. This means that any
+    # content given on the same line as the tag will be preserved. For example,
+    # `%textarea= "Foo\nBar"` compiles to `<textarea>Foo&#x000A;Bar</textarea>`.
+    # Defaults to `['textarea', 'pre']`. See also
+    # {file:REFERENCE.md#whitespace_preservation Whitespace Preservation}.
+    attr_accessor :preserve
+
+    # If set to `true`, all tags are treated as if both
+    # {file:REFERENCE.md#whitespace_removal__and_ whitespace removal} options
+    # were present. Use with caution as this may cause whitespace-related
+    # formatting errors.
+    #
+    # Defaults to `false`.
+    attr_reader :remove_whitespace
+
+    # Whether or not attribute hashes and Ruby scripts designated by `=` or `~`
+    # should be evaluated. If this is `true`, said scripts are rendered as empty
+    # strings.
+    #
+    # Defaults to `false`.
+    attr_accessor :suppress_eval
+
+    # If set to `true`, Haml makes no attempt to properly indent or format the
+    # HTML output. This significantly improves rendering performance but makes
+    # viewing the source unpleasant.
+    #
+    # Defaults to `true` in Rails production  mode, and `false` everywhere else.
+    attr_accessor :ugly
+
+    # Whether to include CDATA sections around javascript and css blocks when
+    # using the `:javascript` or `:css` filters.
+    #
+    # This option also affects the `:sass`, `:scss`, `:less` and `:coffeescript`
+    # filters.
+    #
+    # Defaults to `false` for html, `true` for xhtml. Cannot be changed when using
+    # xhtml.
+    attr_accessor :cdata
+
+    # The parser class to use. Defaults to Haml::Parser.
+    attr_accessor :parser_class
+
+    # The compiler class to use. Defaults to Haml::Compiler.
+    attr_accessor :compiler_class
+
+    # Enable template tracing. If true, it will add a 'data-trace' attribute to
+    # each tag generated by Haml. The value of the attribute will be the
+    # source template name and the line number from which the tag was generated,
+    # separated by a colon. On Rails applications, the path given will be a
+    # relative path as from the views directory. On non-Rails applications,
+    # the path will be the full path.
+    attr_accessor :trace
+
+    def initialize(values = {}, &block)
+      defaults.each {|k, v| instance_variable_set :"@#{k}", v}
+      values.each {|k, v| send("#{k}=", v) if defaults.has_key?(k) && !v.nil?}
+      yield if block_given?
+    end
+
+    # Retrieve an option value.
+    # @param key The value to retrieve.
+    def [](key)
+      send key
+    end
+
+    # Set an option value.
+    # @param key The key to set.
+    # @param value The value to set for the key.
+    def []=(key, value)
+      send "#{key}=", value
+    end
+
+    [:escape_attrs, :hyphenate_data_attrs, :remove_whitespace, :suppress_eval,
+      :ugly].each do |method|
+      class_eval(<<-END)
+        def #{method}?
+          !! @#{method}
+        end
+      END
+    end
+
+    # @return [Boolean] Whether or not the format is XHTML.
+    def xhtml?
+      not html?
+    end
+
+    # @return [Boolean] Whether or not the format is any flavor of HTML.
+    def html?
+      html4? or html5?
+    end
+
+    # @return [Boolean] Whether or not the format is HTML4.
+    def html4?
+      format == :html4
+    end
+
+    # @return [Boolean] Whether or not the format is HTML5.
+    def html5?
+      format == :html5
+    end
+
+    def attr_wrapper=(value)
+      @attr_wrapper = value || self.class.defaults[:attr_wrapper]
+    end
+
+    # Undef :format to suppress warning. It's defined above with the `:attr`
+    # macro in order to make it appear in Yard's list of instance attributes.
+    undef :format
+    def format
+      mime_type == "text/xml" ? :xhtml : @format
+    end
+
+    def format=(value)
+      unless self.class.valid_formats.include?(value)
+        raise ::Hamlit::HamlError, "Invalid output format #{value.inspect}"
+      end
+      @format = value
+    end
+
+    undef :cdata
+    def cdata
+      xhtml? || @cdata
+    end
+
+    def remove_whitespace=(value)
+      @ugly = true if value
+      @remove_whitespace = value
+    end
+
+    def encoding=(value)
+      return unless value
+      @encoding = value.is_a?(Encoding) ? value.name : value.to_s
+      @encoding = "UTF-8" if @encoding.upcase == "US-ASCII"
+    end
+
+    # Returns a subset of options: those that {Haml::Buffer} cares about.
+    # All of the values here are such that when `#inspect` is called on the hash,
+    # it can be `Kernel#eval`ed to get the same result back.
+    #
+    # See {file:REFERENCE.md#options the Haml options documentation}.
+    #
+    # @return [{Symbol => Object}] The options hash
+    def for_buffer
+      self.class.buffer_option_keys.inject({}) do |hash, key|
+        hash[key] = send(key)
+        hash
+      end
+    end
+
+    private
+
+    def defaults
+      self.class.defaults
+    end
+  end
+end
diff --git a/lib/hamlit/parser/haml_parser.rb b/lib/hamlit/parser/haml_parser.rb
new file mode 100644 (file)
index 0000000..fdc1744
--- /dev/null
@@ -0,0 +1,801 @@
+require 'strscan'
+require 'hamlit/parser/haml_util'
+require 'hamlit/parser/haml_buffer'
+require 'hamlit/parser/haml_error'
+
+module Hamlit
+  class HamlParser
+    include ::Hamlit::HamlUtil
+
+    attr_reader :root
+
+    # Designates an XHTML/XML element.
+    ELEMENT         = ?%
+
+    # Designates a `<div>` element with the given class.
+    DIV_CLASS       = ?.
+
+    # Designates a `<div>` element with the given id.
+    DIV_ID          = ?#
+
+    # Designates an XHTML/XML comment.
+    COMMENT         = ?/
+
+    # Designates an XHTML doctype or script that is never HTML-escaped.
+    DOCTYPE         = ?!
+
+    # Designates script, the result of which is output.
+    SCRIPT          = ?=
+
+    # Designates script that is always HTML-escaped.
+    SANITIZE        = ?&
+
+    # Designates script, the result of which is flattened and output.
+    FLAT_SCRIPT     = ?~
+
+    # Designates script which is run but not output.
+    SILENT_SCRIPT   = ?-
+
+    # When following SILENT_SCRIPT, designates a comment that is not output.
+    SILENT_COMMENT  = ?#
+
+    # Designates a non-parsed line.
+    ESCAPE          = ?\\
+
+    # Designates a block of filtered text.
+    FILTER          = ?:
+
+    # Designates a non-parsed line. Not actually a character.
+    PLAIN_TEXT      = -1
+
+    # Keeps track of the ASCII values of the characters that begin a
+    # specially-interpreted line.
+    SPECIAL_CHARACTERS   = [
+      ELEMENT,
+      DIV_CLASS,
+      DIV_ID,
+      COMMENT,
+      DOCTYPE,
+      SCRIPT,
+      SANITIZE,
+      FLAT_SCRIPT,
+      SILENT_SCRIPT,
+      ESCAPE,
+      FILTER
+    ]
+
+    # The value of the character that designates that a line is part
+    # of a multiline string.
+    MULTILINE_CHAR_VALUE = ?|
+
+    # Regex to check for blocks with spaces around arguments. Not to be confused
+    # with multiline script.
+    # For example:
+    #     foo.each do | bar |
+    #       = bar
+    #
+    BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
+
+    MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
+    START_BLOCK_KEYWORDS = %w[if begin case unless]
+    # Try to parse assignments to block starters as best as possible
+    START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
+    BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
+
+    # The Regex that matches a Doctype command.
+    DOCTYPE_REGEX = /(\d(?:\.\d)?)?\s*([a-z]*)\s*([^ ]+)?/i
+
+    # The Regex that matches a literal string or symbol value
+    LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?!\\|\#\{|\#@|\#\$|\2).|\\.)*\2/
+
+    ID_KEY    = 'id'.freeze
+    CLASS_KEY = 'class'.freeze
+
+    def initialize(template, options)
+      @options            = options
+      # Record the indent levels of "if" statements to validate the subsequent
+      # elsif and else statements are indented at the appropriate level.
+      @script_level_stack = []
+      @template_index     = 0
+      @template_tabs      = 0
+
+      match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
+      # discard the last match which is always blank
+      match.pop
+      @template = match.each_with_index.map do |(full, whitespace, text), index|
+        Line.new(whitespace, text.rstrip, full, index, self, false)
+      end
+      # Append special end-of-document marker
+      @template << Line.new(nil, '-#', '-#', @template.size, self, true)
+    end
+
+    def parse
+      @root = @parent = ParseNode.new(:root)
+      @flat = false
+      @filter_buffer = nil
+      @indentation = nil
+      @line = next_line
+
+      raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:indenting_at_start), @line.index) if @line.tabs != 0
+
+      loop do
+        next_line
+
+        process_indent(@line) unless @line.text.empty?
+
+        if flat?
+          text = @line.full.dup
+          text = "" unless text.gsub!(/^#{@flat_spaces}/, '')
+          @filter_buffer << "#{text}\n"
+          @line = @next_line
+          next
+        end
+
+        @tab_up = nil
+        process_line(@line) unless @line.text.empty?
+        if block_opened? || @tab_up
+          @template_tabs += 1
+          @parent = @parent.children.last
+        end
+
+        if !flat? && @next_line.tabs - @line.tabs > 1
+          raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
+        end
+
+        @line = @next_line
+      end
+      # Close all the open tags
+      close until @parent.type == :root
+      @root
+    rescue ::Hamlit::HamlError => e
+      e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
+      raise
+    end
+
+    def compute_tabs(line)
+      return 0 if line.text.empty? || !line.whitespace
+
+      if @indentation.nil?
+        @indentation = line.whitespace
+
+        if @indentation.include?(?\s) && @indentation.include?(?\t)
+          raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:cant_use_tabs_and_spaces), line.index)
+        end
+
+        @flat_spaces = @indentation * (@template_tabs+1) if flat?
+        return 1
+      end
+
+      tabs = line.whitespace.length / @indentation.length
+      return tabs if line.whitespace == @indentation * tabs
+      return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
+
+      message = ::Hamlit::HamlError.message(:inconsistent_indentation,
+        human_indentation(line.whitespace),
+        human_indentation(@indentation)
+      )
+      raise ::Hamlit::HamlSyntaxError.new(message, line.index)
+    end
+
+    private
+
+    # @private
+    class Line < Struct.new(:whitespace, :text, :full, :index, :parser, :eod)
+      alias_method :eod?, :eod
+
+      # @private
+      def tabs
+        @tabs ||= parser.compute_tabs(self)
+      end
+
+      def strip!(from)
+        self.text = text[from..-1]
+        self.text.lstrip!
+        self
+      end
+    end
+
+    # @private
+    class ParseNode < Struct.new(:type, :line, :value, :parent, :children)
+      def initialize(*args)
+        super
+        self.children ||= []
+      end
+
+      def inspect
+        %Q[(#{type} #{value.inspect}#{children.each_with_object('') {|c, s| s << "\n#{c.inspect.gsub!(/^/, '  ')}"}})]
+      end
+    end
+
+    # Processes and deals with lowering indentation.
+    def process_indent(line)
+      return unless line.tabs <= @template_tabs && @template_tabs > 0
+
+      to_close = @template_tabs - line.tabs
+      to_close.times {|i| close unless to_close - 1 - i == 0 && continuation_script?(line.text)}
+    end
+
+    def continuation_script?(text)
+      text[0] == SILENT_SCRIPT && mid_block_keyword?(text)
+    end
+
+    def mid_block_keyword?(text)
+      MID_BLOCK_KEYWORDS.include?(block_keyword(text))
+    end
+
+    # Processes a single line of Haml.
+    #
+    # This method doesn't return anything; it simply processes the line and
+    # adds the appropriate code to `@precompiled`.
+    def process_line(line)
+      case line.text[0]
+      when DIV_CLASS; push div(line)
+      when DIV_ID
+        return push plain(line) if %w[{ @ $].include?(line.text[1])
+        push div(line)
+      when ELEMENT; push tag(line)
+      when COMMENT; push comment(line.text[1..-1].lstrip)
+      when SANITIZE
+        return push plain(line.strip!(3), :escape_html) if line.text[1, 2] == '=='
+        return push script(line.strip!(2), :escape_html) if line.text[1] == SCRIPT
+        return push flat_script(line.strip!(2), :escape_html) if line.text[1] == FLAT_SCRIPT
+        return push plain(line.strip!(1), :escape_html) if line.text[1] == ?\s || line.text[1..2] == '#{'
+        push plain(line)
+      when SCRIPT
+        return push plain(line.strip!(2)) if line.text[1] == SCRIPT
+        line.text = line.text[1..-1]
+        push script(line)
+      when FLAT_SCRIPT; push flat_script(line.strip!(1))
+      when SILENT_SCRIPT
+        return push haml_comment(line.text[2..-1]) if line.text[1] == SILENT_COMMENT
+        push silent_script(line)
+      when FILTER; push filter(line.text[1..-1].downcase)
+      when DOCTYPE
+        return push doctype(line.text) if line.text[0, 3] == '!!!'
+        return push plain(line.strip!(3), false) if line.text[1, 2] == '=='
+        return push script(line.strip!(2), false) if line.text[1] == SCRIPT
+        return push flat_script(line.strip!(2), false) if line.text[1] == FLAT_SCRIPT
+        return push plain(line.strip!(1), false) if line.text[1] == ?\s || line.text[1..2] == '#{'
+        push plain(line)
+      when ESCAPE
+        line.text = line.text[1..-1]
+        push plain(line)
+      else; push plain(line)
+      end
+    end
+
+    def block_keyword(text)
+      return unless keyword = text.scan(BLOCK_KEYWORD_REGEX)[0]
+      keyword[0] || keyword[1]
+    end
+
+    def push(node)
+      @parent.children << node
+      node.parent = @parent
+    end
+
+    def plain(line, escape_html = nil)
+      if block_opened?
+        raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_plain), @next_line.index)
+      end
+
+      unless contains_interpolation?(line.text)
+        return ParseNode.new(:plain, line.index + 1, :text => line.text)
+      end
+
+      escape_html = @options.escape_html if escape_html.nil?
+      line.text = ::Hamlit::HamlUtil.unescape_interpolation(line.text)
+      script(line, false).tap { |n| n.value[:escape_interpolation] = true if escape_html }
+    end
+
+    def script(line, escape_html = nil, preserve = false)
+      raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, '=')) if line.text.empty?
+      line = handle_ruby_multiline(line)
+      escape_html = @options.escape_html if escape_html.nil?
+
+      keyword = block_keyword(line.text)
+      check_push_script_stack(keyword)
+
+      ParseNode.new(:script, line.index + 1, :text => line.text, :escape_html => escape_html,
+        :preserve => preserve, :keyword => keyword)
+    end
+
+    def flat_script(line, escape_html = nil)
+      raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, '~')) if line.text.empty?
+      script(line, escape_html, :preserve)
+    end
+
+    def silent_script(line)
+      raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
+
+      line = handle_ruby_multiline(line)
+      keyword = block_keyword(line.text)
+
+      check_push_script_stack(keyword)
+
+      if ["else", "elsif", "when"].include?(keyword)
+        if @script_level_stack.empty?
+          raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:missing_if, keyword), @line.index)
+        end
+
+        if keyword == 'when' and !@script_level_stack.last[2]
+          if @script_level_stack.last[1] + 1 == @line.tabs
+            @script_level_stack.last[1] += 1
+          end
+          @script_level_stack.last[2] = true
+        end
+
+        if @script_level_stack.last[1] != @line.tabs
+          message = ::Hamlit::HamlError.message(:bad_script_indent, keyword, @script_level_stack.last[1], @line.tabs)
+          raise ::Hamlit::HamlSyntaxError.new(message, @line.index)
+        end
+      end
+
+      ParseNode.new(:silent_script, @line.index + 1,
+        :text => line.text[1..-1], :keyword => keyword)
+    end
+
+    def check_push_script_stack(keyword)
+      if ["if", "case", "unless"].include?(keyword)
+        # @script_level_stack contents are arrays of form
+        # [:keyword, stack_level, other_info]
+        @script_level_stack.push([keyword.to_sym, @line.tabs])
+        @script_level_stack.last << false if keyword == 'case'
+        @tab_up = true
+      end
+    end
+
+    def haml_comment(text)
+      if filter_opened?
+        @flat = true
+        @filter_buffer = String.new
+        @filter_buffer << "#{text}\n" unless text.empty?
+        text = @filter_buffer
+        # If we don't know the indentation by now, it'll be set in Line#tabs
+        @flat_spaces = @indentation * (@template_tabs+1) if @indentation
+      end
+
+      ParseNode.new(:haml_comment, @line.index + 1, :text => text)
+    end
+
+    def tag(line)
+      tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
+        nuke_inner_whitespace, action, value, last_line = parse_tag(line.text)
+
+      preserve_tag = @options.preserve.include?(tag_name)
+      nuke_inner_whitespace ||= preserve_tag
+      preserve_tag = false if @options.ugly
+      escape_html = (action == '&' || (action != '!' && @options.escape_html))
+
+      case action
+      when '/'; self_closing = true
+      when '~'; parse = preserve_script = true
+      when '='
+        parse = true
+        if value[0] == ?=
+          value = ::Hamlit::HamlUtil.unescape_interpolation(value[1..-1].strip)
+          escape_interpolation = true if escape_html
+          escape_html = false
+        end
+      when '&', '!'
+        if value[0] == ?= || value[0] == ?~
+          parse = true
+          preserve_script = (value[0] == ?~)
+          if value[1] == ?=
+            value = ::Hamlit::HamlUtil.unescape_interpolation(value[2..-1].strip)
+            escape_interpolation = true if escape_html
+            escape_html = false
+          else
+            value = value[1..-1].strip
+          end
+        elsif contains_interpolation?(value)
+          value = ::Hamlit::HamlUtil.unescape_interpolation(value)
+          escape_interpolation = true if escape_html
+          parse = true
+          escape_html = false
+        end
+      else
+        if contains_interpolation?(value)
+          value = ::Hamlit::HamlUtil.unescape_interpolation(value)
+          escape_interpolation = true if escape_html
+          parse = true
+          escape_html = false
+        end
+      end
+
+      attributes = ::Hamlit::HamlParser.parse_class_and_id(attributes)
+      attributes_list = []
+
+      if attributes_hashes[:new]
+        static_attributes, attributes_hash = attributes_hashes[:new]
+        ::Hamlit::HamlBuffer.merge_attrs(attributes, static_attributes) if static_attributes
+        attributes_list << attributes_hash
+      end
+
+      if attributes_hashes[:old]
+        static_attributes = parse_static_hash(attributes_hashes[:old])
+        ::Hamlit::HamlBuffer.merge_attrs(attributes, static_attributes) if static_attributes
+        attributes_list << attributes_hashes[:old] unless static_attributes || @options.suppress_eval
+      end
+
+      attributes_list.compact!
+
+      raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
+      raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
+      raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
+
+      if block_opened? && !value.empty? && !is_ruby_multiline?(value)
+        raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_line, tag_name), @next_line.index)
+      end
+
+      self_closing ||= !!(!block_opened? && value.empty? && @options.autoclose.any? {|t| t === tag_name})
+      value = nil if value.empty? && (block_opened? || self_closing)
+      line.text = value
+      line = handle_ruby_multiline(line) if parse
+
+      ParseNode.new(:tag, line.index + 1, :name => tag_name, :attributes => attributes,
+        :attributes_hashes => attributes_list, :self_closing => self_closing,
+        :nuke_inner_whitespace => nuke_inner_whitespace,
+        :nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
+        :escape_html => escape_html, :preserve_tag => preserve_tag,
+        :preserve_script => preserve_script, :parse => parse, :value => line.text,
+        :escape_interpolation => escape_interpolation)
+    end
+
+    # Renders a line that creates an XHTML tag and has an implicit div because of
+    # `.` or `#`.
+    def div(line)
+      line.text = "%div#{line.text}"
+      tag(line)
+    end
+
+    # Renders an XHTML comment.
+    def comment(text)
+      if text[0..1] == '!['
+        revealed = true
+        text = text[1..-1]
+      else
+        revealed = false
+      end
+
+      conditional, text = balance(text, ?[, ?]) if text[0] == ?[
+      text.strip!
+
+      if contains_interpolation?(text)
+        parse = true
+        text = slow_unescape_interpolation(text)
+      else
+        parse = false
+      end
+
+      if block_opened? && !text.empty?
+        raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_content), @next_line.index)
+      end
+
+      ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
+    end
+
+    # Renders an XHTML doctype or XML shebang.
+    def doctype(text)
+      raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_nesting_header), @next_line.index) if block_opened?
+      version, type, encoding = text[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
+      ParseNode.new(:doctype, @line.index + 1, :version => version, :type => type, :encoding => encoding)
+    end
+
+    def filter(name)
+      raise ::Hamlit::HamlError.new(::Hamlit::HamlError.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
+
+      if filter_opened?
+        @flat = true
+        @filter_buffer = String.new
+        # If we don't know the indentation by now, it'll be set in Line#tabs
+        @flat_spaces = @indentation * (@template_tabs+1) if @indentation
+      end
+
+      ParseNode.new(:filter, @line.index + 1, :name => name, :text => @filter_buffer)
+    end
+
+    def close
+      node, @parent = @parent, @parent.parent
+      @template_tabs -= 1
+      send("close_#{node.type}", node) if respond_to?("close_#{node.type}", :include_private)
+    end
+
+    def close_filter(_)
+      close_flat_section
+    end
+
+    def close_haml_comment(_)
+      close_flat_section
+    end
+
+    def close_flat_section
+      @flat = false
+      @flat_spaces = nil
+      @filter_buffer = nil
+    end
+
+    def close_silent_script(node)
+      @script_level_stack.pop if ["if", "case", "unless"].include? node.value[:keyword]
+
+      # Post-process case statements to normalize the nesting of "when" clauses
+      return unless node.value[:keyword] == "case"
+      return unless first = node.children.first
+      return unless first.type == :silent_script && first.value[:keyword] == "when"
+      return if first.children.empty?
+      # If the case node has a "when" child with children, it's the
+      # only child. Then we want to put everything nested beneath it
+      # beneath the case itself (just like "if").
+      node.children = [first, *first.children]
+      first.children = []
+    end
+
+    alias :close_script :close_silent_script
+
+    # This is a class method so it can be accessed from {Haml::Helpers}.
+    #
+    # Iterates through the classes and ids supplied through `.`
+    # and `#` syntax, and returns a hash with them as attributes,
+    # that can then be merged with another attributes hash.
+    def self.parse_class_and_id(list)
+      attributes = {}
+      return attributes if list.empty?
+
+      list.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, property|
+        case type
+        when '.'
+          if attributes[CLASS_KEY]
+            attributes[CLASS_KEY] += " "
+          else
+            attributes[CLASS_KEY] = ""
+          end
+          attributes[CLASS_KEY] += property
+        when '#'; attributes[ID_KEY] = property
+        end
+      end
+      attributes
+    end
+
+    def parse_static_hash(text)
+      attributes = {}
+      return attributes if text.empty?
+
+      scanner = StringScanner.new(text)
+      scanner.scan(/\s+/)
+      until scanner.eos?
+        return unless key = scanner.scan(LITERAL_VALUE_REGEX)
+        return unless scanner.scan(/\s*=>\s*/)
+        return unless value = scanner.scan(LITERAL_VALUE_REGEX)
+        return unless scanner.scan(/\s*(?:,|$)\s*/)
+        attributes[eval(key).to_s] = eval(value).to_s
+      end
+      attributes
+    end
+
+    # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
+    def parse_tag(text)
+      match = text.scan(/%([-:\w]+)([-:\w.#]*)(.+)?/)[0]
+      raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:invalid_tag, text)) unless match
+
+      tag_name, attributes, rest = match
+
+      if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
+        raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:illegal_element))
+      end
+
+      new_attributes_hash = old_attributes_hash = last_line = nil
+      object_ref = :nil
+      attributes_hashes = {}
+      while rest && !rest.empty?
+        case rest[0]
+        when ?{
+          break if old_attributes_hash
+          old_attributes_hash, rest, last_line = parse_old_attributes(rest)
+          attributes_hashes[:old] = old_attributes_hash
+        when ?(
+          break if new_attributes_hash
+          new_attributes_hash, rest, last_line = parse_new_attributes(rest)
+          attributes_hashes[:new] = new_attributes_hash
+        when ?[
+          break unless object_ref == :nil
+          object_ref, rest = balance(rest, ?[, ?])
+        else; break
+        end
+      end
+
+      if rest && !rest.empty?
+        nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
+        if nuke_whitespace
+          nuke_outer_whitespace = nuke_whitespace.include? '>'
+          nuke_inner_whitespace = nuke_whitespace.include? '<'
+        end
+      end
+
+      if @options.remove_whitespace
+        nuke_outer_whitespace = true
+        nuke_inner_whitespace = true
+      end
+
+      if value.nil?
+        value = ''
+      else
+        value.strip!
+      end
+      [tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
+       nuke_inner_whitespace, action, value, last_line || @line.index + 1]
+    end
+
+    def parse_old_attributes(text)
+      text = text.dup
+      last_line = @line.index + 1
+
+      begin
+        attributes_hash, rest = balance(text, ?{, ?})
+      rescue ::Hamlit::HamlSyntaxError => e
+        if text.strip[-1] == ?, && e.message == ::Hamlit::HamlError.message(:unbalanced_brackets)
+          text << "\n#{@next_line.text}"
+          last_line += 1
+          next_line
+          retry
+        end
+
+        raise e
+      end
+
+      attributes_hash = attributes_hash[1...-1] if attributes_hash
+      return attributes_hash, rest, last_line
+    end
+
+    def parse_new_attributes(text)
+      scanner = StringScanner.new(text)
+      last_line = @line.index + 1
+      attributes = {}
+
+      scanner.scan(/\(\s*/)
+      loop do
+        name, value = parse_new_attribute(scanner)
+        break if name.nil?
+
+        if name == false
+          scanned = ::Hamlit::HamlUtil.balance(text, ?(, ?))
+          text = scanned ? scanned.first : text
+          raise ::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:invalid_attribute_list, text.inspect), last_line - 1)
+        end
+        attributes[name] = value
+        scanner.scan(/\s*/)
+
+        if scanner.eos?
+          text << " #{@next_line.text}"
+          last_line += 1
+          next_line
+          scanner.scan(/\s*/)
+        end
+      end
+
+      static_attributes = {}
+      dynamic_attributes = "{"
+      attributes.each do |name, (type, val)|
+        if type == :static
+          static_attributes[name] = val
+        else
+          dynamic_attributes << "#{inspect_obj(name)} => #{val},"
+        end
+      end
+      dynamic_attributes << "}"
+      dynamic_attributes = nil if dynamic_attributes == "{}"
+
+      return [static_attributes, dynamic_attributes], scanner.rest, last_line
+    end
+
+    def parse_new_attribute(scanner)
+      unless name = scanner.scan(/[-:\w]+/)
+        return if scanner.scan(/\)/)
+        return false
+      end
+
+      scanner.scan(/\s*/)
+      return name, [:static, true] unless scanner.scan(/=/) #/end
+
+      scanner.scan(/\s*/)
+      unless quote = scanner.scan(/["']/)
+        return false unless var = scanner.scan(/(@@?|\$)?\w+/)
+        return name, [:dynamic, var]
+      end
+
+      re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
+      content = []
+      loop do
+        return false unless scanner.scan(re)
+        content << [:str, scanner[1].gsub(/\\(.)/, '\1')]
+        break if scanner[2] == quote
+        content << [:ruby, balance(scanner, ?{, ?}, 1).first[0...-1]]
+      end
+
+      return name, [:static, content.first[1]] if content.size == 1
+      return name, [:dynamic,
+        %!"#{content.each_with_object('') {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
+    end
+
+    def next_line
+      line = @template.shift || raise(StopIteration)
+
+      # `flat?' here is a little outdated,
+      # so we have to manually check if either the previous or current line
+      # closes the flat block, as well as whether a new block is opened.
+      line_defined = instance_variable_defined?(:@line)
+      @line.tabs if line_defined
+      unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
+          (line_defined && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
+        return next_line if line.text.empty?
+
+        handle_multiline(line)
+      end
+
+      @next_line = line
+    end
+
+    def closes_flat?(line)
+      line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
+    end
+
+    def handle_multiline(line)
+      return unless is_multiline?(line.text)
+      line.text.slice!(-1)
+      loop do
+        new_line = @template.first
+        break if new_line.eod?
+        next @template.shift if new_line.text.strip.empty?
+        break unless is_multiline?(new_line.text.strip)
+        line.text << new_line.text.strip[0...-1]
+        @template.shift
+      end
+    end
+
+    # Checks whether or not `line` is in a multiline sequence.
+    def is_multiline?(text)
+      text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && text !~ BLOCK_WITH_SPACES
+    end
+
+    def handle_ruby_multiline(line)
+      line.text.rstrip!
+      return line unless is_ruby_multiline?(line.text)
+      begin
+        # Use already fetched @next_line in the first loop. Otherwise, fetch next
+        new_line = new_line.nil? ? @next_line : @template.shift
+        break if new_line.eod?
+        next if new_line.text.empty?
+        line.text << " #{new_line.text.rstrip}"
+      end while is_ruby_multiline?(new_line.text)
+      next_line
+      line
+    end
+
+    # `text' is a Ruby multiline block if it:
+    # - ends with a comma
+    # - but not "?," which is a character literal
+    #   (however, "x?," is a method call and not a literal)
+    # - and not "?\," which is a character literal
+    def is_ruby_multiline?(text)
+      text && text.length > 1 && text[-1] == ?, &&
+        !((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
+    end
+
+    def balance(*args)
+      ::Hamlit::HamlUtil.balance(*args) or raise(::Hamlit::HamlSyntaxError.new(::Hamlit::HamlError.message(:unbalanced_brackets)))
+    end
+
+    def block_opened?
+      @next_line.tabs > @line.tabs
+    end
+
+    # Same semantics as block_opened?, except that block_opened? uses Line#tabs,
+    # which doesn't interact well with filter lines
+    def filter_opened?
+      @next_line.full =~ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/)
+    end
+
+    def flat?
+      @flat
+    end
+  end
+end
diff --git a/lib/hamlit/parser/haml_util.rb b/lib/hamlit/parser/haml_util.rb
new file mode 100644 (file)
index 0000000..447ccc1
--- /dev/null
@@ -0,0 +1,288 @@
+# encoding: utf-8
+
+begin
+  require 'erubis/tiny'
+rescue LoadError
+  require 'erb'
+end
+require 'set'
+require 'stringio'
+require 'strscan'
+
+module Hamlit
+  # A module containing various useful functions.
+  module HamlUtil
+    extend self
+
+    # Silence all output to STDERR within a block.
+    #
+    # @yield A block in which no output will be printed to STDERR
+    def silence_warnings
+      the_real_stderr, $stderr = $stderr, StringIO.new
+      yield
+    ensure
+      $stderr = the_real_stderr
+    end
+
+    ## Rails XSS Safety
+
+    # Whether or not ActionView's XSS protection is available and enabled,
+    # as is the default for Rails 3.0+, and optional for version 2.3.5+.
+    # Overridden in haml/template.rb if this is the case.
+    #
+    # @return [Boolean]
+    def rails_xss_safe?
+      false
+    end
+
+    # Returns the given text, marked as being HTML-safe.
+    # With older versions of the Rails XSS-safety mechanism,
+    # this destructively modifies the HTML-safety of `text`.
+    #
+    # It only works if you are using ActiveSupport or the parameter `text`
+    # implements the #html_safe method.
+    #
+    # @param text [String, nil]
+    # @return [String, nil] `text`, marked as HTML-safe
+    def html_safe(text)
+      return unless text
+      text.html_safe
+    end
+
+    # Checks that the encoding of a string is valid
+    # and cleans up potential encoding gotchas like the UTF-8 BOM.
+    # If it's not, yields an error string describing the invalid character
+    # and the line on which it occurs.
+    #
+    # @param str [String] The string of which to check the encoding
+    # @yield [msg] A block in which an encoding error can be raised.
+    #   Only yields if there is an encoding error
+    # @yieldparam msg [String] The error message to be raised
+    # @return [String] `str`, potentially with encoding gotchas like BOMs removed
+    def check_encoding(str)
+      if str.valid_encoding?
+        # Get rid of the Unicode BOM if possible
+        # Shortcut for UTF-8 which might be the majority case
+        if str.encoding == Encoding::UTF_8
+          return str.gsub(/\A\uFEFF/, '')
+        elsif str.encoding.name =~ /^UTF-(16|32)(BE|LE)?$/
+          return str.gsub(Regexp.new("\\A\uFEFF".encode(str.encoding)), '')
+        else
+          return str
+        end
+      end
+
+      encoding = str.encoding
+      newlines = Regexp.new("\r\n|\r|\n".encode(encoding).force_encoding(Encoding::ASCII_8BIT))
+      str.force_encoding(Encoding::ASCII_8BIT).split(newlines).each_with_index do |line, i|
+        begin
+          line.encode(encoding)
+        rescue Encoding::UndefinedConversionError => e
+          yield <<MSG.rstrip, i + 1
+Invalid #{encoding.name} character #{e.error_char.dump}
+MSG
+        end
+      end
+      return str
+    end
+
+    # Like {\#check\_encoding}, but also checks for a Ruby-style `-# coding:` comment
+    # at the beginning of the template and uses that encoding if it exists.
+    #
+    # The Haml encoding rules are simple.
+    # If a `-# coding:` comment exists,
+    # we assume that that's the original encoding of the document.
+    # Otherwise, we use whatever encoding Ruby has.
+    #
+    # Haml uses the same rules for parsing coding comments as Ruby.
+    # This means that it can understand Emacs-style comments
+    # (e.g. `-*- encoding: "utf-8" -*-`),
+    # and also that it cannot understand non-ASCII-compatible encodings
+    # such as `UTF-16` and `UTF-32`.
+    #
+    # @param str [String] The Haml template of which to check the encoding
+    # @yield [msg] A block in which an encoding error can be raised.
+    #   Only yields if there is an encoding error
+    # @yieldparam msg [String] The error message to be raised
+    # @return [String] The original string encoded properly
+    # @raise [ArgumentError] if the document declares an unknown encoding
+    def check_haml_encoding(str, &block)
+      str = str.dup if str.frozen?
+
+      bom, encoding = parse_haml_magic_comment(str)
+      if encoding; str.force_encoding(encoding)
+      elsif bom; str.force_encoding(Encoding::UTF_8)
+      end
+
+      return check_encoding(str, &block)
+    end
+
+    # Like `Object#inspect`, but preserves non-ASCII characters rather than escaping them.
+    # This is necessary so that the precompiled Haml template can be `#encode`d into `@options[:encoding]`
+    # before being evaluated.
+    #
+    # @param obj {Object}
+    # @return {String}
+    def inspect_obj(obj)
+      case obj
+      when String
+        %Q!"#{obj.gsub(/[\x00-\x7F]+/) {|s| s.inspect[1...-1]}}"!
+      when Symbol
+        ":#{inspect_obj(obj.to_s)}"
+      else
+        obj.inspect
+      end
+    end
+
+    # Scans through a string looking for the interoplation-opening `#{`
+    # and, when it's found, yields the scanner to the calling code
+    # so it can handle it properly.
+    #
+    # The scanner will have any backslashes immediately in front of the `#{`
+    # as the second capture group (`scan[2]`),
+    # and the text prior to that as the first (`scan[1]`).
+    #
+    # @yieldparam scan [StringScanner] The scanner scanning through the string
+    # @return [String] The text remaining in the scanner after all `#{`s have been processed
+    def handle_interpolation(str)
+      scan = StringScanner.new(str)
+      yield scan while scan.scan(/(.*?)(\\*)#([\{@$])/)
+      scan.rest
+    end
+
+    # Moves a scanner through a balanced pair of characters.
+    # For example:
+    #
+    #     Foo (Bar (Baz bang) bop) (Bang (bop bip))
+    #     ^                       ^
+    #     from                    to
+    #
+    # @param scanner [StringScanner] The string scanner to move
+    # @param start [String] The character opening the balanced pair.
+    # @param finish [String] The character closing the balanced pair.
+    # @param count [Fixnum] The number of opening characters matched
+    #   before calling this method
+    # @return [(String, String)] The string matched within the balanced pair
+    #   and the rest of the string.
+    #   `["Foo (Bar (Baz bang) bop)", " (Bang (bop bip))"]` in the example above.
+    def balance(scanner, start, finish, count = 0)
+      str = ''
+      scanner = StringScanner.new(scanner) unless scanner.is_a? StringScanner
+      regexp = Regexp.new("(.*?)[\\#{start.chr}\\#{finish.chr}]", Regexp::MULTILINE)
+      while scanner.scan(regexp)
+        str << scanner.matched
+        count += 1 if scanner.matched[-1] == start
+        count -= 1 if scanner.matched[-1] == finish
+        return [str.strip, scanner.rest] if count == 0
+      end
+    end
+
+    # Formats a string for use in error messages about indentation.
+    #
+    # @param indentation [String] The string used for indentation
+    # @return [String] The name of the indentation (e.g. `"12 spaces"`, `"1 tab"`)
+    def human_indentation(indentation)
+      if !indentation.include?(?\t)
+        noun = 'space'
+      elsif !indentation.include?(?\s)
+        noun = 'tab'
+      else
+        return indentation.inspect
+      end
+
+      singular = indentation.length == 1
+      "#{indentation.length} #{noun}#{'s' unless singular}"
+    end
+
+    def contains_interpolation?(str)
+      /#[\{$@]/ === str
+    end
+
+    # Original Haml::Util.unescape_interpolation
+    # ex) slow_unescape_interpolation('foo#{bar}baz"', escape_html: true)
+    #   #=> "\"foo\#{::Hamlit::HamlHelpers.html_escape((bar))}baz\\\"\""
+    def slow_unescape_interpolation(str, escape_html = nil)
+      res = ''
+      rest = ::Hamlit::HamlUtil.handle_interpolation str.dump do |scan|
+        escapes = (scan[2].size - 1) / 2
+        char = scan[3] # '{', '@' or '$'
+        res << scan.matched[0...-3 - escapes]
+        if escapes % 2 == 1
+          res << "\##{char}"
+        else
+          interpolated = if char == '{'
+            balance(scan, ?{, ?}, 1)[0][0...-1]
+          else
+            scan.scan(/\w+/)
+          end
+          content = eval('"' + interpolated + '"')
+          content.prepend(char) if char == '@' || char == '$'
+          content = "::Hamlit::HamlHelpers.html_escape((#{content}))" if escape_html
+
+          res << "\#{#{content}}"
+        end
+      end
+      res + rest
+    end
+
+    # Customized Haml::Util.unescape_interpolation to handle escape by Hamlit.
+    # It wraps double quotes to given `str` with escaping `"`.
+    #
+    # ex) unescape_interpolation('foo#{bar}baz"') #=> "\"foo\#{bar}baz\\\"\""
+    def unescape_interpolation(str)
+      res = ''
+      rest = ::Hamlit::HamlUtil.handle_interpolation str.dump do |scan|
+        escapes = (scan[2].size - 1) / 2
+        char = scan[3] # '{', '@' or '$'
+        res << scan.matched[0...-3 - escapes]
+        if escapes % 2 == 1
+          res << "\##{char}"
+        else
+          interpolated = if char == '{'
+            balance(scan, ?{, ?}, 1)[0][0...-1]
+          else
+            scan.scan(/\w+/)
+          end
+          content = eval('"' + interpolated + '"')
+          content.prepend(char) if char == '@' || char == '$'
+
+          res << "\#{#{content}}"
+        end
+      end
+      res + rest
+    end
+
+    private
+
+    # Parses a magic comment at the beginning of a Haml file.
+    # The parsing rules are basically the same as Ruby's.
+    #
+    # @return [(Boolean, String or nil)]
+    #   Whether the document begins with a UTF-8 BOM,
+    #   and the declared encoding of the document (or nil if none is declared)
+    def parse_haml_magic_comment(str)
+      scanner = StringScanner.new(str.dup.force_encoding(Encoding::ASCII_8BIT))
+      bom = scanner.scan(/\xEF\xBB\xBF/n)
+      return bom unless scanner.scan(/-\s*#\s*/n)
+      if coding = try_parse_haml_emacs_magic_comment(scanner)
+        return bom, coding
+      end
+
+      return bom unless scanner.scan(/.*?coding[=:]\s*([\w-]+)/in)
+      return bom, scanner[1]
+    end
+
+    def try_parse_haml_emacs_magic_comment(scanner)
+      pos = scanner.pos
+      return unless scanner.scan(/.*?-\*-\s*/n)
+      # From Ruby's parse.y
+      return unless scanner.scan(/([^\s'":;]+)\s*:\s*("(?:\\.|[^"])*"|[^"\s;]+?)[\s;]*-\*-/n)
+      name, val = scanner[1], scanner[2]
+      return unless name =~ /(en)?coding/in
+      val = $1 if val =~ /^"(.*)"$/n
+      return val
+    ensure
+      scanner.pos = pos
+    end
+  end
+end
diff --git a/lib/hamlit/parser/haml_xss_mods.rb b/lib/hamlit/parser/haml_xss_mods.rb
new file mode 100644 (file)
index 0000000..4995ca5
--- /dev/null
@@ -0,0 +1,109 @@
+module Hamlit
+  module HamlHelpers
+    # This module overrides Haml helpers to work properly
+    # in the context of ActionView.
+    # Currently it's only used for modifying the helpers
+    # to work with Rails' XSS protection methods.
+    module XssMods
+      def self.included(base)
+        %w[html_escape find_and_preserve preserve list_of surround
+           precede succeed capture_haml haml_concat haml_internal_concat haml_indent
+           escape_once].each do |name|
+          base.send(:alias_method, "#{name}_without_haml_xss", name)
+          base.send(:alias_method, name, "#{name}_with_haml_xss")
+        end
+      end
+
+      # Don't escape text that's already safe,
+      # output is always HTML safe
+      def html_escape_with_haml_xss(text)
+        str = text.to_s
+        return text if str.html_safe?
+        ::Hamlit::HamlUtil.html_safe(html_escape_without_haml_xss(str))
+      end
+
+      # Output is always HTML safe
+      def find_and_preserve_with_haml_xss(*args, &block)
+        ::Hamlit::HamlUtil.html_safe(find_and_preserve_without_haml_xss(*args, &block))
+      end
+
+      # Output is always HTML safe
+      def preserve_with_haml_xss(*args, &block)
+        ::Hamlit::HamlUtil.html_safe(preserve_without_haml_xss(*args, &block))
+      end
+
+      # Output is always HTML safe
+      def list_of_with_haml_xss(*args, &block)
+        ::Hamlit::HamlUtil.html_safe(list_of_without_haml_xss(*args, &block))
+      end
+
+      # Input is escaped, output is always HTML safe
+      def surround_with_haml_xss(front, back = front, &block)
+        ::Hamlit::HamlUtil.html_safe(
+          surround_without_haml_xss(
+            haml_xss_html_escape(front),
+            haml_xss_html_escape(back),
+            &block))
+      end
+
+      # Input is escaped, output is always HTML safe
+      def precede_with_haml_xss(str, &block)
+        ::Hamlit::HamlUtil.html_safe(precede_without_haml_xss(haml_xss_html_escape(str), &block))
+      end
+
+      # Input is escaped, output is always HTML safe
+      def succeed_with_haml_xss(str, &block)
+        ::Hamlit::HamlUtil.html_safe(succeed_without_haml_xss(haml_xss_html_escape(str), &block))
+      end
+
+      # Output is always HTML safe
+      def capture_haml_with_haml_xss(*args, &block)
+        ::Hamlit::HamlUtil.html_safe(capture_haml_without_haml_xss(*args, &block))
+      end
+
+      # Input will be escaped unless this is in a `with_raw_haml_concat`
+      # block. See #Haml::Helpers::ActionViewExtensions#with_raw_haml_concat.
+      def haml_concat_with_haml_xss(text = "")
+        raw = instance_variable_defined?(:@_haml_concat_raw) ? @_haml_concat_raw : false
+        if raw
+          haml_internal_concat_raw text
+        else
+          haml_internal_concat text
+        end
+        ErrorReturn.new("haml_concat")
+      end
+
+      # Input is escaped
+      def haml_internal_concat_with_haml_xss(text="", newline=true, indent=true)
+        haml_internal_concat_without_haml_xss(haml_xss_html_escape(text), newline, indent)
+      end
+      private :haml_internal_concat_with_haml_xss
+
+      # Output is always HTML safe
+      def haml_indent_with_haml_xss
+        ::Hamlit::HamlUtil.html_safe(haml_indent_without_haml_xss)
+      end
+
+      # Output is always HTML safe
+      def escape_once_with_haml_xss(*args)
+        ::Hamlit::HamlUtil.html_safe(escape_once_without_haml_xss(*args))
+      end
+
+      private
+
+      # Escapes the HTML in the text if and only if
+      # Rails XSS protection is enabled *and* the `:escape_html` option is set.
+      def haml_xss_html_escape(text)
+        return text unless ::Hamlit::HamlUtil.rails_xss_safe? && haml_buffer.options[:escape_html]
+        html_escape(text)
+      end
+    end
+
+    class ErrorReturn
+      # Any attempt to treat ErrorReturn as a string should cause it to blow up.
+      alias_method :html_safe, :to_s
+      alias_method :html_safe?, :to_s
+      alias_method :html_safe!, :to_s
+    end
+  end
+end
diff --git a/lib/hamlit/rails_helpers.rb b/lib/hamlit/rails_helpers.rb
new file mode 100644 (file)
index 0000000..a71b4ff
--- /dev/null
@@ -0,0 +1,51 @@
+# frozen_string_literal: false
+require 'hamlit/helpers'
+
+# Currently this Hamlit::Helpers depends on
+# ActionView internal implementation. (not desired)
+module Hamlit
+  module RailsHelpers
+    include Helpers
+    extend self
+
+    DEFAULT_PRESERVE_TAGS = %w[textarea pre code].freeze
+
+    def find_and_preserve(input = nil, tags = DEFAULT_PRESERVE_TAGS, &block)
+      return find_and_preserve(capture_haml(&block), input || tags) if block
+
+      tags = tags.each_with_object('') do |t, s|
+        s << '|' unless s.empty?
+        s << Regexp.escape(t)
+      end
+
+      re = /<(#{tags})([^>]*)>(.*?)(<\/\1>)/im
+      input.to_s.gsub(re) do |s|
+        s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible
+        "<#{$1}#{$2}>#{preserve($3)}</#{$1}>"
+      end
+    end
+
+    def preserve(input = nil, &block)
+      return preserve(capture_haml(&block)) if block
+      super.html_safe
+    end
+
+    def surround(front, back = front, &block)
+      output = capture_haml(&block)
+
+      "#{escape_once(front)}#{output.chomp}#{escape_once(back)}\n".html_safe
+    end
+
+    def precede(str, &block)
+      "#{escape_once(str)}#{capture_haml(&block).chomp}\n".html_safe
+    end
+
+    def succeed(str, &block)
+      "#{capture_haml(&block).chomp}#{escape_once(str)}\n".html_safe
+    end
+
+    def capture_haml(*args, &block)
+      capture(*args, &block)
+    end
+  end
+end
diff --git a/lib/hamlit/rails_template.rb b/lib/hamlit/rails_template.rb
new file mode 100644 (file)
index 0000000..0bb108b
--- /dev/null
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+require 'temple'
+require 'hamlit/engine'
+require 'hamlit/rails_helpers'
+require 'hamlit/parser/haml_helpers'
+require 'hamlit/parser/haml_util'
+
+module Hamlit
+  class RailsTemplate
+    # Compatible with: https://github.com/judofyr/temple/blob/v0.7.7/lib/temple/mixins/options.rb#L15-L24
+    class << self
+      def options
+        @options ||= {
+          generator:     Temple::Generators::RailsOutputBuffer,
+          use_html_safe: true,
+          streaming:     true,
+          buffer_class:  'ActionView::OutputBuffer',
+        }
+      end
+
+      def set_options(opts)
+        options.update(opts)
+      end
+    end
+
+    def call(template)
+      options = RailsTemplate.options
+
+      # https://github.com/haml/haml/blob/4.0.7/lib/haml/template/plugin.rb#L19-L20
+      # https://github.com/haml/haml/blob/4.0.7/lib/haml/options.rb#L228
+      if template.respond_to?(:type) && template.type == 'text/xml'
+        options = options.merge(format: :xhtml)
+      end
+
+      Engine.new(options).call(template.source)
+    end
+  end
+  ActionView::Template.register_template_handler(:haml, RailsTemplate.new)
+
+  # https://github.com/haml/haml/blob/4.0.7/lib/haml/template.rb
+  module HamlHelpers
+    require 'hamlit/parser/haml_xss_mods'
+    include Hamlit::HamlHelpers::XssMods
+  end
+
+  module HamlUtil
+    undef :rails_xss_safe? if defined? rails_xss_safe?
+    def rails_xss_safe?; true; end
+  end
+end
+
+# Haml extends Haml::Helpers in ActionView each time.
+# It costs much, so Hamlit includes a compatible module at first.
+ActionView::Base.send :include, Hamlit::RailsHelpers
diff --git a/lib/hamlit/railtie.rb b/lib/hamlit/railtie.rb
new file mode 100644 (file)
index 0000000..4139d78
--- /dev/null
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+require 'rails'
+
+module Hamlit
+  class Railtie < ::Rails::Railtie
+    initializer :hamlit do |app|
+      require 'hamlit/rails_template'
+    end
+  end
+end
diff --git a/lib/hamlit/ruby_expression.rb b/lib/hamlit/ruby_expression.rb
new file mode 100644 (file)
index 0000000..5aecb01
--- /dev/null
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+require 'ripper'
+
+module Hamlit
+  class RubyExpression < Ripper
+    class ParseError < StandardError; end
+
+    def self.syntax_error?(code)
+      self.new(code).parse
+      false
+    rescue ParseError
+      true
+    end
+
+    def self.string_literal?(code)
+      return false if syntax_error?(code)
+
+      type, instructions = Ripper.sexp(code)
+      return false if type != :program
+      return false if instructions.size > 1
+
+      type, _ = instructions.first
+      type == :string_literal
+    end
+
+    private
+
+    def on_parse_error(*)
+      raise ParseError
+    end
+  end
+end
diff --git a/lib/hamlit/static_analyzer.rb b/lib/hamlit/static_analyzer.rb
new file mode 100644 (file)
index 0000000..35c61ef
--- /dev/null
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+require 'hamlit/ruby_expression'
+
+module Hamlit
+  class StaticAnalyzer < Temple::Filter
+    STATIC_TOKENS = %i[
+      on_tstring_beg on_tstring_end on_tstring_content
+      on_embexpr_beg on_embexpr_end
+      on_lbracket on_rbracket
+      on_qwords_beg on_words_sep on_qwords_sep
+      on_lparen on_rparen
+      on_lbrace on_rbrace on_label
+      on_int on_float on_imaginary
+      on_comma on_sp
+    ].freeze
+
+    DYNAMIC_TOKENS = %i[
+      on_ident on_period
+    ].freeze
+
+    STATIC_KEYWORDS = %w[
+      true false nil
+    ].freeze
+
+    STATIC_OPERATORS = %w[
+      =>
+    ].freeze
+
+    def self.static?(code)
+      return false if code.nil? || code.strip.empty?
+      return false if RubyExpression.syntax_error?(code)
+
+      Ripper.lex(code).each do |(_, col), token, str|
+        case token
+        when *STATIC_TOKENS
+          # noop
+        when :on_kw
+          return false unless STATIC_KEYWORDS.include?(str)
+        when :on_op
+          return false unless STATIC_OPERATORS.include?(str)
+        when *DYNAMIC_TOKENS
+          return false
+        else
+          return false
+        end
+      end
+      true
+    end
+
+    def on_dynamic(code)
+      if StaticAnalyzer.static?(code)
+        [:static, eval(code).to_s]
+      else
+        [:dynamic, code]
+      end
+    end
+  end
+end
diff --git a/lib/hamlit/string_splitter.rb b/lib/hamlit/string_splitter.rb
new file mode 100644 (file)
index 0000000..36b6e1b
--- /dev/null
@@ -0,0 +1,88 @@
+require 'ripper'
+require 'hamlit/ruby_expression'
+
+module Hamlit
+  class StringSplitter < Temple::Filter
+    class << self
+      # `code` param must be valid string literal
+      def compile(code)
+        [].tap do |exps|
+          tokens = Ripper.lex(code.strip)
+          tokens.pop while tokens.last && %i[on_comment on_sp].include?(tokens.last[1])
+
+          if tokens.size < 2
+            raise Hamlit::InternalError.new("Expected token size >= 2 but got: #{tokens.size}")
+          end
+          compile_tokens!(exps, tokens)
+        end
+      end
+
+      private
+
+      def strip_quotes!(tokens)
+        _, type, beg_str = tokens.shift
+        if type != :on_tstring_beg
+          raise Hamlit::InternalError.new("Expected :on_tstring_beg but got: #{type}")
+        end
+
+        _, type, end_str = tokens.pop
+        if type != :on_tstring_end
+          raise Hamlit::InternalError.new("Expected :on_tstring_end but got: #{type}")
+        end
+
+        [beg_str, end_str]
+      end
+
+      def compile_tokens!(exps, tokens)
+        beg_str, end_str = strip_quotes!(tokens)
+
+        until tokens.empty?
+          _, type, str = tokens.shift
+
+          case type
+          when :on_tstring_content
+            exps << [:static, eval("#{beg_str}#{str}#{end_str}")]
+          when :on_embexpr_beg
+            embedded = shift_balanced_embexpr(tokens)
+            exps << [:dynamic, embedded] unless embedded.empty?
+          end
+        end
+      end
+
+      def shift_balanced_embexpr(tokens)
+        String.new.tap do |embedded|
+          embexpr_open = 1
+
+          until tokens.empty?
+            _, type, str = tokens.shift
+            case type
+            when :on_embexpr_beg
+              embexpr_open += 1
+            when :on_embexpr_end
+              embexpr_open -= 1
+              break if embexpr_open == 0
+            end
+
+            embedded << str
+          end
+        end
+      end
+    end
+
+    def on_dynamic(code)
+      return [:dynamic, code] unless RubyExpression.string_literal?(code)
+      return [:dynamic, code] if code.include?("\n")
+
+      temple = [:multi]
+      StringSplitter.compile(code).each do |type, content|
+        case type
+        when :static
+          temple << [:static, content]
+        when :dynamic
+          temple << on_dynamic(content)
+        end
+      end
+      temple
+    end
+  end
+end
diff --git a/lib/hamlit/template.rb b/lib/hamlit/template.rb
new file mode 100644 (file)
index 0000000..dfd63ba
--- /dev/null
@@ -0,0 +1,28 @@
+# frozen_string_literal: false
+require 'temple'
+require 'hamlit/engine'
+require 'hamlit/helpers'
+
+# Load tilt/haml first to override if available
+begin
+  require 'haml'
+rescue LoadError
+else
+  require 'tilt/haml'
+end
+
+module Hamlit
+  Template = Temple::Templates::Tilt.create(
+    Hamlit::Engine,
+    register_as: :haml,
+  )
+
+  module TemplateExtension
+    # Activate Hamlit::Helpers for tilt templates.
+    # https://github.com/judofyr/temple/blob/v0.7.6/lib/temple/mixins/template.rb#L7-L11
+    def compile(*)
+      "extend Hamlit::Helpers; #{super}"
+    end
+  end
+  Template.send(:extend, TemplateExtension)
+end
diff --git a/lib/hamlit/utils.rb b/lib/hamlit/utils.rb
new file mode 100644 (file)
index 0000000..6097c69
--- /dev/null
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+require 'hamlit/hamlit'
+
+module Hamlit
+  module Utils
+    def self.escape_html_safe(html)
+      html.html_safe? ? html : escape_html(html)
+    end
+  end
+end
diff --git a/lib/hamlit/version.rb b/lib/hamlit/version.rb
new file mode 100644 (file)
index 0000000..942c64e
--- /dev/null
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+module Hamlit
+  VERSION = '2.7.5'
+end
diff --git a/sample/rails/.gitignore b/sample/rails/.gitignore
new file mode 100644 (file)
index 0000000..050c9d9
--- /dev/null
@@ -0,0 +1,17 @@
+# See https://help.github.com/articles/ignoring-files for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+#   git config --global core.excludesfile '~/.gitignore_global'
+
+# Ignore bundler config.
+/.bundle
+
+# Ignore the default SQLite database.
+/db/*.sqlite3
+/db/*.sqlite3-journal
+
+# Ignore all logfiles and tempfiles.
+/log/*
+!/log/.keep
+/tmp
diff --git a/sample/rails/Gemfile b/sample/rails/Gemfile
new file mode 100644 (file)
index 0000000..bfea323
--- /dev/null
@@ -0,0 +1,20 @@
+source 'https://rubygems.org'
+
+gem 'rails', '4.2.4'
+
+gem 'coffee-rails', '~> 4.1.0'
+gem 'hamlit', path: '../../'
+gem 'jbuilder', '~> 2.0'
+gem 'jquery-rails'
+gem 'sass-rails', '~> 5.0'
+gem 'sqlite3'
+gem 'uglifier', '>= 1.3.0'
+
+group :development, :test do
+  gem 'byebug'
+end
+
+group :development do
+  gem 'web-console', '~> 2.0'
+  gem 'spring'
+end
diff --git a/sample/rails/Gemfile.lock b/sample/rails/Gemfile.lock
new file mode 100644 (file)
index 0000000..ef6d296
--- /dev/null
@@ -0,0 +1,159 @@
+PATH
+  remote: ../../
+  specs:
+    hamlit (0.1.0)
+      escape_utils
+      temple (~> 0.7.6)
+      tilt
+
+GEM
+  remote: https://rubygems.org/
+  specs:
+    actionmailer (4.2.4)
+      actionpack (= 4.2.4)
+      actionview (= 4.2.4)
+      activejob (= 4.2.4)
+      mail (~> 2.5, >= 2.5.4)
+      rails-dom-testing (~> 1.0, >= 1.0.5)
+    actionpack (4.2.4)
+      actionview (= 4.2.4)
+      activesupport (= 4.2.4)
+      rack (~> 1.6)
+      rack-test (~> 0.6.2)
+      rails-dom-testing (~> 1.0, >= 1.0.5)
+      rails-html-sanitizer (~> 1.0, >= 1.0.2)
+    actionview (4.2.4)
+      activesupport (= 4.2.4)
+      builder (~> 3.1)
+      erubis (~> 2.7.0)
+      rails-dom-testing (~> 1.0, >= 1.0.5)
+      rails-html-sanitizer (~> 1.0, >= 1.0.2)
+    activejob (4.2.4)
+      activesupport (= 4.2.4)
+      globalid (>= 0.3.0)
+    activemodel (4.2.4)
+      activesupport (= 4.2.4)
+      builder (~> 3.1)
+    activerecord (4.2.4)
+      activemodel (= 4.2.4)
+      activesupport (= 4.2.4)
+      arel (~> 6.0)
+    activesupport (4.2.4)
+      i18n (~> 0.7)
+      json (~> 1.7, >= 1.7.7)
+      minitest (~> 5.1)
+      thread_safe (~> 0.3, >= 0.3.4)
+      tzinfo (~> 1.1)
+    arel (6.0.3)
+    binding_of_caller (0.7.2)
+      debug_inspector (>= 0.0.1)
+    builder (3.2.2)
+    byebug (8.2.0)
+    coffee-rails (4.1.0)
+      coffee-script (>= 2.2.0)
+      railties (>= 4.0.0, < 5.0)
+    coffee-script (2.4.1)
+      coffee-script-source
+      execjs
+    coffee-script-source (1.10.0)
+    debug_inspector (0.0.2)
+    erubis (2.7.0)
+    escape_utils (1.1.0)
+    execjs (2.6.0)
+    globalid (0.3.6)
+      activesupport (>= 4.1.0)
+    i18n (0.7.0)
+    jbuilder (2.3.2)
+      activesupport (>= 3.0.0, < 5)
+      multi_json (~> 1.2)
+    jquery-rails (4.0.5)
+      rails-dom-testing (~> 1.0)
+      railties (>= 4.2.0)
+      thor (>= 0.14, < 2.0)
+    json (1.8.3)
+    loofah (2.0.3)
+      nokogiri (>= 1.5.9)
+    mail (2.6.3)
+      mime-types (>= 1.16, < 3)
+    mime-types (2.6.2)
+    mini_portile (0.6.2)
+    minitest (5.8.3)
+    multi_json (1.11.2)
+    nokogiri (1.6.6.3)
+      mini_portile (~> 0.6.0)
+    rack (1.6.4)
+    rack-test (0.6.3)
+      rack (>= 1.0)
+    rails (4.2.4)
+      actionmailer (= 4.2.4)
+      actionpack (= 4.2.4)
+      actionview (= 4.2.4)
+      activejob (= 4.2.4)
+      activemodel (= 4.2.4)
+      activerecord (= 4.2.4)
+      activesupport (= 4.2.4)
+      bundler (>= 1.3.0, < 2.0)
+      railties (= 4.2.4)
+      sprockets-rails
+    rails-deprecated_sanitizer (1.0.3)
+      activesupport (>= 4.2.0.alpha)
+    rails-dom-testing (1.0.7)
+      activesupport (>= 4.2.0.beta, < 5.0)
+      nokogiri (~> 1.6.0)
+      rails-deprecated_sanitizer (>= 1.0.1)
+    rails-html-sanitizer (1.0.2)
+      loofah (~> 2.0)
+    railties (4.2.4)
+      actionpack (= 4.2.4)
+      activesupport (= 4.2.4)
+      rake (>= 0.8.7)
+      thor (>= 0.18.1, < 2.0)
+    rake (10.4.2)
+    sass (3.4.19)
+    sass-rails (5.0.4)
+      railties (>= 4.0.0, < 5.0)
+      sass (~> 3.1)
+      sprockets (>= 2.8, < 4.0)
+      sprockets-rails (>= 2.0, < 4.0)
+      tilt (>= 1.1, < 3)
+    spring (1.4.3)
+    sprockets (3.4.0)
+      rack (> 1, < 3)
+    sprockets-rails (2.3.3)
+      actionpack (>= 3.0)
+      activesupport (>= 3.0)
+      sprockets (>= 2.8, < 4.0)
+    sqlite3 (1.3.11)
+    temple (0.7.6)
+    thor (0.19.1)
+    thread_safe (0.3.5)
+    tilt (2.0.1)
+    tzinfo (1.2.2)
+      thread_safe (~> 0.1)
+    uglifier (2.7.2)
+      execjs (>= 0.3.0)
+      json (>= 1.8.0)
+    web-console (2.2.1)
+      activemodel (>= 4.0)
+      binding_of_caller (>= 0.7.2)
+      railties (>= 4.0)
+      sprockets-rails (>= 2.0, < 4.0)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  byebug
+  coffee-rails (~> 4.1.0)
+  hamlit!
+  jbuilder (~> 2.0)
+  jquery-rails
+  rails (= 4.2.4)
+  sass-rails (~> 5.0)
+  spring
+  sqlite3
+  uglifier (>= 1.3.0)
+  web-console (~> 2.0)
+
+BUNDLED WITH
+   1.10.6
diff --git a/sample/rails/README.rdoc b/sample/rails/README.rdoc
new file mode 100644 (file)
index 0000000..dd4e97e
--- /dev/null
@@ -0,0 +1,28 @@
+== README
+
+This README would normally document whatever steps are necessary to get the
+application up and running.
+
+Things you may want to cover:
+
+* Ruby version
+
+* System dependencies
+
+* Configuration
+
+* Database creation
+
+* Database initialization
+
+* How to run the test suite
+
+* Services (job queues, cache servers, search engines, etc.)
+
+* Deployment instructions
+
+* ...
+
+
+Please feel free to use a different markup language if you do not plan to run
+<tt>rake doc:app</tt>.
diff --git a/sample/rails/Rakefile b/sample/rails/Rakefile
new file mode 100644 (file)
index 0000000..ba6b733
--- /dev/null
@@ -0,0 +1,6 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require File.expand_path('../config/application', __FILE__)
+
+Rails.application.load_tasks
diff --git a/sample/rails/app/assets/images/.keep b/sample/rails/app/assets/images/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/app/assets/javascripts/application.js b/sample/rails/app/assets/javascripts/application.js
new file mode 100644 (file)
index 0000000..646c5ab
--- /dev/null
@@ -0,0 +1,15 @@
+// This is a manifest file that'll be compiled into application.js, which will include all the files
+// listed below.
+//
+// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
+// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
+//
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// compiled file.
+//
+// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
+// about supported directives.
+//
+//= require jquery
+//= require jquery_ujs
+//= require_tree .
diff --git a/sample/rails/app/assets/javascripts/users.coffee b/sample/rails/app/assets/javascripts/users.coffee
new file mode 100644 (file)
index 0000000..24f83d1
--- /dev/null
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/sample/rails/app/assets/stylesheets/application.css b/sample/rails/app/assets/stylesheets/application.css
new file mode 100644 (file)
index 0000000..f9cd5b3
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any styles
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
+ * file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
diff --git a/sample/rails/app/assets/stylesheets/scaffolds.scss b/sample/rails/app/assets/stylesheets/scaffolds.scss
new file mode 100644 (file)
index 0000000..ed7a765
--- /dev/null
@@ -0,0 +1,73 @@
+body {
+  background-color: #fff;
+  color: #333;
+  font-family: verdana, arial, helvetica, sans-serif;
+  font-size: 13px;
+  line-height: 18px;
+}
+
+p, ol, ul, td {
+  font-family: verdana, arial, helvetica, sans-serif;
+  font-size: 13px;
+  line-height: 18px;
+}
+
+pre {
+  background-color: #eee;
+  padding: 10px;
+  font-size: 11px;
+}
+
+a {
+  color: #000;
+
+  &:visited {
+    color: #666;
+  }
+
+  &:hover {
+    color: #fff;
+    background-color: #000;
+  }
+}
+
+div {
+  &.field, &.actions {
+    margin-bottom: 10px;
+  }
+}
+
+#notice {
+  color: green;
+}
+
+.field_with_errors {
+  padding: 2px;
+  background-color: red;
+  display: table;
+}
+
+#error_explanation {
+  width: 450px;
+  border: 2px solid red;
+  padding: 7px;
+  padding-bottom: 0;
+  margin-bottom: 20px;
+  background-color: #f0f0f0;
+
+  h2 {
+    text-align: left;
+    font-weight: bold;
+    padding: 5px 5px 5px 15px;
+    font-size: 12px;
+    margin: -7px;
+    margin-bottom: 0px;
+    background-color: #c00;
+    color: #fff;
+  }
+
+  ul li {
+    font-size: 12px;
+    list-style: square;
+  }
+}
diff --git a/sample/rails/app/assets/stylesheets/users.scss b/sample/rails/app/assets/stylesheets/users.scss
new file mode 100644 (file)
index 0000000..1efc835
--- /dev/null
@@ -0,0 +1,3 @@
+// Place all the styles related to the users controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/sample/rails/app/controllers/application_controller.rb b/sample/rails/app/controllers/application_controller.rb
new file mode 100644 (file)
index 0000000..d83690e
--- /dev/null
@@ -0,0 +1,5 @@
+class ApplicationController < ActionController::Base
+  # Prevent CSRF attacks by raising an exception.
+  # For APIs, you may want to use :null_session instead.
+  protect_from_forgery with: :exception
+end
diff --git a/sample/rails/app/controllers/concerns/.keep b/sample/rails/app/controllers/concerns/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/app/controllers/users_controller.rb b/sample/rails/app/controllers/users_controller.rb
new file mode 100644 (file)
index 0000000..792ffb7
--- /dev/null
@@ -0,0 +1,54 @@
+class UsersController < ApplicationController
+  before_action :set_user, only: [:show, :edit, :update, :destroy]
+
+  def index
+    @users = User.all
+  end
+
+  def show
+  end
+
+  def new
+    @user = User.new
+  end
+
+  def edit
+  end
+
+  def create
+    @user = User.new(user_params)
+    if @user.save
+      redirect_to @user, notice: 'User was successfully created.'
+    else
+      render :new
+    end
+  end
+
+  def update
+    if @user.update(user_params)
+      redirect_to @user, notice: 'User was successfully updated.'
+    else
+      render :edit
+    end
+  end
+
+  def destroy
+    @user.destroy
+    redirect_to users_url, notice: 'User was successfully destroyed.'
+  end
+
+  def test
+  end
+
+  private
+
+  # Use callbacks to share common setup or constraints between actions.
+  def set_user
+    @user = User.find(params[:id])
+  end
+
+  # Never trust parameters from the scary internet, only allow the white list through.
+  def user_params
+    params[:user]
+  end
+end
diff --git a/sample/rails/app/helpers/application_helper.rb b/sample/rails/app/helpers/application_helper.rb
new file mode 100644 (file)
index 0000000..de6be79
--- /dev/null
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/sample/rails/app/helpers/users_helper.rb b/sample/rails/app/helpers/users_helper.rb
new file mode 100644 (file)
index 0000000..2310a24
--- /dev/null
@@ -0,0 +1,2 @@
+module UsersHelper
+end
diff --git a/sample/rails/app/mailers/.keep b/sample/rails/app/mailers/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/app/models/.keep b/sample/rails/app/models/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/app/models/concerns/.keep b/sample/rails/app/models/concerns/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/app/models/user.rb b/sample/rails/app/models/user.rb
new file mode 100644 (file)
index 0000000..4a57cf0
--- /dev/null
@@ -0,0 +1,2 @@
+class User < ActiveRecord::Base
+end
diff --git a/sample/rails/app/views/layouts/application.html.haml b/sample/rails/app/views/layouts/application.html.haml
new file mode 100644 (file)
index 0000000..795cc3d
--- /dev/null
@@ -0,0 +1,9 @@
+!!!
+%html
+  %head
+    %title Dummy
+    = stylesheet_link_tag    'application', media: 'all'
+    = javascript_include_tag 'application'
+    = csrf_meta_tags
+  %body
+    = yield
diff --git a/sample/rails/app/views/users/_form.html.haml b/sample/rails/app/views/users/_form.html.haml
new file mode 100644 (file)
index 0000000..5ae63f9
--- /dev/null
@@ -0,0 +1,11 @@
+= form_for(@user) do |f|
+  - if @user.errors.any?
+    #error_explanation
+      %h2 #{pluralize(@user.errors.count, "error")} prohibited this user from being saved:
+
+      %ul
+        - @user.errors.full_messages.each do |message|
+          %li= message
+
+  .actions
+    = f.submit
diff --git a/sample/rails/app/views/users/edit.html.haml b/sample/rails/app/views/users/edit.html.haml
new file mode 100644 (file)
index 0000000..78452dc
--- /dev/null
@@ -0,0 +1,5 @@
+%h1 Editing User
+
+= render 'form'
+
+#{link_to 'Show', @user} | #{link_to 'Back', users_path}
diff --git a/sample/rails/app/views/users/index.html.haml b/sample/rails/app/views/users/index.html.haml
new file mode 100644 (file)
index 0000000..fb1d451
--- /dev/null
@@ -0,0 +1,19 @@
+%p#notice= notice
+
+%h1 Listing Users
+
+%table
+  %thead
+    %tr
+      %th{ colspan: "3" }
+
+  %tbody
+    - @users.each do |user|
+      %tr
+        %td= link_to 'Show', user
+        %td= link_to 'Edit', edit_user_path(user)
+        %td= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' }
+
+%br
+
+= link_to 'New User', new_user_path
diff --git a/sample/rails/app/views/users/new.html.haml b/sample/rails/app/views/users/new.html.haml
new file mode 100644 (file)
index 0000000..3760397
--- /dev/null
@@ -0,0 +1,5 @@
+%h1 New User
+
+= render 'form'
+
+= link_to 'Back', users_path
diff --git a/sample/rails/app/views/users/show.html.haml b/sample/rails/app/views/users/show.html.haml
new file mode 100644 (file)
index 0000000..4dae5ac
--- /dev/null
@@ -0,0 +1,3 @@
+#notice= notice
+
+#{link_to 'Edit', edit_user_path(@user)} | #{link_to 'Back', users_path}
diff --git a/sample/rails/app/views/users/test.html.haml b/sample/rails/app/views/users/test.html.haml
new file mode 100644 (file)
index 0000000..81730f1
--- /dev/null
@@ -0,0 +1,6 @@
+= content_tag :div do
+  test
+
+- 3.times do
+  = content_tag :div do
+    test
diff --git a/sample/rails/bin/bundle b/sample/rails/bin/bundle
new file mode 100755 (executable)
index 0000000..66e9889
--- /dev/null
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/sample/rails/bin/rails b/sample/rails/bin/rails
new file mode 100755 (executable)
index 0000000..5191e69
--- /dev/null
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/sample/rails/bin/rake b/sample/rails/bin/rake
new file mode 100755 (executable)
index 0000000..1724048
--- /dev/null
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/sample/rails/bin/setup b/sample/rails/bin/setup
new file mode 100755 (executable)
index 0000000..acdb2c1
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+require 'pathname'
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../',  __FILE__)
+
+Dir.chdir APP_ROOT do
+  # This script is a starting point to setup your application.
+  # Add necessary setup steps to this file:
+
+  puts "== Installing dependencies =="
+  system "gem install bundler --conservative"
+  system "bundle check || bundle install"
+
+  # puts "\n== Copying sample files =="
+  # unless File.exist?("config/database.yml")
+  #   system "cp config/database.yml.sample config/database.yml"
+  # end
+
+  puts "\n== Preparing database =="
+  system "bin/rake db:setup"
+
+  puts "\n== Removing old logs and tempfiles =="
+  system "rm -f log/*"
+  system "rm -rf tmp/cache"
+
+  puts "\n== Restarting application server =="
+  system "touch tmp/restart.txt"
+end
diff --git a/sample/rails/config.ru b/sample/rails/config.ru
new file mode 100644 (file)
index 0000000..bd83b25
--- /dev/null
@@ -0,0 +1,4 @@
+# This file is used by Rack-based servers to start the application.
+
+require ::File.expand_path('../config/environment', __FILE__)
+run Rails.application
diff --git a/sample/rails/config/application.rb b/sample/rails/config/application.rb
new file mode 100644 (file)
index 0000000..7f2b02d
--- /dev/null
@@ -0,0 +1,35 @@
+require File.expand_path('../boot', __FILE__)
+
+require "rails"
+# Pick the frameworks you want:
+require "active_model/railtie"
+require "active_job/railtie"
+require "active_record/railtie"
+require "action_controller/railtie"
+require "action_mailer/railtie"
+require "action_view/railtie"
+require "sprockets/railtie"
+# require "rails/test_unit/railtie"
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module Dummy
+  class Application < Rails::Application
+    # Settings in config/environments/* take precedence over those specified here.
+    # Application configuration should go into files in config/initializers
+    # -- all .rb files in that directory are automatically loaded.
+
+    # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
+    # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
+    # config.time_zone = 'Central Time (US & Canada)'
+
+    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
+    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
+    # config.i18n.default_locale = :de
+
+    # Do not swallow errors in after_commit/after_rollback callbacks.
+    config.active_record.raise_in_transactional_callbacks = true
+  end
+end
diff --git a/sample/rails/config/boot.rb b/sample/rails/config/boot.rb
new file mode 100644 (file)
index 0000000..6b750f0
--- /dev/null
@@ -0,0 +1,3 @@
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/sample/rails/config/database.yml b/sample/rails/config/database.yml
new file mode 100644 (file)
index 0000000..1c1a37c
--- /dev/null
@@ -0,0 +1,25 @@
+# SQLite version 3.x
+#   gem install sqlite3
+#
+#   Ensure the SQLite 3 gem is defined in your Gemfile
+#   gem 'sqlite3'
+#
+default: &default
+  adapter: sqlite3
+  pool: 5
+  timeout: 5000
+
+development:
+  <<: *default
+  database: db/development.sqlite3
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+  <<: *default
+  database: db/test.sqlite3
+
+production:
+  <<: *default
+  database: db/production.sqlite3
diff --git a/sample/rails/config/environment.rb b/sample/rails/config/environment.rb
new file mode 100644 (file)
index 0000000..ee8d90d
--- /dev/null
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require File.expand_path('../application', __FILE__)
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/sample/rails/config/environments/development.rb b/sample/rails/config/environments/development.rb
new file mode 100644 (file)
index 0000000..b55e214
--- /dev/null
@@ -0,0 +1,41 @@
+Rails.application.configure do
+  # Settings specified here will take precedence over those in config/application.rb.
+
+  # In the development environment your application's code is reloaded on
+  # every request. This slows down response time but is perfect for development
+  # since you don't have to restart the web server when you make code changes.
+  config.cache_classes = false
+
+  # Do not eager load code on boot.
+  config.eager_load = false
+
+  # Show full error reports and disable caching.
+  config.consider_all_requests_local       = true
+  config.action_controller.perform_caching = false
+
+  # Don't care if the mailer can't send.
+  config.action_mailer.raise_delivery_errors = false
+
+  # Print deprecation notices to the Rails logger.
+  config.active_support.deprecation = :log
+
+  # Raise an error on page load if there are pending migrations.
+  config.active_record.migration_error = :page_load
+
+  # Debug mode disables concatenation and preprocessing of assets.
+  # This option may cause significant delays in view rendering with a large
+  # number of complex assets.
+  config.assets.debug = true
+
+  # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+  # yet still be able to expire them through the digest params.
+  config.assets.digest = true
+
+  # Adds additional error checking when serving assets at runtime.
+  # Checks for improperly declared sprockets dependencies.
+  # Raises helpful error messages.
+  config.assets.raise_runtime_errors = true
+
+  # Raises error for missing translations
+  # config.action_view.raise_on_missing_translations = true
+end
diff --git a/sample/rails/config/environments/production.rb b/sample/rails/config/environments/production.rb
new file mode 100644 (file)
index 0000000..5c1b32e
--- /dev/null
@@ -0,0 +1,79 @@
+Rails.application.configure do
+  # Settings specified here will take precedence over those in config/application.rb.
+
+  # Code is not reloaded between requests.
+  config.cache_classes = true
+
+  # Eager load code on boot. This eager loads most of Rails and
+  # your application in memory, allowing both threaded web servers
+  # and those relying on copy on write to perform better.
+  # Rake tasks automatically ignore this option for performance.
+  config.eager_load = true
+
+  # Full error reports are disabled and caching is turned on.
+  config.consider_all_requests_local       = false
+  config.action_controller.perform_caching = true
+
+  # Enable Rack::Cache to put a simple HTTP cache in front of your application
+  # Add `rack-cache` to your Gemfile before enabling this.
+  # For large-scale production use, consider using a caching reverse proxy like
+  # NGINX, varnish or squid.
+  # config.action_dispatch.rack_cache = true
+
+  # Disable serving static files from the `/public` folder by default since
+  # Apache or NGINX already handles this.
+  config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+  # Compress JavaScripts and CSS.
+  config.assets.js_compressor = :uglifier
+  # config.assets.css_compressor = :sass
+
+  # Do not fallback to assets pipeline if a precompiled asset is missed.
+  config.assets.compile = false
+
+  # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+  # yet still be able to expire them through the digest params.
+  config.assets.digest = true
+
+  # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
+
+  # Specifies the header that your server uses for sending files.
+  # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+  # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+  # config.force_ssl = true
+
+  # Use the lowest log level to ensure availability of diagnostic information
+  # when problems arise.
+  config.log_level = :debug
+
+  # Prepend all log lines with the following tags.
+  # config.log_tags = [ :subdomain, :uuid ]
+
+  # Use a different logger for distributed setups.
+  # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
+
+  # Use a different cache store in production.
+  # config.cache_store = :mem_cache_store
+
+  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+  # config.action_controller.asset_host = 'http://assets.example.com'
+
+  # Ignore bad email addresses and do not raise email delivery errors.
+  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+  # config.action_mailer.raise_delivery_errors = false
+
+  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+  # the I18n.default_locale when a translation cannot be found).
+  config.i18n.fallbacks = true
+
+  # Send deprecation notices to registered listeners.
+  config.active_support.deprecation = :notify
+
+  # Use default logging formatter so that PID and timestamp are not suppressed.
+  config.log_formatter = ::Logger::Formatter.new
+
+  # Do not dump schema after migrations.
+  config.active_record.dump_schema_after_migration = false
+end
diff --git a/sample/rails/config/environments/test.rb b/sample/rails/config/environments/test.rb
new file mode 100644 (file)
index 0000000..1c19f08
--- /dev/null
@@ -0,0 +1,42 @@
+Rails.application.configure do
+  # Settings specified here will take precedence over those in config/application.rb.
+
+  # The test environment is used exclusively to run your application's
+  # test suite. You never need to work with it otherwise. Remember that
+  # your test database is "scratch space" for the test suite and is wiped
+  # and recreated between test runs. Don't rely on the data there!
+  config.cache_classes = true
+
+  # Do not eager load code on boot. This avoids loading your whole application
+  # just for the purpose of running a single test. If you are using a tool that
+  # preloads Rails for running tests, you may have to set it to true.
+  config.eager_load = false
+
+  # Configure static file server for tests with Cache-Control for performance.
+  config.serve_static_files   = true
+  config.static_cache_control = 'public, max-age=3600'
+
+  # Show full error reports and disable caching.
+  config.consider_all_requests_local       = true
+  config.action_controller.perform_caching = false
+
+  # Raise exceptions instead of rendering exception templates.
+  config.action_dispatch.show_exceptions = false
+
+  # Disable request forgery protection in test environment.
+  config.action_controller.allow_forgery_protection = false
+
+  # Tell Action Mailer not to deliver emails to the real world.
+  # The :test delivery method accumulates sent emails in the
+  # ActionMailer::Base.deliveries array.
+  config.action_mailer.delivery_method = :test
+
+  # Randomize the order test cases are executed.
+  config.active_support.test_order = :random
+
+  # Print deprecation notices to the stderr.
+  config.active_support.deprecation = :stderr
+
+  # Raises error for missing translations
+  # config.action_view.raise_on_missing_translations = true
+end
diff --git a/sample/rails/config/initializers/assets.rb b/sample/rails/config/initializers/assets.rb
new file mode 100644 (file)
index 0000000..01ef3e6
--- /dev/null
@@ -0,0 +1,11 @@
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = '1.0'
+
+# Add additional assets to the asset load path
+# Rails.application.config.assets.paths << Emoji.images_path
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
+# Rails.application.config.assets.precompile += %w( search.js )
diff --git a/sample/rails/config/initializers/backtrace_silencers.rb b/sample/rails/config/initializers/backtrace_silencers.rb
new file mode 100644 (file)
index 0000000..59385cd
--- /dev/null
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
diff --git a/sample/rails/config/initializers/cookies_serializer.rb b/sample/rails/config/initializers/cookies_serializer.rb
new file mode 100644 (file)
index 0000000..7f70458
--- /dev/null
@@ -0,0 +1,3 @@
+# Be sure to restart your server when you modify this file.
+
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/sample/rails/config/initializers/filter_parameter_logging.rb b/sample/rails/config/initializers/filter_parameter_logging.rb
new file mode 100644 (file)
index 0000000..4a994e1
--- /dev/null
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Configure sensitive parameters which will be filtered from the log file.
+Rails.application.config.filter_parameters += [:password]
diff --git a/sample/rails/config/initializers/inflections.rb b/sample/rails/config/initializers/inflections.rb
new file mode 100644 (file)
index 0000000..ac033bf
--- /dev/null
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+#   inflect.plural /^(ox)$/i, '\1en'
+#   inflect.singular /^(ox)en/i, '\1'
+#   inflect.irregular 'person', 'people'
+#   inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+#   inflect.acronym 'RESTful'
+# end
diff --git a/sample/rails/config/initializers/mime_types.rb b/sample/rails/config/initializers/mime_types.rb
new file mode 100644 (file)
index 0000000..dc18996
--- /dev/null
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
diff --git a/sample/rails/config/initializers/session_store.rb b/sample/rails/config/initializers/session_store.rb
new file mode 100644 (file)
index 0000000..e766b67
--- /dev/null
@@ -0,0 +1,3 @@
+# Be sure to restart your server when you modify this file.
+
+Rails.application.config.session_store :cookie_store, key: '_dummy_session'
diff --git a/sample/rails/config/initializers/wrap_parameters.rb b/sample/rails/config/initializers/wrap_parameters.rb
new file mode 100644 (file)
index 0000000..33725e9
--- /dev/null
@@ -0,0 +1,14 @@
+# Be sure to restart your server when you modify this file.
+
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+  wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
+end
+
+# To enable root element in JSON for ActiveRecord objects.
+# ActiveSupport.on_load(:active_record) do
+#  self.include_root_in_json = true
+# end
diff --git a/sample/rails/config/locales/en.yml b/sample/rails/config/locales/en.yml
new file mode 100644 (file)
index 0000000..0653957
--- /dev/null
@@ -0,0 +1,23 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+#     I18n.t 'hello'
+#
+# In views, this is aliased to just `t`:
+#
+#     <%= t('hello') %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+#     I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# To learn more, please read the Rails Internationalization guide
+# available at http://guides.rubyonrails.org/i18n.html.
+
+en:
+  hello: "Hello world"
diff --git a/sample/rails/config/routes.rb b/sample/rails/config/routes.rb
new file mode 100644 (file)
index 0000000..2f1ce73
--- /dev/null
@@ -0,0 +1,8 @@
+Rails.application.routes.draw do
+  resources :users do
+    collection do
+      get :test
+    end
+  end
+  root to: 'users#index'
+end
diff --git a/sample/rails/config/secrets.yml b/sample/rails/config/secrets.yml
new file mode 100644 (file)
index 0000000..732e6a2
--- /dev/null
@@ -0,0 +1,22 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key is used for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+
+# Make sure the secret is at least 30 characters and all random,
+# no regular words or you'll be exposed to dictionary attacks.
+# You can use `rake secret` to generate a secure secret key.
+
+# Make sure the secrets in this file are kept private
+# if you're sharing your code publicly.
+
+development:
+  secret_key_base: c028217ed405ccb13aed84b82a47b491b28b9ba80b7594c7aa0bfe9fd77e979e862bdb122cf3e6a1c892a385fb671a397c9dfc45f847b0568cbd6e7317f23b71
+
+test:
+  secret_key_base: 3505e45a393a297d18c990643916a33116eb1845ceaadece39f2f24a29e1613e3819269098a0df96b6ba61a689e699b31e8258cf3aa5cffef43af12e53e0953c
+
+# Do not keep production secrets in the repository,
+# instead read values from the environment.
+production:
+  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
diff --git a/sample/rails/db/migrate/20151119022746_create_users.rb b/sample/rails/db/migrate/20151119022746_create_users.rb
new file mode 100644 (file)
index 0000000..e6f0ee5
--- /dev/null
@@ -0,0 +1,8 @@
+class CreateUsers < ActiveRecord::Migration
+  def change
+    create_table :users do |t|
+
+      t.timestamps null: false
+    end
+  end
+end
diff --git a/sample/rails/db/schema.rb b/sample/rails/db/schema.rb
new file mode 100644 (file)
index 0000000..6d7aef8
--- /dev/null
@@ -0,0 +1,21 @@
+# encoding: UTF-8
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 20151119022746) do
+
+  create_table "users", force: :cascade do |t|
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
+end
diff --git a/sample/rails/db/seeds.rb b/sample/rails/db/seeds.rb
new file mode 100644 (file)
index 0000000..4edb1e8
--- /dev/null
@@ -0,0 +1,7 @@
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
+#
+# Examples:
+#
+#   cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
+#   Mayor.create(name: 'Emanuel', city: cities.first)
diff --git a/sample/rails/lib/assets/.keep b/sample/rails/lib/assets/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/lib/tasks/.keep b/sample/rails/lib/tasks/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/log/.keep b/sample/rails/log/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/public/404.html b/sample/rails/public/404.html
new file mode 100644 (file)
index 0000000..b612547
--- /dev/null
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>The page you were looking for doesn't exist (404)</title>
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <style>
+  body {
+    background-color: #EFEFEF;
+    color: #2E2F30;
+    text-align: center;
+    font-family: arial, sans-serif;
+    margin: 0;
+  }
+
+  div.dialog {
+    width: 95%;
+    max-width: 33em;
+    margin: 4em auto 0;
+  }
+
+  div.dialog > div {
+    border: 1px solid #CCC;
+    border-right-color: #999;
+    border-left-color: #999;
+    border-bottom-color: #BBB;
+    border-top: #B00100 solid 4px;
+    border-top-left-radius: 9px;
+    border-top-right-radius: 9px;
+    background-color: white;
+    padding: 7px 12% 0;
+    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+  }
+
+  h1 {
+    font-size: 100%;
+    color: #730E15;
+    line-height: 1.5em;
+  }
+
+  div.dialog > p {
+    margin: 0 0 1em;
+    padding: 1em;
+    background-color: #F7F7F7;
+    border: 1px solid #CCC;
+    border-right-color: #999;
+    border-left-color: #999;
+    border-bottom-color: #999;
+    border-bottom-left-radius: 4px;
+    border-bottom-right-radius: 4px;
+    border-top-color: #DADADA;
+    color: #666;
+    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+  }
+  </style>
+</head>
+
+<body>
+  <!-- This file lives in public/404.html -->
+  <div class="dialog">
+    <div>
+      <h1>The page you were looking for doesn't exist.</h1>
+      <p>You may have mistyped the address or the page may have moved.</p>
+    </div>
+    <p>If you are the application owner check the logs for more information.</p>
+  </div>
+</body>
+</html>
diff --git a/sample/rails/public/422.html b/sample/rails/public/422.html
new file mode 100644 (file)
index 0000000..a21f82b
--- /dev/null
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>The change you wanted was rejected (422)</title>
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <style>
+  body {
+    background-color: #EFEFEF;
+    color: #2E2F30;
+    text-align: center;
+    font-family: arial, sans-serif;
+    margin: 0;
+  }
+
+  div.dialog {
+    width: 95%;
+    max-width: 33em;
+    margin: 4em auto 0;
+  }
+
+  div.dialog > div {
+    border: 1px solid #CCC;
+    border-right-color: #999;
+    border-left-color: #999;
+    border-bottom-color: #BBB;
+    border-top: #B00100 solid 4px;
+    border-top-left-radius: 9px;
+    border-top-right-radius: 9px;
+    background-color: white;
+    padding: 7px 12% 0;
+    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+  }
+
+  h1 {
+    font-size: 100%;
+    color: #730E15;
+    line-height: 1.5em;
+  }
+
+  div.dialog > p {
+    margin: 0 0 1em;
+    padding: 1em;
+    background-color: #F7F7F7;
+    border: 1px solid #CCC;
+    border-right-color: #999;
+    border-left-color: #999;
+    border-bottom-color: #999;
+    border-bottom-left-radius: 4px;
+    border-bottom-right-radius: 4px;
+    border-top-color: #DADADA;
+    color: #666;
+    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+  }
+  </style>
+</head>
+
+<body>
+  <!-- This file lives in public/422.html -->
+  <div class="dialog">
+    <div>
+      <h1>The change you wanted was rejected.</h1>
+      <p>Maybe you tried to change something you didn't have access to.</p>
+    </div>
+    <p>If you are the application owner check the logs for more information.</p>
+  </div>
+</body>
+</html>
diff --git a/sample/rails/public/500.html b/sample/rails/public/500.html
new file mode 100644 (file)
index 0000000..061abc5
--- /dev/null
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>We're sorry, but something went wrong (500)</title>
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <style>
+  body {
+    background-color: #EFEFEF;
+    color: #2E2F30;
+    text-align: center;
+    font-family: arial, sans-serif;
+    margin: 0;
+  }
+
+  div.dialog {
+    width: 95%;
+    max-width: 33em;
+    margin: 4em auto 0;
+  }
+
+  div.dialog > div {
+    border: 1px solid #CCC;
+    border-right-color: #999;
+    border-left-color: #999;
+    border-bottom-color: #BBB;
+    border-top: #B00100 solid 4px;
+    border-top-left-radius: 9px;
+    border-top-right-radius: 9px;
+    background-color: white;
+    padding: 7px 12% 0;
+    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+  }
+
+  h1 {
+    font-size: 100%;
+    color: #730E15;
+    line-height: 1.5em;
+  }
+
+  div.dialog > p {
+    margin: 0 0 1em;
+    padding: 1em;
+    background-color: #F7F7F7;
+    border: 1px solid #CCC;
+    border-right-color: #999;
+    border-left-color: #999;
+    border-bottom-color: #999;
+    border-bottom-left-radius: 4px;
+    border-bottom-right-radius: 4px;
+    border-top-color: #DADADA;
+    color: #666;
+    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+  }
+  </style>
+</head>
+
+<body>
+  <!-- This file lives in public/500.html -->
+  <div class="dialog">
+    <div>
+      <h1>We're sorry, but something went wrong.</h1>
+    </div>
+    <p>If you are the application owner check the logs for more information.</p>
+  </div>
+</body>
+</html>
diff --git a/sample/rails/public/favicon.ico b/sample/rails/public/favicon.ico
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/public/robots.txt b/sample/rails/public/robots.txt
new file mode 100644 (file)
index 0000000..3c9c7c0
--- /dev/null
@@ -0,0 +1,5 @@
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/sample/rails/vendor/assets/javascripts/.keep b/sample/rails/vendor/assets/javascripts/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/rails/vendor/assets/stylesheets/.keep b/sample/rails/vendor/assets/stylesheets/.keep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sample/sinatra/Gemfile b/sample/sinatra/Gemfile
new file mode 100644 (file)
index 0000000..0552826
--- /dev/null
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+gem 'sinatra'
+gem 'hamlit', path: '../..'
diff --git a/sample/sinatra/Gemfile.lock b/sample/sinatra/Gemfile.lock
new file mode 100644 (file)
index 0000000..27d7a98
--- /dev/null
@@ -0,0 +1,31 @@
+PATH
+  remote: ../..
+  specs:
+    hamlit (0.1.0)
+      escape_utils
+      temple (~> 0.7.6)
+      tilt
+
+GEM
+  remote: https://rubygems.org/
+  specs:
+    escape_utils (1.1.0)
+    rack (1.6.4)
+    rack-protection (1.5.3)
+      rack
+    sinatra (1.4.6)
+      rack (~> 1.4)
+      rack-protection (~> 1.4)
+      tilt (>= 1.3, < 3)
+    temple (0.7.6)
+    tilt (2.0.1)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  hamlit!
+  sinatra
+
+BUNDLED WITH
+   1.10.6
diff --git a/sample/sinatra/app.rb b/sample/sinatra/app.rb
new file mode 100644 (file)
index 0000000..380cba0
--- /dev/null
@@ -0,0 +1,21 @@
+require 'sinatra'
+require 'hamlit'
+
+User = Struct.new(:id, :name)
+
+set :haml, format: :html5
+
+get '/' do
+  @notice = 'hello'
+  @users = [
+    User.new(1, 'k0kubun'),
+    User.new(2, 'hello'),
+  ]
+  haml :index
+end
+
+get '/center' do
+  @notice = 'centering'
+  @users = [User.new(3, 'neko')]
+  haml :index, layout: :center
+end
diff --git a/sample/sinatra/views/center.haml b/sample/sinatra/views/center.haml
new file mode 100644 (file)
index 0000000..2de547c
--- /dev/null
@@ -0,0 +1,7 @@
+!!!
+%html
+  %head
+    %title layout.haml
+  %body
+    %center
+      = yield
diff --git a/sample/sinatra/views/index.haml b/sample/sinatra/views/index.haml
new file mode 100644 (file)
index 0000000..c650e3e
--- /dev/null
@@ -0,0 +1,14 @@
+%p#notice= @notice
+
+%h1 Listing Users
+
+%table
+  %thead
+    %tr
+      %th{ colspan: "2" }
+
+  %tbody
+    - @users.each do |user|
+      %tr
+        %td= user.id
+        %td= user.name
diff --git a/sample/sinatra/views/layout.haml b/sample/sinatra/views/layout.haml
new file mode 100644 (file)
index 0000000..3f8edb7
--- /dev/null
@@ -0,0 +1,6 @@
+!!!
+%html
+  %head
+    %title layout.haml
+  %body
+    != yield
diff --git a/test/haml/MIT-LICENSE b/test/haml/MIT-LICENSE
new file mode 100644 (file)
index 0000000..758713a
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright (c) 2006-2009 Hampton Catlin and Natalie Weizenbaum
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/test/haml/README.md b/test/haml/README.md
new file mode 100644 (file)
index 0000000..9255e60
--- /dev/null
@@ -0,0 +1,28 @@
+# test/haml/\*\*/\*
+
+All tests in this directory is originally in haml gem.
+
+## License
+
+test/haml/\*\*/\* is:
+
+Copyright (c) 2006-2009 Hampton Catlin and Natalie Weizenbaum
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/test/haml/engine_test.rb b/test/haml/engine_test.rb
new file mode 100644 (file)
index 0000000..ff0de03
--- /dev/null
@@ -0,0 +1,2108 @@
+$:.unshift __dir__
+
+require 'test_helper'
+
+class EngineTest < Haml::TestCase
+  # A map of erroneous Haml documents to the error messages they should produce.
+  # The error messages may be arrays;
+  # if so, the second element should be the line number that should be reported for the error.
+  # If this isn't provided, the tests will assume the line number should be the last line of the document.
+  EXCEPTION_MAP = {
+    "!!!\n  a"                                             => error(:illegal_nesting_header),
+    "a\n  b"                                               => error(:illegal_nesting_plain),
+    "/ a\n  b"                                             => error(:illegal_nesting_content),
+    "% a"                                                  => error(:invalid_tag, '% a'),
+    "%p a\n  b"                                            => error(:illegal_nesting_line, 'p'),
+    "%p="                                                  => error(:no_ruby_code, '='),
+    "%p~"                                                  => error(:no_ruby_code, '~'),
+    "~"                                                    => error(:no_ruby_code, '~'),
+    "="                                                    => error(:no_ruby_code, '='),
+    "%p/\n  a"                                             => error(:illegal_nesting_self_closing),
+    #":a\n  b"                                              => [error(:filter_not_defined, 'a'), 1],
+    ":a= b"                                                => error(:invalid_filter_name, 'a= b'),
+    "."                                                    => error(:illegal_element),
+    ".#"                                                   => error(:illegal_element),
+    ".{} a"                                                => error(:illegal_element),
+    ".() a"                                                => error(:illegal_element),
+    ".= a"                                                 => error(:illegal_element),
+    "%p..a"                                                => error(:illegal_element),
+    "%a/ b"                                                => error(:self_closing_content),
+    " %p foo"                                              => error(:indenting_at_start),
+    "  %p foo"                                             => error(:indenting_at_start),
+    "- end"                                                => error(:no_end),
+    "%p{:a => 'b',\n:c => 'd'}/ e"                         => [error(:self_closing_content), 2],
+    "%p{:a => 'b',\n:c => 'd'}="                           => [error(:no_ruby_code, '='), 2],
+    "%p.{:a => 'b',\n:c => 'd'} e"                         => [error(:illegal_element), 1],
+    "%p{:a => 'b',\n:c => 'd',\n:e => 'f'}\n%p/ a"         => [error(:self_closing_content), 4],
+    "%p{:a => 'b',\n:c => 'd',\n:e => 'f'}\n- raise 'foo'" => ["foo", 4],
+    "%p{:a => 'b',\n:c => raise('foo'),\n:e => 'f'}"       => ["foo", 2],
+    "%p{:a => 'b',\n:c => 'd',\n:e => raise('foo')}"       => ["foo", 3],
+    " \n\t\n %p foo"                                       => [error(:indenting_at_start), 3],
+    "\n\n %p foo"                                          => [error(:indenting_at_start), 3],
+    "%p\n  foo\n foo"                                      => [error(:inconsistent_indentation, "1 space", "2 spaces"), 3],
+    "%p\n  foo\n%p\n foo"                                  => [error(:inconsistent_indentation, "1 space", "2 spaces"), 4],
+    "%p\n\t\tfoo\n\tfoo"                                   => [error(:inconsistent_indentation, "1 tab", "2 tabs"), 3],
+    "%p\n  foo\n   foo"                                    => [error(:inconsistent_indentation, "3 spaces", "2 spaces"), 3],
+    "%p\n  foo\n  %p\n   bar"                              => [error(:inconsistent_indentation, "3 spaces", "2 spaces"), 4],
+    "%p\n  :plain\n     bar\n   \t  baz"                   => [error(:inconsistent_indentation, '"   \t  "', "2 spaces"), 4],
+    "%p\n  foo\n%p\n    bar"                               => [error(:deeper_indenting, 2), 4],
+    "%p\n  foo\n  %p\n        bar"                         => [error(:deeper_indenting, 3), 4],
+    "%p\n \tfoo"                                           => [error(:cant_use_tabs_and_spaces), 2],
+    "%p("                                                  => error(:invalid_attribute_list, '"("'),
+    "%p(foo=)"                                             => error(:invalid_attribute_list, '"(foo=)"'),
+    "%p(foo 'bar')"                                        => error(:invalid_attribute_list, '"(foo \'bar\')"'),
+    "%p(foo=\nbar)"                                        => [error(:invalid_attribute_list, '"(foo="'), 1],
+    "%p(foo 'bar'\nbaz='bang')"                            => [error(:invalid_attribute_list, '"(foo \'bar\'"'), 1],
+    "%p(foo='bar'\nbaz 'bang'\nbip='bop')"                 => [error(:invalid_attribute_list, '"(foo=\'bar\' baz \'bang\'"'), 2],
+    "%p{'foo' => 'bar' 'bar' => 'baz'}"                    => :compile,
+    "%p{:foo => }"                                         => :compile,
+    "%p{=> 'bar'}"                                         => :compile,
+    "%p{'foo => 'bar'}"                                    => :compile,
+    "%p{:foo => 'bar}"                                     => :compile,
+    "%p{:foo => 'bar\"}"                                   => :compile,
+    # Regression tests
+    "foo\n\n\n  bar"                                       => [error(:illegal_nesting_plain), 4],
+    "%p/\n\n  bar"                                         => [error(:illegal_nesting_self_closing), 3],
+    "%p foo\n\n  bar"                                      => [error(:illegal_nesting_line, 'p'), 3],
+    "/ foo\n\n  bar"                                       => [error(:illegal_nesting_content), 3],
+    "!!!\n\n  bar"                                         => [error(:illegal_nesting_header), 3],
+    "- raise 'foo'\n\n\n\nbar"                             => ["foo", 1],
+    "= 'foo'\n-raise 'foo'"                                => ["foo", 2],
+    "\n\n\n- raise 'foo'"                                  => ["foo", 4],
+    "%p foo |\n   bar |\n   baz |\nbop\n- raise 'foo'"     => ["foo", 5],
+    #"foo\n:ruby\n  1\n  2\n  3\n- raise 'foo'"             => ["foo", 6],
+    #"foo\n:erb\n  1\n  2\n  3\n- raise 'foo'"              => ["foo", 6],
+    "foo\n:plain\n  1\n  2\n  3\n- raise 'foo'"            => ["foo", 6],
+    "foo\n:plain\n  1\n  2\n  3\n4\n- raise 'foo'"         => ["foo", 7],
+    "foo\n:plain\n  1\n  2\n  3\#{''}\n- raise 'foo'"      => ["foo", 6],
+    "foo\n:plain\n  1\n  2\n  3\#{''}\n4\n- raise 'foo'"   => ["foo", 7],
+    "foo\n:plain\n  1\n  2\n  \#{raise 'foo'}"             => ["foo", 5],
+    "= raise 'foo'\nfoo\nbar\nbaz\nbang"                   => ["foo", 1],
+    "- case 1\n\n- when 1\n  - raise 'foo'"                => ["foo", 4],
+  }
+
+  User = Struct.new('User', :id)
+  class CustomHamlClass < Struct.new(:id)
+    def haml_object_ref
+      "my_thing"
+    end
+  end
+  CpkRecord = Struct.new('CpkRecord', :id) do
+    def to_key
+      [*self.id] unless id.nil?
+    end
+  end
+
+  def use_test_tracing(options)
+    unless options[:filename]
+      # use caller method name as fake filename. useful for debugging
+      i = -1
+      caller[i+=1] =~ /`(.+?)'/ until $1 and $1.index('test_') == 0
+      options[:filename] = "(#{$1})"
+    end
+    options
+  end
+
+  def render(text, options = {}, &block)
+    options = use_test_tracing(options)
+    super
+  end
+
+  def engine(text, options = {})
+    options = use_test_tracing(options)
+    Hamlit::Template.new(hamlit_base.merge(options)) { text }
+  end
+
+  def setup
+    @old_default_internal = Encoding.default_internal
+    silence_warnings{Encoding.default_internal = nil}
+  end
+
+  def teardown
+    silence_warnings{Encoding.default_internal = @old_default_internal}
+  end
+
+  def test_empty_render
+    assert_equal "", render("")
+  end
+
+  def test_flexible_tabulation
+    assert_haml_ugly("%p\n foo\n%q\n bar\n %a\n  baz")
+    assert_haml_ugly("%p\n\tfoo\n%q\n\tbar\n\t%a\n\t\tbaz")
+    assert_haml_ugly("%p\n  :plain\n        \t \t bar\n     baz")
+  end
+
+  def test_empty_render_should_remain_empty
+    assert_equal('', render(''))
+  end
+
+  def test_attributes_should_render_correctly
+    assert_equal("<div class='atlantis' style='ugly'></div>", render(".atlantis{:style => 'ugly'}").chomp)
+  end
+
+  def test_css_id_as_attribute_should_be_appended_with_underscore
+    assert_equal("<div id='my_id_1'></div>", render("#my_id{:id => '1'}").chomp)
+    assert_equal("<div id='my_id_1'></div>", render("#my_id{:id => 1}").chomp)
+  end
+
+  def test_ruby_code_should_work_inside_attributes
+    assert_equal("<p class='3'>foo</p>", render("%p{:class => 1+2} foo").chomp)
+  end
+
+  def test_class_attr_with_array
+    assert_equal("<p class='a b'>foo</p>\n", render("%p{:class => %w[a b]} foo")) # basic
+    assert_equal("<p class='a b css'>foo</p>\n", render("%p.css{:class => %w[a b]} foo")) # merge with css
+    assert_equal("<p class='b css'>foo</p>\n", render("%p.css{:class => %w[css b]} foo")) # merge uniquely
+    assert_equal("<p class='a b c d'>foo</p>\n", render("%p{:class => [%w[a b], %w[c d]]} foo")) # flatten
+    assert_equal("<p class='a b'>foo</p>\n", render("%p{:class => [:a, :b] } foo")) # stringify
+    # [INCOMPATIBILITY] Hamlit limits boolean attributes
+    # assert_equal("<p>foo</p>\n", render("%p{:class => [nil, false] } foo")) # strip falsey
+    assert_equal("<p class=''>foo</p>\n", render("%p{:class => [nil, false] } foo")) # strip falsey
+    assert_equal("<p class='a'>foo</p>\n", render("%p{:class => :a} foo")) # single stringify
+    # [INCOMPATIBILITY] Hamlit limits boolean attributes
+    # assert_equal("<p>foo</p>\n", render("%p{:class => false} foo")) # single falsey
+    assert_equal("<p class=''>foo</p>\n", render("%p{:class => false} foo")) # single falsey
+    assert_equal("<p class='a b html'>foo</p>\n", render("%p(class='html'){:class => %w[a b]} foo")) # html attrs
+  end
+
+  def test_id_attr_with_array
+    assert_equal("<p id='a_b'>foo</p>\n", render("%p{:id => %w[a b]} foo")) # basic
+    assert_equal("<p id='css_a_b'>foo</p>\n", render("%p#css{:id => %w[a b]} foo")) # merge with css
+    assert_equal("<p id='a_b_c_d'>foo</p>\n", render("%p{:id => [%w[a b], %w[c d]]} foo")) # flatten
+    assert_equal("<p id='a_b'>foo</p>\n", render("%p{:id => [:a, :b] } foo")) # stringify
+    # [INCOMPATIBILITY] Hamlit limits boolean attributes
+    # assert_equal("<p>foo</p>\n", render("%p{:id => [nil, false] } foo")) # strip falsey
+    assert_equal("<p id=''>foo</p>\n", render("%p{:id => [nil, false] } foo")) # strip falsey
+    assert_equal("<p id='a'>foo</p>\n", render("%p{:id => :a} foo")) # single stringify
+    # [INCOMPATIBILITY] Hamlit limits boolean attributes
+    # assert_equal("<p>foo</p>\n", render("%p{:id => false} foo")) # single falsey
+    assert_equal("<p id=''>foo</p>\n", render("%p{:id => false} foo")) # single falsey
+    assert_equal("<p id='html_a_b'>foo</p>\n", render("%p(id='html'){:id => %w[a b]} foo")) # html attrs
+  end
+
+  def test_colon_in_class_attr
+    assert_equal("<p class='foo:bar'>\n", render("%p.foo:bar/"))
+  end
+
+  def test_colon_in_id_attr
+    assert_equal("<p id='foo:bar'>\n", render("%p#foo:bar/"))
+  end
+
+  def test_dynamic_attributes_with_no_content
+    assert_haml_ugly(<<HAML)
+%p
+  %a{:href => "http://" + "haml.info"}
+HAML
+  end
+
+  def test_attributes_with_to_s
+    assert_equal(<<HTML, render(<<HAML))
+<p id='foo_2'></p>
+<p class='2 foo'></p>
+<p blaz='2'></p>
+<p 2='2'></p>
+HTML
+%p#foo{:id => 1+1}
+%p.foo{:class => 1+1}
+%p{:blaz => 1+1}
+%p{(1+1) => 1+1}
+HAML
+  end
+
+  def test_nil_should_render_empty_tag
+    # [INCOMPATIBILITY] Hamlit limits boolean attributes
+    # assert_equal("<div class='no_attributes'></div>",
+    #              render(".no_attributes{:nil => nil}").chomp)
+    assert_equal("<div class='no_attributes' nil=''></div>",
+                 render(".no_attributes{:nil => nil}").chomp)
+  end
+
+  def test_strings_should_get_stripped_inside_tags
+    assert_equal("<div class='stripped'>This should have no spaces in front of it</div>",
+                 render(".stripped    This should have no spaces in front of it").chomp)
+  end
+
+  def test_one_liner_should_be_one_line
+    assert_equal("<p>Hello</p>", render('%p Hello').chomp)
+  end
+
+  def test_one_liner_with_newline_shouldnt_be_one_line
+    assert_haml_ugly('%p= "foo\nbar"')
+  end
+
+  def test_multi_render; skip
+    engine = engine("%strong Hi there!")
+    assert_equal("<strong>Hi there!</strong>\n", engine.render)
+    assert_equal("<strong>Hi there!</strong>\n", engine.render)
+    assert_equal("<strong>Hi there!</strong>\n", engine.render)
+  end
+
+  def test_interpolation
+    assert_haml_ugly('%p Hello #{who}', locals: {who: 'World'}, escape_html: false)
+    assert_haml_ugly("%p\n  Hello \#{who}", locals: {who: 'World'}, escape_html: false)
+    assert_haml_ugly('%p Hello #{who}', locals: {who: 'World'}, escape_html: true)
+    assert_haml_ugly("%p\n  Hello \#{who}", locals: {who: 'World'}, escape_html: true)
+  end
+
+  def test_interpolation_with_instance_var; skip # special interpolation
+    scope = Object.new
+    scope.instance_variable_set(:@who, 'World')
+
+    assert_equal("<p>Hello World</p>\n", render('%p Hello #@who', scope: scope, escape_html: false))
+    assert_equal("<p>\n  Hello World\n</p>\n", render("%p\n  Hello \#@who", scope: scope, escape_html: false))
+    assert_equal("<p>Hello World</p>\n", render('%p Hello #@who', scope: scope, escape_html: true))
+    assert_equal("<p>\n  Hello World\n</p>\n", render("%p\n  Hello \#@who", scope: scope, escape_html: true))
+  end
+
+  def test_interpolation_with_global; skip # special interpolation
+    $global_var_for_testing = 'World'
+
+    assert_equal("<p>Hello World</p>\n", render('%p Hello #$global_var_for_testing', escape_html: false))
+    assert_equal("<p>\n  Hello World\n</p>\n", render("%p\n  Hello \#$global_var_for_testing", escape_html: false))
+    assert_equal("<p>Hello World</p>\n", render('%p Hello #$global_var_for_testing', escape_html: true))
+    assert_equal("<p>\n  Hello World\n</p>\n", render("%p\n  Hello \#$global_var_for_testing", escape_html: true))
+  ensure
+    $global_var_for_testing = nil
+  end
+
+  def test_interpolation_in_the_middle_of_a_string
+    assert_equal("\"title 'Title'. \"\n",
+                 render("\"title '\#{\"Title\"}'. \""))
+  end
+
+  def test_interpolation_with_instance_var_in_the_middle_of_a_string; skip # special interpolation
+    scope = Object.new
+    scope.instance_variable_set(:@title, 'Title')
+
+    assert_equal("\"title 'Title'. \"\n",
+                 render("\"title '\#@title'. \"",  :scope => scope))
+  end
+
+  def test_interpolation_with_global_in_the_middle_of_a_string; skip # special interpolation
+    $global_var_for_testing = 'Title'
+
+    assert_equal("\"title 'Title'. \"\n",
+                 render("\"title '\#$global_var_for_testing'. \""))
+  ensure
+    $global_var_for_testing = nil
+  end
+
+  def test_interpolation_at_the_beginning_of_a_line
+    assert_haml_ugly('%p #{1 + 1}')
+    assert_haml_ugly("%p\n  \#{1 + 1}")
+  end
+
+  def test_interpolation_with_instance_var_at_the_beginning_of_a_line; skip # special interpolation
+    scope = Object.new
+    scope.instance_variable_set(:@foo, 2)
+
+    assert_equal("<p>2</p>\n", render('%p #@foo', :scope => scope))
+    assert_equal("<p>\n  2\n</p>\n", render("%p\n  \#@foo", :scope => scope))
+  end
+
+  def test_interpolation_with_global_at_the_beginning_of_a_line; skip # special interpolation
+    $global_var_for_testing = 2
+
+    assert_equal("<p>2</p>\n", render('%p #$global_var_for_testing'))
+    assert_equal("<p>\n  2\n</p>\n", render("%p\n  \#$global_var_for_testing"))
+  ensure
+    $global_var_for_testing = nil
+  end
+
+  def test_escaped_interpolation
+    assert_equal("<p>Foo &amp; Bar & Baz</p>\n", render('%p& Foo #{"&"} Bar & Baz'))
+  end
+
+  def test_nil_tag_value_should_render_as_empty
+    assert_equal("<p></p>\n", render("%p= nil"))
+  end
+
+  def test_tag_with_failed_if_should_render_as_empty
+    assert_equal("<p></p>\n", render("%p= 'Hello' if false"))
+  end
+
+  def test_static_attributes_with_empty_attr
+    assert_equal("<img alt='' src='/foo.png'>\n", render("%img{:src => '/foo.png', :alt => ''}"))
+  end
+
+  def test_dynamic_attributes_with_empty_attr
+    # [INCOMPATIBILITY] Hamlit limits boolean attributes
+    # assert_equal("<img alt='' src='/foo.png'>\n", render("%img{:width => nil, :src => '/foo.png', :alt => String.new}"))
+    assert_equal("<img alt='' src='/foo.png' width=''>\n", render("%img{:width => nil, :src => '/foo.png', :alt => String.new}"))
+  end
+
+  def test_attribute_hash_with_newlines
+    assert_haml_ugly("%p{:a => 'b',\n   :c => 'd'} foop")
+    assert_haml_ugly("%p{:a => 'b',\n   :c => 'd'}\n  foop")
+    assert_haml_ugly("%p{:a => 'b',\n   :c => 'd'}/")
+    assert_haml_ugly("%p{:a => 'b',\n   :c => 'd',\n   :e => 'f'}")
+  end
+
+  def test_attr_hashes_not_modified
+    hash = {:color => 'red'}
+    assert_haml_ugly(<<HAML, :locals => {:hash => hash})
+<div color='red'></div>
+<div class='special' color='red'></div>
+<div color='red'></div>
+HTML
+%div{hash}
+.special{hash}
+%div{hash}
+HAML
+    assert_equal(hash, {:color => 'red'})
+  end
+
+  def test_ugly_semi_prerendered_tags
+    assert_equal(<<HTML, render(<<HAML, :ugly => true))
+<p a='2'></p>
+<p a='2'>foo</p>
+<p a='2'>
+<p a='2'>foo</p>
+<p a='2'>foo
+bar</p>
+<p a='2'>foo
+bar</p>
+<p a='2'>
+foo
+</p>
+HTML
+%p{:a => 1 + 1}
+%p{:a => 1 + 1} foo
+%p{:a => 1 + 1}/
+%p{:a => 1 + 1}= "foo"
+%p{:a => 1 + 1}= "foo\\nbar"
+%p{:a => 1 + 1}~ "foo\\nbar"
+%p{:a => 1 + 1}
+  foo
+HAML
+  end
+
+  def test_end_of_file_multiline
+    assert_equal("<p>0</p>\n<p>1</p>\n<p>2</p>\n", render("- for i in (0...3)\n  %p= |\n   i |"))
+  end
+
+  def test_cr_newline
+    assert_equal("<p>foo</p>\n<p>bar</p>\n<p>baz</p>\n<p>boom</p>\n", render("%p foo\r%p bar\r\n%p baz\n\r%p boom"))
+  end
+
+  def test_textareas; skip # script bug
+    assert_equal("<textarea>Foo&#x000A;  bar&#x000A;   baz</textarea>\n",
+                 render('%textarea= "Foo\n  bar\n   baz"'))
+
+    assert_equal("<pre>Foo&#x000A;  bar&#x000A;   baz</pre>\n",
+                 render('%pre= "Foo\n  bar\n   baz"'))
+
+    assert_equal("<textarea>#{'a' * 100}</textarea>\n",
+                 render("%textarea #{'a' * 100}"))
+
+    assert_equal("<p>\n  <textarea>Foo\n  Bar\n  Baz</textarea>\n</p>\n", render(<<SOURCE))
+%p
+  %textarea
+    Foo
+    Bar
+    Baz
+SOURCE
+  end
+
+  def test_pre_code; skip # filter
+    assert_equal(<<HTML, render(<<HAML))
+<pre><code>Foo&#x000A;  bar&#x000A;    baz</code></pre>
+HTML
+%pre
+  %code
+    :preserve
+      Foo
+        bar
+          baz
+HAML
+  end
+
+  def test_boolean_attributes
+    # [INCOMPATIBILITY] Hamlit limits boolean attributes
+    # assert_equal("<p bar baz='true' foo='bar'></p>\n",
+    #              render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :html4))
+    # assert_equal("<p bar='bar' baz='true' foo='bar'></p>\n",
+    #              render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :xhtml))
+    #
+    # assert_equal("<p baz='false' foo='bar'></p>\n",
+    #              render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :html4))
+    # assert_equal("<p baz='false' foo='bar'></p>\n",
+    #              render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :xhtml))
+
+    assert_equal("<p bar='true' baz='true' foo='bar'></p>\n",
+                 render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :html4))
+    assert_equal("<p bar='true' baz='true' foo='bar'></p>\n",
+                 render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :xhtml))
+
+    assert_equal("<p bar='false' baz='false' foo='bar'></p>\n",
+                 render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :html4))
+    assert_equal("<p bar='false' baz='false' foo='bar'></p>\n",
+                 render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :xhtml))
+  end
+
+  def test_nuke_inner_whitespace_in_loops
+    assert_equal(<<HTML, render(<<HAML))
+<ul>foobarbaz</ul>
+HTML
+%ul<
+  - for str in %w[foo bar baz]
+    = str
+HAML
+  end
+
+  def test_both_whitespace_nukes_work_together; skip # dynamic indentation
+    assert_equal(<<RESULT, render(<<SOURCE))
+<p><q>Foo
+  Bar</q></p>
+RESULT
+%p
+  %q><= "Foo\\nBar"
+SOURCE
+  end
+
+  def test_nil_option
+    assert_equal("<p foo='bar'></p>\n", render('%p{:foo => "bar"}', :attr_wrapper => nil))
+  end
+
+  def test_comment_with_crazy_nesting
+    assert_equal(<<HTML, render(<<HAML))
+foo
+bar
+HTML
+foo
+-#
+  ul
+    %li{
+  foo
+bar
+HAML
+  end
+
+  # Regression tests
+
+  def test_indentation_after_dynamic_attr_hash; skip # dynamic indentation
+    assert_haml_ugly(<<HAML)
+%html
+  %body
+    %img{:src => 'te'+'st'}
+    = "foo\\nbar"
+HAML
+  end
+
+  def test_whitespace_nuke_with_both_newlines; skip # script bug # runtime nuke
+    assert_equal("<p>foo</p>\n", render('%p<= "\nfoo\n"'))
+    assert_equal(<<HTML, render(<<HAML))
+<p>
+  <p>foo</p>
+</p>
+HTML
+%p
+  %p<= "\\nfoo\\n"
+HAML
+  end
+
+  def test_whitespace_nuke_with_tags_and_else
+    assert_haml_ugly(<<HAML)
+%a
+  %b<
+    - if false
+      = "foo"
+    - else
+      foo
+HAML
+
+    assert_haml_ugly(<<HAML)
+%a
+  %b
+    - if false
+      = "foo"
+    - else
+      foo
+HAML
+  end
+
+  def test_outer_whitespace_nuke_with_empty_script; skip # runtime nuke
+    assert_equal(<<HTML, render(<<HAML))
+<p>
+  foo<a></a></p>
+HTML
+%p
+  foo
+  = "  "
+  %a>
+HAML
+  end
+
+  def test_both_case_indentation_work_with_deeply_nested_code
+    assert_haml_ugly(<<HAML)
+- case 'other'
+- when 'test'
+  %h2
+    hi
+- when 'other'
+  %h2
+    other
+HAML
+    assert_haml_ugly(<<HAML)
+- case 'other'
+  - when 'test'
+    %h2
+      hi
+  - when 'other'
+    %h2
+      other
+HAML
+  end
+
+  def test_equals_block_with_ugly; skip # haml helper # block script
+    assert_equal("foo\n", render(<<HAML, :ugly => true))
+= capture_haml do
+  foo
+HAML
+  end
+
+  def test_plain_equals_with_ugly
+    assert_equal("foo\nbar\n", render(<<HAML, :ugly => true))
+= "foo"
+bar
+HAML
+  end
+
+  def test_inline_if
+    assert_equal(<<HTML, render(<<HAML))
+<p>One</p>
+<p></p>
+<p>Three</p>
+HTML
+- for name in ["One", "Two", "Three"]
+  %p= name unless name == "Two"
+HAML
+  end
+
+  def test_end_with_method_call; skip # block script # silent script
+    assert_equal(<<HTML, render(<<HAML))
+2|3|4
+b-a-r
+HTML
+= [1, 2, 3].map do |i|
+  - i + 1
+- end.join("|")
+= "bar".gsub(/./) do |s|
+  - s + "-"
+- end.gsub(/-$/) do |s|
+  - ''
+HAML
+  end
+
+  def test_nested_end_with_method_call; skip # block script # silent script
+    assert_equal(<<HTML, render(<<HAML))
+<p>
+  2|3|4
+  b-a-r
+</p>
+HTML
+%p
+  = [1, 2, 3].map do |i|
+    - i + 1
+  - end.join("|")
+  = "bar".gsub(/./) do |s|
+    - s + "-"
+  - end.gsub(/-$/) do |s|
+    - ''
+HAML
+  end
+
+  def test_silent_end_with_stuff; skip # silent script
+    assert_equal(<<HTML, render(<<HAML))
+e
+d
+c
+b
+a
+HTML
+- str = "abcde"
+- if true
+  = str.slice!(-1).chr
+- end until str.empty?
+HAML
+
+    assert_equal(<<HTML, render(<<HAML))
+<p>hi!</p>
+HTML
+- if true
+  %p hi!
+- end if "foo".gsub(/f/) do
+  - "z"
+- end + "bar"
+HAML
+  end
+
+  def test_multiline_with_colon_after_filter
+    assert_equal(<<HTML, render(<<HAML))
+Foo
+Bar
+HTML
+:plain
+  Foo
+= { :a => "Bar",      |
+    :b => "Baz" }[:a] |
+HAML
+    assert_equal(<<HTML, render(<<HAML))
+
+Bar
+HTML
+:plain
+= { :a => "Bar",      |
+    :b => "Baz" }[:a] |
+HAML
+  end
+
+  def test_multiline_in_filter
+    assert_equal(<<HTML, render(<<HAML))
+Foo |
+Bar |
+Baz
+HTML
+:plain
+  Foo |
+  Bar |
+  Baz
+HAML
+  end
+
+  def test_curly_brace
+    assert_equal(<<HTML, render(<<HAML))
+Foo { Bar
+HTML
+== Foo { Bar
+HAML
+  end
+
+  def test_escape_attrs_false
+    assert_haml_ugly(<<HAML, :escape_attrs => false)
+#foo{:class => '<?php echo "&quot;" ?>'}
+  bar
+HAML
+  end
+
+  def test_escape_attrs_always; skip # attribute escape
+    assert_equal(<<HTML, render(<<HAML, :escape_attrs => :always))
+<div class='"&amp;lt;&amp;gt;&amp;amp;"' id='foo'>
+  bar
+</div>
+HTML
+#foo{:class => '"&lt;&gt;&amp;"'}
+  bar
+HAML
+  end
+
+  def test_escape_html
+    html = <<HTML
+&amp;
+&
+&amp;
+HTML
+
+    assert_equal(html, render(<<HAML, :escape_html => true))
+&= "&"
+!= "&"
+= "&"
+HAML
+
+    assert_equal(html, render(<<HAML, :escape_html => true))
+&~ "&"
+!~ "&"
+~ "&"
+HAML
+
+    assert_equal(html, render(<<HAML, :escape_html => true))
+& \#{"&"}
+! \#{"&"}
+\#{"&"}
+HAML
+
+    assert_equal(html, render(<<HAML, :escape_html => true))
+&== \#{"&"}
+!== \#{"&"}
+== \#{"&"}
+HAML
+
+    tag_html = <<HTML
+<p>&amp;</p>
+<p>&</p>
+<p>&amp;</p>
+HTML
+
+    assert_equal(tag_html, render(<<HAML, :escape_html => true))
+%p&= "&"
+%p!= "&"
+%p= "&"
+HAML
+
+    assert_equal(tag_html, render(<<HAML, :escape_html => true))
+%p&~ "&"
+%p!~ "&"
+%p~ "&"
+HAML
+
+    assert_equal(tag_html, render(<<HAML, :escape_html => true))
+%p& \#{"&"}
+%p! \#{"&"}
+%p \#{"&"}
+HAML
+
+    assert_equal(tag_html, render(<<HAML, :escape_html => true))
+%p&== \#{"&"}
+%p!== \#{"&"}
+%p== \#{"&"}
+HAML
+  end
+
+  def test_new_attrs_with_hash
+    assert_equal("<a href='#'></a>\n", render('%a(href="#")'))
+  end
+
+  def test_silent_script_with_hyphen_case
+    assert_equal("", render("- a = 'foo-case-bar-case'"))
+  end
+
+  def test_silent_script_with_hyphen_end
+    assert_equal("", render("- a = 'foo-end-bar-end'"))
+  end
+
+  def test_silent_script_with_hyphen_end_and_block; skip # silent script
+    silence_warnings do
+      assert_equal(<<HTML, render(<<HAML))
+<p>foo-end</p>
+<p>bar-end</p>
+HTML
+- ("foo-end-bar-end".gsub(/\\w+-end/) do |s|
+  %p= s
+- end; nil)
+HAML
+    end
+  end
+
+  def test_if_without_content_and_else
+    assert_equal(<<HTML, render(<<HAML))
+foo
+HTML
+- if false
+- else
+  foo
+HAML
+
+    assert_equal(<<HTML, render(<<HAML))
+foo
+HTML
+- if true
+  - if false
+  - else
+    foo
+HAML
+  end
+
+  def test_html_attributes_with_hash; skip # attribute escape
+    assert_equal("<a href='#' rel='top'>Foo</a>\n",
+      render('%a(href="#" rel="top") Foo'))
+    assert_equal("<a href='#'>Foo</a>\n",
+      render('%a(href="#") #{"Foo"}'))
+
+    assert_equal("<a href='#\"'></a>\n", render('%a(href="#\\"")'))
+  end
+
+  def test_case_assigned_to_var
+    assert_equal(<<HTML, render(<<HAML))
+bar
+HTML
+- var = case 12
+- when 1; "foo"
+- when 12; "bar"
+= var
+HAML
+
+    assert_equal(<<HTML, render(<<HAML))
+bar
+HTML
+- var = case 12
+- when 1
+  - "foo"
+- when 12
+  - "bar"
+= var
+HAML
+
+    assert_equal(<<HTML, render(<<HAML))
+bar
+HTML
+- var = case 12
+  - when 1
+    - "foo"
+  - when 12
+    - "bar"
+= var
+HAML
+  end
+
+  def test_nested_case_assigned_to_var
+    assert_equal(<<HTML, render(<<HAML))
+bar
+HTML
+- if true
+  - var = case 12
+  - when 1; "foo"
+  - when 12; "bar"
+  = var
+HAML
+  end
+
+  def test_case_assigned_to_multiple_vars
+    assert_equal(<<HTML, render(<<HAML))
+bar
+bip
+HTML
+- var, vip = case 12
+- when 1; ["foo", "baz"]
+- when 12; ["bar", "bip"]
+= var
+= vip
+HAML
+  end
+
+  def test_if_assigned_to_var
+    assert_equal(<<HTML, render(<<HAML))
+foo
+HTML
+- var = if false
+- else
+  - "foo"
+= var
+HAML
+
+    assert_equal(<<HTML, render(<<HAML))
+foo
+HTML
+- var = if false
+- elsif 12 == 12
+  - "foo"
+- elsif 14 == 14; "bar"
+- else
+  - "baz"
+= var
+HAML
+
+    assert_equal(<<HTML, render(<<HAML))
+foo
+HTML
+- var = if false
+  - "bar"
+- else
+  - "foo"
+= var
+HAML
+  end
+
+  def test_case_with_newline_after_case
+    assert_equal(<<HTML, render(<<HAML))
+foo
+HTML
+- case 1
+
+  - when 1
+    foo
+  - when 2
+    bar
+HAML
+
+    assert_equal(<<HTML, render(<<HAML))
+bar
+HTML
+- case 2
+
+- when 1
+  foo
+- when 2
+  bar
+HAML
+  end
+
+  def test_escape_html_with_interpolated_if_statement
+    assert_equal(<<HTML, render(<<HAML, :escape_html => true))
+foo,
+HTML
+foo\#{"," if true}
+HAML
+  end
+
+  # HTML escaping tests
+
+  def test_ampersand_equals_should_escape
+    assert_haml_ugly("%p\n  &= 'foo & bar'", :escape_html => false)
+  end
+
+  def test_ampersand_equals_inline_should_escape; skip # script bug
+    assert_equal("<p>foo &amp; bar</p>\n", render("%p&= 'foo & bar'", :escape_html => false))
+  end
+
+  def test_ampersand_equals_should_escape_before_preserve; skip # script bug
+    assert_equal("<textarea>foo&#x000A;bar</textarea>\n", render('%textarea&= "foo\nbar"', :escape_html => false))
+  end
+
+  def test_bang_equals_should_not_escape
+    assert_haml_ugly("%p\n  != 'foo & bar'", :escape_html => true)
+  end
+
+  def test_bang_equals_inline_should_not_escape
+    assert_equal("<p>foo & bar</p>\n", render("%p!= 'foo & bar'", :escape_html => true))
+  end
+
+  def test_static_attributes_should_be_escaped; skip # attribute escape
+    assert_equal("<img class='atlantis' style='ugly&amp;stupid'>\n",
+                 render("%img.atlantis{:style => 'ugly&stupid'}"))
+    assert_equal("<div class='atlantis' style='ugly&amp;stupid'>foo</div>\n",
+                 render(".atlantis{:style => 'ugly&stupid'} foo"))
+    assert_equal("<p class='atlantis' style='ugly&amp;stupid'>foo</p>\n",
+                render("%p.atlantis{:style => 'ugly&stupid'}= 'foo'"))
+    assert_equal("<p class='atlantis' style='ugly&#x000A;stupid'></p>\n",
+                render("%p.atlantis{:style => \"ugly\\nstupid\"}"))
+  end
+
+  def test_dynamic_attributes_should_be_escaped; skip # script bug
+    assert_equal("<img alt='' src='&amp;foo.png'>\n",
+                 render("%img{:width => nil, :src => '&foo.png', :alt => String.new}"))
+    assert_equal("<p alt='' src='&amp;foo.png'>foo</p>\n",
+                 render("%p{:width => nil, :src => '&foo.png', :alt => String.new} foo"))
+    assert_equal("<div alt='' src='&amp;foo.png'>foo</div>\n",
+                 render("%div{:width => nil, :src => '&foo.png', :alt => String.new}= 'foo'"))
+    assert_equal("<img alt='' src='foo&#x000A;.png'>\n",
+                 render("%img{:width => nil, :src => \"foo\\n.png\", :alt => String.new}"))
+  end
+
+  def test_string_double_equals_should_be_escaped
+    assert_equal("<p>4&&lt;</p>\n", render("%p== \#{2+2}&\#{'<'}", :escape_html => true))
+    assert_equal("<p>4&<</p>\n", render("%p== \#{2+2}&\#{'<'}", :escape_html => false))
+  end
+
+  def test_escaped_inline_string_double_equals
+    assert_equal("<p>4&&lt;</p>\n", render("%p&== \#{2+2}&\#{'<'}", :escape_html => true))
+    assert_equal("<p>4&&lt;</p>\n", render("%p&== \#{2+2}&\#{'<'}", :escape_html => false))
+  end
+
+  def test_unescaped_inline_string_double_equals
+    assert_equal("<p>4&<</p>\n", render("%p!== \#{2+2}&\#{'<'}", :escape_html => true))
+    assert_equal("<p>4&<</p>\n", render("%p!== \#{2+2}&\#{'<'}", :escape_html => false))
+  end
+
+  def test_escaped_string_double_equals
+    assert_haml_ugly("%p\n  &== \#{2+2}&\#{'<'}", :escape_html => true)
+    assert_haml_ugly("%p\n  &== \#{2+2}&\#{'<'}", :escape_html => false)
+  end
+
+  def test_unescaped_string_double_equals
+    assert_haml_ugly("%p\n  !== \#{2+2}&\#{'<'}", :escape_html => true)
+    assert_haml_ugly("%p\n  !== \#{2+2}&\#{'<'}", :escape_html => false)
+  end
+
+  def test_string_interpolation_should_be_esaped
+    assert_equal("<p>4&&lt;</p>\n", render("%p \#{2+2}&\#{'<'}", :escape_html => true))
+    assert_equal("<p>4&<</p>\n", render("%p \#{2+2}&\#{'<'}", :escape_html => false))
+  end
+
+  def test_escaped_inline_string_interpolation
+    assert_equal("<p>4&&lt;</p>\n", render("%p& \#{2+2}&\#{'<'}", :escape_html => true))
+    assert_equal("<p>4&&lt;</p>\n", render("%p& \#{2+2}&\#{'<'}", :escape_html => false))
+  end
+
+  def test_unescaped_inline_string_interpolation
+    assert_equal("<p>4&<</p>\n", render("%p! \#{2+2}&\#{'<'}", :escape_html => true))
+    assert_equal("<p>4&<</p>\n", render("%p! \#{2+2}&\#{'<'}", :escape_html => false))
+  end
+
+  def test_escaped_string_interpolation
+    assert_haml_ugly("%p\n  & \#{2+2}&\#{'<'}", :escape_html => true)
+    assert_haml_ugly("%p\n  & \#{2+2}&\#{'<'}", :escape_html => false)
+  end
+
+  def test_escaped_string_interpolation_with_no_space
+    assert_equal("&lt;br&gt;\n", render('&#{"<br>"}'))
+    assert_equal("<span>&lt;br&gt;</span>\n", render('%span&#{"<br>"}'))
+  end
+
+  def test_unescaped_string_interpolation
+    assert_haml_ugly("%p\n  ! \#{2+2}&\#{'<'}", :escape_html => true)
+    assert_haml_ugly("%p\n  ! \#{2+2}&\#{'<'}", :escape_html => false)
+  end
+
+  def test_unescaped_string_interpolation_with_no_space
+    assert_equal("<br>\n", render('!#{"<br>"}'))
+    assert_equal("<span><br></span>\n", render('%span!#{"<br>"}'))
+  end
+
+  def test_scripts_should_respect_escape_html_option
+    assert_haml_ugly("%p\n  = 'foo & bar'", :escape_html => true)
+    assert_haml_ugly("%p\n  = 'foo & bar'", :escape_html => false)
+  end
+
+  def test_inline_scripts_should_respect_escape_html_option; skip # escape html
+    assert_equal("<p>foo &amp; bar</p>\n", render("%p= 'foo & bar'", :escape_html => true))
+    assert_equal("<p>foo & bar</p>\n", render("%p= 'foo & bar'", :escape_html => false))
+  end
+
+  def test_script_ending_in_comment_should_render_when_html_is_escaped
+    assert_equal("foo&amp;bar\n", render("= 'foo&bar' #comment", :escape_html => true))
+  end
+
+  def test_script_with_if_shouldnt_output
+    assert_equal(<<HTML, render(<<HAML))
+<p>foo</p>
+<p></p>
+HTML
+%p= "foo"
+%p= "bar" if false
+HAML
+  end
+
+  # Options tests
+
+  def test_filename_and_line; skip # options
+    begin
+      render("\n\n = abc", :filename => 'test', :line => 2)
+    rescue Exception => e
+      assert_kind_of Haml::SyntaxError, e
+      assert_match(/test:4/, e.backtrace.first)
+    end
+
+    begin
+      render("\n\n= 123\n\n= nil[]", :filename => 'test', :line => 2)
+    rescue Exception => e
+      assert_kind_of NoMethodError, e
+      backtrace = e.backtrace
+      backtrace.shift if rubinius?
+      assert_match(/test:6/, backtrace.first)
+    end
+  end
+
+  def test_stop_eval; skip # options
+    assert_equal("", render("= 'Hello'", :suppress_eval => true))
+    assert_equal("", render("- haml_concat 'foo'", :suppress_eval => true))
+    assert_equal("<div id='foo' yes='no'>\n", render("#foo{:yes => 'no'}/", :suppress_eval => true))
+    assert_equal("<div id='foo'>\n", render("#foo{:yes => 'no', :call => a_function() }/", :suppress_eval => true))
+    assert_equal("<div>\n", render("%div[1]/", :suppress_eval => true))
+    assert_equal("", render(":ruby\n  Kernel.puts 'hello'", :suppress_eval => true))
+  end
+
+  def test_doctypes
+    assert_equal('<!DOCTYPE html>',
+      render('!!!', :format => :html5).strip)
+    assert_equal('<!DOCTYPE html>', render('!!! 5').strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
+      render('!!! strict', :format => :xhtml).strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
+      render('!!! frameset', :format => :xhtml).strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">',
+      render('!!! mobile', :format => :xhtml).strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
+      render('!!! basic', :format => :xhtml).strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
+      render('!!! transitional', :format => :xhtml).strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
+      render('!!!', :format => :xhtml).strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
+      render('!!! strict', :format => :html4).strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
+      render('!!! frameset', :format => :html4).strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
+      render('!!! transitional', :format => :html4).strip)
+    assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
+      render('!!!', :format => :html4).strip)
+  end
+
+  def test_attr_wrapper; skip # options
+    assert_equal("<p strange=*attrs*></p>\n", render("%p{ :strange => 'attrs'}", :attr_wrapper => '*'))
+    assert_equal("<p escaped='quo\"te'></p>\n", render("%p{ :escaped => 'quo\"te'}", :attr_wrapper => '"'))
+    assert_equal("<p escaped=\"quo'te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
+    assert_equal("<p escaped=\"q'uo&#x0022;te\"></p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
+    assert_equal("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n", render("!!! XML", :attr_wrapper => '"', :format => :xhtml))
+  end
+
+  def test_autoclose_option
+    assert_equal("<flaz foo='bar'>\n", render("%flaz{:foo => 'bar'}", :autoclose => ["flaz"]))
+    assert_equal(<<HTML, render(<<HAML, :autoclose => [/^flaz/]))
+<flaz>
+<flaznicate>
+<flan></flan>
+HTML
+%flaz
+%flaznicate
+%flan
+HAML
+  end
+
+  def test_attrs_parsed_correctly; skip # attribute escape
+    assert_equal("<p boom=>biddly='bar =&gt; baz'></p>\n", render("%p{'boom=>biddly' => 'bar => baz'}"))
+    assert_equal("<p foo,bar='baz, qux'></p>\n", render("%p{'foo,bar' => 'baz, qux'}"))
+    assert_equal("<p escaped='quo&#x000A;te'></p>\n", render("%p{ :escaped => \"quo\\nte\"}"))
+    assert_equal("<p escaped='quo4te'></p>\n", render("%p{ :escaped => \"quo\#{2 + 2}te\"}"))
+  end
+
+  def test_correct_parsing_with_brackets; skip # script bug
+    assert_equal("<p class='foo'>{tada} foo</p>\n", render("%p{:class => 'foo'} {tada} foo"))
+    assert_equal("<p class='foo'>deep {nested { things }}</p>\n", render("%p{:class => 'foo'} deep {nested { things }}"))
+    assert_equal("<p class='bar foo'>{a { d</p>\n", render("%p{{:class => 'foo'}, :class => 'bar'} {a { d"))
+    assert_equal("<p foo='bar'>a}</p>\n", render("%p{:foo => 'bar'} a}"))
+
+    foo = []
+    foo[0] = Struct.new('Foo', :id).new
+    assert_equal("<p class='struct_foo' id='struct_foo_new'>New User]</p>\n",
+                 render("%p[foo[0]] New User]", :locals => {:foo => foo}))
+    assert_equal("<p class='prefix_struct_foo' id='prefix_struct_foo_new'>New User]</p>\n",
+                 render("%p[foo[0], :prefix] New User]", :locals => {:foo => foo}))
+
+    foo[0].id = 1
+    assert_equal("<p class='struct_foo' id='struct_foo_1'>New User]</p>\n",
+                 render("%p[foo[0]] New User]", :locals => {:foo => foo}))
+    assert_equal("<p class='prefix_struct_foo' id='prefix_struct_foo_1'>New User]</p>\n",
+                 render("%p[foo[0], :prefix] New User]", :locals => {:foo => foo}))
+  end
+
+  def test_empty_attrs
+    assert_haml_ugly("%p{ :attr => '' } empty")
+    assert_haml_ugly("%p{ :attr => x } empty", :locals => {:x => ''})
+  end
+
+  def test_nil_attrs
+    skip '[INCOMPATIBILITY] Hamlit limits boolean attributes'
+    assert_equal("<p>nil</p>\n", render("%p{ :attr => nil } nil"))
+    assert_equal("<p>nil</p>\n", render("%p{ :attr => x } nil", :locals => {:x => nil}))
+  end
+
+  def test_nil_id_with_syntactic_id
+    assert_equal("<p id='foo'>nil</p>\n", render("%p#foo{:id => nil} nil"))
+    assert_equal("<p id='foo_bar'>nil</p>\n", render("%p#foo{{:id => 'bar'}, :id => nil} nil"))
+    assert_equal("<p id='foo_bar'>nil</p>\n", render("%p#foo{{:id => nil}, :id => 'bar'} nil"))
+  end
+
+  def test_nil_class_with_syntactic_class
+    assert_equal("<p class='foo'>nil</p>\n", render("%p.foo{:class => nil} nil"))
+    assert_equal("<p class='bar foo'>nil</p>\n", render("%p.bar.foo{:class => nil} nil"))
+    assert_equal("<p class='bar foo'>nil</p>\n", render("%p.foo{{:class => 'bar'}, :class => nil} nil"))
+    assert_equal("<p class='bar foo'>nil</p>\n", render("%p.foo{{:class => nil}, :class => 'bar'} nil"))
+  end
+
+  def test_locals
+    assert_haml_ugly("%p= text", :locals => { :text => "Paragraph!" })
+  end
+
+  def test_dynamic_attrs_shouldnt_register_as_literal_values
+    assert_equal("<p a='b2c'></p>\n", render('%p{:a => "b#{1 + 1}c"}'))
+    assert_equal("<p a='b2c'></p>\n", render("%p{:a => 'b' + (1 + 1).to_s + 'c'}"))
+  end
+
+  def test_dynamic_attrs_with_self_closed_tag
+    assert_equal("<a b='2'>\nc\n", render("%a{'b' => 1 + 1}/\n= 'c'\n"))
+  end
+
+  EXCEPTION_MAP.each do |key, value|
+    define_method("test_exception (#{key.inspect})") do
+      begin
+        silence_warnings do
+          render(key, :filename => "(test_exception (#{key.inspect}))")
+        end
+      rescue Exception => err
+        value = [value] unless value.is_a?(Array)
+        expected_message, line_no = value
+        line_no ||= key.split("\n").length
+
+
+        if expected_message == :compile
+          assert_match(/(compile error|syntax error|unterminated string|expecting)/, err.message, "Line: #{key}")
+        else
+          assert_equal(expected_message, err.message, "Line: #{key}")
+        end
+
+      else
+        assert(false, "Exception not raised for\n#{key}")
+      end
+    end
+  end
+
+  def test_exception_map
+    skip
+    EXCEPTION_MAP
+  end
+
+  def test_exception_line; skip # error
+    render("a\nb\n!!!\n  c\nd")
+  rescue Haml::SyntaxError => e
+    assert_equal("(test_exception_line):4", e.backtrace[0])
+  else
+    assert(false, '"a\nb\n!!!\n  c\nd" doesn\'t produce an exception')
+  end
+
+  def test_exception; skip # error
+    render("%p\n  hi\n  %a= undefined\n= 12")
+  rescue Exception => e
+    skip
+    backtrace = e.backtrace
+    backtrace.shift if rubinius?
+    assert_match("(test_exception):3", backtrace[0])
+  else
+    # Test failed... should have raised an exception
+    assert(false)
+  end
+
+  def test_compile_error; skip # error
+    render("a\nb\n- fee)\nc")
+  rescue Exception => e
+    skip
+    assert_match(/\(test_compile_error\):3:/i, e.message)
+    assert_match(/(syntax error|expecting \$end)/i, e.message)
+  else
+    assert(false, '"a\nb\n- fee)\nc" doesn\'t produce an exception!')
+  end
+
+  def test_unbalanced_brackets; skip # error
+    render('foo #{1 + 5} foo #{6 + 7 bar #{8 + 9}')
+  rescue Hamlit::SyntaxError => e
+    assert_equal(Hamlit::Error.message(:unbalanced_brackets), e.message)
+  end
+
+  def test_single_line_comments_are_interpolated; skip # comment
+    assert_equal("<!-- Hello 2 -->\n",
+                 render('/ Hello #{1 + 1}'))
+  end
+
+  def test_single_line_comments_are_not_interpolated_with_suppress_eval; skip # comment
+    assert_equal("<!--  -->\n",
+                 render('/ Hello #{1 + 1}', :suppress_eval => true))
+  end
+
+  def test_single_line_comments_with_interpolation_dont_break_tabulation; skip # comment
+    assert_equal("<!-- Hello 2 -->\nconcatted\n",
+                 render("/ Hello \#{1 + 1}\n- haml_concat 'concatted'"))
+  end
+
+  def test_balanced_conditional_comments
+    assert_equal("<!--[if !(IE 6)|(IE 7)]> Bracket: ] <![endif]-->\n",
+                 render("/[if !(IE 6)|(IE 7)] Bracket: ]"))
+  end
+
+  def test_downlevel_revealed_conditional_comments; skip
+    assert_equal("<!--[if !IE]><!--> A comment <!--<![endif]-->\n",
+                  render("/![if !IE] A comment"))
+  end
+
+  def test_downlevel_revealed_conditional_comments_block; skip
+    assert_equal("<!--[if !IE]><!-->\n  A comment\n<!--<![endif]-->\n",
+                  render("/![if !IE]\n  A comment"))
+  end
+
+  def test_local_assigns_dont_modify_class
+    assert_haml_ugly("= foo", :locals => {:foo => 'bar'})
+    assert_equal(nil, defined?(foo))
+  end
+
+  def test_object_ref_with_nil_id; skip # object reference
+    user = User.new
+    assert_equal("<p class='struct_user' id='struct_user_new'>New User</p>\n",
+                 render("%p[user] New User", :locals => {:user => user}))
+  end
+
+  def test_object_ref_before_attrs; skip # object reference
+    user = User.new 42
+    assert_equal("<p class='struct_user' id='struct_user_42' style='width: 100px;'>New User</p>\n",
+                 render("%p[user]{:style => 'width: 100px;'} New User", :locals => {:user => user}))
+  end
+
+  def test_object_ref_with_custom_haml_class; skip # object reference
+    custom = CustomHamlClass.new 42
+    assert_equal("<p class='my_thing' id='my_thing_42' style='width: 100px;'>My Thing</p>\n",
+                 render("%p[custom]{:style => 'width: 100px;'} My Thing", :locals => {:custom => custom}))
+  end
+
+  def test_object_ref_with_multiple_ids; skip # object reference
+    cpk_record = CpkRecord.new([42,6,9])
+    assert_equal("<p class='struct_cpk_record' id='struct_cpk_record_42_6_9' style='width: 100px;'>CPK Record</p>\n",
+                 render("%p[cpk_record]{:style => 'width: 100px;'} CPK Record", :locals => {:cpk_record => cpk_record}))
+  end
+
+  def test_non_literal_attributes
+    assert_haml_ugly("%p{a2, a1, :a3 => 'baz'}",
+                  :locals => {:a1 => {:a1 => 'foo'}, :a2 => {:a2 => 'bar'}})
+  end
+
+  def test_render_should_accept_a_binding_as_scope; skip
+    string = "This is a string!"
+    string.instance_variable_set(:@var, "Instance variable")
+    b = string.instance_eval do
+      var = "Local variable"
+      # Silence unavoidable warning; Ruby doesn't know we're going to use this
+      # later.
+      nil if var
+      binding
+    end
+
+    assert_haml_ugly("%p= upcase\n%p= @var\n%p= var", :scope => b)
+  end
+
+  def test_yield_should_work_with_binding; skip # options
+    assert_equal("12\nFOO\n", render("= yield\n= upcase", :scope => "foo".instance_eval{binding}) { 12 })
+  end
+
+  def test_yield_should_work_with_def_method; skip # def_method
+    s = "foo"
+    engine("= yield\n= upcase").def_method(s, :render)
+    assert_equal("12\nFOO\n", s.render { 12 })
+  end
+
+  def test_def_method_with_module; skip # def_method
+    engine("= yield\n= upcase").def_method(String, :render_haml)
+    assert_equal("12\nFOO\n", "foo".render_haml { 12 })
+  end
+
+  def test_def_method_locals; skip # def_method
+    obj = Object.new
+    engine("%p= foo\n.bar{:baz => baz}= boom").def_method(obj, :render, :foo, :baz, :boom)
+    assert_equal("<p>1</p>\n<div baz='2' class='bar'>3</div>\n", obj.render(:foo => 1, :baz => 2, :boom => 3))
+  end
+
+  def test_render_proc_locals; skip # render_proc
+    proc = engine("%p= foo\n.bar{:baz => baz}= boom").render_proc(Object.new, :foo, :baz, :boom)
+    assert_equal("<p>1</p>\n<div baz='2' class='bar'>3</div>\n", proc[:foo => 1, :baz => 2, :boom => 3])
+  end
+
+  def test_render_proc_with_binding; skip # render_proc
+    assert_equal("FOO\n", engine("= upcase").render_proc("foo".instance_eval{binding}).call)
+  end
+
+  def test_haml_buffer_gets_reset_even_with_exception; skip # haml_buffer
+    scope = Object.new
+    render("- raise Hamlit::Error", :scope => scope)
+    assert(false, "Expected exception")
+  rescue Exception
+    skip
+    assert_nil(scope.send(:haml_buffer))
+  end
+
+  def test_def_method_haml_buffer_gets_reset_even_with_exception; skip # def_method
+    scope = Object.new
+    engine("- raise Hamlit::Error").def_method(scope, :render)
+    scope.render
+    assert(false, "Expected exception")
+  rescue Exception; skip
+    assert_nil(scope.send(:haml_buffer))
+  end
+
+  def test_render_proc_haml_buffer_gets_reset_even_with_exception; skip # render_proc
+    scope = Object.new
+    proc = engine("- raise Hamlit::Error").render_proc(scope)
+    proc.call
+    assert(false, "Expected exception")
+  rescue Exception; skip
+    assert_nil(scope.send(:haml_buffer))
+  end
+
+  def test_render_proc_should_raise_haml_syntax_error_not_ruby_syntax_error
+    assert_raises(Haml::SyntaxError) do
+      Haml::Engine.new("%p{:foo => !}").render_proc(Object.new, :foo).call
+    end
+  end
+
+  def test_render_should_raise_haml_syntax_error_not_ruby_syntax_error
+    assert_raises(Haml::SyntaxError) do
+      Haml::Engine.new("%p{:foo => !}").render
+    end
+  end
+
+  def test_ugly_true
+    assert_equal("<div id='outer'>\n<div id='inner'>\n<p>hello world</p>\n</div>\n</div>\n",
+                 render("#outer\n  #inner\n    %p hello world", :ugly => true))
+
+    assert_equal("<p>#{'s' * 75}</p>\n",
+                 render("%p #{'s' * 75}", :ugly => true))
+
+    assert_equal("<p>#{'s' * 75}</p>\n",
+                 render("%p= 's' * 75", :ugly => true))
+  end
+
+  def test_remove_whitespace_true; skip # options
+    assert_equal("<div id='outer'><div id='inner'><p>hello world</p></div></div>",
+                 render("#outer\n  #inner\n    %p hello world", :remove_whitespace => true))
+    assert_equal("<p>hello world<pre>foo   bar\nbaz</pre></p>", render(<<HAML, :remove_whitespace => true))
+%p
+  hello world
+  %pre
+    foo   bar
+    baz
+HAML
+    assert_equal("<div><span>foo</span> <span>bar</span></div>",
+                 render('%div <span>foo</span> <span>bar</span>', :remove_whitespace => true))
+  end
+
+  def test_auto_preserve_unless_ugly; skip # preserve
+    assert_equal("<pre>foo&#x000A;bar</pre>\n", render('%pre="foo\nbar"'))
+    assert_equal("<pre>foo\nbar</pre>\n", render("%pre\n  foo\n  bar"))
+    assert_equal("<pre>foo\nbar</pre>\n", render('%pre="foo\nbar"', :ugly => true))
+    assert_equal("<pre>foo\nbar</pre>\n", render("%pre\n  foo\n  bar", :ugly => true))
+  end
+
+  def test_xhtml_output_option
+    assert_haml_ugly("%p\n  %br", :format => :xhtml)
+    assert_haml_ugly("%a/", :format => :xhtml)
+  end
+
+  def test_arbitrary_output_option; skip # error
+    assert_raises_message(Hamlit::Error, "Invalid output format :html1") do
+      engine("%br", :format => :html1)
+    end
+  end
+
+  def test_static_hashes
+    assert_equal("<a b='a =&gt; b'></a>\n", render("%a{:b => 'a => b'}", :suppress_eval => true))
+    assert_equal("<a b='a, b'></a>\n", render("%a{:b => 'a, b'}", :suppress_eval => true))
+    assert_equal("<a b='a\tb'></a>\n", render('%a{:b => "a\tb"}', :suppress_eval => true))
+    assert_equal("<a b='a\#{foo}b'></a>\n", render('%a{:b => "a\\#{foo}b"}', :suppress_eval => true))
+    assert_equal("<a b='#f00'></a>\n", render("%a{:b => '#f00'}", :suppress_eval => true))
+  end
+
+  def test_dynamic_hashes_with_suppress_eval; skip # options
+    assert_equal("<a></a>\n", render('%a{:b => "a #{1 + 1} b", :c => "d"}', :suppress_eval => true))
+  end
+
+  def test_interpolates_instance_vars_in_attribute_values; skip # special interpolation
+    scope = Object.new
+    scope.instance_variable_set :@foo, 'bar'
+    assert_haml_ugly('%a{:b => "a #@foo b"}', :scope => scope)
+  end
+
+  def test_interpolates_global_vars_in_attribute_values
+    # make sure the value isn't just interpolated in during template compilation
+    engine = Haml::Engine.new('%a{:b => "a #$global_var_for_testing b"}')
+    $global_var_for_testing = 'bar'
+    assert_equal("<a b='a bar b'></a>\n", engine.to_html)
+  ensure
+    $global_var_for_testing = nil
+  end
+
+  def test_utf8_attrs
+    assert_equal("<a href='héllo'></a>\n", render("%a{:href => 'héllo'}"))
+    assert_equal("<a href='héllo'></a>\n", render("%a(href='héllo')"))
+  end
+
+  # HTML 4.0
+
+  def test_html_has_no_self_closing_tags
+    assert_haml_ugly("%p\n  %br", :format => :html4)
+    assert_haml_ugly("%br/", :format => :html4)
+  end
+
+  def test_html_renders_empty_node_with_closing_tag
+    assert_equal "<div class='foo'></div>\n", render(".foo", :format => :html4)
+  end
+
+  def test_html_doesnt_add_slash_to_self_closing_tags
+    assert_equal "<a>\n", render("%a/", :format => :html4)
+    assert_equal "<a foo='2'>\n", render("%a{:foo => 1 + 1}/", :format => :html4)
+    assert_equal "<meta>\n", render("%meta", :format => :html4)
+    assert_equal "<meta foo='2'>\n", render("%meta{:foo => 1 + 1}", :format => :html4)
+  end
+
+  def test_html_ignores_xml_prolog_declaration
+    assert_equal "", render('!!! XML', :format => :html4)
+  end
+
+  def test_html_has_different_doctype
+    assert_equal %{<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n},
+    render('!!!', :format => :html4)
+  end
+
+  # because anything before the doctype triggers quirks mode in IE
+  def test_xml_prolog_and_doctype_dont_result_in_a_leading_whitespace_in_html
+    refute_match(/^\s+/, render("!!! xml\n!!!", :format => :html4))
+  end
+
+  # HTML5
+  def test_html5_doctype
+    assert_equal %{<!DOCTYPE html>\n}, render('!!!', :format => :html5)
+  end
+
+  # HTML5 custom data attributes
+  def test_html5_data_attributes_without_hyphenation; skip # hyphenate
+    assert_equal("<div data-author_id='123' data-biz='baz' data-foo='bar'></div>\n",
+      render("%div{:data => {:author_id => 123, :foo => 'bar', :biz => 'baz'}}",
+        :hyphenate_data_attrs => false))
+
+    assert_equal("<div data-one_plus_one='2'></div>\n",
+      render("%div{:data => {:one_plus_one => 1+1}}",
+        :hyphenate_data_attrs => false))
+
+    assert_equal("<div data-foo='Here&#x0027;s a \"quoteful\" string.'></div>\n",
+      render(%{%div{:data => {:foo => %{Here's a "quoteful" string.}}}},
+        :hyphenate_data_attrs => false)) #'
+  end
+
+  def test_html5_data_attributes_with_hyphens
+    assert_equal("<div data-foo-bar='blip'></div>\n",
+      render("%div{:data => {:foo_bar => 'blip'}}"))
+    assert_equal("<div data-baz='bang' data-foo-bar='blip'></div>\n",
+      render("%div{:data => {:foo_bar => 'blip', :baz => 'bang'}}"))
+  end
+
+       def test_html5_arbitrary_hash_valued_attributes_with
+    skip '[INCOMPATIBILITY] Hamlit supports hyphenation only for data attributes'
+    assert_equal("<div aria-foo='blip'></div>\n",
+      render("%div{:aria => {:foo => 'blip'}}"))
+    assert_equal("<div foo-baz='bang'></div>\n",
+      render("%div{:foo => {:baz => 'bang'}}"))
+       end
+
+  def test_arbitrary_attribute_hash_merging
+    skip '[INCOMPATIBILITY] Hamlit supports hyphenation only for data attributes'
+    assert_equal(%Q{<a aria-baz='qux' aria-foo='bar'></a>\n}, render(<<-HAML))
+- h1 = {:aria => {:foo => :bar}}
+- h2 = {:baz => :qux}
+%a{h1, :aria => h2}
+HAML
+  end
+
+
+  def test_html5_data_attributes_with_nested_hash; skip # cyclic reference
+    assert_equal("<div data-a-b='c'></div>\n", render(<<-HAML))
+- hash = {:a => {:b => 'c'}}
+- hash[:d] = hash
+%div{:data => hash}
+HAML
+  end
+
+  def test_html5_data_attributes_with_nested_hash_and_without_hyphenation; skip # hyphenate
+    assert_equal("<div data-a_b='c'></div>\n", render(<<-HAML, :hyphenate_data_attrs => false))
+- hash = {:a => {:b => 'c'}}
+- hash[:d] = hash
+%div{:data => hash}
+HAML
+  end
+
+  def test_html5_data_attributes_with_multiple_defs; skip # hyphenate
+    # Should always use the more-explicit attribute
+    assert_equal("<div data-foo='second'></div>\n",
+      render("%div{:data => {:foo => 'first'}, 'data-foo' => 'second'}"))
+    assert_equal("<div data-foo='first'></div>\n",
+      render("%div{'data-foo' => 'first', :data => {:foo => 'second'}}"))
+  end
+
+  def test_html5_data_attributes_with_attr_method; skip # runtime attribute
+    obj = Object.new
+    def obj.data_hash
+      {:data => {:foo => "bar", :baz => "bang"}}
+    end
+
+    def obj.data_val
+      {:data => "dat"}
+    end
+
+    assert_equal("<div data-baz='bang' data-brat='wurst' data-foo='blip'></div>\n",
+      render("%div{data_hash, :data => {:foo => 'blip', :brat => 'wurst'}}", scope: obj))
+    assert_equal("<div data-baz='bang' data-foo='blip'></div>\n",
+      render("%div{data_hash, 'data-foo' => 'blip'}", scope: obj))
+    assert_equal("<div data-baz='bang' data-foo='bar' data='dat'></div>\n",
+      render("%div{data_hash, :data => 'dat'}", scope: obj))
+    assert_equal("<div data-brat='wurst' data-foo='blip' data='dat'></div>\n",
+      render("%div{data_val, :data => {:foo => 'blip', :brat => 'wurst'}}", scope: obj))
+  end
+
+  def test_html5_data_attributes_with_identical_attribute_values
+    assert_equal("<div data-x='50' data-y='50'></div>\n",
+      render("%div{:data => {:x => 50, :y => 50}}"))
+  end
+
+  def test_xml_doc_using_html5_format_and_mime_type; skip # mime_type
+    assert_equal(<<XML, render(<<HAML, { :format => :html5, :mime_type => 'text/xml' }))
+<?xml version='1.0' encoding='utf-8' ?>
+<root>
+  <element />
+  <hr />
+</root>
+XML
+!!! XML
+%root
+  %element/
+  %hr
+HAML
+  end
+
+  def test_xml_doc_using_html4_format_and_mime_type; skip # mime_type
+    assert_equal(<<XML, render(<<HAML, { :format => :html4, :mime_type => 'text/xml' }))
+<?xml version='1.0' encoding='utf-8' ?>
+<root>
+  <element />
+  <hr />
+</root>
+XML
+!!! XML
+%root
+  %element/
+  %hr
+HAML
+  end
+
+  # New attributes
+
+  def test_basic_new_attributes
+    assert_equal("<a>bar</a>\n", render("%a() bar"))
+    assert_equal("<a href='foo'>bar</a>\n", render("%a(href='foo') bar"))
+    assert_equal("<a b='c' c='d' d='e'>baz</a>\n", render(%q{%a(b="c" c='d' d="e") baz}))
+  end
+
+  def test_new_attribute_ids; skip # object reference
+    assert_equal("<div id='foo_bar'></div>\n", render("#foo(id='bar')"))
+    assert_equal("<div id='foo_baz_bar'></div>\n", render("#foo{:id => 'bar'}(id='baz')"))
+    assert_equal("<div id='foo_baz_bar'></div>\n", render("#foo(id='baz'){:id => 'bar'}"))
+    foo = User.new(42)
+    assert_equal("<div class='struct_user' id='foo_baz_bar_struct_user_42'></div>\n",
+      render("#foo(id='baz'){:id => 'bar'}[foo]", :locals => {:foo => foo}))
+    assert_equal("<div class='struct_user' id='foo_baz_bar_struct_user_42'></div>\n",
+      render("#foo(id='baz')[foo]{:id => 'bar'}", :locals => {:foo => foo}))
+    assert_equal("<div class='struct_user' id='foo_baz_bar_struct_user_42'></div>\n",
+      render("#foo[foo](id='baz'){:id => 'bar'}", :locals => {:foo => foo}))
+    assert_equal("<div class='struct_user' id='foo_baz_bar_struct_user_42'></div>\n",
+      render("#foo[foo]{:id => 'bar'}(id='baz')", :locals => {:foo => foo}))
+  end
+
+  def test_new_attribute_classes; skip # object reference
+    assert_equal("<div class='bar foo'></div>\n", render(".foo(class='bar')"))
+    assert_equal("<div class='bar baz foo'></div>\n", render(".foo{:class => 'bar'}(class='baz')"))
+    assert_equal("<div class='bar baz foo'></div>\n", render(".foo(class='baz'){:class => 'bar'}"))
+    foo = User.new(42)
+    assert_equal("<div class='bar baz foo struct_user' id='struct_user_42'></div>\n",
+      render(".foo(class='baz'){:class => 'bar'}[foo]", :locals => {:foo => foo}))
+    assert_equal("<div class='bar baz foo struct_user' id='struct_user_42'></div>\n",
+      render(".foo[foo](class='baz'){:class => 'bar'}", :locals => {:foo => foo}))
+    assert_equal("<div class='bar baz foo struct_user' id='struct_user_42'></div>\n",
+      render(".foo[foo]{:class => 'bar'}(class='baz')", :locals => {:foo => foo}))
+  end
+
+  def test_dynamic_new_attributes
+    assert_haml_ugly("%a(href=foo) bar", :locals => {:foo => 12})
+    assert_haml_ugly("%a(b=b c='13' d=d) bar", :locals => {:b => 12, :d => 14})
+  end
+
+  def test_new_attribute_interpolation
+    assert_haml_ugly('%a(href="1#{1 + 1}") bar')
+    assert_haml_ugly(%q{%a(href='2: #{1 + 1}, 3: #{foo}') bar}, :locals => {:foo => 3})
+    assert_haml_ugly('%a(href="1\#{1 + 1}") bar')
+  end
+
+  def test_truthy_new_attributes; skip # xhtml
+    assert_equal("<a href='href'>bar</a>\n", render("%a(href) bar", :format => :xhtml))
+    assert_equal("<a bar='baz' href>bar</a>\n", render("%a(href bar='baz') bar", :format => :html5))
+    assert_equal("<a href>bar</a>\n", render("%a(href=true) bar"))
+    assert_equal("<a>bar</a>\n", render("%a(href=false) bar"))
+  end
+
+  def test_new_attribute_parsing; skip # attribute escape
+    assert_equal("<a a2='b2'>bar</a>\n", render("%a(a2=b2) bar", :locals => {:b2 => 'b2'}))
+    assert_equal(%Q{<a a='foo"bar'>bar</a>\n}, render(%q{%a(a="#{'foo"bar'}") bar})) #'
+    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #'
+    assert_equal(%Q{<a a='foo"bar'>bar</a>\n}, render(%q{%a(a='foo"bar') bar}))
+    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="foo'bar") bar}))
+    assert_equal("<a a:b='foo'>bar</a>\n", render("%a(a:b='foo') bar"))
+    assert_equal("<a a='foo' b='bar'>bar</a>\n", render("%a(a = 'foo' b = 'bar') bar"))
+    assert_equal("<a a='foo' b='bar'>bar</a>\n", render("%a(a = foo b = bar) bar", :locals => {:foo => 'foo', :bar => 'bar'}))
+    assert_equal("<a a='foo'>(b='bar')</a>\n", render("%a(a='foo')(b='bar')"))
+    assert_equal("<a a='foo)bar'>baz</a>\n", render("%a(a='foo)bar') baz"))
+    assert_equal("<a a='foo'>baz</a>\n", render("%a( a = 'foo' ) baz"))
+  end
+
+  def test_new_attribute_escaping; skip # attribute escape
+    assert_equal(%Q{<a a='foo " bar'>bar</a>\n}, render(%q{%a(a="foo \" bar") bar}))
+    assert_equal(%Q{<a a='foo \\" bar'>bar</a>\n}, render(%q{%a(a="foo \\\\\" bar") bar}))
+
+    assert_equal(%Q{<a a="foo ' bar">bar</a>\n}, render(%q{%a(a='foo \' bar') bar}))
+    assert_equal(%Q{<a a="foo \\' bar">bar</a>\n}, render(%q{%a(a='foo \\\\\' bar') bar}))
+
+    assert_equal(%Q{<a a='foo \\ bar'>bar</a>\n}, render(%q{%a(a="foo \\\\ bar") bar}))
+    assert_equal(%Q{<a a='foo \#{1 + 1} bar'>bar</a>\n}, render(%q{%a(a="foo \#{1 + 1} bar") bar}))
+  end
+
+  def test_multiline_new_attribute
+    assert_haml_ugly("%a(a='b'\n  c='d') bar")
+    assert_haml_ugly("%a(a='b' b='c'\n  c='d' d=e\n  e='f' f='j') bar", :locals => {:e => 'e'})
+  end
+
+  def test_new_and_old_attributes
+    assert_haml_ugly("%a(a='b'){:c => 'd'} bar")
+    assert_haml_ugly("%a{:c => 'd'}(a='b') bar")
+    assert_haml_ugly("%a(c='d'){:a => 'b'} bar")
+    assert_haml_ugly("%a{:a => 'b'}(c='d') bar")
+
+    # Old-style always takes precedence over new-style,
+    # because theoretically old-style could have arbitrary end-of-method-call syntax.
+    assert_haml_ugly("%a{:a => 'b'}(a='d') bar")
+    assert_haml_ugly("%a(a='d'){:a => 'b'} bar")
+
+    assert_haml_ugly("%a{:a => 'b',\n:b => 'c'}(c='d'\nd='e') bar")
+
+    locals = {:b => 'b', :d => 'd'}
+    assert_haml_ugly("%p{:a => b}(c=d)", :locals => locals)
+    assert_haml_ugly("%p(a=b){:c => d}", :locals => locals)
+  end
+
+  # Ruby Multiline
+
+  def test_silent_ruby_multiline
+    assert_equal(<<HTML, render(<<HAML))
+bar, baz, bang
+<p>foo</p>
+HTML
+- foo = ["bar",
+         "baz",
+         "bang"]
+= foo.join(", ")
+%p foo
+HAML
+  end
+
+  def test_loud_ruby_multiline
+    assert_equal(<<HTML, render(<<HAML))
+bar, baz, bang
+<p>foo</p>
+<p>bar</p>
+HTML
+= ["bar",
+   "baz",
+   "bang"].join(", ")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_ruby_multiline_with_punctuated_methods_is_continuation
+    assert_equal(<<HTML, render(<<HAML))
+bar, , true, bang
+<p>foo</p>
+<p>bar</p>
+HTML
+= ["bar",
+   "  ".strip!,
+   "".empty?,
+   "bang"].join(", ")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_ruby_character_literals_are_not_continuation
+    html = ",\n,\n<p>foo</p>\n"
+    assert_equal(html, render(<<HAML))
+= ?,
+= ?\,
+%p foo
+HAML
+  end
+
+  def test_escaped_loud_ruby_multiline
+    assert_equal(<<HTML, render(<<HAML))
+bar&lt;, baz, bang
+<p>foo</p>
+<p>bar</p>
+HTML
+&= ["bar<",
+    "baz",
+    "bang"].join(", ")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_unescaped_loud_ruby_multiline
+    assert_equal(<<HTML, render(<<HAML, :escape_html => true))
+bar<, baz, bang
+<p>foo</p>
+<p>bar</p>
+HTML
+!= ["bar<",
+    "baz",
+    "bang"].join(", ")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_flattened_loud_ruby_multiline
+    assert_equal(<<HTML, render(<<HAML))
+<pre>bar&#x000A;baz&#x000A;bang</pre>
+<p>foo</p>
+<p>bar</p>
+HTML
+~ "<pre>" + ["bar",
+             "baz",
+             "bang"].join("\\n") + "</pre>"
+%p foo
+%p bar
+HAML
+  end
+
+  def test_loud_ruby_multiline_with_block; skip # block script
+    assert_equal(<<HTML, render(<<HAML))
+#{%w[far faz fang]}
+<p>foo</p>
+<p>bar</p>
+HTML
+= ["bar",
+   "baz",
+   "bang"].map do |str|
+  - str.gsub("ba",
+             "fa")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_silent_ruby_multiline_with_block
+    assert_equal(<<HTML, render(<<HAML))
+far
+faz
+fang
+<p>foo</p>
+<p>bar</p>
+HTML
+- ["bar",
+   "baz",
+   "bang"].map do |str|
+  = str.gsub("ba",
+             "fa")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_ruby_multiline_in_tag
+    assert_equal(<<HTML, render(<<HAML))
+<p>foo, bar, baz</p>
+<p>foo</p>
+<p>bar</p>
+HTML
+%p= ["foo",
+     "bar",
+     "baz"].join(", ")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_escaped_ruby_multiline_in_tag; skip # script bug
+    assert_equal(<<HTML, render(<<HAML))
+<p>foo&lt;, bar, baz</p>
+<p>foo</p>
+<p>bar</p>
+HTML
+%p&= ["foo<",
+      "bar",
+      "baz"].join(", ")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_unescaped_ruby_multiline_in_tag
+    assert_equal(<<HTML, render(<<HAML, :escape_html => true))
+<p>foo<, bar, baz</p>
+<p>foo</p>
+<p>bar</p>
+HTML
+%p!= ["foo<",
+      "bar",
+      "baz"].join(", ")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_ruby_multiline_with_normal_multiline
+    assert_equal(<<HTML, render(<<HAML))
+foobarbar, baz, bang
+<p>foo</p>
+<p>bar</p>
+HTML
+= "foo" + |
+  "bar" + |
+  ["bar", |
+   "baz",
+   "bang"].join(", ")
+%p foo
+%p bar
+HAML
+  end
+
+  def test_ruby_multiline_after_filter
+    assert_equal(<<HTML, render(<<HAML))
+foo
+bar
+bar, baz, bang
+<p>foo</p>
+<p>bar</p>
+HTML
+:plain
+  foo
+  bar
+= ["bar",
+   "baz",
+   "bang"].join(", ")
+%p foo
+%p bar
+HAML
+  end
+
+  # Encodings
+
+  def test_utf_8_bom; skip # encoding
+    assert_equal <<HTML, render(<<HAML)
+<div class='foo'>
+  <p>baz</p>
+</div>
+HTML
+\xEF\xBB\xBF.foo
+  %p baz
+HAML
+  end
+
+  def test_default_encoding
+    assert_equal(Encoding.find("utf-8"), render(<<HAML.encode("us-ascii")).encoding)
+%p bar
+%p foo
+HAML
+  end
+
+  def test_fake_ascii_encoding; skip # encoding
+    assert_encoded_equal(<<HTML.force_encoding("ascii-8bit"), render(<<HAML, :encoding => "ascii-8bit"))
+<p>bâr</p>
+<p>föö</p>
+HTML
+%p bâr
+%p föö
+HAML
+  end
+
+  def test_convert_template_render_proc
+    assert_converts_template_properly {|e| e.render_proc.call}
+  end
+
+  def test_convert_template_render
+    assert_converts_template_properly {|e| e.render}
+  end
+
+  def test_convert_template_def_method
+    assert_converts_template_properly do |e|
+      o = Object.new
+      e.def_method(o, :render)
+      o.render
+    end
+  end
+
+  def test_encoding_error; skip # encoding
+    render("foo\nbar\nb\xFEaz".force_encoding("utf-8"))
+    assert(false, "Expected exception")
+  rescue Hamlit::Error => e; skip
+    assert_equal(3, e.line)
+    assert_match(/Invalid .* character/, e.message)
+  end
+
+  def test_ascii_incompatible_encoding_error; skip # encoding
+    template = "foo\nbar\nb_z".encode("utf-16le")
+    template[9] = "\xFE".force_encoding("utf-16le")
+    render(template)
+    assert(false, "Expected exception")
+  rescue Hamlit::Error => e
+    assert_equal(3, e.line)
+    assert_match(/Invalid .* character/, e.message)
+  end
+
+  def test_same_coding_comment_as_encoding
+    assert_renders_encoded(<<HTML, <<HAML)
+<p>bâr</p>
+<p>föö</p>
+HTML
+-# coding: utf-8
+%p bâr
+%p föö
+HAML
+  end
+
+  def test_coding_comments; skip # encoding
+    assert_valid_encoding_comment("-# coding: ibm866")
+    assert_valid_encoding_comment("-# CodINg: IbM866")
+    assert_valid_encoding_comment("-#coding:ibm866")
+    assert_valid_encoding_comment("-# CodINg= ibm866")
+    assert_valid_encoding_comment("-# foo BAR FAOJcoding: ibm866")
+    assert_valid_encoding_comment("-# coding: ibm866 ASFJ (&(&#!$")
+    assert_valid_encoding_comment("-# -*- coding: ibm866")
+    assert_valid_encoding_comment("-# coding: ibm866 -*- coding: blah")
+    assert_valid_encoding_comment("-# -*- coding: ibm866 -*-")
+    assert_valid_encoding_comment("-# -*- encoding: ibm866 -*-")
+    assert_valid_encoding_comment('-# -*- coding: "ibm866" -*-')
+    assert_valid_encoding_comment("-#-*-coding:ibm866-*-")
+    assert_valid_encoding_comment("-#-*-coding:ibm866-*-")
+    assert_valid_encoding_comment("-# -*- foo: bar; coding: ibm866; baz: bang -*-")
+    assert_valid_encoding_comment("-# foo bar coding: baz -*- coding: ibm866 -*-")
+    assert_valid_encoding_comment("-# -*- coding: ibm866 -*- foo bar coding: baz")
+  end
+
+  def test_different_coding_than_system; skip # encoding
+    assert_renders_encoded(<<HTML.encode("IBM866"), <<HAML.encode("IBM866"))
+<p>тАЬ</p>
+HTML
+%p тАЬ
+HAML
+  end
+
+  def test_block_spacing
+    begin
+      assert render(<<-HAML)
+- foo = ["bar", "baz", "kni"]
+- foo.each do | item |
+  = item
+HAML
+    rescue ::SyntaxError
+      flunk("Should not have raised syntax error")
+    end
+  end
+
+  def test_tracing; skip # options
+    result = render('%p{:class => "hello"}', :trace => true, :filename => 'foo').strip
+    assert_equal "<p class='hello' data-trace='foo:1'></p>", result
+  end
+
+  private
+
+  def assert_valid_encoding_comment(comment)
+    assert_renders_encoded(<<HTML.encode("IBM866"), <<HAML.encode("IBM866").force_encoding("UTF-8"))
+<p>ЖЛЫ</p>
+<p>тАЬ</p>
+HTML
+#{comment}
+%p ЖЛЫ
+%p тАЬ
+HAML
+  end
+
+  def assert_converts_template_properly
+    engine = Haml::Engine.new(<<HAML.encode("iso-8859-1"), :encoding => "macRoman")
+%p bâr
+%p föö
+HAML
+    assert_encoded_equal(<<HTML.encode("macRoman"), yield(engine))
+<p>bâr</p>
+<p>föö</p>
+HTML
+  end
+
+  def assert_renders_encoded(html, haml)
+    result = render(haml)
+    assert_encoded_equal html, result
+  end
+
+  def assert_encoded_equal(expected, actual)
+    assert_equal expected.encoding, actual.encoding
+    assert_equal expected, actual
+  end
+end
diff --git a/test/haml/erb/_av_partial_1.erb b/test/haml/erb/_av_partial_1.erb
new file mode 100644 (file)
index 0000000..0c836a1
--- /dev/null
@@ -0,0 +1,12 @@
+<h2>This is a pretty complicated partial</h2>
+<div class="partial">
+  <p>It has several nested partials,</p>
+  <ul>
+    <% 5.times do %>
+      <li>
+        <strong>Partial:</strong>
+        <% @nesting = 5 %>
+        <%= render :partial => 'erb/av_partial_2' %>
+    <% end %>
+  </ul>
+</div>
diff --git a/test/haml/erb/_av_partial_2.erb b/test/haml/erb/_av_partial_2.erb
new file mode 100644 (file)
index 0000000..189d858
--- /dev/null
@@ -0,0 +1,8 @@
+<% @nesting -= 1 %>
+<div class="partial" level="<%= @nesting %>">
+  <h3>This is a crazy deep-nested partial.</h3>
+  <p>Nesting level <%= @nesting %></p>
+  <% if @nesting > 0 %>
+    <%= render :partial => 'erb/av_partial_2' %>
+  <% end %>
+</div>
diff --git a/test/haml/erb/action_view.erb b/test/haml/erb/action_view.erb
new file mode 100644 (file)
index 0000000..389ffe9
--- /dev/null
@@ -0,0 +1,62 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en-US'>
+  <head>
+    <title>Hampton Catlin Is Totally Awesome</title>
+    <meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
+  </head>
+  <body>
+    <h1>
+      This is very much like the standard template,
+      except that it has some ActionView-specific stuff.
+      It's only used for benchmarking.
+    </h1>
+    <div class="crazy_partials">
+      <%= render :partial => 'erb/av_partial_1' %>
+    </div>
+    <!-- You're In my house now! -->
+    <div class='header'>
+      Yes, ladies and gentileman. He is just that egotistical.
+      Fantastic! This should be multi-line output
+      The question is if this would translate! Ahah!
+      <%= 1 + 9 + 8 + 2 %>
+      <%# numbers should work and this should be ignored %>
+    </div>
+    <% 120.times do |number| -%>
+      <%= number %>
+    <% end -%>
+    <div id='body'><%= " Quotes should be loved! Just like people!" %></div>
+    Wow.
+    <p>
+      <%= "Holy cow        " +
+          "multiline       " +
+          "tags!           " +
+          "A pipe (|) even!"  %>
+      <%= [1, 2, 3].collect { |n| "PipesIgnored|" } %>
+      <%= [1, 2, 3].collect { |n|
+          n.to_s
+        }.join("|") %>
+    </p>
+    <div class='silent'>
+      <% foo = String.new
+         foo << "this"
+         foo << " shouldn't"
+         foo << " evaluate" %>
+      <%= foo + "but now it should!" %>
+      <%# Woah crap a comment! %>
+    </div>
+    <ul class='really cool'>
+      <% ('a'..'f').each do |a|%>
+      <li><%= a %>
+      <% end %>
+    <div class='of_divs_with_underscore' id='combo'><%= @should_eval = "with this text" %></div>
+    <%= [ 104, 101, 108, 108, 111 ].map do |byte|
+      byte.chr
+    end %>
+    <div class='footer'>
+      <strong class='shout'>
+        <%= "This is a really long ruby quote. It should be loved and wrapped because its more than 50 characters. This value may change in the future and this test may look stupid.\n" + 
+"        So, I'm just making it *really* long. God, I hope this works" %>
+      </strong>
+    </div>
+  </body>
+</html>
diff --git a/test/haml/erb/standard.erb b/test/haml/erb/standard.erb
new file mode 100644 (file)
index 0000000..0cc8ea7
--- /dev/null
@@ -0,0 +1,55 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en-US' lang='en-US'>
+  <head>
+    <title>Hampton Catlin Is Totally Awesome</title>
+    <meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
+  </head>
+  <body>
+    <!-- You're In my house now! -->
+    <div class='header'>
+      Yes, ladies and gentileman. He is just that egotistical.
+      Fantastic! This should be multi-line output
+      The question is if this would translate! Ahah!
+      <%= 1 + 9 + 8 + 2 %>
+      <%# numbers should work and this should be ignored %>
+    </div>
+    <% 120.times do |number| -%>
+      <%= number %>
+    <% end -%>
+    <div id='body'><%= " Quotes should be loved! Just like people!" %></div>
+    Wow.
+    <p code="<%= 1 + 2 %>">
+      <%= "Holy cow        " +
+          "multiline       " +
+          "tags!           " +
+          "A pipe (|) even!"  %>
+      <%= [1, 2, 3].collect { |n| "PipesIgnored|" }.join %>
+      <%= [1, 2, 3].collect { |n|
+          n.to_s
+        }.join("|") %>
+    </p>
+    <% bar = 17 %>
+    <div class='silent' foo="<%= bar %>">
+      <% foo = String.new
+         foo << "this"
+         foo << " shouldn't"
+         foo << " evaluate" %>
+      <%= foo + "but now it should!" %>
+      <%# Woah crap a comment! %>
+    </div>
+    <ul class='really cool'>
+      <% ('a'..'f').each do |a|%>
+      <li><%= a %></li>
+      <% end %>
+    <div class='of_divs_with_underscore' id='combo'><%= @should_eval = "with this text" %></div>
+    <%= "foo".each_line do |line|
+      nil
+    end %>
+    <div class='footer'>
+      <strong class='shout'>
+        <%= "This is a really long ruby quote. It should be loved and wrapped because its more than 50 characters. This value may change in the future and this test may look stupid.\n" + 
+"        So, I'm just making it *really* long. God, I hope this works" %>
+      </strong>
+    </div>
+  </body>
+</html>
diff --git a/test/haml/filters_test.rb b/test/haml/filters_test.rb
new file mode 100644 (file)
index 0000000..b9e8e8b
--- /dev/null
@@ -0,0 +1,262 @@
+require 'test_helper'
+
+class FiltersTest < Haml::TestCase
+  test "should be registered as filters when including Hamlit::Filters::Base" do; skip
+    begin
+      refute Hamlit::Filters.defined.has_key? "bar"
+      Module.new {def self.name; "Foo::Bar"; end; include Hamlit::Filters::Base}
+      assert Hamlit::Filters.defined.has_key? "bar"
+    ensure
+      Hamlit::Filters.remove_filter "Bar"
+    end
+  end
+
+  test "should raise error when attempting to register a defined Tilt filter" do; skip
+    begin
+      assert_raises RuntimeError do
+        2.times do
+          Hamlit::Filters.register_tilt_filter "Foo"
+        end
+      end
+    ensure
+      Hamlit::Filters.remove_filter "Foo"
+    end
+  end
+
+  test "should raise error when a Tilt filters dependencies are unavailable for extension" do; skip
+    begin
+      assert_raises Hamlit::Error do
+        # ignore warnings from Tilt
+        silence_warnings do
+          Hamlit::Filters.register_tilt_filter "Textile"
+          Hamlit::Filters.defined["textile"].template_class
+        end
+      end
+    ensure
+      Hamlit::Filters.remove_filter "Textile"
+    end
+  end
+
+  test "should raise error when a Tilt filters dependencies are unavailable for filter without extension" do; skip
+    begin
+      assert_raises Hamlit::Error do
+        Hamlit::Filters.register_tilt_filter "Maruku"
+        Hamlit::Filters.defined["maruku"].template_class
+      end
+    ensure
+      Hamlit::Filters.remove_filter "Maruku"
+    end
+  end
+
+  test "should raise informative error about Maruku being moved to haml-contrib" do; skip
+    begin
+      render(":maruku\n  # foo")
+      flunk("Should have raised error with message about the haml-contrib gem.")
+    rescue Hamlit::Error => e
+      assert_equal e.message, Hamlit::Error.message(:install_haml_contrib, "maruku")
+    end
+  end
+
+  test "should raise informative error about Textile being moved to haml-contrib" do; skip
+    begin
+      render(":textile\n  h1. foo")
+      flunk("Should have raised error with message about the haml-contrib gem.")
+    rescue Hamlit::Error => e
+      assert_equal e.message, Hamlit::Error.message(:install_haml_contrib, "textile")
+    end
+  end
+
+  test "should respect escaped newlines and interpolation" do
+    assert_haml_ugly(":plain\n  \\n\#{""}")
+  end
+
+  test "should process an filter with no content" do
+    assert_equal("\n", render(':plain'))
+  end
+
+  test "should be compatible with ugly mode" do
+    expectation = "foo\n"
+    assert_equal(expectation, render(":plain\n  foo", :ugly => true))
+  end
+
+  test "should pass options to Tilt filters that precompile" do; skip
+    begin
+      orig_erb_opts = Hamlit::Filters::Erb.options
+      haml  = ":erb\n  <%= 'foo' %>"
+      refute_match('test_var', Haml::Engine.new(haml).compiler.precompiled)
+      Hamlit::Filters::Erb.options = {:outvar => 'test_var'}
+      assert_match('test_var', Haml::Engine.new(haml).compiler.precompiled)
+    ensure
+      Hamlit::Filters::Erb.options = orig_erb_opts
+    end
+  end
+
+  test "should pass options to Tilt filters that don't precompile" do; skip
+    begin
+      filter = Class.new(Tilt::Template) do
+        def self.name
+          "Foo"
+        end
+
+        def prepare
+          @engine = {:data => data, :options => options}
+        end
+
+        def evaluate(scope, locals, &block)
+          @output = @engine[:options].to_a.join
+        end
+      end
+      Hamlit::Filters.register_tilt_filter "Foo", :template_class => filter
+      Hamlit::Filters::Foo.options[:foo] = "bar"
+      haml = ":foo"
+      assert_equal "foobar\n", render(haml)
+    ensure
+      Hamlit::Filters.remove_filter "Foo"
+    end
+  end
+
+  test "interpolated code should be escaped if escape_html is set" do
+    assert_haml_ugly(":plain\n  \#{'<script>evil</script>'}")
+  end
+
+end
+
+class ErbFilterTest < Haml::TestCase
+  test "multiline expressions should work" do; skip
+    assert_haml_ugly(%Q{:erb\n  <%= "foo" +\n      "bar" +\n      "baz" %>})
+  end
+
+  test "should evaluate in the same context as Haml" do; skip
+    haml  = ":erb\n  <%= foo %>"
+    html  = "bar\n"
+    scope = Object.new.instance_eval {foo = "bar"; nil if foo; binding}
+    assert_equal(html, render(haml, :scope => scope))
+  end
+
+  test "should use Rails's XSS safety features" do; skip
+    assert_equal("&lt;img&gt;\n", render(":erb\n  <%= '<img>' %>"))
+    assert_equal("<img>\n", render(":erb\n  <%= '<img>'.html_safe %>"))
+  end
+
+end
+
+class JavascriptFilterTest < Haml::TestCase
+  test "should interpolate" do; skip
+    scope = Object.new.instance_eval {foo = "bar"; nil if foo; binding}
+    haml  = ":javascript\n  \#{foo}"
+    html  = render(haml, :scope => scope)
+    assert_match(/bar/, html)
+  end
+
+  test "should never HTML-escape non-interpolated ampersands" do; skip
+    html = "<script>\n  & < > &amp;\n</script>\n"
+    haml = %Q{:javascript\n  & < > \#{"&"}}
+    assert_equal(html, render(haml, :escape_html => true))
+  end
+
+  test "should not include type in HTML 5 output" do
+    html = "<script>\n  foo bar\n</script>\n"
+    haml = ":javascript\n  foo bar"
+    assert_equal(html, render(haml, :format => :html5))
+  end
+
+  test "should always include CDATA when format is xhtml" do
+    html = "<script type='text/javascript'>\n  //<![CDATA[\n    foo bar\n  //]]>\n</script>\n"
+    haml = ":javascript\n  foo bar"
+    assert_equal(html, render(haml, :format => :xhtml, :cdata => false))
+  end
+
+  test "should omit CDATA when cdata option is false" do
+    html = "<script>\n  foo bar\n</script>\n"
+    haml = ":javascript\n  foo bar"
+    assert_equal(html, render(haml, :format => :html5, :cdata => false))
+  end
+
+  test "should include CDATA when cdata option is true" do; skip
+    html = "<script>\n  //<![CDATA[\n    foo bar\n  //]]>\n</script>\n"
+    haml = ":javascript\n  foo bar"
+    assert_equal(html, render(haml, :format => :html5, :cdata => true))
+  end
+
+  test "should default to no CDATA when format is html5" do
+    haml = ":javascript\n  foo bar"
+    out = render(haml, :format => :html5)
+    refute_match('//<![CDATA[', out)
+    refute_match('//]]>', out)
+  end
+end
+
+class CSSFilterTest < Haml::TestCase
+  test "should wrap output in CDATA and a CSS tag when output is XHTML" do
+    html = "<style type='text/css'>\n  /*<![CDATA[*/\n    foo\n  /*]]>*/\n</style>\n"
+    haml = ":css\n  foo"
+    assert_equal(html, render(haml, :format => :xhtml))
+  end
+
+  test "should not include type in HTML 5 output" do
+    html = "<style>\n  foo bar\n</style>\n"
+    haml = ":css\n  foo bar"
+    assert_equal(html, render(haml, :format => :html5))
+  end
+
+  test "should always include CDATA when format is xhtml" do
+    html = "<style type='text/css'>\n  /*<![CDATA[*/\n    foo bar\n  /*]]>*/\n</style>\n"
+    haml = ":css\n  foo bar"
+    assert_equal(html, render(haml, :format => :xhtml, :cdata => false))
+  end
+
+  test "should omit CDATA when cdata option is false" do
+    html = "<style>\n  foo bar\n</style>\n"
+    haml = ":css\n  foo bar"
+    assert_equal(html, render(haml, :format => :html5, :cdata => false))
+  end
+
+  test "should include CDATA when cdata option is true" do; skip
+    html = "<style>\n  /*<![CDATA[*/\n    foo bar\n  /*]]>*/\n</style>\n"
+    haml = ":css\n  foo bar"
+    assert_equal(html, render(haml, :format => :html5, :cdata => true))
+  end
+
+  test "should default to no CDATA when format is html5" do
+    haml = ":css\n  foo bar"
+    out = render(haml, :format => :html5)
+    refute_match('<![CDATA[', out)
+    refute_match(']]>', out)
+  end
+end
+
+class CDATAFilterTest < Haml::TestCase
+  test "should wrap output in CDATA tag" do
+    html = "<![CDATA[\n    foo\n]]>\n"
+    haml = ":cdata\n  foo"
+    assert_equal(html, render(haml))
+  end
+end
+
+class EscapedFilterTest < Haml::TestCase
+  test "should escape ampersands" do
+    html = "&amp;\n"
+    haml = ":escaped\n  &"
+    assert_equal(html, render(haml))
+  end
+end
+
+class RubyFilterTest < Haml::TestCase
+  test "can write to haml_io" do; skip
+    haml = ":ruby\n  haml_io.puts 'hello'\n"
+    html = "hello\n"
+    assert_equal(html, render(haml))
+  end
+
+  test "haml_io appends to output" do; skip
+    haml = "hello\n:ruby\n  haml_io.puts 'hello'\n"
+    html = "hello\nhello\n"
+    assert_equal(html, render(haml))
+  end
+
+  test "can create local variables" do; skip
+    haml = ":ruby\n  a = 7\n=a"
+    html = "7\n"
+    assert_equal(html, render(haml))
+  end
+end
diff --git a/test/haml/gemfiles/.bundle/config b/test/haml/gemfiles/.bundle/config
new file mode 100644 (file)
index 0000000..2fbf0ff
--- /dev/null
@@ -0,0 +1 @@
+--- {}
diff --git a/test/haml/gemfiles/Gemfile.rails-4.0.x b/test/haml/gemfiles/Gemfile.rails-4.0.x
new file mode 100644 (file)
index 0000000..9769d16
--- /dev/null
@@ -0,0 +1,10 @@
+source "https://rubygems.org"
+
+if ENV['TRAVIS']
+  platform :mri_21 do
+    gem 'coveralls', require: false
+  end
+end
+
+gem 'rails', '~> 4.0.0'
+gemspec :path => '../..'
diff --git a/test/haml/gemfiles/Gemfile.rails-4.1.x b/test/haml/gemfiles/Gemfile.rails-4.1.x
new file mode 100644 (file)
index 0000000..23af5b1
--- /dev/null
@@ -0,0 +1,4 @@
+source "https://rubygems.org"
+
+gem 'rails', '~> 4.1.0'
+gemspec :path => '../..'
diff --git a/test/haml/gemfiles/Gemfile.rails-4.2.x b/test/haml/gemfiles/Gemfile.rails-4.2.x
new file mode 100644 (file)
index 0000000..fa56c67
--- /dev/null
@@ -0,0 +1,4 @@
+source "https://rubygems.org"
+
+gem 'rails', '~> 4.2.0'
+gemspec :path => '../..'
diff --git a/test/haml/haml-spec/LICENSE b/test/haml/haml-spec/LICENSE
new file mode 100644 (file)
index 0000000..5a8e332
--- /dev/null
@@ -0,0 +1,14 @@
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+                    Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. You just DO WHAT THE FUCK YOU WANT TO.
+
diff --git a/test/haml/haml-spec/README.md b/test/haml/haml-spec/README.md
new file mode 100644 (file)
index 0000000..3366c7b
--- /dev/null
@@ -0,0 +1,106 @@
+# Haml Spec #
+
+Haml Spec provides a basic suite of tests for Haml interpreters.
+
+It is intented for developers who are creating or maintaining an implementation
+of the [Haml](http://haml-lang.com) markup language.
+
+At the moment, there are test runners for the [original
+Haml](http://github.com/nex3/haml) in Ruby, [Lua
+Haml](http://github.com/norman/lua-haml) and the
+[Text::Haml](http://github.com/vti/text-haml) Perl port. Support for other
+versions of Haml will be added if their developers/maintainers are interested in
+using it.
+
+## The Tests ##
+
+The tests are kept in JSON format for portability across languages.  Each test
+is a JSON object with expected input, output, local variables and configuration
+parameters (see below).  The test suite only provides tests for features which
+are portable, therefore no tests for script are provided, nor for external
+filters such as :markdown or :textile.
+
+The one major exception to this are the tests for interpolation, which you may
+need to modify with a regular expression to run under PHP or Perl, which
+require a sigil before variable names. These tests are included despite being
+less than 100% portable because interpolation is an important part of Haml and
+can be tricky to implement. These tests are flagged as "optional" so that you
+can avoid running them if your implementation of Haml will not support this
+feature.
+
+## Running the Tests ##
+
+### Ruby ###
+
+The Ruby test runner uses minitest, the same as the Ruby Haml implementation.
+To run the tests you probably only need to install `haml`, `minitest` and
+possibly `ruby` if your platform doesn't come with it by default. If you're
+using Ruby 1.8.x, you'll also need to install `json`:
+
+    sudo gem install haml
+    sudo gem install minitest
+    # for Ruby 1.8.x; check using "ruby --version" if unsure
+    sudo gem install json
+
+Then, running the Ruby test suite is easy:
+
+    ruby ruby_haml_test.rb
+
+At the moment, running the tests with Ruby 1.8.7 fails because of issues with
+the JSON library. Please use 1.9.2 until this is resolved.
+
+### Lua ###
+
+The Lua test depends on
+[Penlight](http://stevedonovan.github.com/Penlight/),
+[Telescope](http://github.com/norman/telescope),
+[jason4lua](http://json.luaforge.net/), and
+[Lua Haml](http://github.com/norman/lua-haml). Install and run `tsc
+lua_haml_spec.lua`.
+
+### Getting it ###
+
+You can access the [Git repository](http://github.com/norman/haml-spec) at:
+
+    git://github.com/norman/haml-spec.git
+
+Patches are *very* welcome, as are test runners for your Haml implementation.
+
+As long as any test you add run against Ruby Haml and are not redundant, I'll
+be very happy to add them.
+
+### Test JSON format ###
+
+    "test name" : {
+      "haml"     : "haml input",
+      "html"     : "expected html output",
+      "result"   : "expected test result",
+      "locals"   : "local vars",
+      "config"   : "config params",
+      "optional" : true|false
+    }
+
+* test name: This should be a *very* brief description of what's being tested. It can
+  be used by the test runners to name test methods, or to exclude certain tests from being
+  run.
+* haml: The Haml code to be evaluated. Always required.
+* html: The HTML output that should be generated. Required unless "result" is "error".
+* result: Can be "pass" or "error". If it's absent, then "pass" is assumed. If it's "error",
+  then the goal of the test is to make sure that malformed Haml code generates an error.
+* locals: An object containing local variables needed for the test.
+* config: An object containing configuration parameters used to run the test.
+  The configuration parameters should be usable directly by Ruby's Haml with no
+  modification.  If your implementation uses config parameters with different
+  names, you may need to process them to make them match your implementation.
+  If your implementation has options that do not exist in Ruby's Haml, then you
+  should add tests for this in your implementation's test rather than here.
+* optional: whether or not the test is optional
+
+## License ##
+
+  This project is released under the [WTFPL](http://sam.zoy.org/wtfpl/) in order
+  to be as usable as possible in any project, commercial or free.
+
+## Author ##
+
+  [Norman Clarke](mailto:norman@njclarke.com)
diff --git a/test/haml/haml-spec/Rakefile b/test/haml/haml-spec/Rakefile
new file mode 100644 (file)
index 0000000..601bc73
--- /dev/null
@@ -0,0 +1,85 @@
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'yaml'
+require 'unindent'
+require 'open-uri'
+
+def escape_name(name, replacer)
+  name.gsub(/[\s\-\(\)\.\.+'\/<>&=~\!]+/, replacer)
+end
+
+def generate_spec(mode)
+  spec = <<-SPEC.unindent
+    require "minitest/autorun"
+    require "hamlit"
+    require "haml"
+
+    # This is a spec converted by haml-spec.
+    # See: https://github.com/haml/haml-spec
+    class #{mode.capitalize}Test < MiniTest::Test
+      HAML_DEFAULT_OPTIONS = { ugly: #{mode == :ugly}, escape_html: true }.freeze
+      HAMLIT_DEFAULT_OPTIONS = { escape_html: true }.freeze
+
+      def self.haml_result(haml, options, locals)
+        Haml::Engine.new(haml, HAML_DEFAULT_OPTIONS.merge(options)).render(Object.new, locals)
+      end
+
+      def self.hamlit_result(haml, options, locals)
+        eval Hamlit::Engine.new(haml, HAMLIT_DEFAULT_OPTIONS.merge(options)).render(Object.new, locals)
+      end
+
+  SPEC
+
+  contexts = YAML.load(File.read(File.expand_path('./tests.yml', __dir__)))
+  contexts.each_with_index do |context, index|
+    spec += "\n" if index != 0
+    spec += "  class #{escape_name(context[0], '').capitalize} < MiniTest::Test\n"
+
+    tests = []
+    context[1].each do |name, test|
+      tests << {
+        name: name,
+        html: test['html'],
+        haml: test['haml'],
+        locals: test['locals'],
+        config: test['config'],
+      }
+    end
+
+    spec += tests.map { |test|
+      locals  = Hash[(test[:locals] || {}).map {|x, y| [x.to_sym, y]}]
+      options = Hash[(test[:config] || {}).map {|x, y| [x.to_sym, y]}]
+      options[:format] = options[:format].to_sym if options[:format]
+
+      generate_specify(test, locals, options, mode)
+    }.join("\n")
+    spec += "  end\n"
+  end
+
+  spec += "end\n"
+  File.write("#{mode}_test.rb", spec)
+end
+
+def generate_specify(test, locals, options, mode)
+  <<-SPEC
+    def test_#{escape_name(test[:name], '_')}
+      haml    = %q{#{test[:haml]}}
+      html    = %q{#{test[:html]}}
+      locals  = #{locals}
+      options = #{options}
+      haml_result = #{mode.capitalize}Test.haml_result(haml, options, locals)
+      hamlit_result = #{mode.capitalize}Test.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  SPEC
+end
+
+desc 'Convert tests.yml into ugly tests'
+task :pretty do
+  generate_spec(:pretty)
+end
+
+desc 'Convert tests.yml into ugly tests'
+task :ugly do
+  generate_spec(:ugly)
+end
diff --git a/test/haml/haml-spec/tests.yml b/test/haml/haml-spec/tests.yml
new file mode 100644 (file)
index 0000000..4589fe9
--- /dev/null
@@ -0,0 +1,538 @@
+---
+headers:
+  an XHTML XML prolog:
+    haml: "!!! XML"
+    html: "<?xml version='1.0' encoding='utf-8' ?>"
+    config:
+      format: xhtml
+  an XHTML default (transitional) doctype:
+    haml: "!!!"
+    html: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+    config:
+      format: xhtml
+  an XHTML 1.1 doctype:
+    haml: "!!! 1.1"
+    html: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+    config:
+      format: xhtml
+  an XHTML 1.2 mobile doctype:
+    haml: "!!! mobile"
+    html: <!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">
+    config:
+      format: xhtml
+  an XHTML 1.1 basic doctype:
+    haml: "!!! basic"
+    html: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">
+    config:
+      format: xhtml
+  an XHTML 1.0 frameset doctype:
+    haml: "!!! frameset"
+    html: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
+    config:
+      format: xhtml
+  an HTML 5 doctype with XHTML syntax:
+    haml: "!!! 5"
+    html: "<!DOCTYPE html>"
+    config:
+      format: xhtml
+  an HTML 5 XML prolog (silent):
+    haml: "!!! XML"
+    html: ''
+    config:
+      format: html5
+  an HTML 5 doctype:
+    haml: "!!!"
+    html: "<!DOCTYPE html>"
+    config:
+      format: html5
+  an HTML 4 XML prolog (silent):
+    haml: "!!! XML"
+    html: ''
+    config:
+      format: html4
+  an HTML 4 default (transitional) doctype:
+    haml: "!!!"
+    html: <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+    config:
+      format: html4
+  an HTML 4 frameset doctype:
+    haml: "!!! frameset"
+    html: <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
+    config:
+      format: html4
+  an HTML 4 strict doctype:
+    haml: "!!! strict"
+    html: <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+    config:
+      format: html4
+basic Haml tags and CSS:
+  a simple Haml tag:
+    haml: "%p"
+    html: "<p></p>"
+  a self-closing tag (XHTML):
+    haml: "%meta"
+    html: "<meta />"
+    config:
+      format: xhtml
+  a self-closing tag (HTML4):
+    haml: "%meta"
+    html: "<meta>"
+    config:
+      format: html4
+  a self-closing tag (HTML5):
+    haml: "%meta"
+    html: "<meta>"
+    config:
+      format: html5
+  a self-closing tag ('/' modifier + XHTML):
+    haml: "%zzz/"
+    html: "<zzz />"
+    config:
+      format: xhtml
+  a self-closing tag ('/' modifier + HTML5):
+    haml: "%zzz/"
+    html: "<zzz>"
+    config:
+      format: html5
+  a tag with a CSS class:
+    haml: "%p.class1"
+    html: "<p class='class1'></p>"
+  a tag with multiple CSS classes:
+    haml: "%p.class1.class2"
+    html: "<p class='class1 class2'></p>"
+  a tag with a CSS id:
+    haml: "%p#id1"
+    html: "<p id='id1'></p>"
+  a tag with multiple CSS id's:
+    haml: "%p#id1#id2"
+    html: "<p id='id2'></p>"
+  a tag with a class followed by an id:
+    haml: "%p.class1#id1"
+    html: "<p class='class1' id='id1'></p>"
+  a tag with an id followed by a class:
+    haml: "%p#id1.class1"
+    html: "<p class='class1' id='id1'></p>"
+  an implicit div with a CSS id:
+    haml: "#id1"
+    html: "<div id='id1'></div>"
+  an implicit div with a CSS class:
+    haml: ".class1"
+    html: "<div class='class1'></div>"
+  multiple simple Haml tags:
+    haml: |-
+      %div
+        %div
+          %p
+    html: |-
+      <div>
+        <div>
+          <p></p>
+        </div>
+      </div>
+tags with unusual HTML characters:
+  a tag with colons:
+    haml: "%ns:tag"
+    html: "<ns:tag></ns:tag>"
+  a tag with underscores:
+    haml: "%snake_case"
+    html: "<snake_case></snake_case>"
+  a tag with dashes:
+    haml: "%dashed-tag"
+    html: "<dashed-tag></dashed-tag>"
+  a tag with camelCase:
+    haml: "%camelCase"
+    html: "<camelCase></camelCase>"
+  a tag with PascalCase:
+    haml: "%PascalCase"
+    html: "<PascalCase></PascalCase>"
+tags with unusual CSS identifiers:
+  an all-numeric class:
+    haml: ".123"
+    html: "<div class='123'></div>"
+  a class with underscores:
+    haml: ".__"
+    html: "<div class='__'></div>"
+  a class with dashes:
+    haml: ".--"
+    html: "<div class='--'></div>"
+tags with inline content:
+  Inline content simple tag:
+    haml: "%p hello"
+    html: "<p>hello</p>"
+  Inline content tag with CSS:
+    haml: "%p.class1 hello"
+    html: "<p class='class1'>hello</p>"
+  Inline content multiple simple tags:
+    haml: |-
+      %div
+        %div
+          %p text
+    html: |-
+      <div>
+        <div>
+          <p>text</p>
+        </div>
+      </div>
+tags with nested content:
+  Nested content simple tag:
+    haml: |-
+      %p
+        hello
+    html: |-
+      <p>
+        hello
+      </p>
+  Nested content tag with CSS:
+    haml: |-
+      %p.class1
+        hello
+    html: |-
+      <p class='class1'>
+        hello
+      </p>
+  Nested content multiple simple tags:
+    haml: |-
+      %div
+        %div
+          %p
+            text
+    html: |-
+      <div>
+        <div>
+          <p>
+            text
+          </p>
+        </div>
+      </div>
+tags with HTML-style attributes:
+  HTML-style one attribute:
+    haml: "%p(a='b')"
+    html: "<p a='b'></p>"
+  HTML-style multiple attributes:
+    haml: "%p(a='b' c='d')"
+    html: "<p a='b' c='d'></p>"
+  HTML-style attributes separated with newlines:
+    haml: |-
+      %p(a='b'
+        c='d')
+    html: "<p a='b' c='d'></p>"
+  HTML-style interpolated attribute:
+    haml: '%p(a="#{var}")'
+    html: "<p a='value'></p>"
+    locals:
+      var: value
+  HTML-style 'class' as an attribute:
+    haml: "%p(class='class1')"
+    html: "<p class='class1'></p>"
+  HTML-style tag with a CSS class and 'class' as an attribute:
+    haml: "%p.class2(class='class1')"
+    html: "<p class='class1 class2'></p>"
+  HTML-style tag with 'id' as an attribute:
+    haml: "%p(id='1')"
+    html: "<p id='1'></p>"
+  HTML-style tag with a CSS id and 'id' as an attribute:
+    haml: "%p#id(id='1')"
+    html: "<p id='id_1'></p>"
+  HTML-style tag with a variable attribute:
+    haml: "%p(class=var)"
+    html: "<p class='hello'></p>"
+    locals:
+      var: hello
+  HTML-style tag with a CSS class and 'class' as a variable attribute:
+    haml: ".hello(class=var)"
+    html: "<div class='hello world'></div>"
+    locals:
+      var: world
+  HTML-style tag multiple CSS classes (sorted correctly):
+    haml: ".z(class=var)"
+    html: "<div class='a z'></div>"
+    locals:
+      var: a
+  HTML-style tag with an atomic attribute:
+    haml: "%a(flag)"
+    html: "<a flag></a>"
+tags with Ruby-style attributes:
+  Ruby-style one attribute:
+    haml: "%p{:a => 'b'}"
+    html: "<p a='b'></p>"
+    optional: true
+  Ruby-style attributes hash with whitespace:
+    haml: "%p{  :a  =>  'b'  }"
+    html: "<p a='b'></p>"
+    optional: true
+  Ruby-style interpolated attribute:
+    haml: '%p{:a =>"#{var}"}'
+    html: "<p a='value'></p>"
+    optional: true
+    locals:
+      var: value
+  Ruby-style multiple attributes:
+    haml: "%p{ :a => 'b', 'c' => 'd' }"
+    html: "<p a='b' c='d'></p>"
+    optional: true
+  Ruby-style attributes separated with newlines:
+    haml: |-
+      %p{ :a => 'b',
+        'c' => 'd' }
+    html: "<p a='b' c='d'></p>"
+    optional: true
+  Ruby-style 'class' as an attribute:
+    haml: "%p{:class => 'class1'}"
+    html: "<p class='class1'></p>"
+    optional: true
+  Ruby-style tag with a CSS class and 'class' as an attribute:
+    haml: "%p.class2{:class => 'class1'}"
+    html: "<p class='class1 class2'></p>"
+    optional: true
+  Ruby-style tag with 'id' as an attribute:
+    haml: "%p{:id => '1'}"
+    html: "<p id='1'></p>"
+    optional: true
+  Ruby-style tag with a CSS id and 'id' as an attribute:
+    haml: "%p#id{:id => '1'}"
+    html: "<p id='id_1'></p>"
+    optional: true
+  Ruby-style tag with a CSS id and a numeric 'id' as an attribute:
+    haml: "%p#id{:id => 1}"
+    html: "<p id='id_1'></p>"
+    optional: true
+  Ruby-style tag with a variable attribute:
+    haml: "%p{:class => var}"
+    html: "<p class='hello'></p>"
+    optional: true
+    locals:
+      var: hello
+  Ruby-style tag with a CSS class and 'class' as a variable attribute:
+    haml: ".hello{:class => var}"
+    html: "<div class='hello world'></div>"
+    optional: true
+    locals:
+      var: world
+  Ruby-style tag multiple CSS classes (sorted correctly):
+    haml: ".z{:class => var}"
+    html: "<div class='a z'></div>"
+    optional: true
+    locals:
+      var: a
+silent comments:
+  an inline silent comment:
+    haml: "-# hello"
+    html: ''
+  a nested silent comment:
+    haml: |-
+      -#
+        hello
+    html: ''
+  a multiply nested silent comment:
+    haml: |-
+      -#
+        %div
+          foo
+    html: ''
+  a multiply nested silent comment with inconsistent indents:
+    haml: |-
+      -#
+        %div
+            foo
+    html: ''
+markup comments:
+  an inline markup comment:
+    haml: "/ comment"
+    html: "<!-- comment -->"
+  a nested markup comment:
+    haml: |-
+      /
+        comment
+        comment2
+    html: |-
+      <!--
+        comment
+        comment2
+      -->
+conditional comments:
+  a conditional comment:
+    haml: |-
+      /[if IE]
+        %p a
+    html: |-
+      <!--[if IE]>
+        <p>a</p>
+      <![endif]-->
+internal filters:
+  content in an 'escaped' filter:
+    haml: |-
+      :escaped
+        <&">
+    html: "&lt;&amp;&quot;&gt;"
+  content in a 'preserve' filter:
+    haml: |-
+      :preserve
+        hello
+
+      %p
+    html: |-
+      hello&#x000A;
+      <p></p>
+  content in a 'plain' filter:
+    haml: |-
+      :plain
+        hello
+
+      %p
+    html: |-
+      hello
+      <p></p>
+  content in a 'css' filter (XHTML):
+    haml: |-
+      :css
+        hello
+
+      %p
+    html: |-
+      <style type='text/css'>
+        /*<![CDATA[*/
+          hello
+        /*]]>*/
+      </style>
+      <p></p>
+    config:
+      format: xhtml
+  content in a 'javascript' filter (XHTML):
+    haml: |-
+      :javascript
+        a();
+      %p
+    html: |-
+      <script type='text/javascript'>
+        //<![CDATA[
+          a();
+        //]]>
+      </script>
+      <p></p>
+    config:
+      format: xhtml
+  content in a 'css' filter (HTML):
+    haml: |-
+      :css
+        hello
+
+      %p
+    html: |-
+      <style>
+        hello
+      </style>
+      <p></p>
+    config:
+      format: html5
+  content in a 'javascript' filter (HTML):
+    haml: |-
+      :javascript
+        a();
+      %p
+    html: |-
+      <script>
+        a();
+      </script>
+      <p></p>
+    config:
+      format: html5
+Ruby-style interpolation:
+  interpolation inside inline content:
+    haml: "%p #{var}"
+    html: "<p>value</p>"
+    optional: true
+    locals:
+      var: value
+  no interpolation when escaped:
+    haml: "%p \\#{var}"
+    html: "<p>#{var}</p>"
+    optional: true
+    locals:
+      var: value
+  interpolation when the escape character is escaped:
+    haml: "%p \\\\#{var}"
+    html: "<p>\\value</p>"
+    optional: true
+    locals:
+      var: value
+  interpolation inside filtered content:
+    haml: |-
+      :plain
+        #{var} interpolated: #{var}
+    html: 'value interpolated: value'
+    optional: true
+    locals:
+      var: value
+HTML escaping:
+  code following '&=':
+    haml: '&= ''<"&>'''
+    html: "&lt;&quot;&amp;&gt;"
+  code following '=' when escape_haml is set to true:
+    haml: = '<"&>'
+    html: "&lt;&quot;&amp;&gt;"
+    config:
+      escape_html: 'true'
+  code following '!=' when escape_haml is set to true:
+    haml: '!= ''<"&>'''
+    html: <"&>
+    config:
+      escape_html: 'true'
+boolean attributes:
+  boolean attribute with XHTML:
+    haml: "%input(checked=true)"
+    html: "<input checked='checked' />"
+    config:
+      format: xhtml
+  boolean attribute with HTML:
+    haml: "%input(checked=true)"
+    html: "<input checked>"
+    config:
+      format: html5
+whitespace preservation:
+  following the '~' operator:
+    haml: ~ "Foo\n<pre>Bar\nBaz</pre>"
+    html: |-
+      Foo
+      <pre>Bar&#x000A;Baz</pre>
+    optional: true
+  inside a textarea tag:
+    haml: |-
+      %textarea
+        hello
+        hello
+    html: |-
+      <textarea>hello
+      hello</textarea>
+  inside a pre tag:
+    haml: |-
+      %pre
+        hello
+        hello
+    html: |-
+      <pre>hello
+      hello</pre>
+whitespace removal:
+  a tag with '>' appended and inline content:
+    haml: |-
+      %li hello
+      %li> world
+      %li again
+    html: "<li>hello</li><li>world</li><li>again</li>"
+  a tag with '>' appended and nested content:
+    haml: |-
+      %li hello
+      %li>
+        world
+      %li again
+    html: |-
+      <li>hello</li><li>
+        world
+      </li><li>again</li>
+  a tag with '<' appended:
+    haml: |-
+      %p<
+        hello
+        world
+    html: |-
+      <p>hello
+      world</p>
diff --git a/test/haml/haml-spec/ugly_test.rb b/test/haml/haml-spec/ugly_test.rb
new file mode 100644 (file)
index 0000000..a55ab57
--- /dev/null
@@ -0,0 +1,1110 @@
+$:.unshift File.expand_path('../../test', __dir__)
+
+require 'test_helper'
+require 'haml'
+require 'minitest/autorun'
+
+# This is a spec converted by haml-spec.
+# See: https://github.com/haml/haml-spec
+class UglyTest < MiniTest::Test
+  HAML_DEFAULT_OPTIONS = { ugly: true, escape_html: true, escape_attrs: true }.freeze
+  HAMLIT_DEFAULT_OPTIONS = { escape_html: true }.freeze
+
+  def self.haml_result(haml, options, locals)
+    Haml::Engine.new(haml, HAML_DEFAULT_OPTIONS.merge(options)).render(Object.new, locals)
+  end
+
+  def self.hamlit_result(haml, options, locals)
+    Hamlit::Template.new(HAMLIT_DEFAULT_OPTIONS.merge(options)) { haml }.render(Object.new, locals)
+  end
+
+  class Headers < MiniTest::Test
+    def test_an_XHTML_XML_prolog
+      haml    = %q{!!! XML}
+      html    = %q{<?xml version='1.0' encoding='utf-8' ?>}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_XHTML_default_transitional_doctype
+      haml    = %q{!!!}
+      html    = %q{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_XHTML_1_1_doctype
+      haml    = %q{!!! 1.1}
+      html    = %q{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_XHTML_1_2_mobile_doctype
+      haml    = %q{!!! mobile}
+      html    = %q{<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_XHTML_1_1_basic_doctype
+      haml    = %q{!!! basic}
+      html    = %q{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_XHTML_1_0_frameset_doctype
+      haml    = %q{!!! frameset}
+      html    = %q{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_HTML_5_doctype_with_XHTML_syntax
+      haml    = %q{!!! 5}
+      html    = %q{<!DOCTYPE html>}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_HTML_5_XML_prolog_silent_
+      haml    = %q{!!! XML}
+      html    = %q{}
+      locals  = {}
+      options = {:format=>:html5}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_HTML_5_doctype
+      haml    = %q{!!!}
+      html    = %q{<!DOCTYPE html>}
+      locals  = {}
+      options = {:format=>:html5}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_HTML_4_XML_prolog_silent_
+      haml    = %q{!!! XML}
+      html    = %q{}
+      locals  = {}
+      options = {:format=>:html4}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_HTML_4_default_transitional_doctype
+      haml    = %q{!!!}
+      html    = %q{<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">}
+      locals  = {}
+      options = {:format=>:html4}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_HTML_4_frameset_doctype
+      haml    = %q{!!! frameset}
+      html    = %q{<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">}
+      locals  = {}
+      options = {:format=>:html4}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_HTML_4_strict_doctype
+      haml    = %q{!!! strict}
+      html    = %q{<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">}
+      locals  = {}
+      options = {:format=>:html4}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Basichamltagsandcss < MiniTest::Test
+    def test_a_simple_Haml_tag
+      haml    = %q{%p}
+      html    = %q{<p></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_self_closing_tag_XHTML_
+      haml    = %q{%meta}
+      html    = %q{<meta />}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_self_closing_tag_HTML4_
+      haml    = %q{%meta}
+      html    = %q{<meta>}
+      locals  = {}
+      options = {:format=>:html4}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_self_closing_tag_HTML5_
+      haml    = %q{%meta}
+      html    = %q{<meta>}
+      locals  = {}
+      options = {:format=>:html5}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_self_closing_tag_modifier_XHTML_
+      haml    = %q{%zzz/}
+      html    = %q{<zzz />}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_self_closing_tag_modifier_HTML5_
+      haml    = %q{%zzz/}
+      html    = %q{<zzz>}
+      locals  = {}
+      options = {:format=>:html5}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_a_CSS_class
+      haml    = %q{%p.class1}
+      html    = %q{<p class='class1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_multiple_CSS_classes
+      haml    = %q{%p.class1.class2}
+      html    = %q{<p class='class1 class2'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_a_CSS_id
+      haml    = %q{%p#id1}
+      html    = %q{<p id='id1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_multiple_CSS_id_s
+      haml    = %q{%p#id1#id2}
+      html    = %q{<p id='id2'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_a_class_followed_by_an_id
+      haml    = %q{%p.class1#id1}
+      html    = %q{<p class='class1' id='id1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_an_id_followed_by_a_class
+      haml    = %q{%p#id1.class1}
+      html    = %q{<p class='class1' id='id1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_implicit_div_with_a_CSS_id
+      haml    = %q{#id1}
+      html    = %q{<div id='id1'></div>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_an_implicit_div_with_a_CSS_class
+      haml    = %q{.class1}
+      html    = %q{<div class='class1'></div>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_multiple_simple_Haml_tags
+      haml    = %q{%div
+  %div
+    %p}
+      html    = %q{<div>
+  <div>
+    <p></p>
+  </div>
+</div>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Tagswithunusualhtmlcharacters < MiniTest::Test
+    def test_a_tag_with_colons
+      haml    = %q{%ns:tag}
+      html    = %q{<ns:tag></ns:tag>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_underscores
+      haml    = %q{%snake_case}
+      html    = %q{<snake_case></snake_case>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_dashes
+      haml    = %q{%dashed-tag}
+      html    = %q{<dashed-tag></dashed-tag>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_camelCase
+      haml    = %q{%camelCase}
+      html    = %q{<camelCase></camelCase>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_PascalCase
+      haml    = %q{%PascalCase}
+      html    = %q{<PascalCase></PascalCase>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Tagswithunusualcssidentifiers < MiniTest::Test
+    def test_an_all_numeric_class
+      haml    = %q{.123}
+      html    = %q{<div class='123'></div>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_class_with_underscores
+      haml    = %q{.__}
+      html    = %q{<div class='__'></div>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_class_with_dashes
+      haml    = %q{.--}
+      html    = %q{<div class='--'></div>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Tagswithinlinecontent < MiniTest::Test
+    def test_Inline_content_simple_tag
+      haml    = %q{%p hello}
+      html    = %q{<p>hello</p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Inline_content_tag_with_CSS
+      haml    = %q{%p.class1 hello}
+      html    = %q{<p class='class1'>hello</p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Inline_content_multiple_simple_tags
+      haml    = %q{%div
+  %div
+    %p text}
+      html    = %q{<div>
+  <div>
+    <p>text</p>
+  </div>
+</div>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Tagswithnestedcontent < MiniTest::Test
+    def test_Nested_content_simple_tag
+      haml    = %q{%p
+  hello}
+      html    = %q{<p>
+  hello
+</p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Nested_content_tag_with_CSS
+      haml    = %q{%p.class1
+  hello}
+      html    = %q{<p class='class1'>
+  hello
+</p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Nested_content_multiple_simple_tags
+      haml    = %q{%div
+  %div
+    %p
+      text}
+      html    = %q{<div>
+  <div>
+    <p>
+      text
+    </p>
+  </div>
+</div>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Tagswithhtmlstyleattributes < MiniTest::Test
+    def test_HTML_style_one_attribute
+      haml    = %q{%p(a='b')}
+      html    = %q{<p a='b'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_multiple_attributes
+      haml    = %q{%p(a='b' c='d')}
+      html    = %q{<p a='b' c='d'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_attributes_separated_with_newlines
+      haml    = %q{%p(a='b'
+  c='d')}
+      html    = %q{<p a='b' c='d'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_interpolated_attribute
+      haml    = %q{%p(a="#{var}")}
+      html    = %q{<p a='value'></p>}
+      locals  = {:var=>"value"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_class_as_an_attribute
+      haml    = %q{%p(class='class1')}
+      html    = %q{<p class='class1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_tag_with_a_CSS_class_and_class_as_an_attribute
+      haml    = %q{%p.class2(class='class1')}
+      html    = %q{<p class='class1 class2'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_tag_with_id_as_an_attribute
+      haml    = %q{%p(id='1')}
+      html    = %q{<p id='1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_tag_with_a_CSS_id_and_id_as_an_attribute
+      haml    = %q{%p#id(id='1')}
+      html    = %q{<p id='id_1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_tag_with_a_variable_attribute
+      haml    = %q{%p(class=var)}
+      html    = %q{<p class='hello'></p>}
+      locals  = {:var=>"hello"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_tag_with_a_CSS_class_and_class_as_a_variable_attribute
+      haml    = %q{.hello(class=var)}
+      html    = %q{<div class='hello world'></div>}
+      locals  = {:var=>"world"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_tag_multiple_CSS_classes_sorted_correctly_
+      haml    = %q{.z(class=var)}
+      html    = %q{<div class='a z'></div>}
+      locals  = {:var=>"a"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_HTML_style_tag_with_an_atomic_attribute
+      skip '[INCOMPATIBILITY] Hamlit limits boolean attributes'
+      haml    = %q{%a(flag)}
+      html    = %q{<a flag></a>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Tagswithrubystyleattributes < MiniTest::Test
+    def test_Ruby_style_one_attribute
+      haml    = %q{%p{:a => 'b'}}
+      html    = %q{<p a='b'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_attributes_hash_with_whitespace
+      haml    = %q{%p{  :a  =>  'b'  }}
+      html    = %q{<p a='b'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_interpolated_attribute
+      haml    = %q{%p{:a =>"#{var}"}}
+      html    = %q{<p a='value'></p>}
+      locals  = {:var=>"value"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_multiple_attributes
+      haml    = %q{%p{ :a => 'b', 'c' => 'd' }}
+      html    = %q{<p a='b' c='d'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_attributes_separated_with_newlines
+      haml    = %q{%p{ :a => 'b',
+  'c' => 'd' }}
+      html    = %q{<p a='b' c='d'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_class_as_an_attribute
+      haml    = %q{%p{:class => 'class1'}}
+      html    = %q{<p class='class1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_tag_with_a_CSS_class_and_class_as_an_attribute
+      haml    = %q{%p.class2{:class => 'class1'}}
+      html    = %q{<p class='class1 class2'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_tag_with_id_as_an_attribute
+      haml    = %q{%p{:id => '1'}}
+      html    = %q{<p id='1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_tag_with_a_CSS_id_and_id_as_an_attribute
+      haml    = %q{%p#id{:id => '1'}}
+      html    = %q{<p id='id_1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_tag_with_a_CSS_id_and_a_numeric_id_as_an_attribute
+      haml    = %q{%p#id{:id => 1}}
+      html    = %q{<p id='id_1'></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_tag_with_a_variable_attribute
+      haml    = %q{%p{:class => var}}
+      html    = %q{<p class='hello'></p>}
+      locals  = {:var=>"hello"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_tag_with_a_CSS_class_and_class_as_a_variable_attribute
+      haml    = %q{.hello{:class => var}}
+      html    = %q{<div class='hello world'></div>}
+      locals  = {:var=>"world"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_Ruby_style_tag_multiple_CSS_classes_sorted_correctly_
+      haml    = %q{.z{:class => var}}
+      html    = %q{<div class='a z'></div>}
+      locals  = {:var=>"a"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Silentcomments < MiniTest::Test
+    def test_an_inline_silent_comment
+      haml    = %q{-# hello}
+      html    = %q{}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_nested_silent_comment
+      haml    = %q{-#
+  hello}
+      html    = %q{}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_multiply_nested_silent_comment
+      haml    = %q{-#
+  %div
+    foo}
+      html    = %q{}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_multiply_nested_silent_comment_with_inconsistent_indents
+      haml    = %q{-#
+  %div
+      foo}
+      html    = %q{}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Markupcomments < MiniTest::Test
+    def test_an_inline_markup_comment
+      haml    = %q{/ comment}
+      html    = %q{<!-- comment -->}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_nested_markup_comment
+      haml    = %q{/
+  comment
+  comment2}
+      html    = %q{<!--
+  comment
+  comment2
+-->}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Conditionalcomments < MiniTest::Test
+    def test_a_conditional_comment
+      haml    = %q{/[if IE]
+  %p a}
+      html    = %q{<!--[if IE]>
+  <p>a</p>
+<![endif]-->}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Internalfilters < MiniTest::Test
+    def test_content_in_an_escaped_filter
+      haml    = %q{:escaped
+  <&">}
+      html    = %q{&lt;&amp;&quot;&gt;}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_content_in_a_preserve_filter
+      haml    = %q{:preserve
+  hello
+
+%p}
+      html    = %q{hello&#x000A;
+<p></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_content_in_a_plain_filter
+      haml    = %q{:plain
+  hello
+
+%p}
+      html    = %q{hello
+<p></p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_content_in_a_css_filter_XHTML_
+      haml    = %q{:css
+  hello
+
+%p}
+      html    = %q{<style type='text/css'>
+  /*<![CDATA[*/
+    hello
+  /*]]>*/
+</style>
+<p></p>}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_content_in_a_javascript_filter_XHTML_
+      haml    = %q{:javascript
+  a();
+%p}
+      html    = %q{<script type='text/javascript'>
+  //<![CDATA[
+    a();
+  //]]>
+</script>
+<p></p>}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_content_in_a_css_filter_HTML_
+      haml    = %q{:css
+  hello
+
+%p}
+      html    = %q{<style>
+  hello
+</style>
+<p></p>}
+      locals  = {}
+      options = {:format=>:html5}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_content_in_a_javascript_filter_HTML_
+      haml    = %q{:javascript
+  a();
+%p}
+      html    = %q{<script>
+  a();
+</script>
+<p></p>}
+      locals  = {}
+      options = {:format=>:html5}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Rubystyleinterpolation < MiniTest::Test
+    def test_interpolation_inside_inline_content
+      haml    = %q{%p #{var}}
+      html    = %q{<p>value</p>}
+      locals  = {:var=>"value"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_no_interpolation_when_escaped
+      haml    = %q{%p \#{var}}
+      html    = %q{<p>#{var}</p>}
+      locals  = {:var=>"value"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_interpolation_when_the_escape_character_is_escaped
+      haml    = %q{%p \\#{var}}
+      html    = %q{<p>\value</p>}
+      locals  = {:var=>"value"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_interpolation_inside_filtered_content
+      haml    = %q{:plain
+  #{var} interpolated: #{var}}
+      html    = %q{value interpolated: value}
+      locals  = {:var=>"value"}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Htmlescaping < MiniTest::Test
+    def test_code_following_
+      haml    = %q{&= '<"&>'}
+      html    = %q{&lt;&quot;&amp;&gt;}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_code_following_when_escape_haml_is_set_to_true
+      haml    = %q{= '<"&>'}
+      html    = %q{&lt;&quot;&amp;&gt;}
+      locals  = {}
+      options = {:escape_html=>"true"}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_code_following_when_escape_haml_is_set_to_true
+      haml    = %q{!= '<"&>'}
+      html    = %q{<"&>}
+      locals  = {}
+      options = {:escape_html=>"true"}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Booleanattributes < MiniTest::Test
+    def test_boolean_attribute_with_XHTML
+      haml    = %q{%input(checked=true)}
+      html    = %q{<input checked='checked' />}
+      locals  = {}
+      options = {:format=>:xhtml}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_boolean_attribute_with_HTML
+      haml    = %q{%input(checked=true)}
+      html    = %q{<input checked>}
+      locals  = {}
+      options = {:format=>:html5}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Whitespacepreservation < MiniTest::Test
+    def test_following_the_operator
+      haml    = %q{~ "Foo\n<pre>Bar\nBaz</pre>"}
+      html    = %q{Foo
+<pre>Bar&#x000A;Baz</pre>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_inside_a_textarea_tag
+      haml    = %q{%textarea
+  hello
+  hello}
+      html    = %q{<textarea>hello
+hello</textarea>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_inside_a_pre_tag
+      haml    = %q{%pre
+  hello
+  hello}
+      html    = %q{<pre>hello
+hello</pre>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+
+  class Whitespaceremoval < MiniTest::Test
+    def test_a_tag_with_appended_and_inline_content
+      haml    = %q{%li hello
+%li> world
+%li again}
+      html    = %q{<li>hello</li><li>world</li><li>again</li>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_appended_and_nested_content
+      haml    = %q{%li hello
+%li>
+  world
+%li again}
+      html    = %q{<li>hello</li><li>
+  world
+</li><li>again</li>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+
+    def test_a_tag_with_appended
+      haml    = %q{%p<
+  hello
+  world}
+      html    = %q{<p>hello
+world</p>}
+      locals  = {}
+      options = {}
+      haml_result = UglyTest.haml_result(haml, options, locals)
+      hamlit_result = UglyTest.hamlit_result(haml, options, locals)
+      assert_equal haml_result, hamlit_result
+    end
+  end
+end
diff --git a/test/haml/helper_test.rb b/test/haml/helper_test.rb
new file mode 100644 (file)
index 0000000..fd76da7
--- /dev/null
@@ -0,0 +1,695 @@
+require 'test_helper'
+require "active_model/naming"
+
+class FormModel
+  extend ActiveModel::Naming
+end
+
+class HelperTest < Haml::TestCase
+  TEXT_AREA_CONTENT_REGEX = /<(textarea)[^>]*>\n(.*?)<\/\1>/im
+
+  Post = Struct.new('Post', :body, :error_field, :errors)
+  class PostErrors
+    def on(name)
+      return unless name == 'error_field'
+      ["Really bad error"]
+    end
+    alias_method :full_messages, :on
+
+    def [](name)
+      on(name) || []
+    end
+  end
+
+  def setup
+    @base = Class.new(ActionView::Base) {
+      def nested_tag
+        content_tag(:span) {content_tag(:div) {"something"}}
+      end
+
+      def wacky_form
+        form_tag("/foo") {"bar"}
+      end
+    }.new
+    @base.controller = ActionController::Base.new
+    @base.view_paths << File.expand_path("../templates", __FILE__)
+    @base.instance_variable_set(:@post, Post.new("Foo bar\nbaz", nil, PostErrors.new))
+  end
+
+  def render(text, options = {})
+    return @base.render :inline => text, :type => :haml if options == :action_view
+    super
+  end
+
+  def test_rendering_with_escapes; skip
+    def @base.render_something_with_haml_concat
+      haml_concat "<p>"
+    end
+    def @base.render_something_with_haml_tag_and_concat
+      haml_tag 'p' do
+        haml_concat '<foo>'
+      end
+    end
+
+    output = render(<<-HAML, :action_view)
+- render_something_with_haml_concat
+- render_something_with_haml_tag_and_concat
+- render_something_with_haml_concat
+HAML
+    assert_equal("&lt;p&gt;\n<p>\n  &lt;foo&gt;\n</p>\n&lt;p&gt;\n", output)
+  end
+
+  def test_with_raw_haml_concat; skip
+    haml = <<HAML
+- with_raw_haml_concat do
+  - haml_concat "<>&"
+HAML
+    assert_equal("<>&\n", render(haml, :action_view))
+  end
+
+  def test_flatten
+    assert_equal("FooBar", Haml::Helpers.flatten("FooBar"))
+
+    assert_equal("FooBar", Haml::Helpers.flatten("Foo\rBar"))
+
+    assert_equal("Foo&#x000A;Bar", Haml::Helpers.flatten("Foo\nBar"))
+
+    assert_equal("Hello&#x000A;World!&#x000A;YOU ARE FLAT?&#x000A;OMGZ!",
+      Haml::Helpers.flatten("Hello\nWorld!\nYOU ARE \rFLAT?\n\rOMGZ!"))
+  end
+
+  def test_list_of_should_render_correctly; skip
+    assert_equal("<li>1</li>\n<li>2</li>\n", render("= list_of([1, 2]) do |i|\n  = i"))
+    assert_equal("<li>[1]</li>\n", render("= list_of([[1]]) do |i|\n  = i.inspect"))
+    assert_equal("<li>\n  <h1>Fee</h1>\n  <p>A word!</p>\n</li>\n<li>\n  <h1>Fi</h1>\n  <p>A word!</p>\n</li>\n<li>\n  <h1>Fo</h1>\n  <p>A word!</p>\n</li>\n<li>\n  <h1>Fum</h1>\n  <p>A word!</p>\n</li>\n",
+      render("= list_of(['Fee', 'Fi', 'Fo', 'Fum']) do |title|\n  %h1= title\n  %p A word!"))
+    assert_equal("<li c='3'>1</li>\n<li c='3'>2</li>\n", render("= list_of([1, 2], {:c => 3}) do |i|\n  = i"))
+    assert_equal("<li c='3'>[1]</li>\n", render("= list_of([[1]], {:c => 3}) do |i|\n  = i.inspect"))
+    assert_equal("<li c='3'>\n  <h1>Fee</h1>\n  <p>A word!</p>\n</li>\n<li c='3'>\n  <h1>Fi</h1>\n  <p>A word!</p>\n</li>\n<li c='3'>\n  <h1>Fo</h1>\n  <p>A word!</p>\n</li>\n<li c='3'>\n  <h1>Fum</h1>\n  <p>A word!</p>\n</li>\n",
+      render("= list_of(['Fee', 'Fi', 'Fo', 'Fum'], {:c => 3}) do |title|\n  %h1= title\n  %p A word!"))
+  end
+
+  def test_buffer_access; skip
+    assert(render("= buffer") =~ /#<Haml::Buffer:0x[a-z0-9]+>/)
+    assert_equal(render("= (buffer == _hamlout)"), "true\n")
+  end
+
+  def test_tabs; skip
+    assert_equal("foo\n  bar\nbaz\n", render("foo\n- tab_up\nbar\n- tab_down\nbaz"))
+    assert_equal("          <p>tabbed</p>\n", render("- buffer.tabulation=5\n%p tabbed"))
+  end
+
+  def test_with_tabs; skip
+    assert_equal(<<HTML, render(<<HAML))
+Foo
+    Bar
+    Baz
+Baz
+HTML
+Foo
+- with_tabs 2 do
+  = "Bar\\nBaz"
+Baz
+HAML
+  end
+
+  def test_helpers_dont_leak
+    # Haml helpers shouldn't be accessible from ERB
+    render("foo")
+    proper_behavior = false
+
+    begin
+      ActionView::Base.new.render(:inline => "<%= flatten('Foo\\nBar') %>")
+    rescue NoMethodError, ActionView::Template::Error
+      proper_behavior = true
+    end
+    assert(proper_behavior)
+
+    begin
+      ActionView::Base.new.render(:inline => "<%= concat('foo') %>")
+    rescue ArgumentError, NameError
+      proper_behavior = true
+    end
+    assert(proper_behavior)
+  end
+
+  def test_action_view_included; skip
+    assert(Haml::Helpers.action_view?)
+  end
+
+  def test_form_tag; skip
+    def @base.protect_against_forgery?; false; end
+    rendered = render(<<HAML, :action_view)
+= form_tag 'foo' do
+ %p bar
+ %strong baz
+HAML
+   fragment = Nokogiri::HTML.fragment(rendered)
+   assert_equal 'foo', fragment.css('form').first.attributes['action'].to_s
+   assert_equal 'bar', fragment.css('form p').first.text.strip
+   assert_equal 'baz', fragment.css('form strong').first.text.strip
+  end
+
+  def test_form_for; skip
+    # FIXME: current HAML doesn't do proper indentation with form_for (it's the capture { output } in #form_for).
+    def @base.protect_against_forgery?; false; end
+    rendered = render(<<HAML, :action_view)
+= form_for OpenStruct.new, url: 'foo', as: :post do |f|
+  = f.text_field :name
+HAML
+    assert_match(/<(form|div)[^>]+><input/, rendered)
+  end
+
+  def test_pre; skip
+    assert_equal(%(<pre>Foo bar&#x000A;   baz</pre>\n),
+                 render('= content_tag "pre", "Foo bar\n   baz"', :action_view))
+  end
+
+  def test_text_area_tag; skip
+    output = render('= text_area_tag "body", "Foo\nBar\n Baz\n   Boom"', :action_view)
+    match_data = output.match(TEXT_AREA_CONTENT_REGEX)
+    assert_equal "Foo&#x000A;Bar&#x000A; Baz&#x000A;   Boom", match_data[2]
+  end
+
+  def test_text_area; skip
+    output = render('= text_area :post, :body', :action_view)
+    match_data = output.match(TEXT_AREA_CONTENT_REGEX)
+    assert_equal "Foo bar&#x000A;baz", match_data[2]
+  end
+
+  def test_partials_should_not_cause_textareas_to_be_indented; skip
+    # non-indentation of textareas rendered inside partials
+    @base.instance_variable_set(:@post, Post.new("Foo", nil, PostErrors.new))
+    output = render(".foo\n  .bar\n    = render '/text_area_helper'", :action_view)
+    match_data = output.match(TEXT_AREA_CONTENT_REGEX)
+    assert_equal 'Foo', match_data[2]
+  end
+
+  def test_textareas_should_preserve_leading_whitespace; skip
+    # leading whitespace preservation
+    @base.instance_variable_set(:@post, Post.new("    Foo", nil, PostErrors.new))
+    output = render(".foo\n  = text_area :post, :body", :action_view)
+    match_data = output.match(TEXT_AREA_CONTENT_REGEX)
+    assert_equal '&#x0020;   Foo', match_data[2]
+  end
+
+  def test_textareas_should_preserve_leading_whitespace_in_partials; skip
+    # leading whitespace in textareas rendered inside partials
+    @base.instance_variable_set(:@post, Post.new("    Foo", nil, PostErrors.new))
+    output = render(".foo\n  .bar\n    = render '/text_area_helper'", :action_view)
+    match_data = output.match(TEXT_AREA_CONTENT_REGEX)
+    assert_equal '&#x0020;   Foo', match_data[2]
+  end
+
+  def test_capture_haml; skip
+    assert_equal(<<HTML, render(<<HAML))
+"<p>13</p>\\n"
+HTML
+- (foo = capture_haml(13) do |a|
+  %p= a
+- end)
+= foo.inspect
+HAML
+  end
+
+  def test_content_tag_block; skip
+    assert_equal(<<HTML.strip, render(<<HAML, :action_view).strip)
+<div><p>bar</p>
+<strong>bar</strong>
+</div>
+HTML
+= content_tag :div do
+  %p bar
+  %strong bar
+HAML
+  end
+
+  def test_content_tag_error_wrapping; skip
+    def @base.protect_against_forgery?; false; end
+    output = render(<<HAML, :action_view)
+= form_for @post, :as => :post, :html => {:class => nil, :id => nil}, :url => '' do |f|
+  = f.label 'error_field'
+HAML
+    fragment = Nokogiri::HTML.fragment(output)
+    refute_nil fragment.css('form div.field_with_errors label[for=post_error_field]').first
+  end
+
+  def test_form_tag_in_helper_with_string_block; skip
+    def @base.protect_against_forgery?; false; end
+    rendered = render('= wacky_form', :action_view)
+    fragment = Nokogiri::HTML.fragment(rendered)
+    assert_equal 'bar', fragment.text.strip
+    assert_equal '/foo', fragment.css('form').first.attributes['action'].to_s
+  end
+
+  def test_haml_tag_name_attribute_with_id; skip
+    assert_equal("<p id='some_id'></p>\n", render("- haml_tag 'p#some_id'"))
+  end
+
+  def test_haml_tag_name_attribute_with_colon_id; skip
+    assert_equal("<p id='some:id'></p>\n", render("- haml_tag 'p#some:id'"))
+  end
+
+  def test_haml_tag_without_name_but_with_id; skip
+    assert_equal("<div id='some_id'></div>\n", render("- haml_tag '#some_id'"))
+  end
+
+  def test_haml_tag_without_name_but_with_class; skip
+    assert_equal("<div class='foo'></div>\n", render("- haml_tag '.foo'"))
+  end
+
+  def test_haml_tag_without_name_but_with_colon_class; skip
+    assert_equal("<div class='foo:bar'></div>\n", render("- haml_tag '.foo:bar'"))
+  end
+
+  def test_haml_tag_name_with_id_and_class; skip
+    assert_equal("<p class='foo' id='some_id'></p>\n", render("- haml_tag 'p#some_id.foo'"))
+  end
+
+  def test_haml_tag_name_with_class; skip
+    assert_equal("<p class='foo'></p>\n", render("- haml_tag 'p.foo'"))
+  end
+
+  def test_haml_tag_name_with_class_and_id; skip
+    assert_equal("<p class='foo' id='some_id'></p>\n", render("- haml_tag 'p.foo#some_id'"))
+  end
+
+  def test_haml_tag_name_with_id_and_multiple_classes; skip
+    assert_equal("<p class='foo bar' id='some_id'></p>\n", render("- haml_tag 'p#some_id.foo.bar'"))
+  end
+
+  def test_haml_tag_name_with_multiple_classes_and_id; skip
+    assert_equal("<p class='foo bar' id='some_id'></p>\n", render("- haml_tag 'p.foo.bar#some_id'"))
+  end
+
+  def test_haml_tag_name_and_attribute_classes_merging_with_id; skip
+    assert_equal("<p class='bar foo' id='some_id'></p>\n", render("- haml_tag 'p#some_id.foo', :class => 'bar'"))
+  end
+
+  def test_haml_tag_name_and_attribute_classes_merging; skip
+    assert_equal("<p class='bar foo'></p>\n", render("- haml_tag 'p.foo', :class => 'bar'"))
+  end
+
+  def test_haml_tag_name_merges_id_and_attribute_id; skip
+    assert_equal("<p id='foo_bar'></p>\n", render("- haml_tag 'p#foo', :id => 'bar'"))
+  end
+
+  def test_haml_tag_attribute_html_escaping; skip
+    assert_equal("<p id='foo&amp;bar'>baz</p>\n", render("%p{:id => 'foo&bar'} baz", :escape_html => true))
+  end
+
+  def test_haml_tag_autoclosed_tags_are_closed_xhtml; skip
+    assert_equal("<br class='foo' />\n", render("- haml_tag :br, :class => 'foo'", :format => :xhtml))
+  end
+
+  def test_haml_tag_autoclosed_tags_are_closed_html; skip
+    assert_equal("<br class='foo'>\n", render("- haml_tag :br, :class => 'foo'", :format => :html5))
+  end
+
+  def test_haml_tag_with_class_array; skip
+    assert_equal("<p class='a b'>foo</p>\n", render("- haml_tag :p, 'foo', :class => %w[a b]"))
+    assert_equal("<p class='a b c d'>foo</p>\n", render("- haml_tag 'p.c.d', 'foo', :class => %w[a b]"))
+  end
+
+  def test_haml_tag_with_id_array; skip
+    assert_equal("<p id='a_b'>foo</p>\n", render("- haml_tag :p, 'foo', :id => %w[a b]"))
+    assert_equal("<p id='c_a_b'>foo</p>\n", render("- haml_tag 'p#c', 'foo', :id => %w[a b]"))
+  end
+
+  def test_haml_tag_with_data_hash; skip
+    assert_equal("<p data-baz data-foo='bar'>foo</p>\n",
+      render("- haml_tag :p, 'foo', :data => {:foo => 'bar', :baz => true}"))
+  end
+
+  def test_haml_tag_non_autoclosed_tags_arent_closed; skip
+    assert_equal("<p></p>\n", render("- haml_tag :p"))
+  end
+
+  def test_haml_tag_renders_text_on_a_single_line; skip
+    assert_equal("<p>#{'a' * 100}</p>\n", render("- haml_tag :p, 'a' * 100"))
+  end
+
+  def test_haml_tag_raises_error_for_multiple_content; skip
+    assert_raises(Haml::Error) { render("- haml_tag :p, 'foo' do\n  bar") }
+  end
+
+  def test_haml_tag_flags; skip
+    assert_equal("<p />\n", render("- haml_tag :p, :/", :format => :xhtml))
+    assert_equal("<p>\n", render("- haml_tag :p, :/", :format => :html5))
+    assert_equal("<p>kumquat</p>\n", render("- haml_tag :p, :< do\n  kumquat"))
+
+    assert_raises(Haml::Error) { render("- haml_tag :p, 'foo', :/") }
+    assert_raises(Haml::Error) { render("- haml_tag :p, :/ do\n  foo") }
+  end
+
+  def test_haml_tag_error_return; skip
+    assert_raises(Haml::Error) { render("= haml_tag :p") }
+  end
+
+  def test_haml_tag_with_multiline_string; skip
+    assert_equal(<<HTML, render(<<HAML))
+<p>
+  foo
+  bar
+  baz
+</p>
+HTML
+- haml_tag :p, "foo\\nbar\\nbaz"
+HAML
+  end
+
+  def test_haml_concat_inside_haml_tag_escaped_with_xss; skip
+    assert_equal("<p>\n  &lt;&gt;&amp;\n</p>\n", render(<<HAML, :action_view))
+- haml_tag :p do
+  - haml_concat "<>&"
+HAML
+  end
+
+  def test_haml_concat_with_multiline_string; skip
+    assert_equal(<<HTML, render(<<HAML))
+<p>
+  foo
+  bar
+  baz
+</p>
+HTML
+%p
+  - haml_concat "foo\\nbar\\nbaz"
+HAML
+  end
+
+  def test_haml_tag_with_ugly; skip
+    assert_equal(<<HTML, render(<<HAML, :ugly => true))
+<p>
+<strong>Hi!</strong>
+</p>
+HTML
+- haml_tag :p do
+  - haml_tag :strong, "Hi!"
+HAML
+  end
+
+  def test_haml_tag_if_positive; skip
+    assert_equal(<<HTML, render(<<HAML))
+<div class='conditional'>
+  <p>A para</p>
+</div>
+HTML
+- haml_tag_if true, '.conditional' do
+  %p A para
+HAML
+  end
+
+  def test_haml_tag_if_positive_with_attributes; skip
+    assert_equal(<<HTML, render(<<HAML))
+<div class='conditional' foo='bar'>
+  <p>A para</p>
+</div>
+HTML
+- haml_tag_if true, '.conditional',  {:foo => 'bar'} do
+  %p A para
+HAML
+  end
+
+  def test_haml_tag_if_negative; skip
+    assert_equal(<<HTML, render(<<HAML))
+<p>A para</p>
+HTML
+- haml_tag_if false, '.conditional' do
+  %p A para
+HAML
+  end
+
+  def test_haml_tag_if_error_return; skip
+    assert_raises(Haml::Error) { render("= haml_tag_if false, '.conditional' do\n  %p Hello") }
+  end
+
+  def test_is_haml; skip
+    assert(!ActionView::Base.new.is_haml?)
+    assert_equal("true\n", render("= is_haml?"))
+    assert_equal("true\n", render("= is_haml?", :action_view))
+    assert_equal("false", @base.render(:inline => '<%= is_haml? %>'))
+    assert_equal("false\n", render("= render :inline => '<%= is_haml? %>'", :action_view))
+  end
+
+  def test_page_class; skip
+    controller = Struct.new(:controller_name, :action_name).new('troller', 'tion')
+    scope = Struct.new(:controller).new(controller)
+    result = render("%div{:class => page_class} MyDiv", :scope => scope)
+    expected = "<div class='troller tion'>MyDiv</div>\n"
+    assert_equal expected, result
+  end
+
+  def test_indented_capture
+    assert_equal("  Foo\n  ", @base.render(:inline => "  <% res = capture do %>\n  Foo\n  <% end %><%= res %>"))
+  end
+
+  def test_capture_deals_properly_with_collections; skip
+    obj = Object.new
+    def obj.trc(collection, &block)
+      collection.each do |record|
+        haml_concat capture_haml(record, &block)
+      end
+    end
+
+    assert_equal("1\n\n2\n\n3\n\n", render("- trc([1, 2, 3]) do |i|\n  = i.inspect", scope: obj))
+  end
+
+  def test_capture_with_string_block; skip
+    assert_equal("foo\n", render("= capture { 'foo' }", :action_view))
+  end
+
+  def test_capture_with_non_string_value_reurns_nil; skip
+    def @base.check_capture_returns_nil(&block)
+      contents = capture(&block)
+
+      contents << "ERROR" if contents
+    end
+
+    assert_equal("\n", render("= check_capture_returns_nil { 2 }", :action_view))
+  end
+
+
+  class HomemadeViewContext
+    include ActionView::Context
+    include ActionView::Helpers::FormHelper
+
+    def initialize
+      _prepare_context
+    end
+
+    def url_for(*)
+      "/"
+    end
+
+    def dom_class(*)
+    end
+
+    def dom_id(*)
+    end
+
+    def m # I have to inject the model into the view using an instance method, using locals doesn't work.
+      FormModel.new
+    end
+
+    def protect_against_forgery?
+    end
+
+    # def capture(*args, &block)
+    #   capture_haml(*args, &block)
+    # end
+  end
+
+  def test_form_for_with_homemade_view_context; skip
+    handler  = ActionView::Template.handler_for_extension("haml")
+    template = ActionView::Template.new(<<HAML, "inline template", handler, {})
+= form_for(m, :url => "/") do
+  %b Bold!
+HAML
+
+    # see if Bold is within form tags:
+    assert_match(/<form.*>.*<b>Bold!<\/b>.*<\/form>/m, template.render(HomemadeViewContext.new, {}))
+  end
+
+  def test_find_and_preserve_with_block; skip
+    assert_equal("<pre>Foo&#x000A;Bar</pre>\nFoo\nBar\n",
+                 render("= find_and_preserve do\n  %pre\n    Foo\n    Bar\n  Foo\n  Bar"))
+  end
+
+  def test_find_and_preserve_with_block_and_tags; skip
+    assert_equal("<pre>Foo\nBar</pre>\nFoo\nBar\n",
+                 render("= find_and_preserve([]) do\n  %pre\n    Foo\n    Bar\n  Foo\n  Bar"))
+  end
+
+  def test_preserve_with_block; skip
+    assert_equal("<pre>Foo&#x000A;Bar</pre>&#x000A;Foo&#x000A;Bar\n",
+                 render("= preserve do\n  %pre\n    Foo\n    Bar\n  Foo\n  Bar"))
+  end
+
+  def test_init_haml_helpers
+    context = Object.new
+    class << context
+      include Haml::Helpers
+    end
+    context.init_haml_helpers
+
+    result = context.capture_haml do
+      context.haml_tag :p, :attr => "val" do
+        context.haml_concat "Blah"
+      end
+    end
+
+    assert_equal("<p attr='val'>\n  Blah\n</p>\n", result)
+  end
+
+  def test_non_haml; skip
+    assert_equal("false\n", render("= non_haml { is_haml? }"))
+  end
+
+  def test_content_tag_nested; skip
+    assert_equal "<span><div>something</div></span>", render("= nested_tag", :action_view).strip
+  end
+
+  def test_error_return; skip
+    assert_raises(Haml::Error, <<MESSAGE) {render("= haml_concat 'foo'")}
+haml_concat outputs directly to the Haml template.
+Disregard its return value and use the - operator,
+or use capture_haml to get the value as a String.
+MESSAGE
+  end
+
+  def test_error_return_line; skip
+    render("%p foo\n= haml_concat 'foo'\n%p bar")
+    assert false, "Expected Haml::Error"
+  rescue Haml::Error => e
+    assert_equal 2, e.backtrace[1].scan(/:(\d+)/).first.first.to_i
+  end
+
+  def test_error_return_line_in_helper; skip
+    obj = Object.new
+    def obj.something_that_uses_haml_concat
+      haml_concat('foo').to_s
+    end
+
+    render("- something_that_uses_haml_concat", scope: obj)
+    assert false, "Expected Haml::Error"
+  rescue Haml::Error => e
+    assert_equal __LINE__ - 6, e.backtrace[0].scan(/:(\d+)/).first.first.to_i
+  end
+
+  class ActsLikeTag
+    # We want to be able to have people include monkeypatched ActionView helpers
+    # without redefining is_haml?.
+    # This is accomplished via Object#is_haml?, and this is a test for it.
+    include ActionView::Helpers::TagHelper
+    def to_s
+      content_tag :p, 'some tag content'
+    end
+  end
+
+  def test_random_class_includes_tag_helper
+    assert_equal "<p>some tag content</p>", ActsLikeTag.new.to_s
+  end
+
+  def test_capture_with_nuke_outer; skip
+    assert_equal "<div></div>\n*<div>hi there!</div>\n", render(<<HAML)
+%div
+= precede("*") do
+  %div> hi there!
+HAML
+
+    assert_equal "<div></div>\n*<div>hi there!</div>\n", render(<<HAML)
+%div
+= precede("*") do
+  = "  "
+  %div> hi there!
+HAML
+  end
+
+  def test_html_escape
+    assert_equal "&quot;&gt;&lt;&amp;", Haml::Helpers.html_escape('"><&')
+  end
+
+  def test_html_escape_should_work_on_frozen_strings
+    begin
+      assert Haml::Helpers.html_escape('foo'.freeze)
+    rescue => e
+      flunk e.message
+    end
+  end
+
+  def test_html_escape_encoding
+    old_stderr, $stderr = $stderr, StringIO.new
+    string = "\"><&\u00e9" # if you're curious, u00e9 is "LATIN SMALL LETTER E WITH ACUTE"
+    assert_equal "&quot;&gt;&lt;&amp;\u00e9", Haml::Helpers.html_escape(string)
+    assert $stderr.string == "", "html_escape shouldn't generate warnings with UTF-8 strings: #{$stderr.string}"
+  ensure
+    $stderr = old_stderr
+  end
+
+  def test_html_escape_non_string; skip
+    assert_equal('4.58', Haml::Helpers.html_escape(4.58))
+    assert_equal('4.58', Haml::Helpers.html_escape_without_haml_xss(4.58))
+  end
+
+  def test_escape_once
+    assert_equal "&quot;&gt;&lt;&amp;", Haml::Helpers.escape_once('"><&')
+  end
+
+  def test_escape_once_leaves_entity_references
+    assert_equal "&quot;&gt;&lt;&amp; &nbsp;", Haml::Helpers.escape_once('"><& &nbsp;')
+  end
+
+  def test_escape_once_leaves_numeric_references; skip
+    assert_equal "&quot;&gt;&lt;&amp; &#160;", Haml::Helpers.escape_once('"><& &#160;') #decimal
+    assert_equal "&quot;&gt;&lt;&amp; &#x00a0;", Haml::Helpers.escape_once('"><& &#x00a0;') #hexadecimal
+  end
+
+  def test_escape_once_encoding
+    old_stderr, $stderr = $stderr, StringIO.new
+    string = "\"><&\u00e9 &nbsp;"
+    assert_equal "&quot;&gt;&lt;&amp;\u00e9 &nbsp;", Haml::Helpers.escape_once(string)
+    assert $stderr.string == "", "html_escape shouldn't generate warnings with UTF-8 strings: #{$stderr.string}"
+  ensure
+    $stderr = old_stderr
+  end
+
+  def test_html_attrs_xhtml; skip
+    assert_equal("<html lang='en-US' xml:lang='en-US' xmlns='http://www.w3.org/1999/xhtml'></html>\n",
+                  render("%html{html_attrs}", :format => :xhtml))
+  end
+
+  def test_html_attrs_html4; skip
+    assert_equal("<html lang='en-US'></html>\n",
+                  render("%html{html_attrs}", :format => :html4))
+  end
+
+  def test_html_attrs_html5; skip
+    assert_equal("<html lang='en-US'></html>\n",
+                  render("%html{html_attrs}", :format => :html5))
+  end
+
+  def test_html_attrs_xhtml_other_lang; skip
+    assert_equal("<html lang='es-AR' xml:lang='es-AR' xmlns='http://www.w3.org/1999/xhtml'></html>\n",
+                  render("%html{html_attrs('es-AR')}", :format => :xhtml))
+  end
+
+  def test_html_attrs_html4_other_lang; skip
+    assert_equal("<html lang='es-AR'></html>\n",
+                  render("%html{html_attrs('es-AR')}", :format => :html4))
+  end
+
+  def test_html_attrs_html5_other_lang; skip
+    assert_equal("<html lang='es-AR'></html>\n",
+                  render("%html{html_attrs('es-AR')}", :format => :html5))
+  end
+
+  def test_escape_once_should_work_on_frozen_strings
+    begin
+      Haml::Helpers.escape_once('foo'.freeze)
+    rescue => e
+      flunk e.message
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/test/haml/markaby/standard.mab b/test/haml/markaby/standard.mab
new file mode 100644 (file)
index 0000000..aff8641
--- /dev/null
@@ -0,0 +1,52 @@
+self << '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
+html(:xmlns=>'http://www.w3.org/1999/xhtml', 'xml:lang'=>'en-US') do
+  head do
+    title "Hampton Catlin Is Totally Awesome"
+    meta("http-equiv" => "Content-Type", :content => "text/html; charset=utf-8")
+  end
+  body do
+    # You're In my house now!
+    div :class => "header" do
+      self << %|Yes, ladies and gentileman. He is just that egotistical.
+      Fantastic! This should be multi-line output
+      The question is if this would translate! Ahah!|
+      self << 1 + 9 + 8 + 2 #numbers should work and this should be ignored
+    end
+    div(:id => "body") { self << "Quotes should be loved! Just like people!"}
+    120.times do |number|
+      number
+    end
+    self << "Wow.|"
+    p do
+      self << "Holy cow        " + 
+        "multiline       " +       
+        "tags!           " + 
+        "A pipe (|) even!"   
+      self << [1, 2, 3].collect { |n| "PipesIgnored|" }
+      self << [1, 2, 3].collect { |n|     
+          n.to_s                    
+        }.join("|")                
+    end
+    div(:class => "silent") do
+      foo = String.new
+      foo << "this"
+      foo << " shouldn't"
+      foo << " evaluate"
+      self << foo + " but now it should!"
+      # Woah crap a comment!
+    end
+    # That was a line that shouldn't close everything.
+    ul(:class => "really cool") do
+      ('a'..'f').each do |a|
+        li a
+      end
+    end
+    div((@should_eval = "with this text"), :id => "combo", :class => "of_divs_with_underscore")
+    [ 104, 101, 108, 108, 111 ].map do |byte|
+      byte.chr
+    end
+    div(:class => "footer") do
+      strong("This is a really long ruby quote. It should be loved and wrapped because its more than 50 characters. This value may change in the future and this test may look stupid. \nSo, I'm just making it *really* long. God, I hope this works", :class => "shout")
+    end
+  end
+end
diff --git a/test/haml/mocks/article.rb b/test/haml/mocks/article.rb
new file mode 100644 (file)
index 0000000..805f8ca
--- /dev/null
@@ -0,0 +1,6 @@
+class Article
+  attr_accessor :id, :title, :body
+  def initialize
+    @id, @title, @body = 1, 'Hello', 'World'
+  end
+end
\ No newline at end of file
diff --git a/test/haml/results/content_for_layout.xhtml b/test/haml/results/content_for_layout.xhtml
new file mode 100644 (file)
index 0000000..63bbd22
--- /dev/null
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head></head>
+  <body>
+    <div id='yieldy'>
+      Lorem ipsum dolor sit amet
+    </div>
+    <div id='nosym'>
+      Lorem ipsum dolor sit amet
+    </div>
+  </body>
+</html>
diff --git a/test/haml/results/eval_suppressed.xhtml b/test/haml/results/eval_suppressed.xhtml
new file mode 100644 (file)
index 0000000..fb7bd33
--- /dev/null
@@ -0,0 +1,9 @@
+<p></p>
+<p></p>
+<h1>Me!</h1>
+<div id='foo'>
+<p id='bar'>All</p>
+<br>
+<p class='baz'>This</p>
+Should render
+</div>
diff --git a/test/haml/results/helpers.xhtml b/test/haml/results/helpers.xhtml
new file mode 100644 (file)
index 0000000..1d6d8fa
--- /dev/null
@@ -0,0 +1,72 @@
+&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;
+<div>
+<p class='title'>Title</p>
+<p class='text'>
+Woah this is really crazy
+I mean wow,
+man.
+</p>
+</div>
+
+<div>
+<p class='title'>Title</p>
+<p class='text'>
+Woah this is really crazy
+I mean wow,
+man.
+</p>
+</div>
+
+<div>
+<p class='title'>Title</p>
+<p class='text'>
+Woah this is really crazy
+I mean wow,
+man.
+</p>
+</div>
+
+<p>foo</p>
+<p>reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally loooooooooooooooooong</p>
+<div class='woah'>
+<div id='funky'>
+<div>
+<h1>Big!</h1>
+<p>Small</p>
+<!-- Invisible -->
+</div>
+<div class='dilly'>
+<p>foo</p>
+<h1>bar</h1>
+</div>
+</div>
+(<strong>parentheses!</strong>)
+</div>
+*<span class='small'>Not really</span>
+click
+<a href='thing'>here</a>.
+<p>baz</p>
+<p>boom</p>
+foo
+<li><a href='http://www.google.com'>google</a></li><p>
+foo
+<div>
+bar
+</div>
+boom
+baz
+boom, again
+</p>
+<table>
+<tr>
+<td class='cell'>
+<strong>strong!</strong>
+data
+</td>
+<td>
+more_data
+</td>
+</tr>
+</table>
+<hr>
+<div></div>
diff --git a/test/haml/results/helpful.xhtml b/test/haml/results/helpful.xhtml
new file mode 100644 (file)
index 0000000..93c05a5
--- /dev/null
@@ -0,0 +1,13 @@
+<div class='article' id='article_1'>
+<h1>Hello</h1>
+<div>World</div>
+</div>
+<div class='article' id='id_article_1'>id</div>
+<div class='article class' id='article_1'>class</div>
+<div class='article class' id='id_article_1'>id class</div>
+<div class='article full' id='article_1'>boo</div>
+<div class='article full' id='article_1'>moo</div>
+<div class='article articleFull' id='article_1'>foo</div>
+<span>
+Boo
+</span>
diff --git a/test/haml/results/just_stuff.xhtml b/test/haml/results/just_stuff.xhtml
new file mode 100644 (file)
index 0000000..3ce2adf
--- /dev/null
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!DOCTYPE html>
+<!DOCTYPE html>
+<!DOCTYPE html>
+<!DOCTYPE html>
+<strong apos="Foo's bar!">Boo!</strong>
+Embedded? false!
+Embedded? true!
+Embedded? true!
+Embedded? twice! true!
+Embedded? one af"t"er another!
+<p>Embedded? false!</p>
+<p>Embedded? true!</p>
+<p>Embedded? true!</p>
+<p>Embedded? twice! true!</p>
+<p>Embedded? one af"t"er another!</p>
+stuff followed by whitespace
+<strong>block with whitespace</strong>
+<p>
+Escape
+- character
+%p foo
+yee\ha
+   don't lstrip me
+</p>
+<!-- Short comment -->
+<!--
+This is a block comment
+cool, huh?
+<strong>there can even be sub-tags!</strong>
+Or script!
+-->
+<p class=''>class attribute should appear!</p>
+<p>this attribute shouldn't appear</p>
+<!--[if lte IE6]> conditional comment! <![endif]-->
+<!--[if gte IE7]>
+<p>Block conditional comment</p>
+<div>
+<h1>Cool, eh?</h1>
+</div>
+<![endif]-->
+<!--[if gte IE5.2]>
+Woah a period.
+<![endif]-->
+testtest
+<br>
+<meta foo='bar'>
+<img>
+<hr>
+<link>
+<script>Inline content</script>
+<br>
+Nested content
+</br>
+<p class='article bar foo' id='article_1'>Blah</p>
+<p class='article foo' id='article_1'>Blah</p>
+<p class='article bar baz foo' id='article_1'>Blah</p>
+<p class='article quux qux' id='article_1'>Blump</p>
+<p class='article' id='foo_bar_baz_article_1'>Whee</p>
+Woah inner quotes
+<p class='dynamic_quote' dyn='3' quotes="single '"></p>
+<p class='dynamic_self_closing' dyn='3'>
+<body>
+hello
+<div>
+<img>
+</div>
+</body>
diff --git a/test/haml/results/list.xhtml b/test/haml/results/list.xhtml
new file mode 100644 (file)
index 0000000..c481811
--- /dev/null
@@ -0,0 +1,12 @@
+!Not a Doctype!
+<ul>
+<li>a</li>
+<li>b</li>
+<li>c</li>
+<li>d</li>
+<li>e</li>
+<li>f</li>
+<li>g</li>
+<li>h</li>
+<li>i</li>
+</ul>
diff --git a/test/haml/results/nuke_inner_whitespace.xhtml b/test/haml/results/nuke_inner_whitespace.xhtml
new file mode 100644 (file)
index 0000000..4a7bf83
--- /dev/null
@@ -0,0 +1,40 @@
+<p>
+<q>Foo</q>
+</p>
+<p>
+<q a='2'>Foo</q>
+</p>
+<p>
+<q>Foo
+Bar</q>
+</p>
+<p>
+<q a='2'>Foo
+Bar</q>
+</p>
+<p>
+<q>Foo
+Bar</q>
+</p>
+<p>
+<q a='2'>Foo
+Bar</q>
+</p>
+<p>
+<q><div>
+Foo
+Bar
+</div></q>
+</p>
+<p>
+<q a='2'><div>
+Foo
+Bar
+</div></q>
+</p>
+<p>
+<q>foo</q>
+<q a='2'>
+bar
+</q>
+</p>
diff --git a/test/haml/results/nuke_outer_whitespace.xhtml b/test/haml/results/nuke_outer_whitespace.xhtml
new file mode 100644 (file)
index 0000000..913dc10
--- /dev/null
@@ -0,0 +1,140 @@
+<p>
+<p><q>
+Foo
+</q></p>
+</p>
+<p>
+<p><q a='2'>
+Foo
+</q></p>
+</p>
+<p>
+<p><q>Foo</q></p>
+</p>
+<p>
+<p><q a='2'>Foo</q></p>
+</p>
+<p>
+<p><q>
+Foo
+</q></p>
+</p>
+<p>
+<p><q a='2'>
+Foo
+</q></p>
+</p>
+<p>
+<p><q>Foo</q></p>
+</p>
+<p>
+<p><q a='2'>Foo</q></p>
+</p>
+<p>
+<p><q>
+Foo
+Bar
+</q></p>
+</p>
+<p>
+<p><q a='2'>
+Foo
+Bar
+</q></p>
+</p>
+<p>
+<p><q>Foo
+Bar</q></p>
+</p>
+<p>
+<p><q a='2'>Foo
+Bar</q></p>
+</p>
+<p>
+<p>
+foo<q>
+Foo
+</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q a='2'>
+Foo
+</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q>Foo</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q a='2'>Foo</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q>
+Foo
+</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q a='2'>
+Foo
+</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q>Foo</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q a='2'>Foo</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q>
+Foo
+Bar
+</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q a='2'>
+Foo
+Bar
+</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q>Foo
+Bar</q>bar
+</p>
+</p>
+<p>
+<p>
+foo<q a='2'>Foo
+Bar</q>bar
+</p>
+</p>
+<p>
+<p><q></q></p>
+</p>
+<p>
+<p><q></p>
+</p>
+<p>
+<p><q a='2'></q></p>
+</p>
+<p>
+<p><q a='2'></p>
+</p>
diff --git a/test/haml/results/original_engine.xhtml b/test/haml/results/original_engine.xhtml
new file mode 100644 (file)
index 0000000..e277475
--- /dev/null
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Stop. haml time</title>
+<div id='content'>
+<h1>This is a title!</h1>
+<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit</p>
+<p class='foo'>Cigarettes!</p>
+<h2>Man alive!</h2>
+<ul class='things'>
+<li>Slippers</li>
+<li>Shoes</li>
+<li>Bathrobe</li>
+<li>Coffee</li>
+</ul>
+<pre>This is some text that's in a pre block!
+Let's see what happens when it's rendered! What about now, since we're on a new line?</pre>
+</div>
+</head>
+</html>
diff --git a/test/haml/results/partial_layout.xhtml b/test/haml/results/partial_layout.xhtml
new file mode 100644 (file)
index 0000000..6a60101
--- /dev/null
@@ -0,0 +1,6 @@
+<h1>Partial layout used with for block:</h1>
+<div class='partial-layout'>
+<h2>This is inside a partial layout</h2>
+<p>Some content within a layout</p>
+
+</div>
diff --git a/test/haml/results/partial_layout_erb.xhtml b/test/haml/results/partial_layout_erb.xhtml
new file mode 100644 (file)
index 0000000..fe433e9
--- /dev/null
@@ -0,0 +1,6 @@
+<h1>Partial layout used with for block:</h1>
+<div class='partial-layout'>
+<h2>This is inside a partial layout</h2>
+Some content within a layout
+
+</div>
diff --git a/test/haml/results/partials.xhtml b/test/haml/results/partials.xhtml
new file mode 100644 (file)
index 0000000..675c526
--- /dev/null
@@ -0,0 +1,22 @@
+<p>
+@foo =
+value one
+</p>
+<p>
+@foo =
+value two
+</p>
+<p>
+  @foo =
+  value two
+</p>
+Toplevel? false
+<p>
+  @foo =
+  value three
+</p>
+
+<p>
+@foo =
+value three
+</p>
diff --git a/test/haml/results/render_layout.xhtml b/test/haml/results/render_layout.xhtml
new file mode 100644 (file)
index 0000000..9712bb5
--- /dev/null
@@ -0,0 +1,3 @@
+Before
+During
+After
diff --git a/test/haml/results/silent_script.xhtml b/test/haml/results/silent_script.xhtml
new file mode 100644 (file)
index 0000000..cd7b86f
--- /dev/null
@@ -0,0 +1,74 @@
+<div>
+<h1>I can count!</h1>
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+<h1>I know my ABCs!</h1>
+<ul>
+<li>a</li>
+<li>b</li>
+<li>c</li>
+<li>d</li>
+<li>e</li>
+<li>f</li>
+<li>g</li>
+<li>h</li>
+<li>i</li>
+<li>j</li>
+<li>k</li>
+<li>l</li>
+<li>m</li>
+<li>n</li>
+<li>o</li>
+<li>p</li>
+<li>q</li>
+<li>r</li>
+<li>s</li>
+<li>t</li>
+<li>u</li>
+<li>v</li>
+<li>w</li>
+<li>x</li>
+<li>y</li>
+<li>z</li>
+</ul>
+<h1>I can catch errors!</h1>
+Oh no! &quot;foo&quot; happened!
+<p>
+"false" is:
+false
+</p>
+Even!
+Odd!
+Even!
+Odd!
+Even!
+</div>
+<div class='foo'>
+<strong>foobar</strong>
+</div>
+<strong>0</strong>
+<strong>1</strong>
+<strong>2</strong>
+<strong>3</strong>
+<strong>4</strong>
+<div class='test'>
+<p>boom</p>
+</div>
diff --git a/test/haml/results/standard.xhtml b/test/haml/results/standard.xhtml
new file mode 100644 (file)
index 0000000..7d7c6e4
--- /dev/null
@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html lang='en-US' xml:lang='en-US' xmlns='http://www.w3.org/1999/xhtml'>
+<head>
+<title>Hampton Catlin Is Totally Awesome</title>
+<meta content='text/html; charset=utf-8' http-equiv='Content-Type'>
+</head>
+<body>
+<!-- You're In my house now! -->
+<div class='header'>
+Yes, ladies and gentileman. He is just that egotistical.
+Fantastic! This should be multi-line output
+The question is if this would translate! Ahah!
+20
+</div>
+<div id='body'> Quotes should be loved! Just like people!</div>
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+Wow.|
+<p code='3'>
+Holy cow        multiline       tags!           A pipe (|) even!
+PipesIgnored|PipesIgnored|PipesIgnored|
+1|2|3
+</p>
+<div class='silent' foo='17'>
+this shouldn't evaluate but now it should!
+</div>
+<ul class='really cool'>
+<li>a</li>
+<li>b</li>
+<li>c</li>
+<li>d</li>
+<li>e</li>
+<li>f</li>
+</ul>
+<div class='of_divs_with_underscore' id='combo'>with this text</div>
+foo<div class='footer'>
+<strong class='shout'>This is a really long ruby quote. It should be loved and wrapped because its more than 50 characters. This value may change in the future and this test may look stupid. 
+So, I'm just making it *really* long. God, I hope this works</strong>
+</div>
+</body>
+</html>
diff --git a/test/haml/results/tag_parsing.xhtml b/test/haml/results/tag_parsing.xhtml
new file mode 100644 (file)
index 0000000..a575d86
--- /dev/null
@@ -0,0 +1,23 @@
+<div class='tags'>
+<foo>1</foo>
+<FOO>2</FOO>
+<fooBAR>3</fooBAR>
+<fooBar>4</fooBar>
+<foo_bar>5</foo_bar>
+<foo-bar>6</foo-bar>
+<foo:bar>7</foo:bar>
+<foo class='bar'>8</foo>
+<fooBAr_baz:boom_bar>9</fooBAr_baz:boom_bar>
+<foo13>10</foo13>
+<foo2u>11</foo2u>
+</div>
+<div class='classes'>
+<p class='foo bar' id='boom'></p>
+<div class='fooBar'>a</div>
+<div class='foo-bar'>b</div>
+<div class='foo_bar'>c</div>
+<div class='FOOBAR'>d</div>
+<div class='foo16'>e</div>
+<div class='123'>f</div>
+<div class='foo2u'>g</div>
+</div>
diff --git a/test/haml/results/very_basic.xhtml b/test/haml/results/very_basic.xhtml
new file mode 100644 (file)
index 0000000..25f83eb
--- /dev/null
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<head></head>
+<body></body>
+</html>
diff --git a/test/haml/results/whitespace_handling.xhtml b/test/haml/results/whitespace_handling.xhtml
new file mode 100644 (file)
index 0000000..8c86da8
--- /dev/null
@@ -0,0 +1,94 @@
+<div id='whitespace_test'>
+<div class='text_area_test_area'>
+  <textarea>Oneline</textarea>
+</div>
+<textarea>BLAH
+</textarea>
+
+<div class='text_area_test_area'>
+  <textarea>Two&#x000A;lines</textarea>
+</div>
+<textarea>BLAH
+</textarea>
+
+<div class='text_area_test_area'>
+  <textarea>Oneline</textarea>
+</div>
+<textarea>BLAH</textarea>
+
+<div class='text_area_test_area'>
+  <textarea>Two&#x000A;lines</textarea>
+</div>
+<textarea>BLAH</textarea>
+
+<div id='flattened'><div class='text_area_test_area'>
+  <textarea>Two&#x000A;lines</textarea>
+</div>
+<textarea>BLAH</textarea>
+</div>
+</div>
+<div class='hithere'>
+Foo bar
+&lt;pre&gt;foo bar&lt;/pre&gt;
+&lt;pre&gt;foo
+bar&lt;/pre&gt;
+<p>&lt;pre&gt;foo
+bar&lt;/pre&gt;</p>
+<p>foo
+bar</p>
+</div>
+<div class='foo'>
+13
+&lt;textarea&gt;
+a
+&lt;/textarea&gt;
+&lt;textarea&gt;
+a
+&lt;/textarea&gt;</div>
+<div id='whitespace_test'>
+<div class='text_area_test_area'>
+  <textarea>Oneline</textarea>
+</div>
+<textarea>BLAH
+</textarea>
+
+<div class='text_area_test_area'>
+  <textarea>Two&#x000A;lines</textarea>
+</div>
+<textarea>BLAH
+</textarea>
+
+<div class='text_area_test_area'>
+  <textarea>Oneline</textarea>
+</div>
+<textarea>BLAH</textarea>
+
+<div class='text_area_test_area'>
+  <textarea>Two&#x000A;lines</textarea>
+</div>
+<textarea>BLAH</textarea>
+
+<div id='flattened'><div class='text_area_test_area'>
+  <textarea>Two&#x000A;lines</textarea>
+</div>
+<textarea>BLAH</textarea>
+</div>
+</div>
+<div class='hithere'>
+Foo bar
+<pre>foo bar</pre>
+<pre>foo&#x000A;bar</pre>
+<p><pre>foo&#x000A;bar</pre></p>
+<p>foo
+bar</p>
+<pre>                                                 ___&#x000A;                                              ,o88888&#x000A;                                           ,o8888888'&#x000A;                     ,:o:o:oooo.        ,8O88Pd8888"&#x000A;                 ,.::.::o:ooooOoOoO. ,oO8O8Pd888'"&#x000A;               ,.:.::o:ooOoOoOO8O8OOo.8OOPd8O8O"&#x000A;              , ..:.::o:ooOoOOOO8OOOOo.FdO8O8"&#x000A;             , ..:.::o:ooOoOO8O888O8O,COCOO"&#x000A;            , . ..:.::o:ooOoOOOO8OOOOCOCO"&#x000A;             . ..:.::o:ooOoOoOO8O8OCCCC"o&#x000A;                . ..:.::o:ooooOoCoCCC"o:o&#x000A;                . ..:.::o:o:,cooooCo"oo:o:&#x000A;             `   . . ..:.:cocoooo"'o:o:::'&#x000A;             .`   . ..::ccccoc"'o:o:o:::'&#x000A;            :.:.    ,c:cccc"':.:.:.:.:.'&#x000A;          ..:.:"'`::::c:"'..:.:.:.:.:.'  http://www.chris.com/ASCII/&#x000A;        ...:.'.:.::::"'    . . . . .'&#x000A;       .. . ....:."' `   .  . . ''&#x000A;     . . . ...."'&#x000A;     .. . ."'     -hrr-&#x000A;    .&#x000A;&#x000A;&#x000A;                                              It's a planet!&#x000A;%strong This shouldn't be bold!</pre>
+<strong>This should!</strong>
+<textarea>      ___           ___           ___           ___ &#x000A;     /\__\         /\  \         /\__\         /\__\&#x000A;    /:/  /        /::\  \       /::|  |       /:/  /&#x000A;   /:/__/        /:/\:\  \     /:|:|  |      /:/  / &#x000A;  /::\  \ ___   /::\~\:\  \   /:/|:|__|__   /:/  /  &#x000A; /:/\:\  /\__\ /:/\:\ \:\__\ /:/ |::::\__\ /:/__/   &#x000A; \/__\:\/:/  / \/__\:\/:/  / \/__/~~/:/  / \:\  \   &#x000A;      \::/  /       \::/  /        /:/  /   \:\  \  &#x000A;      /:/  /        /:/  /        /:/  /     \:\  \ &#x000A;     /:/  /        /:/  /        /:/  /       \:\__\&#x000A;     \/__/         \/__/         \/__/         \/__/&#x000A;     &#x000A;     Many&#x000A;                   thanks&#x000A;           to&#x000A;                                http://www.network-science.de/ascii/
+<strong>indeed!</strong></textarea>
+</div>
+<div class='foo'>
+13
+</div>
+<pre>       __     ______        __               ______&#x000A;.----.|  |--.|__    |.----.|  |--..--------.|  __  |&#x000A;|  __||     ||__    ||  __||    < |        ||  __  |&#x000A;|____||__|__||______||____||__|__||__|__|__||______|</pre>
+<pre>foo
+bar</pre>
diff --git a/test/haml/template_test.rb b/test/haml/template_test.rb
new file mode 100644 (file)
index 0000000..5235466
--- /dev/null
@@ -0,0 +1,368 @@
+require 'test_helper'
+require 'haml/mocks/article'
+
+require 'action_pack/version'
+require 'hamlit/rails_template'
+
+module Haml::Filters::Test
+  include Haml::Filters::Base
+
+  def render(text)
+    "TESTING HAHAHAHA!"
+  end
+end
+
+module Hamlit::RailsHelpers
+  def test_partial(name, locals = {})
+    Hamlit::Template.new { File.read(File.join(TemplateTest::TEMPLATE_PATH, "_#{name}.haml")) }.render(self, locals)
+  end
+end
+
+class Egocentic
+  def method_missing(*args)
+    self
+  end
+end
+
+class DummyController
+  attr_accessor :logger
+  def initialize
+    @logger = Egocentic.new
+  end
+
+  def self.controller_path
+    ''
+  end
+
+  def controller_path
+    ''
+  end
+end
+
+class TemplateTest < Haml::TestCase
+  TEMPLATE_PATH = File.join(File.dirname(__FILE__), "templates")
+  TEMPLATES = [
+    'very_basic',
+    #'standard',
+    #'helpers',
+    #'whitespace_handling',
+    'original_engine',
+    'list',
+    'helpful',
+    'silent_script',
+    'tag_parsing',
+    #'just_stuff',
+    #'partials',
+    #'nuke_outer_whitespace',
+    #'nuke_inner_whitespace',
+    #'render_layout',
+    #'partial_layout',
+    'partial_layout_erb',
+  ]
+
+  def setup
+    @base = create_base
+
+    # filters template uses :sass
+    # Sass::Plugin.options.update(:line_comments => true, :style => :compact)
+  end
+
+  def create_base
+    vars = { 'article' => Article.new, 'foo' => 'value one' }
+
+    base = ActionView::Base.new(TEMPLATE_PATH, vars)
+
+    # This is needed by RJS in (at least) Rails 3
+    base.instance_variable_set(:@template, base)
+
+    # This is used by form_for.
+    # It's usually provided by ActionController::Base.
+    def base.protect_against_forgery?; false; end
+
+    base
+  end
+
+  def render(text, options = {})
+    return @base.render(:inline => text, :type => :haml) if options == :action_view
+    options = options.merge(:format => :xhtml)
+    super(text, options, @base)
+  end
+
+  def load_result(name)
+    @result = ''
+    File.new(File.dirname(__FILE__) + "/results/#{name}.xhtml").each_line { |l| @result += l }
+    @result
+  end
+
+  def assert_renders_correctly(name, &render_method)
+    old_options = Haml::Template.options.dup
+    Haml::Template.options[:escape_html] = false
+    render_method ||= proc { |n| @base.render(:file => n) }
+
+    silence_warnings do
+      load_result(name).split("\n").zip(render_method[name].split("\n")).each_with_index do |pair, line|
+        message = "template: #{name}\nline:     #{line}"
+        assert_equal(pair.first, pair.last, message)
+      end
+    end
+  rescue ActionView::Template::Error => e
+    if e.message =~ /Can't run [\w:]+ filter; required (one of|file) ((?:'\w+'(?: or )?)+)(, but none were found| not found)/
+      puts "\nCouldn't require #{$2}; skipping a test."
+    else
+      raise e
+    end
+  ensure
+    Haml::Template.options = old_options
+  end
+
+  def test_empty_render_should_remain_empty
+    assert_equal('', render(''))
+  end
+
+  TEMPLATES.each do |template|
+    define_method "test_template_should_render_correctly [template: #{template}]" do
+      assert_renders_correctly template
+    end
+  end
+
+  def test_templates
+    skip
+    TEMPLATES
+  end
+
+  def test_render_method_returning_null_with_ugly; skip
+    @base.instance_eval do
+      def empty
+        nil
+      end
+      def render_something(&block)
+        capture(self, &block)
+      end
+    end
+
+    content_to_render = "%h1 This is part of the broken view.\n= render_something do |thing|\n  = thing.empty do\n    = 'test'"
+    result = render(content_to_render, :ugly => true)
+    expected_result = "<h1>This is part of the broken view.</h1>\n"
+    assert_equal(expected_result, result)
+  end
+
+  def test_simple_rendering_with_ugly
+    skip
+    assert_haml_ugly("%p test\n= capture { 'foo' }")
+  end
+
+  def test_templates_should_render_correctly_with_render_proc; skip
+    assert_renders_correctly("standard") do |name|
+      engine = Hamlit::HamlEngine.new(File.read(File.dirname(__FILE__) + "/templates/#{name}.haml"), :format => :xhtml)
+      engine.render_proc(@base).call
+    end
+  end
+
+  def test_templates_should_render_correctly_with_def_method; skip
+    assert_renders_correctly("standard") do |name|
+      engine = Haml::HamlEngine.new(File.read(File.dirname(__FILE__) + "/templates/#{name}.haml"), :format => :xhtml)
+      engine.def_method(@base, "render_standard")
+      @base.render_standard
+    end
+  end
+
+  def test_instance_variables_should_work_inside_templates
+    @base.instance_variable_set(:@content_for_layout, 'something')
+    assert_haml_ugly("%p= @content_for_layout")
+
+    @base.instance_eval("@author = 'Hampton Catlin'")
+    assert_haml_ugly(".author= @author")
+
+    @base.instance_eval("@author = 'Hampton'")
+    assert_haml_ugly("= @author")
+
+    @base.instance_eval("@author = 'Catlin'")
+    assert_haml_ugly("= @author")
+  end
+
+  def test_instance_variables_should_work_inside_attributes
+    skip
+    @base.instance_eval("@author = 'hcatlin'")
+    assert_haml_ugly("%p{:class => @author} foo")
+  end
+
+  def test_template_renders_should_eval
+    assert_equal("2\n", render("= 1+1"))
+  end
+
+  def test_haml_options; skip
+    old_options = Haml::Template.options.dup
+    Haml::Template.options[:suppress_eval] = true
+    old_base, @base = @base, create_base
+    assert_renders_correctly("eval_suppressed")
+  ensure
+    skip
+    @base = old_base
+    Haml::Template.options = old_options
+  end
+
+  def test_with_output_buffer_with_ugly; skip
+    assert_equal(<<HTML, render(<<HAML, :ugly => true))
+<p>
+foo
+baz
+</p>
+HTML
+%p
+  foo
+  -# Parenthesis required due to Rails 3.0 deprecation of block helpers
+  -# that return strings.
+  - (with_output_buffer do
+    bar
+    = "foo".gsub(/./) do |s|
+      - "flup"
+  - end)
+  baz
+HAML
+  end
+
+  def test_exceptions_should_work_correctly; skip
+    begin
+      render("- raise 'oops!'")
+    rescue Exception => e
+      assert_equal("oops!", e.message)
+      assert_match(/^\(haml\):1/, e.backtrace[0])
+    else
+      assert false
+    end
+
+    template = <<END
+%p
+  %h1 Hello!
+  = "lots of lines"
+  = "even more!"
+  - raise 'oh no!'
+  %p
+    this is after the exception
+    %strong yes it is!
+ho ho ho.
+END
+
+    begin
+      render(template.chomp)
+    rescue Exception => e
+      assert_match(/^\(haml\):5/, e.backtrace[0])
+    else
+      assert false
+    end
+  end
+
+  def test_form_builder_label_with_block; skip
+    output = render(<<HAML, :action_view)
+= form_for @article, :as => :article, :html => {:class => nil, :id => nil}, :url => '' do |f|
+  = f.label :title do
+    Block content
+HAML
+    fragment = Nokogiri::HTML.fragment output
+    assert_equal "Block content", fragment.css('form label').first.content.strip
+  end
+
+  ## XSS Protection Tests
+
+  def test_escape_html_option_set; skip
+    assert Haml::Template.options[:escape_html]
+  end
+
+  def test_xss_protection; skip
+    assert_equal("Foo &amp; Bar\n", render('= "Foo & Bar"', :action_view))
+  end
+
+  def test_xss_protection_with_safe_strings; skip
+    assert_equal("Foo & Bar\n", render('= Haml::Util.html_safe("Foo & Bar")', :action_view))
+  end
+
+  def test_xss_protection_with_bang; skip
+    assert_haml_ugly('!= "Foo & Bar"', :action_view)
+  end
+
+  def test_xss_protection_in_interpolation; skip
+    assert_equal("Foo &amp; Bar\n", render('Foo #{"&"} Bar', :action_view))
+  end
+
+  def test_xss_protection_in_attributes; skip
+    assert_equal("<div data-html='&lt;foo&gt;bar&lt;/foo&gt;'></div>\n", render('%div{ "data-html" => "<foo>bar</foo>" }', :action_view))
+  end
+
+  def test_xss_protection_in_attributes_with_safe_strings; skip
+    assert_equal("<div data-html='<foo>bar</foo>'></div>\n", render('%div{ "data-html" => "<foo>bar</foo>".html_safe }', :action_view))
+  end
+
+  def test_xss_protection_with_bang_in_interpolation; skip
+    assert_haml_ugly('! Foo #{"&"} Bar', :action_view)
+  end
+
+  def test_xss_protection_with_safe_strings_in_interpolation; skip
+    assert_equal("Foo & Bar\n", render('Foo #{Haml::Util.html_safe("&")} Bar', :action_view))
+  end
+
+  def test_xss_protection_with_mixed_strings_in_interpolation; skip
+    assert_equal("Foo & Bar &amp; Baz\n", render('Foo #{Haml::Util.html_safe("&")} Bar #{"&"} Baz', :action_view))
+  end
+
+  def test_rendered_string_is_html_safe; skip
+    assert(render("Foo").html_safe?)
+  end
+
+  def test_rendered_string_is_html_safe_with_action_view
+    assert(render("Foo", :action_view).html_safe?)
+  end
+
+  def test_xss_html_escaping_with_non_strings
+    assert_haml_ugly("= html_escape(4)")
+  end
+
+  def test_xss_protection_with_concat; skip
+    assert_equal("Foo &amp; Bar", render('- concat "Foo & Bar"', :action_view))
+  end
+
+  def test_xss_protection_with_concat_with_safe_string; skip
+    assert_equal("Foo & Bar", render('- concat(Haml::Util.html_safe("Foo & Bar"))', :action_view))
+  end
+
+  def test_xss_protection_with_safe_concat; skip
+    assert_equal("Foo & Bar", render('- safe_concat "Foo & Bar"', :action_view))
+  end
+
+  ## Regression
+
+  def test_xss_protection_with_nested_haml_tag; skip
+    assert_equal(<<HTML, render(<<HAML, :action_view))
+<div>
+  <ul>
+    <li>Content!</li>
+  </ul>
+</div>
+HTML
+- haml_tag :div do
+  - haml_tag :ul do
+    - haml_tag :li, "Content!"
+HAML
+  end
+
+  if defined?(ActionView::Helpers::PrototypeHelper)
+    def test_rjs
+      assert_equal(<<HTML, render(<<HAML, :action_view))
+window.location.reload();
+HTML
+= update_page do |p|
+  - p.reload
+HAML
+    end
+  end
+
+  def test_cache; skip
+    @base.controller = ActionController::Base.new
+    @base.controller.perform_caching = false
+    assert_equal(<<HTML, render(<<HAML, :action_view))
+Test
+HTML
+- cache do
+  Test
+HAML
+  end
+end
diff --git a/test/haml/templates/_av_partial_1.haml b/test/haml/templates/_av_partial_1.haml
new file mode 100644 (file)
index 0000000..b46502c
--- /dev/null
@@ -0,0 +1,9 @@
+%h2 This is a pretty complicated partial
+.partial
+  %p It has several nested partials,
+  %ul
+    - 5.times do
+      %li
+        %strong Partial:
+        - @nesting = 5
+        = render :partial => 'templates/av_partial_2'
\ No newline at end of file
diff --git a/test/haml/templates/_av_partial_1_ugly.haml b/test/haml/templates/_av_partial_1_ugly.haml
new file mode 100644 (file)
index 0000000..02aa9d0
--- /dev/null
@@ -0,0 +1,9 @@
+%h2 This is a pretty complicated partial
+.partial
+  %p It has several nested partials,
+  %ul
+    - 5.times do
+      %li
+        %strong Partial:
+        - @nesting = 5
+        = render :partial => 'templates/av_partial_2_ugly'
\ No newline at end of file
diff --git a/test/haml/templates/_av_partial_2.haml b/test/haml/templates/_av_partial_2.haml
new file mode 100644 (file)
index 0000000..e7d2008
--- /dev/null
@@ -0,0 +1,5 @@
+- @nesting -= 1
+.partial{:level => @nesting}
+  %h3 This is a crazy deep-nested partial.
+  %p== Nesting level #{@nesting}
+  = render :partial => 'templates/av_partial_2' if @nesting > 0
\ No newline at end of file
diff --git a/test/haml/templates/_av_partial_2_ugly.haml b/test/haml/templates/_av_partial_2_ugly.haml
new file mode 100644 (file)
index 0000000..0b854fc
--- /dev/null
@@ -0,0 +1,5 @@
+- @nesting -= 1
+.partial{:level => @nesting}
+  %h3 This is a crazy deep-nested partial.
+  %p== Nesting level #{@nesting}
+  = render :partial => 'templates/av_partial_2_ugly' if @nesting > 0
\ No newline at end of file
diff --git a/test/haml/templates/_layout.erb b/test/haml/templates/_layout.erb
new file mode 100644 (file)
index 0000000..91c839d
--- /dev/null
@@ -0,0 +1,3 @@
+Before
+<%= yield -%>
+After
diff --git a/test/haml/templates/_layout_for_partial.haml b/test/haml/templates/_layout_for_partial.haml
new file mode 100644 (file)
index 0000000..7cf538b
--- /dev/null
@@ -0,0 +1,3 @@
+.partial-layout
+  %h2 This is inside a partial layout
+  = yield
\ No newline at end of file
diff --git a/test/haml/templates/_partial.haml b/test/haml/templates/_partial.haml
new file mode 100644 (file)
index 0000000..756b54b
--- /dev/null
@@ -0,0 +1,8 @@
+%p
+  @foo = 
+  = @foo
+- @foo = 'value three'
+== Toplevel? #{haml_buffer.toplevel?}
+%p
+  @foo = 
+  = @foo
diff --git a/test/haml/templates/_text_area.haml b/test/haml/templates/_text_area.haml
new file mode 100644 (file)
index 0000000..896b975
--- /dev/null
@@ -0,0 +1,3 @@
+.text_area_test_area
+  ~ "<textarea>" + value + "</textarea>"
+= "<textarea>BLAH\n</textarea>"
diff --git a/test/haml/templates/_text_area_helper.html.haml b/test/haml/templates/_text_area_helper.html.haml
new file mode 100644 (file)
index 0000000..f70d044
--- /dev/null
@@ -0,0 +1,4 @@
+- defined?(text_area_helper) and nil # silence a warning
+.foo
+  .bar
+    = text_area :post, :body
diff --git a/test/haml/templates/action_view.haml b/test/haml/templates/action_view.haml
new file mode 100644 (file)
index 0000000..a90f423
--- /dev/null
@@ -0,0 +1,47 @@
+!!!
+%html{html_attrs}
+  %head
+    %title Hampton Catlin Is Totally Awesome
+    %meta{"http-equiv" => "Content-Type", :content => "text/html; charset=utf-8"}
+  %body
+    %h1
+      This is very much like the standard template,
+      except that it has some ActionView-specific stuff.
+      It's only used for benchmarking.
+    .crazy_partials= render :partial => 'templates/av_partial_1'
+    / You're In my house now!
+    .header
+      Yes, ladies and gentileman. He is just that egotistical.
+      Fantastic! This should be multi-line output
+      The question is if this would translate! Ahah!
+      = 1 + 9 + 8 + 2 #numbers should work and this should be ignored
+    #body= " Quotes should be loved! Just like people!"
+    - 120.times do |number|
+      - number
+    Wow.|
+    %p
+      = "Holy cow        " + |
+        "multiline       " + |      
+        "tags!           " + |
+        "A pipe (|) even!"   |
+      = [1, 2, 3].collect { |n| "PipesIgnored|" }
+      = [1, 2, 3].collect { |n|     |
+          n.to_s                    |
+        }.join("|")                 |
+    %div.silent
+      - foo = String.new
+      - foo << "this"
+      - foo << " shouldn't"
+      - foo << " evaluate"
+      = foo + " but now it should!"
+      -# Woah crap a comment!
+
+    -# That was a line that shouldn't close everything.
+    %ul.really.cool
+      - ('a'..'f').each do |a|
+        %li= a
+    #combo.of_divs_with_underscore= @should_eval = "with this text"
+    = [ 104, 101, 108, 108, 111 ].map do |byte|
+      - byte.chr
+    .footer
+      %strong.shout= "This is a really long ruby quote. It should be loved and wrapped because its more than 50 characters. This value may change in the future and this test may look stupid. \nSo, I'm just making it *really* long. God, I hope this works"
diff --git a/test/haml/templates/action_view_ugly.haml b/test/haml/templates/action_view_ugly.haml
new file mode 100644 (file)
index 0000000..9e441a3
--- /dev/null
@@ -0,0 +1,47 @@
+!!!
+%html{html_attrs}
+  %head
+    %title Hampton Catlin Is Totally Awesome
+    %meta{"http-equiv" => "Content-Type", :content => "text/html; charset=utf-8"}
+  %body
+    %h1
+      This is very much like the standard template,
+      except that it has some ActionView-specific stuff.
+      It's only used for benchmarking.
+    .crazy_partials= render :partial => 'templates/av_partial_1_ugly'
+    / You're In my house now!
+    .header
+      Yes, ladies and gentileman. He is just that egotistical.
+      Fantastic! This should be multi-line output
+      The question is if this would translate! Ahah!
+      = 1 + 9 + 8 + 2 #numbers should work and this should be ignored
+    #body= " Quotes should be loved! Just like people!"
+    - 120.times do |number|
+      - number
+    Wow.|
+    %p
+      = "Holy cow        " + |
+        "multiline       " + |      
+        "tags!           " + |
+        "A pipe (|) even!"   |
+      = [1, 2, 3].collect { |n| "PipesIgnored|" }
+      = [1, 2, 3].collect { |n|     |
+          n.to_s                    |
+        }.join("|")                 |
+    %div.silent
+      - foo = String.new
+      - foo << "this"
+      - foo << " shouldn't"
+      - foo << " evaluate"
+      = foo + " but now it should!"
+      -# Woah crap a comment!
+
+    -# That was a line that shouldn't close everything.
+    %ul.really.cool
+      - ('a'..'f').each do |a|
+        %li= a
+    #combo.of_divs_with_underscore= @should_eval = "with this text"
+    = [ 104, 101, 108, 108, 111 ].map do |byte|
+      - byte.chr
+    .footer
+      %strong.shout= "This is a really long ruby quote. It should be loved and wrapped because its more than 50 characters. This value may change in the future and this test may look stupid. \nSo, I'm just making it *really* long. God, I hope this works"
diff --git a/test/haml/templates/breakage.haml b/test/haml/templates/breakage.haml
new file mode 100644 (file)
index 0000000..57c1734
--- /dev/null
@@ -0,0 +1,8 @@
+%p
+  %h1 Hello!
+  = "lots of lines"
+  - raise "Oh no!"
+  %p
+    this is after the exception
+    %strong yes it is!
+ho ho ho.
diff --git a/test/haml/templates/content_for_layout.haml b/test/haml/templates/content_for_layout.haml
new file mode 100644 (file)
index 0000000..e9d7e6d
--- /dev/null
@@ -0,0 +1,8 @@
+!!!
+%html
+  %head
+  %body
+    #yieldy
+      = yield :layout
+    #nosym
+      = yield
diff --git a/test/haml/templates/eval_suppressed.haml b/test/haml/templates/eval_suppressed.haml
new file mode 100644 (file)
index 0000000..1e3c034
--- /dev/null
@@ -0,0 +1,11 @@
+= "not me!"
+= "nor me!"
+- haml_concat "not even me!"
+%p= "NO!"
+%p~ "UH-UH!"
+%h1 Me!
+#foo
+  %p#bar All
+  %br/
+  %p.baz This
+  Should render
diff --git a/test/haml/templates/helpers.haml b/test/haml/templates/helpers.haml
new file mode 100644 (file)
index 0000000..c4be300
--- /dev/null
@@ -0,0 +1,55 @@
+= h("&&&&&&&&&&&") # This is an ActionView Helper... should load
+- foo = capture do # This ActionView Helper is designed for ERB, but should work with haml
+  %div
+    %p.title Title
+    %p.text
+      Woah this is really crazy
+      I mean wow,
+      man.
+- 3.times do
+  = foo
+%p foo
+- tab_up
+%p reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally loooooooooooooooooong
+- tab_down
+.woah
+  #funky
+    = capture_haml do
+      %div
+        %h1 Big!
+        %p Small
+        / Invisible
+    = capture do
+      .dilly
+        %p foo
+        %h1 bar
+  = surround '(', ')' do
+    %strong parentheses!
+= precede '*' do
+  %span.small Not really
+click
+= succeed '.' do
+  %a{:href=>"thing"} here
+%p baz
+- haml_buffer.tabulation = 10
+%p boom
+- concat "foo\n"
+- haml_buffer.tabulation = 0
+= list_of({:google => 'http://www.google.com'}) do |name, link|
+  %a{ :href => link }= name
+%p
+  - haml_concat "foo"
+  %div
+    - haml_concat "bar"
+  - haml_concat "boom"
+  baz
+  - haml_concat "boom, again"
+- haml_tag :table do
+  - haml_tag :tr do
+    - haml_tag :td, {:class => 'cell'} do
+      - haml_tag :strong, "strong!"
+      - haml_concat "data"
+    - haml_tag :td do
+      - haml_concat "more_data"
+- haml_tag :hr
+- haml_tag :div, ''
diff --git a/test/haml/templates/helpful.haml b/test/haml/templates/helpful.haml
new file mode 100644 (file)
index 0000000..3e44a50
--- /dev/null
@@ -0,0 +1,11 @@
+%div[@article]
+  %h1= @article.title
+  %div= @article.body
+#id[@article] id
+.class[@article] class
+#id.class[@article] id class
+%div{:class => "article full"}[@article]= "boo"
+%div{'class' => "article full"}[@article]= "moo"
+%div.articleFull[@article]= "foo"
+%span[@not_a_real_variable_and_will_be_nil]
+  Boo
diff --git a/test/haml/templates/just_stuff.haml b/test/haml/templates/just_stuff.haml
new file mode 100644 (file)
index 0000000..0dd82c7
--- /dev/null
@@ -0,0 +1,86 @@
+!!! XML
+!!! XML ISO-8859-1
+!!! XML UtF-8 Foo bar
+!!!
+!!! 1.1
+!!! 1.1 Strict
+!!! Strict foo bar
+!!! FRAMESET
+%strong{:apos => "Foo's bar!"} Boo!
+== Embedded? false!
+== Embedded? #{true}!
+- embedded = true
+== Embedded? #{embedded}!
+== Embedded? #{"twice! #{true}"}!
+== Embedded? #{"one"} af"t"er #{"another"}!
+%p== Embedded? false!
+%p== Embedded? #{true}!
+- embedded = true
+%p== Embedded? #{embedded}!
+%p== Embedded? #{"twice! #{true}"}!
+%p== Embedded? #{"one"} af"t"er #{"another"}!
+= "stuff followed by whitespace"
+  
+- if true
+
+  %strong block with whitespace
+%p
+  \Escape
+  \- character
+  \%p foo
+  \yee\ha
+  \   don't lstrip me
+/ Short comment
+/
+  This is a block comment
+  cool, huh?
+  %strong there can even be sub-tags!
+  = "Or script!"
+-# Haml comment
+-#
+  Nested Haml comment
+  - raise 'dead'
+%p{ :class => "" } class attribute should appear!
+%p{ :gorbachev => nil } this attribute shouldn't appear
+/[if lte IE6] conditional comment!
+/[if gte IE7]
+  %p Block conditional comment
+  %div
+    %h1 Cool, eh?
+/[if gte IE5.2]
+  Woah a period.
+= "test" |
+  "test" |
+-# Hard tabs shouldn't throw errors.
+       
+- case :foo
+- when :bar
+  %br Blah
+- when :foo
+  %br
+- case :foo
+  - when :bar
+    %meta{ :foo => 'blah'}
+  - when :foo
+    %meta{ :foo => 'bar'}
+%img
+%hr
+%link
+%script Inline content
+%br
+  Nested content
+%p.foo{:class => true ? 'bar' : 'baz'}[@article] Blah
+%p.foo{:class => false ? 'bar' : ''}[@article] Blah
+%p.foo{:class => %w[bar baz]}[@article] Blah
+%p.qux{:class => 'quux'}[@article] Blump
+%p#foo{:id => %w[bar baz]}[@article] Whee
+== #{"Woah inner quotes"}
+%p.dynamic_quote{:quotes => "single '", :dyn => 1 + 2}
+%p.dynamic_self_closing{:dyn => 1 + 2}/
+%body
+  :plain
+    hello
+  %div
+
+    %img
+
diff --git a/test/haml/templates/list.haml b/test/haml/templates/list.haml
new file mode 100644 (file)
index 0000000..40a80e6
--- /dev/null
@@ -0,0 +1,12 @@
+!Not a Doctype!
+%ul
+  %li a
+  %li b
+  %li c
+  %li d
+  %li e
+  %li f
+  %li g
+  %li h
+  %li i
+
diff --git a/test/haml/templates/nuke_inner_whitespace.haml b/test/haml/templates/nuke_inner_whitespace.haml
new file mode 100644 (file)
index 0000000..8eebd41
--- /dev/null
@@ -0,0 +1,32 @@
+%p
+  %q< Foo
+%p
+  %q{:a => 1 + 1}< Foo
+%p
+  %q<= "Foo\nBar"
+%p
+  %q{:a => 1 + 1}<= "Foo\nBar"
+%p
+  %q<
+    Foo
+    Bar
+%p
+  %q{:a => 1 + 1}<
+    Foo
+    Bar
+%p
+  %q<
+    %div
+      Foo
+      Bar
+%p
+  %q{:a => 1 + 1}<
+    %div
+      Foo
+      Bar
+
+-# Regression test
+%p
+  %q<= "foo"
+  %q{:a => 1 + 1}
+    bar
diff --git a/test/haml/templates/nuke_outer_whitespace.haml b/test/haml/templates/nuke_outer_whitespace.haml
new file mode 100644 (file)
index 0000000..1e2a7f5
--- /dev/null
@@ -0,0 +1,144 @@
+%p
+  %p
+    %q>
+      Foo
+%p
+  %p
+    %q{:a => 1 + 1}>
+      Foo
+%p
+  %p
+    %q> Foo
+%p
+  %p
+    %q{:a => 1 + 1}> Foo
+%p
+  %p
+    %q>
+      = "Foo"
+%p
+  %p
+    %q{:a => 1 + 1}>
+      = "Foo"
+%p
+  %p
+    %q>= "Foo"
+%p
+  %p
+    %q{:a => 1 + 1}>= "Foo"
+%p
+  %p
+    %q>
+      = "Foo\nBar"
+%p
+  %p
+    %q{:a => 1 + 1}>
+      = "Foo\nBar"
+%p
+  %p
+    %q>= "Foo\nBar"
+%p
+  %p
+    %q{:a => 1 + 1}>= "Foo\nBar"
+%p
+  %p
+    - tab_up
+    foo
+    %q>
+      Foo
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q{:a => 1 + 1}>
+      Foo
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q> Foo
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q{:a => 1 + 1}> Foo
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q>
+      = "Foo"
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q{:a => 1 + 1}>
+      = "Foo"
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q>= "Foo"
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q{:a => 1 + 1}>= "Foo"
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q>
+      = "Foo\nBar"
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q{:a => 1 + 1}>
+      = "Foo\nBar"
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q>= "Foo\nBar"
+    bar
+    - tab_down
+%p
+  %p
+    - tab_up
+    foo
+    %q{:a => 1 + 1}>= "Foo\nBar"
+    bar
+    - tab_down
+%p
+  %p
+    %q>
+%p
+  %p
+    %q>/
+%p
+  %p
+    %q{:a => 1 + 1}>
+%p
+  %p
+    %q{:a => 1 + 1}>/
diff --git a/test/haml/templates/original_engine.haml b/test/haml/templates/original_engine.haml
new file mode 100644 (file)
index 0000000..df31a5a
--- /dev/null
@@ -0,0 +1,17 @@
+!!!
+%html
+  %head
+    %title Stop. haml time
+    #content
+      %h1 This is a title!
+      %p Lorem ipsum dolor sit amet, consectetur adipisicing elit
+      %p{:class => 'foo'} Cigarettes!
+      %h2 Man alive!
+      %ul.things
+        %li Slippers
+        %li Shoes
+        %li Bathrobe
+        %li Coffee
+      %pre
+        This is some text that's in a pre block!
+        Let's see what happens when it's rendered! What about now, since we're on a new line?
diff --git a/test/haml/templates/partial_layout.haml b/test/haml/templates/partial_layout.haml
new file mode 100644 (file)
index 0000000..a463ea1
--- /dev/null
@@ -0,0 +1,3 @@
+%h1 Partial layout used with for block:
+= render :layout => 'layout_for_partial' do
+  %p Some content within a layout
diff --git a/test/haml/templates/partial_layout_erb.erb b/test/haml/templates/partial_layout_erb.erb
new file mode 100644 (file)
index 0000000..7f88377
--- /dev/null
@@ -0,0 +1,4 @@
+<h1>Partial layout used with for block:</h1>
+<%= render :layout => 'layout_for_partial' do -%>
+Some content within a layout
+<% end %>
diff --git a/test/haml/templates/partialize.haml b/test/haml/templates/partialize.haml
new file mode 100644 (file)
index 0000000..327d90d
--- /dev/null
@@ -0,0 +1 @@
+= render :file => "#{name}.haml"
diff --git a/test/haml/templates/partials.haml b/test/haml/templates/partials.haml
new file mode 100644 (file)
index 0000000..d74f4b4
--- /dev/null
@@ -0,0 +1,12 @@
+- @foo = 'value one'
+%p
+  @foo = 
+  = @foo
+- @foo = 'value two'
+%p
+  @foo = 
+  = @foo
+= test_partial "partial"
+%p
+  @foo = 
+  = @foo
diff --git a/test/haml/templates/render_layout.haml b/test/haml/templates/render_layout.haml
new file mode 100644 (file)
index 0000000..549742b
--- /dev/null
@@ -0,0 +1,2 @@
+= render :layout => 'layout' do
+  During
diff --git a/test/haml/templates/silent_script.haml b/test/haml/templates/silent_script.haml
new file mode 100644 (file)
index 0000000..2df83e8
--- /dev/null
@@ -0,0 +1,45 @@
+%div
+  %h1 I can count!
+  - (1..20).each do |i|
+    = i
+  %h1 I know my ABCs!
+  %ul
+    - ('a'..'z').each do |i|
+      %li= i
+  %h1 I can catch errors!
+  - begin
+    - raise "foo"
+  - rescue RuntimeError => e
+    = "Oh no! \"#{e}\" happened!"
+  %p
+    "false" is:
+    - if false
+      = "true"
+    - else
+      = "false"
+  - if true
+    - 5.times do |i|
+      - if i % 2 == 1
+        Odd!
+      - else
+        Even!
+    - unless true
+      Testing else indent
+    - case 1
+    - when 2
+      Also testing else indent
+  - else
+    = "This can't happen!"
+- 13 |
+.foo
+  %strong foobar
+- 5.times   |
+    do      |
+      |a|   |
+  %strong= a
+.test
+  - "foo  |
+     bar  |
+     baz" |
+    
+  %p boom
diff --git a/test/haml/templates/standard.haml b/test/haml/templates/standard.haml
new file mode 100644 (file)
index 0000000..c1d4866
--- /dev/null
@@ -0,0 +1,43 @@
+!!!
+%html{:xmlns => "http://www.w3.org/1999/xhtml", "xml:lang" => "en-US", "lang" => "en-US"}
+  %head
+    %title Hampton Catlin Is Totally Awesome
+    %meta{"http-equiv" => "Content-Type", :content => "text/html; charset=utf-8"}
+  %body
+    / You're In my house now!
+    .header
+      Yes, ladies and gentileman. He is just that egotistical.
+      Fantastic! This should be multi-line output
+      The question is if this would translate! Ahah!
+      = 1 + 9 + 8 + 2 #numbers should work and this should be ignored
+    #body= " Quotes should be loved! Just like people!"
+    - 120.times do |number|
+      = number
+    Wow.|
+    %p{:code => 1 + 2}
+      = "Holy cow        " + |
+        "multiline       " + |      
+        "tags!           " + |
+        "A pipe (|) even!"   |
+      = [1, 2, 3].collect { |n| "PipesIgnored|" }.join
+      = [1, 2, 3].collect { |n|     |
+          n.to_s                    |
+        }.join("|")                 |
+    - bar = 17
+    %div.silent{:foo => bar}
+      - foo = String.new
+      - foo << "this"
+      - foo << " shouldn't"
+      - foo << " evaluate"
+      = foo + " but now it should!"
+      -# Woah crap a comment!
+
+    -# That was a line that shouldn't close everything.
+    %ul.really.cool
+      - ('a'..'f').each do |a|
+        %li= a
+    #combo.of_divs_with_underscore= @should_eval = "with this text"
+    = "foo".each_line do |line|
+      - nil
+    .footer
+      %strong.shout= "This is a really long ruby quote. It should be loved and wrapped because its more than 50 characters. This value may change in the future and this test may look stupid. \nSo, I'm just making it *really* long. God, I hope this works"
diff --git a/test/haml/templates/standard_ugly.haml b/test/haml/templates/standard_ugly.haml
new file mode 100644 (file)
index 0000000..c1d4866
--- /dev/null
@@ -0,0 +1,43 @@
+!!!
+%html{:xmlns => "http://www.w3.org/1999/xhtml", "xml:lang" => "en-US", "lang" => "en-US"}
+  %head
+    %title Hampton Catlin Is Totally Awesome
+    %meta{"http-equiv" => "Content-Type", :content => "text/html; charset=utf-8"}
+  %body
+    / You're In my house now!
+    .header
+      Yes, ladies and gentileman. He is just that egotistical.
+      Fantastic! This should be multi-line output
+      The question is if this would translate! Ahah!
+      = 1 + 9 + 8 + 2 #numbers should work and this should be ignored
+    #body= " Quotes should be loved! Just like people!"
+    - 120.times do |number|
+      = number
+    Wow.|
+    %p{:code => 1 + 2}
+      = "Holy cow        " + |
+        "multiline       " + |      
+        "tags!           " + |
+        "A pipe (|) even!"   |
+      = [1, 2, 3].collect { |n| "PipesIgnored|" }.join
+      = [1, 2, 3].collect { |n|     |
+          n.to_s                    |
+        }.join("|")                 |
+    - bar = 17
+    %div.silent{:foo => bar}
+      - foo = String.new
+      - foo << "this"
+      - foo << " shouldn't"
+      - foo << " evaluate"
+      = foo + " but now it should!"
+      -# Woah crap a comment!
+
+    -# That was a line that shouldn't close everything.
+    %ul.really.cool
+      - ('a'..'f').each do |a|
+        %li= a
+    #combo.of_divs_with_underscore= @should_eval = "with this text"
+    = "foo".each_line do |line|
+      - nil
+    .footer
+      %strong.shout= "This is a really long ruby quote. It should be loved and wrapped because its more than 50 characters. This value may change in the future and this test may look stupid. \nSo, I'm just making it *really* long. God, I hope this works"
diff --git a/test/haml/templates/tag_parsing.haml b/test/haml/templates/tag_parsing.haml
new file mode 100644 (file)
index 0000000..f142ebb
--- /dev/null
@@ -0,0 +1,21 @@
+%div.tags
+  %foo 1
+  %FOO 2
+  %fooBAR 3
+  %fooBar 4
+  %foo_bar 5
+  %foo-bar 6
+  %foo:bar 7
+  %foo.bar 8
+  %fooBAr_baz:boom_bar 9
+  %foo13 10
+  %foo2u 11
+%div.classes
+  %p.foo.bar#baz#boom
+  .fooBar a
+  .foo-bar b
+  .foo_bar c
+  .FOOBAR d
+  .foo16 e
+  .123 f
+  .foo2u g
diff --git a/test/haml/templates/very_basic.haml b/test/haml/templates/very_basic.haml
new file mode 100644 (file)
index 0000000..93396b9
--- /dev/null
@@ -0,0 +1,4 @@
+!!!
+%html
+  %head
+  %body
diff --git a/test/haml/templates/whitespace_handling.haml b/test/haml/templates/whitespace_handling.haml
new file mode 100644 (file)
index 0000000..f459e75
--- /dev/null
@@ -0,0 +1,87 @@
+#whitespace_test
+  = test_partial "text_area", :value => "Oneline"
+  = test_partial "text_area", :value => "Two\nlines"
+  ~ test_partial "text_area", :value => "Oneline"
+  ~ test_partial "text_area", :value => "Two\nlines"
+  #flattened~ test_partial "text_area", :value => "Two\nlines"
+.hithere
+  ~ "Foo bar"
+  ~ "<pre>foo bar</pre>"
+  ~ "<pre>foo\nbar</pre>"
+  %p~ "<pre>foo\nbar</pre>"
+  %p~ "foo\nbar"
+.foo
+  ~ 13
+  ~ "<textarea>\na\n</textarea>".each_line do |l|
+    - haml_concat l.strip
+#whitespace_test
+  = test_partial "text_area", :value => "Oneline"
+  = test_partial "text_area", :value => "Two\nlines"
+  = find_and_preserve test_partial("text_area", :value => "Oneline")
+  = find_and_preserve test_partial("text_area", :value => "Two\nlines")
+  #flattened= find_and_preserve test_partial("text_area", :value => "Two\nlines")
+.hithere
+  = find_and_preserve("Foo bar")
+  = find_and_preserve("<pre>foo bar</pre>")
+  = find_and_preserve("<pre>foo\nbar</pre>")
+  %p= find_and_preserve("<pre>foo\nbar</pre>")
+  %p= find_and_preserve("foo\nbar")
+  %pre
+    :preserve
+                                                       ___
+                                                    ,o88888
+                                                 ,o8888888'
+                           ,:o:o:oooo.        ,8O88Pd8888"
+                       ,.::.::o:ooooOoOoO. ,oO8O8Pd888'"
+                     ,.:.::o:ooOoOoOO8O8OOo.8OOPd8O8O"
+                    , ..:.::o:ooOoOOOO8OOOOo.FdO8O8"
+                   , ..:.::o:ooOoOO8O888O8O,COCOO"
+                  , . ..:.::o:ooOoOOOO8OOOOCOCO"
+                   . ..:.::o:ooOoOoOO8O8OCCCC"o
+                      . ..:.::o:ooooOoCoCCC"o:o
+                      . ..:.::o:o:,cooooCo"oo:o:
+                   `   . . ..:.:cocoooo"'o:o:::'
+                   .`   . ..::ccccoc"'o:o:o:::'
+                  :.:.    ,c:cccc"':.:.:.:.:.'
+                ..:.:"'`::::c:"'..:.:.:.:.:.'  http://www.chris.com/ASCII/
+              ...:.'.:.::::"'    . . . . .'
+             .. . ....:."' `   .  . . ''
+           . . . ...."'
+           .. . ."'     -hrr-
+          .
+
+
+                                                    It's a planet!
+      %strong This shouldn't be bold!
+  %strong This should!
+  %textarea
+    :preserve
+            ___           ___           ___           ___ 
+           /\__\         /\  \         /\__\         /\__\
+          /:/  /        /::\  \       /::|  |       /:/  /
+         /:/__/        /:/\:\  \     /:|:|  |      /:/  / 
+        /::\  \ ___   /::\~\:\  \   /:/|:|__|__   /:/  /  
+       /:/\:\  /\__\ /:/\:\ \:\__\ /:/ |::::\__\ /:/__/   
+       \/__\:\/:/  / \/__\:\/:/  / \/__/~~/:/  / \:\  \   
+            \::/  /       \::/  /        /:/  /   \:\  \  
+            /:/  /        /:/  /        /:/  /     \:\  \ 
+           /:/  /        /:/  /        /:/  /       \:\__\
+           \/__/         \/__/         \/__/         \/__/
+           
+           Many
+                         thanks
+                 to
+                                      http://www.network-science.de/ascii/
+    %strong indeed!
+.foo
+  = find_and_preserve(13)
+%pre
+  :preserve
+           __     ______        __               ______
+    .----.|  |--.|__    |.----.|  |--..--------.|  __  |
+    |  __||     ||__    ||  __||    < |        ||  __  |
+    |____||__|__||______||____||__|__||__|__|__||______|
+%pre
+  :preserve
+    foo
+  bar
diff --git a/test/haml/templates/with_bom.haml b/test/haml/templates/with_bom.haml
new file mode 100644 (file)
index 0000000..c1aa69f
--- /dev/null
@@ -0,0 +1 @@
+BOMG
\ No newline at end of file
diff --git a/test/hamlit/attribute_parser_test.rb b/test/hamlit/attribute_parser_test.rb
new file mode 100644 (file)
index 0000000..543edae
--- /dev/null
@@ -0,0 +1,97 @@
+describe Hamlit::AttributeParser do
+  describe '.parse' do
+    def assert_parse(expected, haml)
+      actual = Hamlit::AttributeParser.parse(haml)
+      assert_equal expected, actual
+    end
+
+    it { assert_parse({}, '') }
+    it { assert_parse({}, '{}') }
+
+    describe 'invalid hash' do
+      it { assert_parse(nil, ' hash ') }
+      it { assert_parse(nil, 'hash, foo: bar') }
+      it { assert_parse(nil, ' {hash} ') }
+      it { assert_parse(nil, ' { hash, foo: bar } ') }
+    end
+
+    describe 'dynamic key' do
+      it { assert_parse(nil, 'foo => bar') }
+      it { assert_parse(nil, '[] => bar') }
+      it { assert_parse(nil, '[1,2,3] => bar') }
+    end
+
+    describe 'foo: bar' do
+      it { assert_parse({ '_' => '1' }, '_:1,') }
+      it { assert_parse({ 'foo' => 'bar' }, ' foo:  bar ') }
+      it { assert_parse({ 'a' => 'b', 'c' => ':d' }, 'a: b, c: :d') }
+      it { assert_parse({ 'a' => '[]', 'c' => '"d"' }, 'a: [], c: "d"') }
+      it { assert_parse({ '_' => '1' }, ' { _:1, } ') }
+      it { assert_parse({ 'foo' => 'bar' }, ' {  foo:  bar } ') }
+      it { assert_parse({ 'a' => 'b', 'c' => ':d' }, ' { a: b, c: :d } ') }
+      it { assert_parse({ 'a' => '[]', 'c' => '"d"' }, ' { a: [], c: "d" } ') }
+    end
+
+    describe ':foo => bar' do
+      it { assert_parse({ 'foo' => ':bar' }, '  :foo   =>  :bar  ') }
+      it { assert_parse({ '_' => '"foo"' }, ':_=>"foo"') }
+      it { assert_parse({ 'a' => '[]', 'c' => '""', 'b' => '"#{3}"' }, ':a => [], c: "", :b => "#{3}"') }
+      it { assert_parse({ 'foo' => ':bar' }, ' {   :foo   =>  :bar } ') }
+      it { assert_parse({ '_' => '"foo"' }, ' { :_=>"foo" } ') }
+      it { assert_parse({ 'a' => '[]', 'c' => '""', 'b' => '"#{3}"' }, ' { :a => [], c: "", :b => "#{3}" } ') }
+      it { assert_parse(nil, ':"f#{o}o" => bar') }
+      it { assert_parse(nil, ':"#{f}oo" => bar') }
+      it { assert_parse(nil, ':"#{foo}" => bar') }
+    end
+
+    describe '"foo" => bar' do
+      it { assert_parse({ 'foo' => '[1]' }, '"foo"=>[1]') }
+      it { assert_parse({ 'foo' => 'nya' }, " 'foo' => nya ") }
+      it { assert_parse({ 'foo' => 'bar' }, '%q[foo] => bar ') }
+      it { assert_parse({ 'foo' => '[1]' }, ' { "foo"=>[1] } ') }
+      it { assert_parse({ 'foo' => 'nya' }, " {  'foo' => nya } ") }
+      it { assert_parse({ 'foo' => 'bar' }, ' { %q[foo] => bar } ') }
+      it { assert_parse(nil, '"f#{o}o" => bar') }
+      it { assert_parse(nil, '"#{f}oo" => bar') }
+      it { assert_parse(nil, '"#{foo}" => bar') }
+      it { assert_parse({ 'f#{o}o' => 'bar' }, '%q[f#{o}o] => bar ') }
+      it { assert_parse({ 'f#{o}o' => 'bar' }, ' { %q[f#{o}o] => bar,  } ') }
+      it { assert_parse(nil, '%Q[f#{o}o] => bar ') }
+    end
+
+    if RUBY_VERSION >= '2.2.0'
+      describe '"foo": bar' do
+        it { assert_parse({ 'foo' => '()' }, '"foo":()') }
+        it { assert_parse({ 'foo' => 'nya' }, " 'foo': nya ") }
+        it { assert_parse({ 'foo' => '()' }, ' { "foo":() , }') }
+        it { assert_parse({ 'foo' => 'nya' }, " {  'foo': nya , }") }
+        it { assert_parse(nil, '"f#{o}o": bar') }
+        it { assert_parse(nil, '"#{f}oo": bar') }
+        it { assert_parse(nil, '"#{foo}": bar') }
+      end
+    end
+
+    describe 'nested array' do
+      it { assert_parse({ 'foo' => '[1,2,]' }, 'foo: [1,2,],') }
+      it { assert_parse({ 'foo' => '[1,2,[3,4],5]' }, 'foo: [1,2,[3,4],5],') }
+      it { assert_parse({ 'foo' => '[1,2,[3,4],5]', 'bar' => '[[1,2],]'}, 'foo: [1,2,[3,4],5],bar: [[1,2],],') }
+      it { assert_parse({ 'foo' => '[1,2,]' }, ' { foo: [1,2,], } ') }
+      it { assert_parse({ 'foo' => '[1,2,[3,4],5]' }, ' { foo: [1,2,[3,4],5], } ') }
+      it { assert_parse({ 'foo' => '[1,2,[3,4],5]', 'bar' => '[[1,2],]'}, ' { foo: [1,2,[3,4],5],bar: [[1,2],], } ') }
+    end
+
+    describe 'nested hash' do
+      it { assert_parse({ 'foo' => '{ }', 'bar' => '{}' }, 'foo: { }, bar: {}') }
+      it { assert_parse({ 'foo' => '{ bar: baz, hoge: fuga, }' }, 'foo: { bar: baz, hoge: fuga, }, ') }
+      it { assert_parse({ 'data' => '{ confirm: true, disable: false }', 'hello' => '{ world: foo, }' }, 'data: { confirm: true, disable: false }, :hello => { world: foo, },') }
+      it { assert_parse({ 'foo' => '{ }', 'bar' => '{}' }, ' { foo: { }, bar: {} } ') }
+      it { assert_parse({ 'foo' => '{ bar: baz, hoge: fuga, }' }, ' { foo: { bar: baz, hoge: fuga, }, } ') }
+      it { assert_parse({ 'data' => '{ confirm: true, disable: false }', 'hello' => '{ world: foo, }' }, ' { data: { confirm: true, disable: false }, :hello => { world: foo, }, } ') }
+    end
+
+    describe 'nested method' do
+      it { assert_parse({ 'foo' => 'bar(a, b)', 'hoge' => 'piyo(a, b,)' }, 'foo: bar(a, b), hoge: piyo(a, b,),') }
+      it { assert_parse({ 'foo' => 'bar(a, b)', 'hoge' => 'piyo(a, b,)' }, ' { foo: bar(a, b), hoge: piyo(a, b,), } ') }
+    end
+  end
+end
diff --git a/test/hamlit/cli_test.rb b/test/hamlit/cli_test.rb
new file mode 100644 (file)
index 0000000..02bf398
--- /dev/null
@@ -0,0 +1,21 @@
+require 'hamlit/cli'
+
+describe Hamlit::CLI do
+  describe '#temple' do
+    def redirect_output
+      out, $stdout = $stdout, StringIO.new
+      yield
+    ensure
+      $stdout = out
+    end
+
+    it 'does not crash when compiling a tag' do
+      redirect_output do
+        f = Tempfile.open('hamlit')
+        f.write('%input{ hash }')
+        f.close
+        Hamlit::CLI.new.temple(f.path)
+      end
+    end
+  end
+end
diff --git a/test/hamlit/engine/attributes_test.rb b/test/hamlit/engine/attributes_test.rb
new file mode 100644 (file)
index 0000000..1ba0916
--- /dev/null
@@ -0,0 +1,345 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'id attributes' do
+    describe 'compatilibity' do
+      it { assert_haml(%q|#a|) }
+      it { assert_haml(%q|#a{ id: nil }|) }
+      it { assert_haml(%q|#a{ id: nil }(id=nil)|) }
+      it { assert_haml(%q|#a{ id: false }|) }
+      it { assert_haml(%q|#a{ id: 'b' }|) }
+      it { assert_haml(%q|#b{ id: 'a' }|) }
+      it { assert_haml(%q|%a{ 'id' => 60 }|) }
+
+      it { assert_haml(%q|#a{ id: 'b' }(id=id)|,       locals: { id: 'c' }) }
+      it { assert_haml(%q|#c{ id: a = 'a' }(id=id)|,   locals: { id: 'b' }) }
+      it { assert_haml(%q|#d#c{ id: a = 'b' }(id=id)|, locals: { id: 'a' }) }
+      it { assert_haml(%q|#d#c{ id: %w[b e] }(id=id)|, locals: { id: 'a' }) }
+
+      it { assert_haml(%q|%div{ hash }|,       locals: { hash: { id: 'a' } }) }
+      it { assert_haml(%q|#b{ hash }|,         locals: { hash: { id: 'a' } }) }
+      it { assert_haml(%q|#b{ hash }(id='c')|, locals: { hash: { id: 'a' }, id: 'c' }) }
+      it { assert_haml(%q|#b{ hash }(id=id)|,  locals: { hash: { id: 'a' }, id: 'c' }) }
+    end
+
+    describe 'incompatibility' do
+      it { assert_render(%Q|<div id='a'></div>\n|,   %q|#a{ id: [] }|) }
+      it { assert_render(%Q|<div id=''></div>\n|,    %q|%div{ id: [nil, false] }|) }
+      it { assert_render(%Q|<div id='c_a'></div>\n|, %q|#d#c{ id: [] }(id=id)|, locals: { id: 'a' }) }
+      it { assert_render(%Q|<div id=''></div>\n|,    %q|%div{ id: nil }|) }
+      it { assert_render(%Q|<input id=''>\n|,        %q|%input{ id: false }|) }
+      it { assert_render(%Q|<input id=''>\n|,        %q|%input{ id: val }|, locals: { val: false }) }
+      it { assert_render(%Q|<input id=''>\n|,        %q|%input{ hash }|, locals: { hash: { id: false } }) }
+    end
+  end
+
+  describe 'class attributes' do
+    describe 'compatibility' do
+      it { assert_haml(%q|.bar.foo|) }
+      it { assert_haml(%q|.foo.bar|) }
+      it { assert_haml(%q|%div(class='bar foo')|) }
+      it { assert_haml(%q|%div(class='foo bar')|) }
+      it { assert_haml(%q|%div{ class: 'bar foo' }|) }
+
+      it { assert_haml(%q|.b{ class: 'a' }|) }
+      it { assert_haml(%q|.a{ class: 'b a' }|) }
+      it { assert_haml(%q|.b.a{ class: 'b a' }|) }
+      it { assert_haml(%q|.b{ class: 'b a' }|) }
+
+      it { assert_haml(%q|.a{ class: klass }|,   locals: { klass: 'b a' }) }
+      it { assert_haml(%q|.b{ class: klass }|,   locals: { klass: 'b a' }) }
+      it { assert_haml(%q|.b.a{ class: klass }|, locals: { klass: 'b a' }) }
+
+      it { assert_haml(%q|.b{ class: 'c a' }|) }
+      it { assert_haml(%q|.b{ class: 'a c' }|) }
+      it { assert_haml(%q|.a{ class: [] }|) }
+      it { assert_haml(%q|.a{ class: %w[c b] }|) }
+      it { assert_haml(%q|.a.c(class='b')|) }
+      it { assert_haml(%q|%a{ 'class' => 60 }|) }
+
+      it { assert_haml(%q|%div{ class: 'b a' }(class=klass)|, locals: { klass: 'b a' }) }
+      it { assert_haml(%q|%div(class=klass){ class: 'b a' }|, locals: { klass: 'b a' }) }
+      it { assert_haml(%q|.a.d(class=klass){ class: 'c d' }|, locals: { klass: 'b a' }) }
+      it { assert_haml(%q|.a.d(class=klass)|,                 locals: { klass: 'b a' }) }
+
+      it { assert_haml(%q|.a{:class => klass}|,               locals: { klass: nil }) }
+      it { assert_haml(%q|.a{:class => nil}(class=klass)|,    locals: { klass: nil }) }
+      it { assert_haml(%q|.a{:class => nil}|) }
+      it { assert_haml(%q|.a{:class => false}|) }
+
+      it { assert_haml(%q|.b{ hash, class: 'a' }|,     locals: { hash: { class: nil } }) }
+      it { assert_haml(%q|.b{ hash, :class => 'a' }|,  locals: { hash: { class: nil } }) }
+      it { assert_haml(%q|.b{ hash, 'class' => 'a' }|, locals: { hash: { class: nil } }) }
+
+      it { assert_haml(%q|.a{ hash }|,                          locals: { hash: { class: 'd' } }) }
+      it { assert_haml(%q|.b{ hash, class: 'a' }(class='c')|,   locals: { hash: { class: 'd' } }) }
+      it { assert_haml(%q|.b{ hash, class: 'a' }(class=klass)|, locals: { hash: { class: 'd' }, klass: nil }) }
+
+      it { assert_haml(%q|%div{ class: 'b a' }|) }
+      it { assert_haml(%q|%div{ class: klass }|, locals: { klass: 'b a' }) }
+      it { assert_haml(%q|%div(class='b a')|) }
+      it { assert_haml(%q|%div(class=klass)|, locals: { klass: 'b a' }) }
+
+      it { assert_haml(%q|%div{ class: [false, 'a', nil] }|) }
+      it { assert_haml(%q|%div{ class: %q[b a] }|) }
+      it { assert_haml(%q|%div{ class: %q[b a b] }|) }
+
+      it { assert_haml(%q|%span.c2{class: ["c1", "c3", :c2]}|) }
+      it { assert_haml(%q|%span{class: [1, nil, false, true]}|) }
+      it do
+        assert_haml(<<-HAML.unindent)
+          - v = [1, nil, false, true]
+          %span{class: v}
+        HAML
+      end
+      it do
+        assert_haml(<<-HAML.unindent)
+          - h1 = {class: 'c1', id: ['id1', 'id3']}
+          - h2 = {class: [{}, 'c2'], id: 'id2'}
+          %span#main.content{h1, h2} hello
+        HAML
+      end
+    end
+
+    describe 'incompatibility' do
+      it { assert_render(%Q|<div class=''></div>\n|, %q|%div{ class: nil }|) }
+      it { assert_render(%Q|<div class=''></div>\n|, %q|%div{ class: false }|) }
+      it { assert_render(%Q|<div class=''></div>\n|, %q|%div{ class: false }|) }
+      it { assert_render(%Q|<div class=''></div>\n|, %q|%div{ class: val }|, locals: { val: false }) }
+      it { assert_render(%Q|<div class=''></div>\n|, %q|%div{ hash }|, locals: { hash: { class: false } }) }
+    end
+  end
+
+  describe 'data attributes' do
+    it { assert_haml(%q|#foo.bar{ data: { disabled: val } }|, locals: { val: false }) }
+    it { skip; assert_haml(%q|%div{:data => hash}|, locals: { hash: { :a => { :b => 'c' } }.tap { |h| h[:d] = h } }) }
+    it { skip; assert_haml(%q|%div{ hash }|, locals: { hash: { data: { :a => { :b => 'c' } }.tap { |h| h[:d] = h } } }) }
+    it { assert_haml(%q|%div{:data => {:foo_bar => 'blip', :baz => 'bang'}}|) }
+    it { assert_haml(%q|%div{ data: { raw_src: 'foo' } }|) }
+    it { assert_haml(%q|%a{ data: { value: [count: 1] } }|) }
+    it { assert_haml(%q|%a{ 'data-disabled' => true }|) }
+    it { assert_haml(%q|%a{ :'data-disabled' => true }|) }
+    it { assert_haml(%q|%a{ data: { nil => 3 } }|) }
+    it { assert_haml(%q|%a{ data: 3 }|) }
+    it { assert_haml(%q|%a(data=3)|) }
+    it { assert_haml(%q|%a{ 'data-bar' => 60 }|) }
+
+    it { assert_haml(%q|%a{ data: { overlay_modal: 'foo' } }|) }
+    it { assert_haml(%q|%a{ data: { overlay_modal: true } }|) }
+    it { assert_haml(%q|%a{ data: { overlay_modal: false } }|) }
+
+    it { assert_haml(%q|%a{ data: true }|) }
+    it { assert_haml(%q|%a{ data: { nil => true } }|) }
+    it { assert_haml(%q|%a{ data: { false => true } }|) }
+
+    it { skip; assert_haml(%q|%a{ { data: { 'foo-bar' => 1 } }, data: { foo: { bar: 2 } } }|) }
+    it { assert_haml(%q|%a{ { data: { foo: { bar: 2 } } }, data: { 'foo-bar' => 2 } }|) }
+    it { assert_haml(%q|%a{ { data: { :'foo-bar' => 1 } }, data: { 'foo-bar' => 2 } }|) }
+
+    it do
+      assert_haml(<<-HAML.unindent)
+        - old = { disabled: true,  checked: false, href: false, 'hello-world' => '<>/' }
+        - new = { disabled: false, checked: true,  href: '<>/', hello: {}, 'hello_hoge' => true, foo: { 'bar&baz' => 'hoge' } }
+        - hash = { data: { href: true, hash: true } }
+        %a(data=new){ hash, data: old }
+      HAML
+    end
+    it do
+      assert_haml(<<-HAML.unindent)
+        - h1 = { data: 'should be overwritten' }
+        - h2 = { data: nil }
+        %div{ h1, h2 }
+      HAML
+    end
+  end
+
+  describe 'boolean attributes' do
+    it { assert_haml(%q|%input{ disabled: nil }|) }
+    it { assert_haml(%q|%input{ disabled: false }|) }
+    it { assert_haml(%q|%input{ disabled: true }|) }
+    it { assert_haml(%q|%input{ disabled: 'false' }|) }
+
+    it { assert_haml(%q|%input{ disabled: val = nil }|) }
+    it { assert_haml(%q|%input{ disabled: val = false }|) }
+    it { assert_haml(%q|%input{ disabled: val = true }|) }
+    it { assert_haml(%q|%input{ disabled: val = 'false' }|) }
+
+    it { assert_haml(%q|%input{ disabled: nil }(disabled=true)|) }
+    it { assert_haml(%q|%input{ disabled: false }(disabled=true)|) }
+    it { assert_haml(%q|%input{ disabled: true }(disabled=false)|) }
+    it { assert_haml(%q|%a{ hash }|, locals: { hash: { disabled: false } }) }
+    it { assert_haml(%q|%a{ hash }|, locals: { hash: { disabled: nil } }) }
+
+    it { assert_haml(%q|input(disabled=true){ disabled: nil }|) }
+    it { assert_haml(%q|input(disabled=true){ disabled: false }|) }
+    it { assert_haml(%q|input(disabled=false){ disabled: true }|) }
+    it { assert_haml(%q|%input(disabled=val){ disabled: false }|, locals: { val: true }) }
+    it { assert_haml(%q|%input(disabled=val){ disabled: false }|, locals: { val: false }) }
+
+    it { assert_haml(%q|%input(disabled=nil)|) }
+    it { assert_haml(%q|%input(disabled=false)|) }
+    it { assert_haml(%q|%input(disabled=true)|) }
+    it { assert_haml(%q|%input(disabled='false')|) }
+    it { assert_haml(%q|%input(disabled=val)|, locals: { val: 'false' }) }
+
+    it { assert_haml(%q|%input(disabled='false'){ disabled: true }|) }
+    it { assert_haml(%q|%input(disabled='false'){ disabled: false }|) }
+    it { assert_haml(%q|%input(disabled='false'){ disabled: nil }|) }
+    it { assert_haml(%q|%input(disabled=''){ disabled: nil }|) }
+
+    it { assert_haml(%q|%input(checked=true)|) }
+    it { assert_haml(%q|%input(checked=true)|, format: :xhtml) }
+
+    it { assert_haml(%q|%input{ 'data-overlay_modal' => nil }|) }
+    it { assert_haml(%q|%input{ 'data-overlay_modal' => false }|) }
+    it { assert_haml(%q|%input{ 'data-overlay_modal' => true }|) }
+    it { assert_haml(%q|%input{ 'data-overlay_modal' => 'false' }|) }
+
+    it { assert_haml(%q|%input{ :'data-overlay_modal' => val = nil }|) }
+    it { assert_haml(%q|%input{ :'data-overlay_modal' => val = false }|) }
+    it { assert_haml(%q|%input{ :'data-overlay_modal' => val = true }|) }
+    it { assert_haml(%q|%input{ :'data-overlay_modal' => val = 'false' }|) }
+
+    it { assert_haml(%q|%a{ hash }|, locals: { hash: { 'data-overlay_modal' => false } }) }
+    it { assert_haml(%q|%a{ hash }|, locals: { hash: { 'data-overlay_modal' => true } }) }
+
+    it { assert_haml(%q|%a{ 'disabled' => 60 }|) }
+  end
+
+  describe 'common attributes' do
+    describe 'compatibility' do
+      it { assert_haml(%Q|%a{ href: '/search?foo=bar&hoge=<fuga>' }|) }
+      it { assert_haml(%Q|- h = {foo: 1, 'foo' => 2}\n%span{ h }|) }
+      it { assert_haml(%q|%span(foo='new'){ foo: 'old' }|, locals: { new: 'new', old: 'old' }) }
+      it { assert_haml(%q|%span(foo=new){ foo: 'old' }|,   locals: { new: 'new', old: 'old' }) }
+      it { assert_haml(%q|%span(foo=new){ foo: old }|,     locals: { new: 'new', old: 'old' }) }
+      it { assert_haml(%q|%span{ foo: 'old' }(foo='new')|, locals: { new: 'new', old: 'old' }) }
+      it { assert_haml(%q|%span{ foo: 'old' }(foo=new)|,   locals: { new: 'new', old: 'old' }) }
+      it { assert_haml(%q|%span{ foo: old }(foo=new)|,     locals: { new: 'new', old: 'old' }) }
+      it do
+        assert_haml(<<-HAML.unindent)
+          - h1 = { foo: 1 }
+          - h2 = { foo: 2 }
+          %div{ h1, h2 }
+        HAML
+      end
+      it do
+        assert_haml(<<-'HAML'.unindent)
+          - h = { "class\0with null" => 'is not class' }
+          %div{ h }
+        HAML
+      end
+      it { assert_haml(%q|%a{ 'href' => 60 }|) }
+    end
+
+    describe 'incompatibility' do
+      it { assert_render(%Q|<a href='&#39;&quot;'></a>\n|, %q|%a{ href: "'\"" }|) }
+      it { assert_render(%Q|<input value=''>\n|,      %q|%input{ value: nil }|) }
+      it { assert_render(%Q|<input value='false'>\n|, %q|%input{ value: false }|) }
+      it { assert_render(%Q|<input value='false'>\n|, %q|%input{ value: val }|, locals: { val: false }) }
+      it { assert_render(%Q|<input value='false'>\n|, %q|%input{ hash }|, locals: { hash: { value: false } }) }
+      it do
+        assert_render(%Q|<div foo=''></div>\n|, <<-HAML.unindent)
+          - h1 = { foo: 'should be overwritten' }
+          - h2 = { foo: nil }
+          %div{ h1, h2 }
+        HAML
+      end
+    end
+  end
+
+  describe 'object reference' do
+    ::TestObject = Struct.new(:id) unless defined?(::TestObject)
+
+    it { assert_render(%Q|<a class='test_object' id='test_object_10'></a>\n|, %q|%a[foo]|,      locals: { foo: TestObject.new(10) }) }
+    it { assert_render(%Q|<a class='test_object' id='test_object_10'></a>\n|, %q|%a[foo, nil]|, locals: { foo: TestObject.new(10) }) }
+    it { assert_render(%Q|<a class='test_object' id='test_object_new'></a>\n|, %q|%a[foo]|,     locals: { foo: TestObject.new(nil) }) }
+    it { assert_render(%Q|<a class='pre_test_object' id='pre_test_object_10'></a>\n|, %q|%a[foo, 'pre']|, locals: { foo: TestObject.new(10) }) }
+    it { assert_render(%Q|<div class='static test_object' id='static_test_object_10'></div>\n|, %q|.static#static[TestObject.new(10)]|) }
+    it { assert_render(%Q|<div class='static' id='static'></div>\n|, %q|.static#static[nil]|) }
+    it do
+      assert_render(
+        %Q|<a class='dynamic pre_test_object static' id='static_dynamic_pre_test_object_10'></a>\n|,
+        %q|%a.static#static[foo, 'pre']{ id: dynamic, class: dynamic }|,
+        locals: { foo: TestObject.new(10), dynamic: 'dynamic' },
+      )
+    end
+  end
+
+  describe 'engine options' do
+    describe 'attr_quote' do
+      it { assert_render(%Q|<a href='/'></a>\n|, %q|%a{ href: '/' }|) }
+      it { assert_render(%Q|<a href='/'></a>\n|, %q|%a{ href: '/' }|, attr_quote: ?') }
+      it { assert_render(%Q|<a href=*/*></a>\n|, %q|%a{ href: '/' }|, attr_quote: ?*) }
+
+      it { assert_render(%Q|<a id="/"></a>\n|, %q|%a{ id: '/' }|, attr_quote: ?") }
+      it { assert_render(%Q|<a id="/"></a>\n|, %q|%a{ id: val }|, attr_quote: ?", locals: { val: '/' }) }
+      it { assert_render(%Q|<a id="/"></a>\n|, %q|%a{ hash }|,    attr_quote: ?", locals: { hash: { id: '/' } }) }
+
+      it { assert_render(%Q|<a class="/"></a>\n|, %q|%a{ class: '/' }|, attr_quote: ?") }
+      it { assert_render(%Q|<a class="/"></a>\n|, %q|%a{ class: val }|, attr_quote: ?", locals: { val: '/' }) }
+      it { assert_render(%Q|<a class="/"></a>\n|, %q|%a{ hash }|,       attr_quote: ?", locals: { hash: { class: '/' } }) }
+
+      it { assert_render(%Q|<a data="/"></a>\n|,     %q|%a{ data: '/' }|,          attr_quote: ?") }
+      it { assert_render(%Q|<a data="/"></a>\n|,     %q|%a{ data: val }|,          attr_quote: ?", locals: { val: '/' }) }
+      it { assert_render(%Q|<a data-url="/"></a>\n|, %q|%a{ data: { url: '/' } }|, attr_quote: ?") }
+      it { assert_render(%Q|<a data-url="/"></a>\n|, %q|%a{ data: val }|,          attr_quote: ?", locals: { val: { url: '/' } }) }
+      it { assert_render(%Q|<a data-url="/"></a>\n|, %q|%a{ hash }|,               attr_quote: ?", locals: { hash: { data: { url: '/' } } }) }
+
+      it { assert_render(%Q|<a disabled="/"></a>\n|,        %q|%a{ disabled: '/' }|, attr_quote: ?") }
+      it { assert_render(%Q|<a disabled="/"></a>\n|,        %Q|%a{ disabled: val }|, attr_quote: ?", locals: { val: '/' }) }
+      it { assert_render(%Q|<a disabled="/"></a>\n|,        %Q|%a{ hash }|,          attr_quote: ?", locals: { hash: { disabled: '/' } }) }
+      it { assert_render(%Q|<a disabled="disabled"></a>\n|, %Q|%a{ hash }|,          attr_quote: ?", format: :xhtml, locals: { hash: { disabled: true } }) }
+
+      it { assert_render(%Q|<a href="/"></a>\n|, %q|%a{ href: '/' }|, attr_quote: ?") }
+      it { assert_render(%Q|<a href="/"></a>\n|, %q|%a{ href: val }|, attr_quote: ?", locals: { val: '/' }) }
+      it { assert_render(%Q|<a href="/"></a>\n|, %q|%a{ hash }|,      attr_quote: ?", locals: { hash: { href: '/' } }) }
+    end
+
+    describe 'escape_attrs' do
+      it { assert_render(%Q|<a id='&<>"/'></a>\n|,                %q|%a{ id: '&<>"/' }|, escape_attrs: false) }
+      it { assert_render(%Q|<a id='&<>"/'></a>\n|,                %Q|%a{ id: val }|,     escape_attrs: false, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a id='&<>"/'></a>\n|,                %Q|%a{ hash }|,        escape_attrs: false, locals: { hash: { id: '&<>"/' } }) }
+      it { assert_render(%Q|<a id='&amp;&lt;&gt;&quot;/'></a>\n|, %q|%a{ id: '&<>"/' }|, escape_attrs: true) }
+      it { assert_render(%Q|<a id='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ id: val }|,     escape_attrs: true, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a id='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ hash }|,        escape_attrs: true, locals: { hash: { id: '&<>"/' } }) }
+
+      it { assert_render(%Q|<a class='&<>"/'></a>\n|,                %q|%a{ class: '&<>"/' }|, escape_attrs: false) }
+      it { assert_render(%Q|<a class='&<>"/'></a>\n|,                %Q|%a{ class: val }|,     escape_attrs: false, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a class='&<>"/'></a>\n|,                %Q|%a{ hash }|,           escape_attrs: false, locals: { hash: { class: '&<>"/' } }) }
+      it { assert_render(%Q|<a class='&amp;&lt;&gt;&quot;/'></a>\n|, %q|%a{ class: '&<>"/' }|, escape_attrs: true) }
+      it { assert_render(%Q|<a class='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ class: val }|,     escape_attrs: true, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a class='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ hash }|,           escape_attrs: true, locals: { hash: { class: '&<>"/' } }) }
+
+      it { assert_render(%Q|<a data='&<>"/'></a>\n|,                %q|%a{ data: '&<>"/' }|, escape_attrs: false) }
+      it { assert_render(%Q|<a data='&<>"/'></a>\n|,                %Q|%a{ data: val }|,     escape_attrs: false, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a data='&<>"/'></a>\n|,                %Q|%a{ hash }|,          escape_attrs: false, locals: { hash: { data: '&<>"/' } }) }
+      it { assert_render(%Q|<a data='&amp;&lt;&gt;&quot;/'></a>\n|, %q|%a{ data: '&<>"/' }|, escape_attrs: true) }
+      it { assert_render(%Q|<a data='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ data: val }|,     escape_attrs: true, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a data='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ hash }|,          escape_attrs: true, locals: { hash: { data: '&<>"/' } }) }
+
+      it { assert_render(%Q|<a disabled='&<>"/'></a>\n|,                %q|%a{ disabled: '&<>"/' }|, escape_attrs: false) }
+      it { assert_render(%Q|<a disabled='&<>"/'></a>\n|,                %Q|%a{ disabled: val }|,     escape_attrs: false, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a disabled='&<>"/'></a>\n|,                %Q|%a{ hash }|,              escape_attrs: false, locals: { hash: { disabled: '&<>"/' } }) }
+      it { assert_render(%Q|<a disabled='&amp;&lt;&gt;&quot;/'></a>\n|, %q|%a{ disabled: '&<>"/' }|, escape_attrs: true) }
+      it { assert_render(%Q|<a disabled='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ disabled: val }|,     escape_attrs: true, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a disabled='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ hash }|,              escape_attrs: true, locals: { hash: { disabled: '&<>"/' } }) }
+
+      it { assert_render(%Q|<a href='&<>"/'></a>\n|,                %q|%a{ href: '&<>"/' }|, escape_attrs: false) }
+      it { assert_render(%Q|<a href='&<>"/'></a>\n|,                %Q|%a{ href: val }|,     escape_attrs: false, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a href='&<>"/'></a>\n|,                %Q|%a{ hash }|,          escape_attrs: false, locals: { hash: { href: '&<>"/' } }) }
+      it { assert_render(%Q|<a href='&amp;&lt;&gt;&quot;/'></a>\n|, %q|%a{ href: '&<>"/' }|, escape_attrs: true) }
+      it { assert_render(%Q|<a href='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ href: val }|,     escape_attrs: true, locals: { val: '&<>"/' }) }
+      it { assert_render(%Q|<a href='&amp;&lt;&gt;&quot;/'></a>\n|, %Q|%a{ hash }|,          escape_attrs: true, locals: { hash: { href: '&<>"/' } }) }
+    end
+
+    describe 'format' do
+      it { assert_render(%Q|<a disabled></a>\n|,            %q|%a{ disabled: true }|, format: :html) }
+      it { assert_render(%Q|<a disabled></a>\n|,            %q|%a{ disabled: val }|,  format: :html, locals: { val: true }) }
+      it { assert_render(%Q|<a disabled></a>\n|,            %q|%a{ hash }|,           format: :html, locals: { hash: { disabled: true } }) }
+      it { assert_render(%Q|<a disabled='disabled'></a>\n|, %q|%a{ disabled: true }|, format: :xhtml) }
+      it { assert_render(%Q|<a disabled='disabled'></a>\n|, %q|%a{ disabled: val }|,  format: :xhtml, locals: { val: true }) }
+      it { assert_render(%Q|<a disabled='disabled'></a>\n|, %q|%a{ hash }|,           format: :xhtml, locals: { hash: { disabled: true } }) }
+    end
+  end
+end
diff --git a/test/hamlit/engine/comment_test.rb b/test/hamlit/engine/comment_test.rb
new file mode 100644 (file)
index 0000000..a1e0aa7
--- /dev/null
@@ -0,0 +1,75 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'comment' do
+    it 'renders html comment' do
+      assert_render(%Q|<!-- comments -->\n|, '/ comments')
+    end
+
+    it 'strips html comment ignoring around spcaes' do
+      assert_render(%Q|<!-- comments -->\n|, '/   comments    ')
+    end
+
+    it 'accepts backslash-only line in a comment' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <!--
+
+        -->
+      HTML
+        /
+          \
+      HAML
+    end
+
+    it 'renders a deeply indented comment starting with backslash' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <!--
+               a
+        -->
+        <!--
+        a
+        -->
+      HTML
+        /
+          \       a
+        /
+          a
+      HAML
+    end
+
+    it 'ignores multiline comment' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        ok
+      HTML
+        -# if true
+          - raise 'ng'
+            = invalid script
+                too deep indent
+        ok
+      HAML
+    end
+
+    it 'renders conditional comment' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <!--[[if IE]]>
+        <span>hello</span>
+        world
+        <![endif]-->
+      HTML
+        /[[if IE]]
+          %span hello
+          world
+      HAML
+    end
+    it 'renders conditional comment' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <!--[if lt IE 9]>
+        hello
+        <![endif]-->
+      HTML
+        /[if lt IE 9]
+          hello
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/engine/doctype_test.rb b/test/hamlit/engine/doctype_test.rb
new file mode 100644 (file)
index 0000000..2b79a6e
--- /dev/null
@@ -0,0 +1,21 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'doctype' do
+    it 'renders html5 doctype' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <!DOCTYPE html>
+      HTML
+        !!!
+      HAML
+    end
+
+    it 'renders xml doctype' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent, format: :xhtml)
+        <?xml version='1.0' encoding='utf-8' ?>
+      HTML
+        !!! XML
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/engine/indent_test.rb b/test/hamlit/engine/indent_test.rb
new file mode 100644 (file)
index 0000000..d91b2e3
--- /dev/null
@@ -0,0 +1,44 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'tab indent' do
+    it 'accepts tab indentation' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <p>
+        <a></a>
+        </p>
+      HTML
+        %p
+        \t%a
+      HAML
+    end
+
+    it 'accepts N-space indentation' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <p>
+        <span>
+        foo
+        </span>
+        </p>
+      HTML
+        %p
+           %span
+              foo
+      HAML
+    end
+
+    it 'accepts N-tab indentation' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <p>
+        <span>
+        foo
+        </span>
+        </p>
+      HTML
+        %p
+        \t%span
+        \t\tfoo
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/engine/multiline_test.rb b/test/hamlit/engine/multiline_test.rb
new file mode 100644 (file)
index 0000000..40510ee
--- /dev/null
@@ -0,0 +1,46 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'multiline' do
+    it 'joins multi-lines ending with pipe' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        a b 
+      HTML
+        a |
+          b |
+      HAML
+    end
+
+    it 'renders multi lines' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        abc
+        'd'
+      HTML
+        = 'a' +  |
+             'b' + |
+             'c' |
+        'd'
+      HAML
+    end
+
+    it 'accepts invalid indent' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        <div>
+        12
+        </div>
+        <div>
+        3
+        </div>
+        </span>
+      HTML
+        %span
+          %div
+            = '1' + |
+        '2' |
+          %div
+            3
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/engine/new_attribute_test.rb b/test/hamlit/engine/new_attribute_test.rb
new file mode 100644 (file)
index 0000000..f2b6621
--- /dev/null
@@ -0,0 +1,101 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'new attributes' do
+    it 'renders attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <p class='foo'>bar</p>
+      HTML
+        %p(class='foo') bar
+      HAML
+    end
+
+    it 'renders multiple attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <p a='1' b='2'>bar</p>
+      HTML
+        %p(a=1 b=2) bar
+      HAML
+    end
+
+    it 'renders hyphenated attributes properly' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <p data-foo='bar'>bar</p>
+      HTML
+        %p(data-foo='bar') bar
+      HAML
+    end
+
+    it 'renders multiply hyphenated attributes properly' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <p data-x-foo='bar'>bar</p>
+      HTML
+        %p(data-x-foo='bar') bar
+      HAML
+    end
+
+    describe 'html escape' do
+      it 'escapes attribute values on static attributes' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <a title='&#39;'></a>
+          <a title='&#39;&quot;'></a>
+          <a href='/search?foo=bar&amp;hoge=&lt;fuga&gt;'></a>
+        HTML
+          %a(title="'")
+          %a(title = "'\"")
+          %a(href='/search?foo=bar&hoge=<fuga>')
+        HAML
+      end
+
+      it 'escapes attribute values on dynamic attributes' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <a title='&#39;&quot;'></a>
+          <a href='/search?foo=bar&amp;hoge=&lt;fuga&gt;'></a>
+        HTML
+          - title = "'\""
+          - href  = '/search?foo=bar&hoge=<fuga>'
+          %a(title=title)
+          %a(href=href)
+        HAML
+      end
+    end
+
+    describe 'element class with attribute class' do
+      it 'does not generate double classes' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div class='first item'></div>
+        HTML
+          .item(class='first')
+        HAML
+      end
+
+      it 'does not generate double classes for a variable' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div class='element val'></div>
+        HTML
+          - val = 'val'
+          .element(class=val)
+        HAML
+      end
+    end
+
+    describe 'element id with attribute id' do
+      it 'concatenates ids with underscore' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div id='item_first'></div>
+        HTML
+          #item(id='first')
+        HAML
+      end
+
+      it 'concatenates ids with underscore for a variable' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div id='item_first'></div>
+        HTML
+          - val = 'first'
+          #item(id=val)
+        HAML
+      end
+    end
+  end
+end
diff --git a/test/hamlit/engine/old_attribute_test.rb b/test/hamlit/engine/old_attribute_test.rb
new file mode 100644 (file)
index 0000000..296b707
--- /dev/null
@@ -0,0 +1,416 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'old attributes' do
+    it 'renders attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span class='foo'>bar</span>
+      HTML
+        %span{class: 'foo'} bar
+      HAML
+    end
+
+    it 'renders attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span data='2'>bar</span>
+      HTML
+        %span{ data: 2 } bar
+      HAML
+    end
+
+    it 'renders attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span class='foo'>bar</span>
+      HTML
+        %span{ :class => 'foo' } bar
+      HAML
+    end
+
+    it 'renders attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span class='foo' id='bar'>bar</span>
+      HTML
+        %span{ :class => 'foo', id: 'bar' } bar
+      HAML
+    end
+
+    it 'renders attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span data-disabled>bar</span>
+      HTML
+        %span{ :'data-disabled' => true } bar
+      HAML
+    end
+
+    it 'accepts method call including comma' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <body class='bb' data-confirm='really?' data-disabled id='a'></body>
+      HTML
+        %body{ class: "#{"ab".gsub(/a/, 'b')}", data: { confirm: 'really?', disabled: true }, id: 'c'.gsub(/c/, 'a') }
+      HAML
+    end
+
+    it 'accepts tag content' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span class='foo'><b>bar</b></span>
+      HTML
+        %span{ class: 'foo' } <b>bar</b>
+      HAML
+    end
+
+    it 'renders multi-byte chars as static attribute value' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <img alt='こんにちは'>
+      HTML
+        %img{ alt: 'こんにちは' }
+      HAML
+    end
+
+    it 'sorts static attributes by name' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span foo='bar' hoge='piyo'></span>
+        <span foo='bar' hoge='piyo'></span>
+      HTML
+        %span{ :foo => "bar", :hoge => "piyo"}
+        %span{ :hoge => "piyo", :foo => "bar"}
+      HAML
+    end
+
+    describe 'runtime attributes' do
+      it 'renders runtime hash attribute' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <span foo='bar'></span>
+        HTML
+          - hash = { foo: 'bar' }
+          %span{ hash }
+        HAML
+      end
+
+      it 'renders multiples hashes' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <span a='b' c='d' e='f'></span>
+        HTML
+          - h1 = { a: 'b' }
+          - h2 = { c: 'd' }
+          - h3 = { e: 'f' }
+          %span{ h1, h2, h3 }
+        HAML
+      end
+
+      it 'renders multiples hashes and literal hash' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <span a='b' c='d' e='f' g='h' i='j'></span>
+        HTML
+          - h1 = { a: 'b' }
+          - h2 = { c: 'd' }
+          - h3 = { e: 'f' }
+          %span{ h1, h2, h3, g: 'h', i: 'j' }
+        HAML
+      end
+    end
+
+    describe 'joinable attributes' do
+      it 'joins class with a space' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <p class='a b c'></p>
+          <p class='a b c'></p>
+          <p class='a b c'></p>
+        HTML
+          - val = ['a', 'b', 'c']
+          %p{ class: val }
+          %p{ class: %w[a b c] }
+          %p{ class: ['a', 'b', 'c'] }
+        HAML
+      end
+
+      it 'joins attribute class and element class' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div class='bar foo'></div>
+          <div class='bar foo'></div>
+          <div class='bar foo'></div>
+          <div class='bar baz foo'></div>
+        HTML
+          .foo{ class: ['bar'] }
+          .foo{ class: ['bar', 'foo'] }
+          .foo{ class: ['bar', nil] }
+          .foo{ class: ['bar', 'baz'] }
+        HAML
+      end
+
+      it 'joins id with an underscore' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <p id='a_b_c'></p>
+          <p id='a_b_c'></p>
+          <p id='a_b_c'></p>
+        HTML
+          - val = ['a', 'b', 'c']
+          %p{ id: val }
+          %p{ id: %w[a b c] }
+          %p{ id: ['a', 'b', 'c'] }
+        HAML
+      end
+
+      it 'does not join others' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <a data-value='[{:count=&gt;1}]'></a>
+        HTML
+          %a{ data: { value: [count: 1] } }
+        HAML
+      end
+    end
+
+    describe 'deletable attributes' do
+      it 'deletes attributes whose value is nil or false' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <input>
+          <input>
+          <input>
+          <input>
+          <input>
+        HTML
+          - hash = { checked: false }
+          %input{ hash }
+          %input{ checked: false }
+          %input{ checked: nil }
+          - checked = nil
+          %input{ checked: checked }
+          - checked = false
+          %input{ checked: checked }
+        HAML
+      end
+
+      it 'deletes some limited attributes with dynamic value' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div class='bar' id='foo'></div>
+          <div class='bar' id='foo'></div>
+          <div class='bar' id='foo'></div>
+          <div class='bar' id='foo'></div>
+          <div class='bar' id='foo'></div>
+          <div class='bar' id='foo'></div>
+          <div class='bar' id='foo'></div>
+          <div class='bar' id='foo'></div>
+        HTML
+          - val = false
+          #foo.bar{ autofocus: val }
+          #foo.bar{ checked: val }
+          #foo.bar{ data: { disabled: val } }
+          #foo.bar{ disabled: val }
+          #foo.bar{ formnovalidate: val }
+          #foo.bar{ multiple: val }
+          #foo.bar{ readonly: val }
+          #foo.bar{ required: val }
+        HAML
+      end
+
+      it 'does not delete non-boolean attributes, for optimization' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <a href='false'></a>
+          <a href='false'></a>
+          <a href='false'></a>
+          <a></a>
+          <a></a>
+          <a></a>
+          <a href=''></a>
+          <a href=''></a>
+          <a href=''></a>
+          <a></a>
+          <a></a>
+          <a></a>
+        HTML
+          %a{ href: false }
+          - val = false
+          %a{ href: val }
+          - hash = { href: false }
+          %a{ hash }
+
+          %a{ disabled: false }
+          - val = false
+          %a{ disabled: val }
+          - hash = { disabled: false }
+          %a{ hash }
+
+          %a{ href: nil }
+          - val = nil
+          %a{ href: val }
+          - hash = { href: nil }
+          %a{ hash }
+
+          %a{ disabled: nil }
+          - val = nil
+          %a{ disabled: val }
+          - hash = { disabled: nil }
+          %a{ hash }
+        HAML
+      end
+    end
+
+    describe 'html escape' do
+      it 'escapes attribute values on static attributes' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <a title='&#39;'></a>
+          <a title='&#39;&quot;'></a>
+          <a href='/search?foo=bar&amp;hoge=&lt;fuga&gt;'></a>
+        HTML
+          %a{title: "'"}
+          %a{title: "'\""}
+          %a{href: '/search?foo=bar&hoge=<fuga>'}
+        HAML
+      end
+
+      it 'escapes attribute values on dynamic attributes' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <a title='&#39;&quot;'></a>
+          <a href='/search?foo=bar&amp;hoge=&lt;fuga&gt;'></a>
+        HTML
+          - title = "'\""
+          - href  = '/search?foo=bar&hoge=<fuga>'
+          %a{title: title}
+          %a{href: href}
+        HAML
+      end
+
+      it 'escapes attribute values on hash attributes' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <a title='&#39;&quot;'></a>
+          <a href='/search?foo=bar&amp;hoge=&lt;fuga&gt;'></a>
+        HTML
+          - title = { title: "'\"" }
+          - href  = { href:  '/search?foo=bar&hoge=<fuga>' }
+          %a{ title }
+          %a{ href }
+        HAML
+      end
+    end
+
+    describe 'nested attributes' do
+      it 'renders data attribute by hash' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <span class='foo' data-bar='baz'></span>
+        HTML
+          - hash = { bar: 'baz' }
+          %span.foo{ data: hash }
+        HAML
+      end
+
+      it 'renders true attributes' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <span data-disabled>bar</span>
+        HTML
+          %span{ data: { disabled: true } } bar
+        HAML
+      end
+
+      it 'renders nested hash whose value is variable' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <span data-disabled>bar</span>
+        HTML
+          - hash = { disabled: true }
+          %span{ data: hash } bar
+        HAML
+      end
+
+      it 'changes an underscore in a nested key to a hyphen' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div data-raw-src='foo'></div>
+        HTML
+          %div{ data: { raw_src: 'foo' } }
+        HAML
+      end
+
+      it 'changes an underscore in a nested dynamic attribute' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div data-raw-src='foo'></div>
+        HTML
+          - hash = { raw_src: 'foo' }
+          %div{ data: hash }
+        HAML
+      end
+    end
+
+    describe 'element class with attribute class' do
+      it 'does not generate double classes' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div class='first item'></div>
+        HTML
+          .item{ class: 'first' }
+        HAML
+      end
+
+      it 'does not generate double classes for a variable' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div class='element val'></div>
+        HTML
+          - val = 'val'
+          .element{ class: val }
+        HAML
+      end
+
+      it 'does not generate double classes for hash attributes' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div class='element val'></div>
+        HTML
+          - hash = { class: 'val' }
+          .element{ hash }
+        HAML
+      end
+    end
+
+    describe 'element id with attribute id' do
+      it 'does not generate double ids' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div id='item_first'></div>
+        HTML
+          #item{ id: 'first' }
+        HAML
+      end
+
+      it 'does not generate double ids for a variable' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div id='item_first'></div>
+        HTML
+          - val = 'first'
+          #item{ id: val }
+        HAML
+      end
+
+      it 'does not generate double ids for hash attributes' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div id='item_first'></div>
+        HTML
+          - hash = { id: 'first' }
+          #item{ hash }
+        HAML
+      end
+
+      it 'does not generate double ids and classes for hash attributes' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <div class='bar foo' id='item_first'></div>
+        HTML
+          - hash = { id: 'first', class: 'foo' }
+          #item.bar{ hash }
+        HAML
+      end
+    end
+
+    if RUBY_VERSION >= "2.2.0"
+      describe 'Ruby 2.2 syntax' do
+        it 'renders static attributes' do
+          assert_render(<<-HTML.unindent, <<-HAML.unindent)
+            <meta content='IE=edge' http-equiv='X-UA-Compatible'>
+          HTML
+            %meta{ content: 'IE=edge', 'http-equiv': 'X-UA-Compatible' }
+          HAML
+        end
+
+        it 'renders dynamic attributes' do
+          assert_render(<<-HTML.unindent, <<-HAML.unindent)
+            <meta content='IE=edge' http-equiv='X-UA-Compatible'>
+          HTML
+            - hash = { content: 'IE=edge' }
+            %meta{ hash, 'http-equiv': 'X-UA-Compatible' }
+          HAML
+        end
+      end
+    end
+  end
+end
diff --git a/test/hamlit/engine/script_test.rb b/test/hamlit/engine/script_test.rb
new file mode 100644 (file)
index 0000000..ee7559e
--- /dev/null
@@ -0,0 +1,146 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'script' do
+    it 'renders one-line script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        3
+        <span>12</span>
+      HTML
+        = 1 + 2
+        %span= 3 * 4
+      HAML
+    end
+
+    it 'renders dynamic interpolated string' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        hello nya world
+      HTML
+        - nya = 'nya'
+        = "hello #{nya} world"
+      HAML
+    end
+
+    it 'renders array with escape_html: false' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent, escape_html: false)
+        ["<", ">"]
+      HTML
+        = ['<', '>']
+      HAML
+    end
+
+    it 'renders one-line script with comment' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+
+        ##
+        [&quot;#&quot;, &quot;#&quot;]
+      HTML
+        = # comment_only
+        = '#' + "#" # = 3 #
+        = ['#',
+          "#"]  # comment
+      HAML
+    end
+
+    it 'renders multi-lines script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        3
+        4 / 2
+        <a>-1</a>
+        </span>
+      HTML
+        %span
+          = 1 + 2
+          4 / 2
+          %a= 3 - 4
+      HAML
+    end
+
+    it 'renders block script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        0
+        1
+        2
+        34
+      HTML
+        = 3.times do |i|
+          = i
+        4
+      HAML
+    end
+
+    it 'renders tag internal block script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        0
+        1</span>
+      HTML
+        %span
+          = 1.times do |i|
+            = i
+      HAML
+    end
+
+    it 'renders block and a variable with spaces' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        0
+      HTML
+        - 1.times do | i |
+          = i
+      HAML
+    end
+
+    it 'accepts a continuing script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        3
+      HTML
+        - def foo(a, b); a + b; end
+        = foo(1,
+        2)
+      HAML
+    end
+
+    it 'renders !=' do
+      assert_render(<<-HTML.unindent.strip, <<-HAML.unindent, escape_html: false)
+        <"&>
+        <"&>
+      HTML
+        != '<"&>'
+        != '<"&>'.tap do |str|
+          -# no operation
+      HAML
+    end
+
+    it 'renders &=' do
+      assert_render(<<-HTML.unindent.strip, <<-HAML.unindent, escape_html: false)
+        &lt;&quot;&amp;&gt;
+        &lt;&quot;&amp;&gt;
+      HTML
+        &= '<"&>'
+        &= '<"&>'.tap do |str|
+          -# no operation
+      HAML
+    end
+
+    it 'regards ~ operator as =' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        &lt;code&gt;hello
+        world&lt;/code&gt;
+      HTML
+        ~ "<code>hello\nworld</code>"
+      HAML
+    end
+
+    it 'renders comment-only nested script' do
+      assert_render('1', <<-HAML.unindent)
+        = 1.times do # comment
+          - # comment only
+      HAML
+    end
+
+    it 'renders inline script with comment' do
+      assert_render(%Q|<span>3</span>\n|, %q|%span= 1 + 2 # comments|)
+    end
+  end
+end
diff --git a/test/hamlit/engine/silent_script_test.rb b/test/hamlit/engine/silent_script_test.rb
new file mode 100644 (file)
index 0000000..6e9e6de
--- /dev/null
@@ -0,0 +1,222 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'silent script' do
+    it 'renders nothing' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+      HTML
+        - nil
+        - 3
+        - 'foo'
+      HAML
+    end
+
+    it 'renders silent script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        5
+      HTML
+        - foo = 3
+        - bar = 2
+        = foo + bar
+      HAML
+    end
+
+    it 'renders nested block' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        0
+        1
+        2
+        3
+        4
+      HTML
+        - 2.times do |i|
+          = i
+        2
+        - 3.upto(4).each do |i|
+          = i
+      HAML
+    end
+
+    it 'renders if' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        ok
+      HTML
+        - if true
+          ok
+      HAML
+    end
+
+    it 'renders if-else' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        ok
+        ok
+      HTML
+        - if true
+          ok
+        - else
+          ng
+
+        - if false
+          ng
+
+        - else
+          ok
+      HAML
+    end
+
+    it 'renders nested if-else' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        ok
+        </span>
+      HTML
+        %span
+          - if false
+            ng
+          - else
+            ok
+      HAML
+    end
+
+    it 'renders empty elsif statement' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        </span>
+      HTML
+        %span
+          - if false
+          - elsif false
+      HAML
+    end
+
+    it 'renders empty else statement' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        </span>
+      HTML
+        %span
+          - if false
+            ng
+          - else
+      HAML
+    end
+
+    it 'renders empty when statement' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        </span>
+      HTML
+        %span
+          - case
+          - when false
+      HAML
+    end
+
+    it 'accept if inside if-else' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        ok
+      HTML
+        - if false
+          - if true
+            ng
+        - else
+          ok
+      HAML
+    end
+
+    it 'renders if-elsif' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        ok
+        ok
+      HTML
+        - if false
+        - elsif true
+          ok
+
+        - if false
+        - elsif false
+        - else
+          ok
+      HAML
+    end
+
+    it 'renders case-when' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        ok
+      HTML
+        - case 'foo'
+        - when /\Ao/
+          ng
+        - when /\Af/
+          ok
+        - else
+          ng
+      HAML
+    end
+
+    it 'renders case-when with multiple candidates' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        ok
+      HTML
+        - case 'a'
+        - when 'a', 'b'
+          ok
+      HAML
+    end
+
+    it 'renders begin-rescue' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        hello
+        world
+      HTML
+        - begin
+          - raise 'error'
+        - rescue
+          hello
+        - ensure
+          world
+      HAML
+    end
+
+    it 'renders rescue with error' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        hello
+      HTML
+        - begin
+          - raise 'error'
+        - rescue RuntimeError => e
+          hello
+      HAML
+    end
+
+    it 'joins a next line if a current line ends with ","' do
+      assert_render(<<-HTML.unindent, "- foo = [',  \n     ']\n= foo")
+        [&quot;, &quot;]
+      HTML
+    end
+
+    it 'accepts illegal indent in continuing code' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        <div>
+        3
+        </div>
+        </span>
+      HTML
+        %span
+          %div
+            - def foo(a, b); a + b; end
+            - num = foo(1,
+        2)
+            = num
+      HAML
+    end
+
+    it 'renders comment-only nested silent script' do
+      assert_render('', <<-HAML.unindent)
+        - if true
+          - # comment only
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/engine/tag_test.rb b/test/hamlit/engine/tag_test.rb
new file mode 100644 (file)
index 0000000..821447c
--- /dev/null
@@ -0,0 +1,200 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'tag' do
+    it 'renders one-line tag' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>hello</span>
+      HTML
+        %span hello
+      HAML
+    end
+
+    it 'accepts multi-line =' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>o</span>
+      HTML
+        %span= 'hello'.gsub('hell',
+          '')
+      HAML
+    end
+
+    it 'renders multi-line tag' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        hello
+        </span>
+      HTML
+        %span
+          hello
+      HAML
+    end
+
+    it 'renders a nested tag' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        <b>
+        hello
+        </b>
+        <i>
+        <small>world</small>
+        </i>
+        </span>
+      HTML
+        %span
+          %b
+            hello
+          %i
+            %small world
+      HAML
+    end
+
+    it 'renders multi-line texts' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        <b>
+        hello
+        world
+        </b>
+        </span>
+      HTML
+        %span
+          %b
+            hello
+            world
+      HAML
+    end
+
+    it 'ignores empty lines' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>
+        <b>
+        hello
+        </b>
+        </span>
+      HTML
+        %span
+
+          %b
+
+            hello
+
+      HAML
+    end
+
+    it 'renders classes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span class='foo-1 bar_A'>hello</span>
+      HTML
+        %span.foo-1.bar_A hello
+      HAML
+    end
+
+    it 'renders ids only last one' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span id='bar-'>
+        hello
+        </span>
+      HTML
+        %span#Bar_0#bar-
+          hello
+      HAML
+    end
+
+    it 'renders ids and classes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span class='b d' id='c'>hello</span>
+      HTML
+        %span#a.b#c.d hello
+      HAML
+    end
+
+    it 'renders implicit div tag starting with id' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <div class='world' id='hello'></div>
+      HTML
+        #hello.world
+      HAML
+    end
+
+    it 'renders implicit div tag starting with class' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <div class='world' id='hello'>
+        foo
+        </div>
+      HTML
+        .world#hello
+          foo
+      HAML
+    end
+
+    it 'renders large-case tag' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <SPAN>
+        foo
+        </SPAN>
+      HTML
+        %SPAN
+          foo
+      HAML
+    end
+
+    it 'renders h1 tag' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <h1>foo</h1>
+      HTML
+        %h1 foo
+      HAML
+    end
+
+    it 'renders tag including hyphen or underscore' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <-_>foo</-_>
+      HTML
+        %-_ foo
+      HAML
+    end
+
+    it 'does not render silent script just after a tag' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <span->raise 'a'</span->
+      HTML
+        %span- raise 'a'
+      HAML
+    end
+
+    it 'renders a text just after attributes' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <span a='2'>a</span>
+      HTML
+        %span{a: 2}a
+      HAML
+    end
+
+    it 'strips a text' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>foo</span>
+      HTML
+        %span    foo
+      HAML
+    end
+
+    it 'ignores spaces after tag' do
+      assert_render(<<-HTML.unindent, "%span  \n  a")
+        <span>
+        a
+        </span>
+      HTML
+    end
+
+    it 'parses self-closing tag' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent, format: :xhtml)
+        <div />
+        <div></div>
+      HTML
+        %div/
+        %div
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/engine/text_test.rb b/test/hamlit/engine/text_test.rb
new file mode 100644 (file)
index 0000000..0c2e023
--- /dev/null
@@ -0,0 +1,207 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'text' do
+    it 'renders string interpolation' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        a3aa" [&quot;1&quot;, 2] b " !
+        a{:a=&gt;3}
+        <ht2ml>
+      HTML
+        #{ "a#{3}a" }a" #{["1", 2]} b " !
+        a#{{ a: 3 }}
+        <ht#{2}ml>
+      HAML
+    end
+
+    it 'escapes all operators by backslash' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        a
+        = 'a'
+        -
+      HTML
+        = 'a'
+        -
+        \= 'a'
+        \-
+      HAML
+    end
+
+    it 'renders == operator' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        =
+        =
+        <a>
+        &lt;a&gt;
+      HTML
+        ===
+        == =
+        == <a>
+        == #{'<a>'}
+      HAML
+    end
+
+    it 'renders !== operator' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        &lt;a&gt;
+        <a>
+        =
+        =
+      HTML
+        == #{'<a>'}
+        !== #{'<a>'}
+        !===
+        !== =
+      HAML
+    end
+
+    it 'leaves empty spaces after backslash' do
+      assert_render("       a\n", '\       a')
+    end
+
+    it 'renders spaced - properly' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <div>
+        foo
+        <div class='test'>- bar</div>
+        <div class='test'>- baz</div>
+        </div>
+      HTML
+        %div
+          foo
+          .test - bar
+          .test - baz
+      HAML
+    end
+
+    describe 'inline operator' do
+      it 'renders ! operator' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <span><nyaa></span>
+          <span><nyaa></span>
+          <nyaa>
+        HTML
+          %span!#{'<nyaa>'}
+          %span! #{'<nyaa>'}
+          ! #{'<nyaa>'}
+        HAML
+      end
+
+      it 'renders & operator' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <span>&lt;nyaa&gt;</span>
+          <span>&lt;nyaa&gt;</span>
+          &lt;nyaa&gt;
+        HTML
+          %span& #{'<nyaa>'}
+          %span&#{'<nyaa>'}
+          & #{'<nyaa>'}
+        HAML
+      end
+
+      it 'renders !, & operator right before a non-space character' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          &nbsp;
+          &nbsp;
+          !hello
+          !hello
+        HTML
+          &nbsp;
+          \&nbsp;
+          !hello
+          \!hello
+        HAML
+      end
+
+      it 'renders &, ! operator inside a tag' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <span>&nbsp;</span>
+          <span>nbsp;</span>
+          <span>nbsp;</span>
+          <span>!hello</span>
+          <span>hello</span>
+          <span>hello</span>
+        HTML
+          %span &nbsp;
+          %span&nbsp;
+          %span& nbsp;
+          %span !hello
+          %span!hello
+          %span! hello
+        HAML
+      end
+
+      it 'does not accept backslash operator' do
+        assert_render(<<-'HTML'.unindent, <<-'HAML'.unindent)
+          <span>\    foo</span>
+        HTML
+          %span\    foo
+        HAML
+      end
+
+      it 'renders != operator' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <span><nyaa></span>
+        HTML
+          %span!= '<nyaa>'
+        HAML
+      end
+
+      it 'renders !== operator' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <span><nyaa></span>
+          <span><nyaa></span>
+          <nyaa>
+          <nyaa>
+        HTML
+          %span!==#{'<nyaa>'}
+          %span!== #{'<nyaa>'}
+          !==#{'<nyaa>'}
+          !== #{'<nyaa>'}
+        HAML
+      end
+
+      it 'renders &= operator' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <span>&lt;nyaa&gt;</span>
+        HTML
+          %span&= '<nyaa>'
+        HAML
+      end
+
+      it 'renders &== operator' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          =
+          =
+          &lt;p&gt;
+        HTML
+          &===
+          &== =
+          &== #{'<p>'}
+        HAML
+      end
+
+      it 'renders ~ operator' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent, escape_html: false)
+          <span>1</span>
+        HTML
+          %span~ 1
+        HAML
+      end
+    end
+
+    describe 'string interpolation' do
+      it { assert_render("\n", '#{}') }
+      it { assert_render("1\n", '1#{}') }
+      it { assert_render("12\n", '1#{2}') }
+      it { assert_render("}1\n", '}#{1}') }
+      it { assert_render("12\n", '#{1}2') }
+      it { assert_render("12345\n", '1#{ "2#{3}4" }5') }
+      it { assert_render("123456789\n", '#{1}2#{3}4#{5}6#{7}8#{9}') }
+      it { assert_render(%Q{'"!@$%^&*|=1112\n}, %q{'"!@$%^&*|=#{1}1#{1}2}) }
+      it { assert_render("あ1\n", 'あ#{1}') }
+      it { assert_render("あいう\n", 'あ#{"い"}う') }
+      it { assert_render("a&lt;b&gt;c\n", 'a#{"<b>"}c') }
+    end
+  end
+end
diff --git a/test/hamlit/engine/whitespace_test.rb b/test/hamlit/engine/whitespace_test.rb
new file mode 100644 (file)
index 0000000..20ebd00
--- /dev/null
@@ -0,0 +1,115 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'whitespace removal' do
+    it 'removes outer whitespace by >' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>a</span><span>b</span>
+        <span>c</span><span>
+        d
+        </span><span>
+        e
+        </span>
+        <span>f</span>
+      HTML
+        %span> a
+        %span b
+        %span c
+        %span>
+          d
+        %span
+          e
+        %span f
+      HAML
+    end
+
+    it 'removes outer whitespace by > from inside of block' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>a</span><span>
+        b
+        </span><span>
+        c
+        </span>
+      HTML
+        %span a
+        - if true
+          %span>
+            b
+        %span
+          c
+      HAML
+    end
+
+    it 'removes whitespaces inside block script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span>foofoo2<span>bar</span></span>
+      HTML
+        %span<
+          = 2.times do
+            = 'foo'
+          %span> bar
+      HAML
+    end
+
+    it 'removes whitespace inside script inside silent script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <div class='bar'>foofoofoo</div>
+      HTML
+        .bar<
+          - 3.times do
+            = 'foo'
+      HAML
+    end
+
+    it 'removes whitespace inside script recursively' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <div class='foo'>bar1bar1bar1bar12</div>
+      HTML
+        .foo<
+          - 1.times do
+            = 2.times do
+              - 2.times do
+                = 1.times do
+                  = 'bar'
+      HAML
+    end
+
+    it 'does not remove whitespace after string interpolation' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <div>helloworld</div>
+      HTML
+        %div<
+          #{'hello'}
+          world
+      HAML
+    end
+
+    it 'removes whitespace inside script inside silent script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <div class='bar'>12</div>
+      HTML
+        .bar<
+          - 1.times do
+            = '1'
+            = '2'
+      HAML
+    end
+
+    it 'does not nuke internal recursively' do
+      assert_render(%Q|<div><span>\nhello\n</span></div>|, <<-HAML.unindent)
+        %div><
+          %span>
+            hello
+      HAML
+    end
+
+    it 'does not nuke inside script' do
+      assert_render(%Q|<div><span>\nhello\n</span>1</div>|, <<-HAML.unindent)
+        %div><
+          = 1.times do
+            %span>
+              hello
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/error_test.rb b/test/hamlit/error_test.rb
new file mode 100644 (file)
index 0000000..f1c464c
--- /dev/null
@@ -0,0 +1,54 @@
+describe Hamlit::Engine do
+  describe 'HamlSyntaxError' do
+    it 'raises on runtime' do
+      code = Hamlit::Engine.new.call("  %a")
+      assert_raises(Hamlit::HamlSyntaxError) do
+        eval code
+      end
+    end
+
+    it 'returns error with lines before error' do
+      code = Hamlit::Engine.new.call("\n\n  %a")
+      begin
+        eval code
+      rescue Hamlit::HamlSyntaxError => e
+        assert_equal(2, e.line)
+      end
+    end
+
+    describe 'Hamlit v1 syntax' do
+      it 'returns an error with proper line number' do
+        code = Hamlit::Engine.new.call(<<-HAML.unindent)
+          %span
+          - if true
+            %div{ data: {
+              hello: 'world',
+            } }
+        HAML
+        begin
+          eval code
+        rescue Hamlit::HamlSyntaxError => e
+          assert_equal(3, e.line)
+        end
+      end
+    end
+  end
+
+  describe 'FilterNotFound' do
+    it 'raises on runtime' do
+      code = Hamlit::Engine.new.call(":k0kubun")
+      assert_raises(Hamlit::FilterNotFound) do
+        eval code
+      end
+    end
+
+    it 'returns error with lines before error' do
+      code = Hamlit::Engine.new.call("\n\n:k0kubun")
+      begin
+        eval code
+      rescue Hamlit::FilterNotFound => e
+        assert_equal(2, e.line)
+      end
+    end
+  end
+end
diff --git a/test/hamlit/filters/cdata_test.rb b/test/hamlit/filters/cdata_test.rb
new file mode 100644 (file)
index 0000000..f9497a0
--- /dev/null
@@ -0,0 +1,27 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  describe '#compile' do
+    it 'renders cdata' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <![CDATA[
+            foo bar
+        ]]>
+      HTML
+        :cdata
+          foo bar
+      HAML
+    end
+
+    it 'parses string interpolation' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <![CDATA[
+            foo <&> bar
+        ]]>
+      HTML
+        :cdata
+          foo #{'<&>'} bar
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/filters/coffee_test.rb b/test/hamlit/filters/coffee_test.rb
new file mode 100644 (file)
index 0000000..c8e315b
--- /dev/null
@@ -0,0 +1,62 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  describe '#compile' do
+    it 'renders coffee filter' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <script>
+          (function() {
+            var foo;
+          
+            foo = function() {
+              return alert('hello');
+            };
+          
+          }).call(this);
+        </script>
+      HTML
+        :coffee
+          foo = ->
+            alert('hello')
+      HAML
+    end
+
+    it 'renders coffeescript filter' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <script>
+          (function() {
+            var foo;
+          
+            foo = function() {
+              return alert('hello');
+            };
+          
+          }).call(this);
+        </script>
+      HTML
+        :coffeescript
+          foo = ->
+            alert('hello')
+      HAML
+    end
+
+    it 'renders coffeescript filter' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <script>
+          (function() {
+            var foo;
+          
+            foo = function() {
+              return alert("<&>");
+            };
+          
+          }).call(this);
+        </script>
+      HTML
+        :coffee
+          foo = ->
+            alert("#{'<&>'}")
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/filters/css_test.rb b/test/hamlit/filters/css_test.rb
new file mode 100644 (file)
index 0000000..8e3ad29
--- /dev/null
@@ -0,0 +1,35 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  describe '#compile' do
+    it 'renders css' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <style>
+          .foo {
+            width: 100px;
+          }
+        </style>
+      HTML
+        :css
+          .foo {
+            width: 100px;
+          }
+      HAML
+    end
+
+    it 'parses string interpolation' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <style>
+          .foo {
+            content: "<&>";
+          }
+        </style>
+      HTML
+        :css
+          .foo {
+            content: "#{'<&>'}";
+          }
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/filters/erb_test.rb b/test/hamlit/filters/erb_test.rb
new file mode 100644 (file)
index 0000000..baf3834
--- /dev/null
@@ -0,0 +1,19 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  describe '#compile' do
+    it 'renders erb filter' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        ok
+
+      HTML
+        :erb
+          <% if true %>
+          ok
+          <% else %>
+          ng
+          <% end %>
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/filters/javascript_test.rb b/test/hamlit/filters/javascript_test.rb
new file mode 100644 (file)
index 0000000..2a0b4b0
--- /dev/null
@@ -0,0 +1,84 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  describe '#compile' do
+    it 'just renders script tag for empty filter' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        before
+        <script>
+          
+        </script>
+        after
+      HTML
+        before
+        :javascript
+        after
+      HAML
+    end
+
+    it 'compiles javascript filter' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        before
+        <script>
+          alert('hello');
+        </script>
+        after
+      HTML
+        before
+        :javascript
+          alert('hello');
+        after
+      HAML
+    end
+
+    it 'accepts illegal indentation' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <script>
+          if {
+           alert('hello');
+            }
+        </script>
+        <script>
+            if {
+             alert('hello');
+              }
+        </script>
+      HTML
+        :javascript
+         if {
+          alert('hello');
+           }
+        :javascript
+           if {
+            alert('hello');
+             }
+      HAML
+    end
+
+    it 'accepts illegal indentation' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <script>
+          if {
+           alert('a');
+          }
+        </script>
+      HTML
+        :javascript
+           if {
+            alert('a');
+           }
+      HAML
+    end
+
+    it 'parses string interpolation' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <script>
+          var a = "<&>";
+        </script>
+      HTML
+        :javascript
+          var a = "#{'<&>'}";
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/filters/markdown_test.rb b/test/hamlit/filters/markdown_test.rb
new file mode 100644 (file)
index 0000000..64efc12
--- /dev/null
@@ -0,0 +1,34 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  describe '#compile' do
+    it 'renders markdown filter' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <h1>Hamlit</h1>
+
+        <p>Yet another haml implementation</p>
+
+      HTML
+        :markdown
+          # Hamlit
+          Yet another haml implementation
+      HAML
+    end
+
+    it 'renders markdown filter with string interpolation' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <h1><Hamlit></h1>
+
+        <p>&lt;&amp;&gt;
+        Yet another haml implementation</p>
+
+      HTML
+        - project = '<Hamlit>'
+        :markdown
+          # #{project}
+          #{'<&>'}
+          Yet another haml implementation
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/filters/plain_test.rb b/test/hamlit/filters/plain_test.rb
new file mode 100644 (file)
index 0000000..78e4a64
--- /dev/null
@@ -0,0 +1,26 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  describe '#compile' do
+    it 'does not escape content without interpolation' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <script>
+      HTML
+        :plain
+          <script>
+      HAML
+    end
+
+    it 'does not escape interpolated content' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <script>
+        <script>
+
+      HTML
+        :plain
+          <script>
+          #{'<script>'}
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/filters/ruby_test.rb b/test/hamlit/filters/ruby_test.rb
new file mode 100644 (file)
index 0000000..ea337e8
--- /dev/null
@@ -0,0 +1,24 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  it 'renders ruby filter' do
+    assert_render(<<-HTML.unindent, <<-HAML.unindent)
+      hello
+    HTML
+      :ruby
+      hello
+    HAML
+  end
+
+  it 'renders ruby filter' do
+    assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+      &lt;&amp;&gt;
+    HTML
+      :ruby
+        hash = {
+          a: "#{'<&>'}",
+        }
+      = hash[:a]
+    HAML
+  end
+end
diff --git a/test/hamlit/filters/sass_test.rb b/test/hamlit/filters/sass_test.rb
new file mode 100644 (file)
index 0000000..b16968d
--- /dev/null
@@ -0,0 +1,35 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  describe '#compile' do
+    it 'renders sass filter' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <style>
+          .users_controller .show_action {
+            margin: 10px;
+            padding: 20px; }
+        </style>
+      HTML
+        :sass
+          .users_controller
+            .show_action
+              margin: 10px
+              padding: 20px
+      HAML
+    end
+
+    it 'renders sass filter with string interpolation' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <style>
+          .users_controller .show_action {
+            content: "<&>"; }
+        </style>
+      HTML
+        :sass
+          .users_controller
+            .show_action
+              content: "#{'<&>'}"
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/filters/scss_test.rb b/test/hamlit/filters/scss_test.rb
new file mode 100644 (file)
index 0000000..0061bf7
--- /dev/null
@@ -0,0 +1,39 @@
+describe Hamlit::Filters do
+  include RenderHelper
+
+  describe '#compile' do
+    it 'renders scss filter' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <style>
+          .users_controller .show_action {
+            margin: 10px;
+            padding: 20px; }
+        </style>
+      HTML
+        :scss
+          .users_controller {
+            .show_action {
+              margin: 10px;
+              padding: 20px;
+            }
+          }
+      HAML
+    end
+
+    it 'parses string interpolation' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        <style>
+          .users_controller .show_action {
+            content: "<&>"; }
+        </style>
+      HTML
+        :scss
+          .users_controller {
+            .show_action {
+              content: "#{'<&>'}";
+            }
+          }
+      HAML
+    end
+  end
+end
diff --git a/test/hamlit/helpers_test.rb b/test/hamlit/helpers_test.rb
new file mode 100644 (file)
index 0000000..3ac061f
--- /dev/null
@@ -0,0 +1,8 @@
+describe Hamlit::Helpers do
+  describe '.preserve' do
+    it 'works without block' do
+      result = Hamlit::Helpers.preserve("hello\nworld")
+      assert_equal 'hello&#x000A;world', result
+    end
+  end
+end
diff --git a/test/hamlit/line_number_test.rb b/test/hamlit/line_number_test.rb
new file mode 100644 (file)
index 0000000..78ed449
--- /dev/null
@@ -0,0 +1,245 @@
+describe Hamlit::Engine do
+  include RenderHelper
+
+  describe 'script' do
+    it 'renders optimized script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        1
+        2
+      HTML
+        = '1'
+        = __LINE__
+      HAML
+    end
+
+    it 'renders dynamic script' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        1
+        2
+      HTML
+        = 'a'.gsub(/a/, '1')
+        = __LINE__
+      HAML
+    end
+
+    it 'renders dynamic script with children' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        1
+        3
+        3
+        24
+      HTML
+        = __LINE__
+        = __LINE__.times do
+          = __LINE__
+        = __LINE__
+      HAML
+    end
+  end
+
+  describe 'silent script' do
+    it 'renders silent script' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        2:3
+        4
+      HTML
+        - __LINE__.times do
+          - a = __LINE__
+          = "#{a}:#{__LINE__}"
+        = __LINE__
+      HAML
+    end
+
+    it 'renders silent script with children' do
+      assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+        1:2
+      HTML
+        - a = __LINE__
+        = "#{a}:#{__LINE__}"
+      HAML
+    end
+  end
+
+  describe 'old attributes' do
+    it 'renders multi-line old attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span a='1' b='2'>2</span>
+        3
+      HTML
+        %span{ a: __LINE__,
+          b: __LINE__ }= __LINE__
+        = __LINE__
+      HAML
+    end
+
+    it 'renders optimized old attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span a='a' b='b'></span>
+        3
+        <span a='a' b='b'>5</span>
+        6
+      HTML
+        %span{ a: 'a',
+          b: 'b' }
+        = __LINE__
+        %span{ a: 'a',
+          b: 'b' }= __LINE__
+        = __LINE__
+      HAML
+    end
+  end
+
+  describe 'new attributes' do
+    it 'renders multi-line new attributes' do
+      assert_render(<<-HTML.unindent, <<-HAML.unindent)
+        <span a='1' b='1'>1</span>
+        3
+      HTML
+        %span(a=__LINE__
+         b=__LINE__)= __LINE__
+        = __LINE__
+      HAML
+    end
+  end
+
+  describe 'filters' do
+    describe 'coffee filter' do
+      it 'renders static filter' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <script>
+            (function() {
+              jQuery(function($) {
+                console.log('3');
+                return console.log('4');
+              });
+            
+            }).call(this);
+          </script>
+          5
+        HTML
+          :coffee
+            jQuery ($) ->
+              console.log('#{__LINE__}')
+              console.log('#{__LINE__}')
+          = __LINE__
+        HAML
+      end
+
+      it 'renders dynamic filter' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <script>
+            (function() {
+              jQuery(function($) {
+                console.log('3');
+                return console.log('4');
+              });
+            
+            }).call(this);
+          </script>
+          5
+        HTML
+          :coffee
+            jQuery ($) ->
+              console.log('3')
+              console.log('4')
+          = __LINE__
+        HAML
+      end
+    end
+
+    describe 'css filter' do
+      it 'renders static filter' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <style>
+            body {
+              width: 3px;
+              height: 4px;
+            }
+          </style>
+          6
+        HTML
+          :css
+            body {
+              width: 3px;
+              height: 4px;
+            }
+          = __LINE__
+        HAML
+      end
+
+      it 'renders dynamic filter' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <style>
+            body {
+              width: 3px;
+              height: 4px;
+            }
+          </style>
+          6
+        HTML
+          :css
+            body {
+              width: #{__LINE__}px;
+              height: #{__LINE__}px;
+            }
+          = __LINE__
+        HAML
+      end
+
+      it 'renders dynamic filter with trailing newlines' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <style>
+            body {
+              width: 3px;
+              height: 4px;
+            }
+          </style>
+          8
+        HTML
+          :css
+            body {
+              width: #{__LINE__}px;
+              height: #{__LINE__}px;
+            }
+
+
+          = __LINE__
+        HAML
+      end
+    end
+
+    describe 'javascript filter' do
+      it 'renders static filter' do
+        assert_render(<<-HTML.unindent, <<-HAML.unindent)
+          <script>
+            console.log("2");
+            console.log("3");
+          </script>
+          5
+        HTML
+          :javascript
+            console.log("2");
+            console.log("3");
+
+          = __LINE__
+        HAML
+      end
+
+      it 'renders dynamic filter' do
+        assert_render(<<-HTML.unindent, <<-'HAML'.unindent)
+          <script>
+            console.log("2");
+            console.log("3");
+          </script>
+          5
+        HTML
+          :javascript
+            console.log("#{__LINE__}");
+            console.log("#{__LINE__}");
+
+          = __LINE__
+        HAML
+      end
+    end
+  end
+end
diff --git a/test/hamlit/optimization_test.rb b/test/hamlit/optimization_test.rb
new file mode 100644 (file)
index 0000000..ab5d69c
--- /dev/null
@@ -0,0 +1,45 @@
+describe 'optimization' do
+  def compiled_code(haml)
+    Hamlit::Engine.new.call(haml)
+  end
+
+  describe 'static analysis' do
+    it 'renders static value for href statically' do
+      haml = %|%a{ href: 1 }|
+      assert_equal true, compiled_code(haml).include?(%|href='1'|)
+    end
+
+    it 'renders static script statically' do
+      haml = <<-HAML.unindent
+        %span
+          1
+      HAML
+      assert_equal true, compiled_code(haml).include?(%q|<span>\n1\n</span>|)
+    end
+
+    it 'renders inline static script statically' do
+      haml = %|%span= 1|
+      assert_equal true, compiled_code(haml).include?(%|<span>1</span>|)
+    end
+  end
+
+  describe 'string interpolation' do
+    it 'renders a static part of string literal statically' do
+      haml = %q|%input{ value: "jruby#{9000}#{dynamic}" }|
+      assert_equal true, compiled_code(haml).include?(%|value='jruby9000|)
+
+      haml = %q|%span= "jruby#{9000}#{dynamic}"|
+      assert_equal true, compiled_code(haml).include?(%|<span>jruby9000|)
+    end
+
+    it 'optimizes script' do
+      haml = %q|= "jruby#{ "#{9000}" }#{dynamic}"|
+      assert_equal true, compiled_code(haml).include?(%|jruby9000|)
+    end
+
+    it 'detects a static part recursively' do
+      haml = %q|%input{ value: "#{ "hello#{ hello }" }" }|
+      assert_equal true, compiled_code(haml).include?(%|value='hello|)
+    end
+  end
+end
diff --git a/test/hamlit/rails_template_test.rb b/test/hamlit/rails_template_test.rb
new file mode 100644 (file)
index 0000000..7487670
--- /dev/null
@@ -0,0 +1,175 @@
+# Explicitly requiring rails_template because rails initializers is not executed here.
+require 'hamlit/rails_template'
+
+describe Hamlit::RailsTemplate do
+  def render(haml)
+    ActionView::Template.register_template_handler(:haml, Hamlit::RailsTemplate.new)
+    base = ActionView::Base.new(__dir__, {})
+    base.render(inline: haml, type: :haml)
+  end
+
+  specify 'html escape' do
+    assert_equal %Q|&lt;script&gt;alert(&quot;a&quot;);&lt;/script&gt;\n|, render(<<-HAML.unindent)
+      = '<script>alert("a");</script>'
+    HAML
+    assert_equal %Q|<script>alert("a");</script>\n|, render(<<-HAML.unindent)
+      = '<script>alert("a");</script>'.html_safe
+    HAML
+    assert_equal %Q|&lt;script&gt;alert(&quot;a&quot;);&lt;/script&gt;\n|, render(<<-'HAML'.unindent)
+      #{'<script>alert("a");</script>'}
+    HAML
+    assert_equal %Q|<script>alert("a");</script>\n|, render(<<-'HAML'.unindent)
+      #{'<script>alert("a");</script>'.html_safe}
+    HAML
+  end
+
+  specify 'attribute escape' do
+    assert_equal %Q|<a href='&lt;script&gt;alert(&quot;a&quot;);&lt;/script&gt;'></a>\n|, render(<<-HAML.unindent)
+      %a{ href: '<script>alert("a");</script>' }
+    HAML
+    assert_equal %Q|<a href='&lt;script&gt;'></a>\n|, render(<<-HAML.unindent)
+      %a{ href: '<script>'.html_safe }
+    HAML
+  end
+
+  it 'forces to escape html_safe attributes' do
+    assert_equal <<-'HTML'.unindent, render(<<-HAML.unindent)
+      <meta content='&#39;&quot;'>
+      <meta content='&#39;&quot;'>
+      <meta content='&#39;&quot;'>
+    HTML
+      %meta{ content: %{'"}.html_safe }
+      - val = %{'"}.html_safe
+      %meta{ content: val }
+      - hash = { content: val }
+      %meta{ hash }
+    HAML
+  end
+
+  specify 'boolean attributes' do
+    assert_equal %Q|<span checked='no' disabled></span>\n|, render(<<-HAML.unindent)
+      - val = 'no'
+      %span{ disabled: true, checked: val, autoplay: false }
+    HAML
+  end
+
+  specify 'link_to' do
+    assert_equal %Q|<a class="bar" href="#">foo</a>\n|, render(%q|= link_to 'foo', '#', class: 'bar'|)
+  end
+
+  specify 'content_tag' do
+    assert_equal <<-HTML.unindent.strip, render(<<-HAML.unindent)
+      <div>text
+      </div>
+    HTML
+      = content_tag :div do
+        text
+    HAML
+    assert_equal <<-HTML.unindent.strip, render(<<-HAML.unindent)
+      <div>text
+      </div><div>text
+      </div><div>text
+      </div>
+    HTML
+      - 3.times do
+        = content_tag :div do
+          text
+    HAML
+  end
+
+  specify 'find_and_preserve' do
+    assert_equal <<-HTML.unindent, render(<<-'HAML'.unindent)
+      Foo bar
+      &lt;pre&gt;foo bar&lt;/pre&gt;
+      &lt;pre&gt;foo&amp;#x000A;bar&lt;/pre&gt;
+    HTML
+      = find_and_preserve("Foo bar")
+      = find_and_preserve("<pre>foo bar</pre>")
+      = find_and_preserve("<pre>foo\nbar</pre>")
+    HAML
+  end
+
+  specify 'capture_haml' do
+    assert_equal <<-HTML.unindent, render(<<-'HAML'.unindent)
+      <div class='capture'><span>
+      <p>Capture</p>
+      </span>
+      </div>
+    HTML
+      - html = capture_haml do
+        %span
+          %p Capture
+
+      .capture= html
+    HAML
+  end
+
+  specify 'preserve' do
+    assert_equal %q|Foo&#x000A;Bar|, render(<<-'HAML'.unindent)
+      = preserve do
+        Foo
+        Bar
+    HAML
+    assert_equal %q|<div />|, render(<<-'HAML'.unindent)
+      = preserve do
+        <div />
+    HAML
+  end
+
+  specify 'succeed' do
+    assert_equal %Q|<i>succeed</i>&amp;\n|, render(<<-'HAML'.unindent)
+      = succeed '&' do
+        %i succeed
+    HAML
+  end
+
+  specify 'precede' do
+    assert_equal %Q|&amp;<i>precede</i>\n|, render(<<-'HAML'.unindent)
+      = precede '&' do
+        %i precede
+    HAML
+  end
+
+  specify 'surround' do
+    assert_equal %Q|&amp;<i>surround</i>&amp;\n|, render(<<-'HAML'.unindent)
+      = surround '&' do
+        %i surround
+    HAML
+  end
+
+  specify 'object which returns SafeBuffer for to_s (like kaminari)' do
+    class ::TosUnsafeObject; def to_s; "<hr>"; end; end
+    class ::TosSafeObject; def to_s; "<hr>".html_safe; end; end
+
+    assert_equal %Q|<hr>\n|, render(%q|= ::TosSafeObject.new|)
+    assert_equal %Q|&lt;hr&gt;\n|, render(%q|= ::TosUnsafeObject.new|)
+  end
+
+  specify 'encoding' do
+    assert_equal Encoding.default_external, render('Test').encoding
+  end
+
+  specify '.set_options' do
+    original = Hamlit::RailsTemplate.options[:use_html_safe]
+    begin
+      Hamlit::RailsTemplate.set_options(use_html_safe: !original)
+      assert_equal !original, Hamlit::RailsTemplate.options[:use_html_safe]
+    ensure
+      Hamlit::RailsTemplate.set_options(use_html_safe: original)
+    end
+  end
+
+  specify 'xml mime_type' do
+    base = ActionView::Base.new(__dir__, {})
+    handler = Hamlit::RailsTemplate.new
+    html_template = ActionView::Template.new('%link', 'test.html.haml', handler, {
+      format: Mime::Type.new('text/html', :html, ['application/xhtml+xml']),
+    })
+    xml_template  = ActionView::Template.new('%link', 'test.xml.haml', handler, {
+      format: Mime::Type.new('application/xml', :xml, ['text/xml', 'application/x-xml']),
+    })
+
+    assert_equal %Q|<link>\n|, base.render(template: html_template)
+    assert_equal %Q|<link />\n|, base.render(template: xml_template)
+  end
+end
diff --git a/test/hamlit/ruby_expression_test.rb b/test/hamlit/ruby_expression_test.rb
new file mode 100644 (file)
index 0000000..c58e72e
--- /dev/null
@@ -0,0 +1,44 @@
+describe Hamlit::RubyExpression do
+  describe '.syntax_error?' do
+    it { assert_equal(true,  Hamlit::RubyExpression.syntax_error?('{ hash }')) }
+    it { assert_equal(false, Hamlit::RubyExpression.syntax_error?('{ a: b }')) }
+  end
+
+  describe '.string_literal?' do
+    def assert_literal(expected, code)
+      actual = Hamlit::RubyExpression.string_literal?(code)
+      assert_equal expected, actual
+    end
+
+    describe 'invalid expressions' do
+      it { assert_literal(false, %q|{ hash }|) }
+      it { assert_literal(false, %q|"hello".|) }
+    end
+
+    describe 'string literal' do
+      it { assert_literal(true, %q|''|) }
+      it { assert_literal(true, %q|""|) }
+      it { assert_literal(true, %Q|'\n'|) }
+      it { assert_literal(true, %q|'';   |) }
+      it { assert_literal(true, %q|  ""  |) }
+      it { assert_literal(true, %q|'hello world'|) }
+      it { assert_literal(true, %q|"hello world"|) }
+      it { assert_literal(true, %q|"h#{ %Q[e#{ "llo wor" }l] }d"|) }
+      it { assert_literal(true, %q|%Q[nya]|) }
+      it { assert_literal(true, %q|%Q[#{123}]|) }
+    end
+
+    describe 'not string literal' do
+      it { assert_literal(false, %q|123|) }
+      it { assert_literal(false, %q|'hello' + ''|) }
+      it { assert_literal(false, %q|'hello'.to_s|) }
+      it { assert_literal(false, %Q|'' \\ \n ''|) }
+      it { assert_literal(false, %q|['']|) }
+      it { assert_literal(false, %q|return ''|) }
+    end
+
+    describe 'multiple instructions' do
+      it { assert_literal(false, %Q|''\n''|) }
+    end
+  end
+end
diff --git a/test/hamlit/static_analyzer_test.rb b/test/hamlit/static_analyzer_test.rb
new file mode 100644 (file)
index 0000000..32a6f2f
--- /dev/null
@@ -0,0 +1,57 @@
+describe Hamlit::StaticAnalyzer do
+  describe '.static?' do
+    def assert_static(expected, ruby_exp)
+      actual = Hamlit::StaticAnalyzer.static?(ruby_exp)
+      assert_equal expected, actual
+    end
+
+    describe 'static expression' do
+      it { assert_static(true, 'true') }
+      it { assert_static(true, 'false') }
+      it { assert_static(true, 'nil') }
+      it { assert_static(true, '()') }
+      it { assert_static(true, '(nil)') }
+      it { assert_static(true, '[true, false, nil, (true)]') }
+      it { assert_static(true, '%w[1 2 3]') }
+      it { assert_static(true, '3') }
+      it { assert_static(true, '1.2') }
+      it { assert_static(true, '[3, 1.2, [false, "hello #{ 123 } world"]]') }
+      it { assert_static(true, '(3)') }
+      it { assert_static(true, '""') }
+      it { assert_static(true, '"hello world"') }
+      it { assert_static(true, '"a#{}b"') }
+      it { assert_static(true, '{}') }
+      it { assert_static(true, '{ "key" => "value" }') }
+      it { assert_static(true, '{ key: "value" }') }
+
+      if RUBY_VERSION >= '2.1.0'
+        it { assert_static(true, '2i') }
+      end
+    end
+
+    describe 'dynamic expression' do
+      it { assert_static(false, 'if true') }
+      it { assert_static(false, 'foo') }
+      it { assert_static(false, '"hello #{ world }"') }
+      it { assert_static(false, '"" + bar') }
+      it { assert_static(false, '"" ** bar') }
+      it { assert_static(false, '"".gsub(/foo/, "bar")') }
+      it { assert_static(false, '"".freeze') }
+      it { assert_static(false, '1.times {}') }
+      it { assert_static(false, '[3, 1.2, [2i, "hello #{ nya } world"]]') }
+      it { assert_static(false, 'self') }
+      it { assert_static(false, '__FILE__') }
+      it { assert_static(false, '__LINE__') }
+      it { assert_static(false, '__ENCODING__') }
+      it { assert_static(false, '__dir__') }
+    end
+
+    describe 'invalid expression' do
+      it { assert_static(false, nil) }
+      it { assert_static(false, ' ') }
+      it { assert_static(false, '}') }
+      it { assert_static(false, '(') }
+      it { assert_static(false, '+') }
+    end
+  end
+end
diff --git a/test/hamlit/string_splitter_test.rb b/test/hamlit/string_splitter_test.rb
new file mode 100644 (file)
index 0000000..970abf5
--- /dev/null
@@ -0,0 +1,47 @@
+describe Hamlit::StringSplitter do
+  describe '.compile' do
+    def assert_compile(expected, code)
+      actual = Hamlit::StringSplitter.compile(code)
+      assert_equal expected, actual
+    end
+
+    it { assert_compile([], %q|''|) }
+    it { assert_compile([], %q|""|) }
+    it { assert_compile([[:static, 'hello']], %q|"hello"|) }
+    it { assert_compile([[:static, 'hello '], [:static, 'world']], %q|"hello #{}world"|) }
+    it { assert_compile([[:dynamic, 'hello']], %q|"#{hello}"|) }
+    it { assert_compile([[:static, 'nya'], [:dynamic, '123']], %q|"nya#{123}"|) }
+    it { assert_compile([[:dynamic, '()'], [:static, '()']], %q|"#{()}()"|) }
+    it { assert_compile([[:static, ' '], [:dynamic, %q[ " #{ '#{}' } " ]]], %q|" #{ " #{ '#{}' } " }"|) }
+    it { assert_compile([[:static, 'a'], [:dynamic, 'b'], [:static, 'c'], [:dynamic, 'd'], [:static, 'e']], %q|%Q[a#{b}c#{d}e]|) }
+    it { assert_compile([[:static, 'a#{b}c#{d}e']], %q|%q[a#{b}c#{d}e]|) }
+    it { assert_compile([[:static, '#{}'], [:dynamic, '123']], %q|"\#{}#{123}"|) }
+    it { assert_compile([[:dynamic, " '}' "]], %q|"#{ '}' }"|) }
+    it { assert_compile([[:static, 'a']], %q| "a" # hello |) }
+    it { assert_compile([[:static, '"']], %q|"\""|) }
+    it { assert_compile([[:static, '\\"']], %q|"\\\\\\""|) }
+    it { assert_compile([[:static, '\"']], %q|'\"'|) }
+    it { assert_compile([[:static, '\"']], %q|'\\"'|) }
+    it { assert_compile([[:static, '\\"']], %q|'\\\"'|) }
+
+    describe 'invalid argument' do
+      it 'raises internal error' do
+        assert_raises Hamlit::InternalError do
+          Hamlit::StringSplitter.compile('1')
+        end
+      end
+
+      it 'raises internal error' do
+        assert_raises Hamlit::InternalError do
+          Hamlit::StringSplitter.compile('[]')
+        end
+      end
+
+      it 'raises internal error' do
+        assert_raises Hamlit::InternalError do
+          Hamlit::StringSplitter.compile('"]')
+        end
+      end
+    end
+  end
+end
diff --git a/test/hamlit/template_test.rb b/test/hamlit/template_test.rb
new file mode 100644 (file)
index 0000000..fc7a715
--- /dev/null
@@ -0,0 +1,45 @@
+describe Hamlit::Template do
+  # Simple imitation of Sinatra::Templates#compila_template
+  def compile_template(engine, data, options = {})
+    template = Tilt[engine]
+    template.new(nil, 1, options) { data }
+  end
+
+  specify 'Tilt returns Hamlit::Template for haml engine' do
+    assert_equal Hamlit::Template, Tilt[:haml]
+  end
+
+  it 'renders properly via tilt' do
+    result = compile_template(:haml, %q|%p hello world|).render(Object.new, {})
+    assert_equal %Q|<p>hello world</p>\n|, result
+  end
+
+  it 'has preserve method' do
+    result = compile_template(:haml, %q|= preserve "hello\nworld"|).render(Object.new, {})
+    assert_equal %Q|hello&amp;#x000A;world\n|, result
+  end
+
+  describe 'escape_attrs' do
+    it 'escapes attrs by default' do
+      result = compile_template(:haml, %q|%div{ data: '<script>' }|).render(Object.new, {})
+      assert_equal %Q|<div data='&lt;script&gt;'></div>\n|, result
+    end
+
+    it 'can be configured not to escape attrs' do
+      result = compile_template(:haml, %q|%div{ data: '<script>' }|, escape_attrs: false).render(Object.new, {})
+      assert_equal %Q|<div data='<script>'></div>\n|, result
+    end
+  end
+
+  describe 'escape_html' do
+    it 'escapes html' do
+      result = compile_template(:haml, %q|= '<script>' |).render(Object.new, {})
+      assert_equal %Q|&lt;script&gt;\n|, result
+    end
+
+    it 'can be configured not to escape attrs' do
+      result = compile_template(:haml, %q|= '<script>' |, escape_html: false).render(Object.new, {})
+      assert_equal %Q|<script>\n|, result
+    end
+  end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644 (file)
index 0000000..e3d887c
--- /dev/null
@@ -0,0 +1,103 @@
+require 'unindent'
+require 'bundler/setup'
+require 'minitest/autorun'
+require 'action_pack'
+require 'action_controller'
+require 'action_view'
+require 'rails'
+
+require 'hamlit'
+require 'haml'
+
+# require 'minitest/reporters'
+# Minitest::Reporters.use!
+
+BASE_TEST_CLASS = if defined?(Minitest::Test)
+                    Minitest::Test
+                  else
+                    MiniTest::Unit::TestCase
+                  end
+
+module Declarative
+  def test(name, &block)
+    define_method("test_ #{name}", &block)
+  end
+end
+
+module RenderHelper
+  def assert_render(expected, haml, options = {})
+    actual = render_hamlit(haml, options)
+    assert_equal expected, actual
+  end
+
+  def render_haml(haml, options = {})
+    options = options.dup
+    locals  = options.delete(:locals) || {}
+    haml_options = { escape_html: true, escape_attrs: true, ugly: true }
+    Haml::Engine.new(haml, haml_options.merge(options)).render(Object.new, locals)
+  end
+
+  def render_hamlit(haml, options = {})
+    options = options.dup
+    locals  = options.delete(:locals) || {}
+    Hamlit::Template.new(options) { haml }.render(Object.new, locals)
+  end
+
+  def assert_haml(haml, options = {})
+    expected = render_haml(haml, options)
+    actual = render_hamlit(haml, options)
+    assert_equal expected, actual
+  end
+end
+
+class Haml::TestCase < BASE_TEST_CLASS
+  extend Declarative
+
+  def render(text, options = {}, base = nil, &block)
+    options = { escape_html: false }.merge(options) # incompatible default
+    scope  = options.delete(:scope)  || Object.new
+    locals = options.delete(:locals) || {}
+    options.delete(:ugly)
+    eval Hamlit::Engine.new(options).call(text)
+  end
+
+  def assert_haml_ugly(text, options = {}, base = nil)
+    haml_base = { ugly: true, escape_html: true, escape_attrs: true }
+    hamlit_base = { escape_html: true }
+    scope  = options.delete(:scope)  || Object.new
+    locals = options.delete(:locals) || {}
+    haml_result   = Haml::Engine.new(text, haml_base.merge(options)).render(scope, locals)
+    hamlit_result = Hamlit::Template.new(hamlit_base.merge(options)) { text }.render(scope, locals)
+    assert_equal haml_result, hamlit_result
+  end
+
+  def assert_warning(message)
+    the_real_stderr, $stderr = $stderr, StringIO.new
+    yield
+
+    if message.is_a?(Regexp)
+      assert_match message, $stderr.string.strip
+    else
+      assert_equal message.strip, $stderr.string.strip
+    end
+  ensure
+    $stderr = the_real_stderr
+  end
+
+  def silence_warnings(&block)
+    Hamlit::HamlUtil.silence_warnings(&block)
+  end
+
+  def assert_raises_message(klass, message)
+    yield
+  rescue Exception => e
+    assert_instance_of(klass, e)
+    assert_equal(message, e.message)
+  else
+    flunk "Expected exception #{klass}, none raised"
+  end
+
+  def self.error(*args)
+    Hamlit::HamlError.message(*args)
+  end
+end