From: Joseph Herlant Date: Sun, 22 Dec 2019 19:50:04 +0000 (+0000) Subject: Import ruby-hamlit_2.11.0.orig.tar.gz X-Git-Tag: archive/raspbian/2.15.1-2+rpi1~1^2^2~4 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=4f69aa70ca8f93cec69db0e811e08cefcd3d2e9b;p=ruby-hamlit.git Import ruby-hamlit_2.11.0.orig.tar.gz [dgit import orig ruby-hamlit_2.11.0.orig.tar.gz] --- 4f69aa70ca8f93cec69db0e811e08cefcd3d2e9b diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1850c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +.sass-cache +.ruby-version +*.bundle +*.so +*.su +*.o +*.a +*.swp diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..33fe46d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,47 @@ +language: ruby +cache: bundler +branches: + only: + - master +script: + - "RUBYOPT='-w' bundle exec rake $TASK" +matrix: + include: + - rvm: 2.3.8 + env: TASK=test + - rvm: 2.4.5 + env: TASK=test + - rvm: 2.5.3 + env: TASK=test + - rvm: 2.6.0 + env: TASK=test + - rvm: ruby-head + env: TASK=test + - rvm: jruby-9.2.8.0 + env: TASK=test + - rvm: truffleruby + env: TASK=test + - rvm: 2.6.0 + 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.6.0 + 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.6.0 + env: TASK=bench SLIM_BENCH=1 + - rvm: 2.6.0 + env: TASK=bench TEMPLATE=benchmark/etc/attribute_builder.haml + - rvm: 2.6.0 + env: TASK=bench TEMPLATE=benchmark/etc/static_analyzer.haml + - rvm: 2.6.0 + env: TASK=bench TEMPLATE=benchmark/etc/string_interpolation.haml + - rvm: 2.6.0 + 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 index 0000000..e432f98 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,718 @@ +# 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.11.0](https://github.com/k0kubun/hamlit/compare/v2.10.0...v2.10.1) - 2019-12-12 + +### Added + +- Support Haml's _revealed_ conditional comment feature on `/![if !IE]` [#153](https://github.com/k0kubun/hamlit/issues/153). + *Thanks to @esb* + +## [2.10.1](https://github.com/k0kubun/hamlit/compare/v2.10.0...v2.10.1) - 2019-11-28 + +### Added + +- Register `Hamlit::Template` to Tilt as :hamlit as well, in addition to :haml + +## [2.10.0](https://github.com/k0kubun/hamlit/compare/v2.9.5...v2.10.0) - 2019-09-15 + +### Added + +- Optimize template rendering by string interpolation [#146](https://github.com/k0kubun/hamlit/issues/146) + - Exploiting pre-allocation of string interpolation introduced in Ruby 2.5 [ruby/ruby#1626](https://github.com/ruby/ruby/pull/1626) + +### Changed + +- Require temple.gem >= 0.8.2 + +## [2.9.5](https://github.com/k0kubun/hamlit/compare/v2.9.4...v2.9.5) - 2019-09-08 + +### Added + +- Supported `:plain` filter in truffleruby + +## [2.9.4](https://github.com/k0kubun/hamlit/compare/v2.9.3...v2.9.4) - 2019-09-08 + +### Added + +- Experimental support of truffleruby [#145](https://github.com/k0kubun/hamlit/issues/145). + +## [2.9.3](https://github.com/k0kubun/hamlit/compare/v2.9.2...v2.9.3) - 2019-04-09 + +### Fixed + +- Fix deprecation warning on Rails 6 [#138](https://github.com/k0kubun/hamlit/issues/138). + *Thanks to @r7kamura* + +## [2.9.2](https://github.com/k0kubun/hamlit/compare/v2.9.1...v2.9.2) - 2018-11-30 + +### Fixed + +- Fix possible `autoload` failure of dependency [#131](https://github.com/k0kubun/hamlit/issues/131). + *Thanks to @wimrijnders* + +## [2.9.1](https://github.com/k0kubun/hamlit/compare/v2.9.0...v2.9.1) - 2018-11-01 + +### Added + +- Start supporting JRuby [#100](https://github.com/k0kubun/hamlit/issues/100). + +## [2.9.0](https://github.com/k0kubun/hamlit/compare/v2.8.10...v2.9.0) - 2018-10-16 + +### Added + +- Consider aria attribute as another attribute that supports hyphenation and boolean like data attribute + [#57](https://github.com/k0kubun/hamlit/pull/57). *Thanks to @francesco-loreti* + +## [2.8.10](https://github.com/k0kubun/hamlit/compare/v2.8.9...v2.8.10) - 2018-09-05 + +### Fixed + +- Fix uninitialized constant error introduced in v2.8.9 + [#125](https://github.com/k0kubun/hamlit/pull/125). *Thanks to @vovchynniko* + +## [2.8.9](https://github.com/k0kubun/hamlit/compare/v2.8.8...v2.8.9) - 2018-09-05 [YANKED] + +### Fixed + +- Don't raise an error on UTF-8 BOM [#117](https://github.com/k0kubun/hamlit/pull/117) + [#124](https://github.com/k0kubun/hamlit/pull/124). *Thanks to @southwolf* + +## [2.8.8](https://github.com/k0kubun/hamlit/compare/v2.8.7...v2.8.8) - 2018-04-06 + +### Fixed + +- Don't require Tilt dependencies if unregistered + [#121](https://github.com/k0kubun/hamlit/pull/121). *Thanks to @michaelglass* + +## [2.8.7](https://github.com/k0kubun/hamlit/compare/v2.8.6...v2.8.7) - 2018-02-17 + +### Fixed + +- Fix parser error on string interpolation in attributes + +## [2.8.6](https://github.com/k0kubun/hamlit/compare/v2.8.5...v2.8.6) - 2017-12-22 + +### Fixed + +- Fix some unused-variable / method-redefinition warnings + +## [2.8.5](https://github.com/k0kubun/hamlit/compare/v2.8.4...v2.8.5) - 2017-11-06 + +### Fixed + +- Fix lexer to work with Ripper of Ruby 2.5 + +## [2.8.4](https://github.com/k0kubun/hamlit/compare/v2.8.3...v2.8.4) - 2017-06-23 + +### Added + +- Allow filename `-` to read input from STDIN for `hamlit [parse|temple|compile|render]` + [#113](https://github.com/k0kubun/hamlit/issues/113). *Thanks to @gfx* + +## [2.8.3](https://github.com/k0kubun/hamlit/compare/v2.8.2...v2.8.3) - 2017-06-19 + +### Added + +- Add `--color` option to `hamlit parse` and `hamlit temple` commands too. + +## [2.8.2](https://github.com/k0kubun/hamlit/compare/v2.8.1...v2.8.2) - 2017-06-19 + +### Added + +- Add `--color` option to opt-in coloring in `hamlit compile` command + [#111](https://github.com/k0kubun/hamlit/issues/111). + +## [2.8.1](https://github.com/k0kubun/hamlit/compare/v2.8.0...v2.8.1) - 2017-04-03 + +### Fixed + +- Fix SEGV caused by nil in old attributes + [#101](https://github.com/k0kubun/hamlit/issues/101). *Thanks to @FND* + +## [2.8.0](https://github.com/k0kubun/hamlit/compare/v2.7.5...v2.8.0) - 2017-02-12 + +### Changed + +- Support Temple >= 0.8.0 and change to use StaticAnalyzer in Temple +- Optimize attribute building code a little + +## [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.4](https://github.com/k0kubun/hamlit/compare/v2.2.3...v2.2.4) - 2017-12-05 + +### Fixed + +- Fix to work with Ruby 2.5. This version is usable with both 2.0 and 2.5. + +## [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 index 0000000..b6c105b --- /dev/null +++ b/Gemfile @@ -0,0 +1,30 @@ +source 'https://rubygems.org' + +git_source(:github) do |repo_name| + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") + "https://github.com/#{repo_name}.git" +end + +# Specify your gem's dependencies in hamlit.gemspec +gemspec + +if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.2') + gem 'rack', '< 2' +end + +gem 'benchmark-ips', '2.3.0' +gem 'maxitest' +gem 'pry' + +if /java/ === RUBY_PLATFORM # JRuby + gem 'pandoc-ruby' +else + gem 'redcarpet' + + if RUBY_PLATFORM !~ /mswin|mingw/ && RUBY_ENGINE != 'truffleruby' + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0') # Travis cannot compile ruby.h with C++ + gem 'faml' + end + gem 'stackprof' + end +end diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..fe29eae --- /dev/null +++ b/LICENSE.txt @@ -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 index 0000000..ba46141 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# 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 **2.39x 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.8/benchmarks/run-benchmarks.rb) for fairness. ([Result on Travis](https://travis-ci.org/k0kubun/hamlit/jobs/236567391)) + +Hamlit Benchmark + +``` + hamlit v2.8.1: 131048.9 i/s + erubi v1.6.0: 125445.4 i/s - 1.04x slower + slim v3.0.8: 121390.4 i/s - 1.08x slower + faml v0.8.1: 100750.5 i/s - 1.30x slower + haml v5.0.1: 54882.6 i/s - 2.39x 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. + +#### 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". + +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 << ("\n".freeze); _buf = _buf.join + +# Render html +$ hamlit render in.haml + +``` + +## 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 + +### Coding styles + +Please follow the existing coding styles and do not send patches including cosmetic changes. + +## License + +Copyright (c) 2015 Takashi Kokubun diff --git a/REFERENCE.md b/REFERENCE.md new file mode 100644 index 0000000..15ea8cd --- /dev/null +++ b/REFERENCE.md @@ -0,0 +1,281 @@ +# 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. + +- [ ] Using Haml + - [x] Rails XSS Protection + - [x] Ruby Module + - [x] Options + - [ ] Encodings +- [x] Plain Text + - [x] Escaping: \ +- [ ] HTML Elements + - [x] Element Name: % + - [ ] 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: != +- [ ] Filters + - [x] :cdata + - [x] :coffee + - [x] :css + - [x] :erb + - [x] :escaped + - [x] :javascript + - [x] :less + - [x] :markdown + - [ ] :maruku + - [x] :plain + - [x] :preserve + - [x] :ruby + - `haml_io` API is not supported. Use [hamlit-haml\_io.gem](https://github.com/hamlit/hamlit-haml_io) if you need. + - [x] :sass + - [x] :scss + - [ ] :textile + - [ ] Custom Filters +- [x] Helper Methods + - [x] preserve + - [x] surround + - [x] precede + - [x] succeed +- [x] Multiline: | +- [x] Whitespace Preservation +- [ ] Helpers + + +## Limitations + +### 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 ``, whatever foo is. +In Hamlit, this feature is supported only for aria and data attribute. Hamlit renders `%a{ data: { foo: 'bar' } }` +as `` 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 ``, 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, aria-\* and data-\* is also regarded as boolean. + +Since `foo` is not boolean attribute, `%a{ foo: false }` is rendered as `` +This is the same behavior as Rails helpers. Also for `%a{ foo: nil }`, +Hamlit does not remove non-boolean attributes and render `` +(`foo` is not removed). This design allows us to reduce string concatenation and +is the only difference between Faml and Hamlit. + +You may be also interested in +[hamlit/hamlit-boolean\_attributes](https://github.com/hamlit/hamlit-boolean_attributes). + +## 5 Types of Attributes + +Haml has 3 types of attributes: id, class and others. +In addition, Hamlit treats aria/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 +
+
+
+
+``` + +### 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 +
+
+
+
+``` + +### aria / data attribute +Completely compatible with Haml, hyphenation and boolean are supported. + +```rb +# Input +%div{ data: { disabled: true } } +%div{ data: { foo: 'bar' } } + +# Output +
+
+``` + +aria attribute works in the same way as data attribute. + +### boolean attributes +No hyphenation but complete boolean support. + +```rb +# Input +%div{ disabled: 'foo' } +%div{ disabled: true } +%div{ disabled: false } + +# Output +
+
+
+``` + +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 +``` + +If you want to customize the list of boolean attributes, you can use +[hamlit/hamlit-boolean\_attributes](https://github.com/hamlit/hamlit-boolean_attributes). + +"aria-\*" and "data-\*" are 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 + + +``` + +## 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: '"' } +``` + +## Ruby module + +`Hamlit::Template` is a module registered to `Tilt`. You can use it like: + +```rb +Hamlit::Template.new { "%strong Yay for HAML!" }.render +``` + +## Creating a custom filter + +Currently it doesn't have filter registering interface compatible with Haml. +But you can easily define and register a filter using Tilt like this. + +```rb +module Hamlit + class Filters + class Es6 < TiltBase + def compile(node) + # branch with `@format` here if you want + compile_html(node) + end + + private + + def compile_html(node) + temple = [:multi] + temple << [:static, ""] + temple + end + end + + register :es6, Es6 + end +end +``` + +After requiring the script, you can do: + +```haml +:es6 + const a = 1; +``` + +and it's rendered as: + +```html + +``` diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..a8554ff --- /dev/null +++ b/Rakefile @@ -0,0 +1,117 @@ +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' +if /java/ === RUBY_PLATFORM + # require 'rake/javaextensiontask' + # Rake::JavaExtensionTask.new(:hamlit) do |ext| + # ext.ext_dir = 'ext/java' + # ext.lib_dir = 'lib/hamlit' + # end + + task :compile do + # dummy for now + end +else + require 'rake/extensiontask' + Rake::ExtensionTask.new(:hamlit) do |ext| + ext.lib_dir = 'lib/hamlit' + end +end + +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 + +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 index 0000000..82587a6 --- /dev/null +++ b/benchmark/boolean_attribute.haml @@ -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 index 0000000..2faaf7e --- /dev/null +++ b/benchmark/class_attribute.haml @@ -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 index 0000000..7516da9 --- /dev/null +++ b/benchmark/common_attribute.haml @@ -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 index 0000000..b2ba20c --- /dev/null +++ b/benchmark/data_attribute.haml @@ -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 index 0000000..f619d53 --- /dev/null +++ b/benchmark/dynamic_attributes/boolean_attribute.haml @@ -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 index 0000000..3c750de --- /dev/null +++ b/benchmark/dynamic_attributes/class_attribute.haml @@ -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 index 0000000..9c3e19f --- /dev/null +++ b/benchmark/dynamic_attributes/common_attribute.haml @@ -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 index 0000000..a53d89b --- /dev/null +++ b/benchmark/dynamic_attributes/data_attribute.haml @@ -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 index 0000000..2969893 --- /dev/null +++ b/benchmark/dynamic_attributes/id_attribute.haml @@ -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 index 0000000..e8b9c90 --- /dev/null +++ b/benchmark/dynamic_boolean_attribute.haml @@ -0,0 +1,4 @@ +- disabled = false +%input{ disabled: disabled } +- disabled = true +%input{ disabled: disabled } diff --git a/benchmark/dynamic_merger/benchmark.rb b/benchmark/dynamic_merger/benchmark.rb new file mode 100644 index 0000000..220281f --- /dev/null +++ b/benchmark/dynamic_merger/benchmark.rb @@ -0,0 +1,25 @@ +# Original: https://github.com/amatsuda/string_template/blob/master/benchmark.rb +require 'benchmark_driver' + +Benchmark.driver(repeat_count: 8) do |x| + x.prelude %{ + require 'rails' + require 'action_view' + require 'string_template' + StringTemplate::Railtie.run_initializers + require 'hamlit' + Hamlit::Railtie.run_initializers + Hamlit::RailsTemplate.set_options(escape_html: false, generator: Temple::Generators::ArrayBuffer) + require 'action_view/base' + + (view = Class.new(ActionView::Base).new(ActionView::LookupContext.new(''))).instance_variable_set(:@world, 'world!') + + # compile template + hello = 'benchmark/dynamic_merger/hello' + view.render(template: hello, handlers: 'string') + view.render(template: hello, handlers: 'haml') + } + x.report 'string', %{ view.render(template: hello, handlers: 'string') } + x.report 'hamlit', %{ view.render(template: hello, handlers: 'haml') } + x.loop_count 100_000 +end diff --git a/benchmark/dynamic_merger/hello.haml b/benchmark/dynamic_merger/hello.haml new file mode 100644 index 0000000..57755b0 --- /dev/null +++ b/benchmark/dynamic_merger/hello.haml @@ -0,0 +1,50 @@ +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } diff --git a/benchmark/dynamic_merger/hello.string b/benchmark/dynamic_merger/hello.string new file mode 100644 index 0000000..57755b0 --- /dev/null +++ b/benchmark/dynamic_merger/hello.string @@ -0,0 +1,50 @@ +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } +hello, #{ @world } diff --git a/benchmark/etc/attribute_builder.haml b/benchmark/etc/attribute_builder.haml new file mode 100644 index 0000000..7c607e1 --- /dev/null +++ b/benchmark/etc/attribute_builder.haml @@ -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 index 0000000..1119690 --- /dev/null +++ b/benchmark/etc/real_sample.haml @@ -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 index 0000000..5cefec7 --- /dev/null +++ b/benchmark/etc/real_sample.rb @@ -0,0 +1,11 @@ +def render(*) + '
' +end + +def link_to(a, b, *c) + "'.freeze +end + +def image_tag(*) + '' +end diff --git a/benchmark/etc/static_analyzer.haml b/benchmark/etc/static_analyzer.haml new file mode 100644 index 0000000..210a551 --- /dev/null +++ b/benchmark/etc/static_analyzer.haml @@ -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 index 0000000..6ccfe11 --- /dev/null +++ b/benchmark/etc/string_interpolation.haml @@ -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 index 0000000..49c139d --- /dev/null +++ b/benchmark/etc/tags.haml @@ -0,0 +1,3 @@ +%span hello +%div + world diff --git a/benchmark/etc/tags_loop.haml b/benchmark/etc/tags_loop.haml new file mode 100644 index 0000000..e5cf716 --- /dev/null +++ b/benchmark/etc/tags_loop.haml @@ -0,0 +1,2 @@ +- 100.times do + %span hello diff --git a/benchmark/ext/build_data.rb b/benchmark/ext/build_data.rb new file mode 100755 index 0000000..5d47242 --- /dev/null +++ b/benchmark/ext/build_data.rb @@ -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 index 0000000..0551c74 --- /dev/null +++ b/benchmark/ext/build_id.rb @@ -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 index 0000000..af6acf6 --- /dev/null +++ b/benchmark/id_attribute.haml @@ -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 index 0000000..4c7cdc3 --- /dev/null +++ b/benchmark/plain.haml @@ -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 index 0000000..f318d7b --- /dev/null +++ b/benchmark/script.haml @@ -0,0 +1,4 @@ +- dynamic = 'dynamic' += "#{ dynamic } script" += "#{ 'static'} script" += ['&', '"', "'", '<', '>'] diff --git a/benchmark/slim/LICENSE b/benchmark/slim/LICENSE new file mode 100644 index 0000000..6af6518 --- /dev/null +++ b/benchmark/slim/LICENSE @@ -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 index 0000000..4d71e34 --- /dev/null +++ b/benchmark/slim/context.rb @@ -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 index 0000000..3f59cc1 --- /dev/null +++ b/benchmark/slim/run-benchmarks.rb @@ -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 'erubi' +require 'erb' +require 'haml' +require 'faml' +require 'hamlit' + +class SlimBenchmarks + def initialize(only_haml) + @only_haml = only_haml + @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 + context = Context.new + + haml_ugly = Haml::Engine.new(@haml_code, format: :html5, escape_html: true) + haml_ugly.def_method(context, :run_haml_ugly) + context.instance_eval %{ + def run_erubi; #{Erubi::Engine.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("erubi v#{Erubi::VERSION}") { context.run_erubi } unless @only_haml + bench("slim v#{Slim::VERSION}") { context.run_slim_ugly } unless @only_haml + 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['ONLY_HAML'] == '1').run diff --git a/benchmark/slim/view.erb b/benchmark/slim/view.erb new file mode 100644 index 0000000..3ffc829 --- /dev/null +++ b/benchmark/slim/view.erb @@ -0,0 +1,23 @@ + + + + + Simple Benchmark + + +

<%== header %>

+ <% unless item.empty? %> +
+ <% else %> +

The list is empty.

+ <% end %> + + diff --git a/benchmark/slim/view.haml b/benchmark/slim/view.haml new file mode 100644 index 0000000..2e37c85 --- /dev/null +++ b/benchmark/slim/view.haml @@ -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 index 0000000..9853bc7 --- /dev/null +++ b/benchmark/slim/view.slim @@ -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 index 0000000..a8fbe1e --- /dev/null +++ b/benchmark/utils/benchmark_ips_extension.rb @@ -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 index 0000000..93ca8b5 --- /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).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).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).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 index 0000000..9249aa5 --- /dev/null +++ b/bin/console @@ -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/ruby b/bin/ruby new file mode 100755 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 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 index 0000000..76078d4 --- /dev/null +++ b/bin/stackprof @@ -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 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 index 0000000..0978976 --- /dev/null +++ b/exe/hamlit @@ -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 index 0000000..182716d --- /dev/null +++ b/ext/hamlit/extconf.rb @@ -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 index 0000000..1169451 --- /dev/null +++ b/ext/hamlit/hamlit.c @@ -0,0 +1,555 @@ +#include +#include +#ifndef TRUFFLERUBY +#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_aria, id_data, id_equal, id_hyphen, id_space, id_underscore; +static ID id_boolean_attributes, id_xhtml; + +static VALUE str_aria() { return rb_const_get(mAttributeBuilder, id_aria); } +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); + } +} + +struct merge_data_attrs_var { + VALUE merged; + VALUE key_str; +}; + +static int +merge_data_attrs_i(VALUE key, VALUE value, VALUE ptr) +{ + struct merge_data_attrs_var *arg = (struct merge_data_attrs_var *)ptr; + VALUE merged = arg->merged; + VALUE key_str = arg->key_str; + + if (NIL_P(key)) { + rb_hash_aset(merged, key_str, value); + } else { + key = rb_str_concat(rb_str_concat(rb_str_dup(key_str), rb_str_new_cstr("-")), to_s(key)); + rb_hash_aset(merged, key, value); + } + return ST_CONTINUE; +} + +static VALUE +merge_data_attrs(VALUE values, VALUE key_str) +{ + long i; + VALUE value, merged = rb_hash_new(); + + for (i = 0; i < RARRAY_LEN(values); i++) { + struct merge_data_attrs_var arg; + arg.merged = merged; + arg.key_str = key_str; + + value = rb_ary_entry(values, i); + switch (TYPE(value)) { + case T_HASH: + rb_hash_foreach(value, merge_data_attrs_i, (VALUE)&arg); + break; + default: + rb_hash_aset(merged, key_str, 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, VALUE key_str) +{ + long i; + VALUE attrs, buf, keys, key, value; + + attrs = merge_data_attrs(values, key_str); + 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) || str_eq(key, "aria", 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); + if (!RB_TYPE_P(hash, T_HASH)) { + rb_raise(rb_eArgError, "Non-hash object is given to attributes!"); + } + 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; + if (str_eq(rb_str_substr(key, 0, 5), "aria-", 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, str_data())); +} + +void +hamlit_build_for_aria(VALUE escape_attrs, VALUE quote, VALUE buf, VALUE values) +{ + rb_str_concat(buf, hamlit_build_data(escape_attrs, quote, values, str_aria())); +} + +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 (str_eq(key, "aria", 4)) { + hamlit_build_for_aria(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_aria(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, str_aria()); +} + +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, str_data()); +} + +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_aria", rb_hamlit_build_aria, -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_aria = rb_intern("ARIA"); + 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_aria, rb_obj_freeze(rb_str_new_cstr("aria"))); + 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("_"))); +} +#endif diff --git a/ext/hamlit/hescape.c b/ext/hamlit/hescape.c new file mode 100644 index 0000000..3400cba --- /dev/null +++ b/ext/hamlit/hescape.c @@ -0,0 +1,108 @@ +#include +#include +#include +#include "hescape.h" + +static const char *ESCAPED_STRING[] = { + "", + """, + "&", + "'", + "<", + ">", +}; + +// 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 (") + * & (38) => 2 (&) + * ' (39) => 3 (') + * < (60) => 4 (<) + * > (62) => 5 (>) + */ +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 = 0, 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 index 0000000..df18f4b --- /dev/null +++ b/ext/hamlit/hescape.h @@ -0,0 +1,20 @@ +#ifndef HESCAPE_H +#define HESCAPE_H + +#include + +/* + * Replace characters according to the following rules. + * Note that this function can handle only ASCII-compatible string. + * + * " => " + * & => & + * ' => ' + * < => < + * > => > + * + * @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 index 0000000..b58c5ce --- /dev/null +++ b/hamlit.gemspec @@ -0,0 +1,47 @@ +# 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.require_paths = ['lib'] + + if /java/ === RUBY_PLATFORM + spec.platform = 'java' + else + spec.extensions = ['ext/hamlit/extconf.rb'] + spec.required_ruby_version = '>= 2.1.0' + end + + spec.add_dependency 'temple', '>= 0.8.2' + spec.add_dependency 'thor' + spec.add_dependency 'tilt' + + spec.add_development_dependency 'benchmark_driver' + spec.add_development_dependency 'bundler' + spec.add_development_dependency 'coffee-script' + spec.add_development_dependency 'erubi' + spec.add_development_dependency 'haml', '>= 5' + 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' + spec.add_development_dependency 'rake-compiler' + spec.add_development_dependency 'sass' + spec.add_development_dependency 'slim' + spec.add_development_dependency 'string_template' + spec.add_development_dependency 'unindent' +end diff --git a/lib/hamlit.rb b/lib/hamlit.rb new file mode 100644 index 0000000..bcfa264 --- /dev/null +++ b/lib/hamlit.rb @@ -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 index 0000000..81072de --- /dev/null +++ b/lib/hamlit/attribute_builder.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true +require 'hamlit/object_ref' + +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 + + # Java extension is not implemented for JRuby yet. + # TruffleRuby does not implement `rb_ary_sort_bang`, etc. + if /java/ === RUBY_PLATFORM || RUBY_ENGINE == 'truffleruby' + class << self + def build(escape_attrs, quote, format, object_ref, *hashes) + hashes << Hamlit::ObjectRef.parse(object_ref) if object_ref + buf = [] + hash = merge_all_attrs(hashes) + + keys = hash.keys.sort! + keys.each do |key| + case key + when 'id'.freeze + buf << " id=#{quote}#{build_id(escape_attrs, *hash[key])}#{quote}" + when 'class'.freeze + buf << " class=#{quote}#{build_class(escape_attrs, *hash[key])}#{quote}" + when 'data'.freeze + buf << build_data(escape_attrs, quote, *hash[key]) + when *BOOLEAN_ATTRIBUTES, /\Adata-/ + build_boolean!(escape_attrs, quote, format, buf, key, hash[key]) + else + buf << " #{key}=#{quote}#{escape_html(escape_attrs, hash[key].to_s)}#{quote}" + end + end + buf.join + end + + def build_id(escape_attrs, *values) + escape_html(escape_attrs, values.flatten.select { |v| v }.join('_')) + end + + def build_class(escape_attrs, *values) + if values.size == 1 + value = values.first + case + when value.is_a?(String) + # noop + when value.is_a?(Array) + value = value.flatten.select { |v| v }.map(&:to_s).sort.uniq.join(' ') + when value + value = value.to_s + else + return '' + end + return escape_html(escape_attrs, value) + end + + classes = [] + values.each do |value| + case + when value.is_a?(String) + classes += value.split(' ') + when value.is_a?(Array) + classes += value.select { |v| v } + when value + classes << value.to_s + end + end + escape_html(escape_attrs, classes.map(&:to_s).sort.uniq.join(' ')) + end + + def build_data(escape_attrs, quote, *hashes) + build_data_attribute(:data, escape_attrs, quote, *hashes) + end + + def build_aria(escape_attrs, quote, *hashes) + build_data_attribute(:aria, escape_attrs, quote, *hashes) + end + + private + + def build_data_attribute(key, escape_attrs, quote, *hashes) + attrs = [] + if hashes.size > 1 && hashes.all? { |h| h.is_a?(Hash) } + data_value = merge_all_attrs(hashes) + else + data_value = hashes.last + end + hash = flatten_attributes(key => data_value) + + hash.sort_by(&:first).each do |key, value| + case value + when true + attrs << " #{key}" + when nil, false + # noop + else + attrs << " #{key}=#{quote}#{escape_html(escape_attrs, value.to_s)}#{quote}" + end + end + attrs.join + end + + def flatten_attributes(attributes) + flattened = {} + + attributes.each do |key, value| + case value + when attributes + when Hash + flatten_attributes(value).each do |k, v| + if k.nil? + flattened[key] = v + else + flattened["#{key}-#{k.to_s.gsub(/_/, '-')}"] = v + end + end + else + flattened[key] = value if value + end + end + flattened + end + + def merge_all_attrs(hashes) + merged = {} + hashes.each do |hash| + hash.each do |key, value| + key = key.to_s + case key + when 'id'.freeze, 'class'.freeze, 'data'.freeze + merged[key] ||= [] + merged[key] << value + else + merged[key] = value + end + end + end + merged + end + + def build_boolean!(escape_attrs, quote, format, buf, key, value) + case value + when true + case format + when :xhtml + buf << " #{key}=#{quote}#{key}#{quote}" + else + buf << " #{key}" + end + when false, nil + # omitted + else + buf << " #{key}=#{quote}#{escape_html(escape_attrs, value)}#{quote}" + end + end + + def escape_html(escape_attrs, str) + if escape_attrs + Hamlit::Utils.escape_html(str) + else + str + end + end + end + else + # Hamlit::AttributeBuilder.build + # Hamlit::AttributeBuilder.build_id + # Hamlit::AttributeBuilder.build_class + # Hamlit::AttributeBuilder.build_data + # Hamlit::AttributeBuilder.build_aria + require 'hamlit/hamlit' + end +end diff --git a/lib/hamlit/attribute_compiler.rb b/lib/hamlit/attribute_compiler.rb new file mode 100644 index 0000000..692b4c0 --- /dev/null +++ b/lib/hamlit/attribute_compiler.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true +require 'hamlit/attribute_builder' +require 'hamlit/attribute_parser' +require 'hamlit/ruby_expression' + +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 = [] + if node.value[:object_ref] != :nil || !Ripper.respond_to?(:lex) # No Ripper.lex in truffleruby + return runtime_compile(node) + end + 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.inspect, "#{@quote.inspect}.freeze", @format.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', 'aria' + compile_data!(temple, key, values) + when *AttributeBuilder::BOOLEAN_ATTRIBUTES, /\Adata-/, /\Aaria-/ + 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 || Temple::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 || Temple::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}.freeze", values.map { |v| literal_for(v) }] + build_code = "::Hamlit::AttributeBuilder.build_#{key}(#{args.join(', ')})" + + if values.all? { |type, exp| type == :static || Temple::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 Temple::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}.freeze" : exp + end + end +end diff --git a/lib/hamlit/attribute_parser.rb b/lib/hamlit/attribute_parser.rb new file mode 100644 index 0000000..6636b20 --- /dev/null +++ b/lib/hamlit/attribute_parser.rb @@ -0,0 +1,110 @@ +# 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 { |t| t[2] }.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 = [] + open_tokens = Hash.new { |h, k| h[k] = 0 } + + tokens.each do |token| + _, type, _ = token + case type + when :on_comma + if open_tokens.values.all?(&:zero?) + yield(attr_tokens) + attr_tokens = [] + next + end + when :on_lbracket + open_tokens[:array] += 1 + when :on_rbracket + open_tokens[:array] -= 1 + when :on_lbrace + open_tokens[:block] += 1 + when :on_rbrace + open_tokens[:block] -= 1 + when :on_lparen + open_tokens[:paren] += 1 + when :on_rparen + open_tokens[:paren] -= 1 + when :on_embexpr_beg + open_tokens[:embexpr] += 1 + when :on_embexpr_end + open_tokens[:embexpr] -= 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 index 0000000..29cb5b7 --- /dev/null +++ b/lib/hamlit/cli.rb @@ -0,0 +1,130 @@ +# 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] + option :color, type: :boolean, default: false, aliases: %w[-c] + def compile(file) + code = generate_code(file) + puts_code(code, color: options[:color]) + end + + desc 'temple HAML', 'Show temple intermediate expression' + option :color, type: :boolean, default: false, aliases: %w[-c] + def temple(file) + pp_object(generate_temple(file), color: options[:color]) + end + + desc 'parse HAML', 'Show parse result' + option :color, type: :boolean, default: false, aliases: %w[-c] + def parse(file) + pp_object(generate_ast(file), color: options[:color]) + 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 read_file(file) + if file == '-' + STDIN.read + else + File.read(file) + end + end + + def generate_code(file) + template = read_file(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) + <<-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 = read_file(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 puts_code(code, color: false) + if color + require 'pry' + puts Pry.Code(code).highlighted + else + puts code + end + end + + # Enable colored pretty printing only for development environment. + def pp_object(arg, color: false) + if color + require 'pry' + Pry::ColorPrinter.pp(arg) + else + require 'pp' + pp(arg) + end + end + end +end diff --git a/lib/hamlit/compiler.rb b/lib/hamlit/compiler.rb new file mode 100644 index 0000000..4f2584f --- /dev/null +++ b/lib/hamlit/compiler.rb @@ -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 index 0000000..507beae --- /dev/null +++ b/lib/hamlit/compiler/children_compiler.rb @@ -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 index 0000000..dcafb5d --- /dev/null +++ b/lib/hamlit/compiler/comment_compiler.rb @@ -0,0 +1,38 @@ +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 + + content = + if node.children.empty? + [:static, " #{node.value[:text]} "] + else + yield(node) + end + [:html, :condcomment, condition, content, node.value[:revealed]] + end + end + end +end diff --git a/lib/hamlit/compiler/doctype_compiler.rb b/lib/hamlit/compiler/doctype_compiler.rb new file mode 100644 index 0000000..5ad4299 --- /dev/null +++ b/lib/hamlit/compiler/doctype_compiler.rb @@ -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, "\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 index 0000000..18d4002 --- /dev/null +++ b/lib/hamlit/compiler/script_compiler.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true +require 'temple/static_analyzer' +require 'hamlit/ruby_expression' +require 'hamlit/string_splitter' + +module Hamlit + class Compiler + class ScriptCompiler + def initialize(identity) + @identity = identity + end + + def compile(node, &block) + unless Ripper.respond_to?(:lex) # No Ripper.lex in truffleruby + return dynamic_compile(node, &block) + end + + 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 && Temple::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 index 0000000..29bed67 --- /dev/null +++ b/lib/hamlit/compiler/silent_script_compiler.rb @@ -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 index 0000000..8e03e2b --- /dev/null +++ b/lib/hamlit/compiler/tag_compiler.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true +require 'hamlit/parser/haml_util' +require 'hamlit/attribute_compiler' +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] + if Ripper.respond_to?(:lex) # No Ripper.lex in truffleruby + return delegate_optimization(node) if RubyExpression.string_literal?(node.value[:value]) + return delegate_optimization(node) if Temple::StaticAnalyzer.static?(node.value[:value]) + end + + 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/dynamic_merger.rb b/lib/hamlit/dynamic_merger.rb new file mode 100644 index 0000000..35cfa11 --- /dev/null +++ b/lib/hamlit/dynamic_merger.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true +module Hamlit + # Compile [:multi, [:static, 'foo'], [:dynamic, 'bar']] to [:dynamic, '"foo#{bar}"'] + class DynamicMerger < Temple::Filter + def on_multi(*exps) + exps = exps.dup + result = [:multi] + buffer = [] + + until exps.empty? + type, arg = exps.first + if type == :dynamic && arg.count("\n") == 0 + buffer << exps.shift + elsif type == :static && exps.size > (count = arg.count("\n")) && + exps[1, count].all? { |e| e == [:newline] } + (1 + count).times { buffer << exps.shift } + elsif type == :newline && exps.size > (count = count_newline(exps)) && + exps[count].first == :static && count == exps[count].last.count("\n") + (count + 1).times { buffer << exps.shift } + else + result.concat(merge_dynamic(buffer)) + buffer = [] + result << compile(exps.shift) + end + end + result.concat(merge_dynamic(buffer)) + + result.size == 2 ? result[1] : result + end + + private + + def merge_dynamic(exps) + # Merge exps only when they have both :static and :dynamic + unless exps.any? { |type,| type == :static } && exps.any? { |type,| type == :dynamic } + return exps + end + + strlit_body = String.new + exps.each do |type, arg| + case type + when :static + strlit_body << arg.dump.sub!(/\A"/, '').sub!(/"\z/, '').gsub('\n', "\n") + when :dynamic + strlit_body << "\#{#{arg}}" + when :newline + # newline is added by `gsub('\n', "\n")` + else + raise "unexpected type #{type.inspect} is given to #merge_dynamic" + end + end + [[:dynamic, "%Q\0#{strlit_body}\0"]] + end + + def count_newline(exps) + count = 0 + exps.each do |exp| + if exp == [:newline] + count += 1 + else + return count + end + end + return count + end + end +end diff --git a/lib/hamlit/engine.rb b/lib/hamlit/engine.rb new file mode 100644 index 0000000..069f96b --- /dev/null +++ b/lib/hamlit/engine.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require 'temple' +require 'hamlit/parser' +require 'hamlit/compiler' +require 'hamlit/html' +require 'hamlit/escapable' +require 'hamlit/force_escapable' +require 'hamlit/dynamic_merger' + +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 + filter :StringSplitter + filter :StaticAnalyzer + use Escapable + use ForceEscapable + filter :ControlFlow + filter :MultiFlattener + filter :StaticMerger + use DynamicMerger + use :Generator, -> { options[:generator] } + end +end diff --git a/lib/hamlit/error.rb b/lib/hamlit/error.rb new file mode 100644 index 0000000..85989f2 --- /dev/null +++ b/lib/hamlit/error.rb @@ -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 index 0000000..53f71af --- /dev/null +++ b/lib/hamlit/escapable.rb @@ -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 index 0000000..d81f338 --- /dev/null +++ b/lib/hamlit/filters.rb @@ -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 index 0000000..d98d457 --- /dev/null +++ b/lib/hamlit/filters/base.rb @@ -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 index 0000000..c90f143 --- /dev/null +++ b/lib/hamlit/filters/cdata.rb @@ -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, ""] + temple + end + end + end +end diff --git a/lib/hamlit/filters/coffee.rb b/lib/hamlit/filters/coffee.rb new file mode 100644 index 0000000..5aee191 --- /dev/null +++ b/lib/hamlit/filters/coffee.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +module Hamlit + class Filters + class Coffee < TiltBase + def compile(node) + require 'tilt/coffee' if explicit_require?('coffee') + temple = [:multi] + temple << [:static, ""] + 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 index 0000000..db7769b --- /dev/null +++ b/lib/hamlit/filters/css.rb @@ -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, ""] + temple + end + + def compile_xhtml(node) + temple = [:multi] + temple << [:static, ""] + temple + end + end + end +end diff --git a/lib/hamlit/filters/erb.rb b/lib/hamlit/filters/erb.rb new file mode 100644 index 0000000..67d9297 --- /dev/null +++ b/lib/hamlit/filters/erb.rb @@ -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 index 0000000..e26c533 --- /dev/null +++ b/lib/hamlit/filters/escaped.rb @@ -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 index 0000000..57ec162 --- /dev/null +++ b/lib/hamlit/filters/javascript.rb @@ -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, ""] + temple + end + + def compile_xhtml(node) + temple = [:multi] + temple << [:static, ""] + temple + end + end + end +end diff --git a/lib/hamlit/filters/less.rb b/lib/hamlit/filters/less.rb new file mode 100644 index 0000000..c5530a8 --- /dev/null +++ b/lib/hamlit/filters/less.rb @@ -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?('less') + temple = [:multi] + temple << [:static, "'] + temple + end + end + end +end diff --git a/lib/hamlit/filters/markdown.rb b/lib/hamlit/filters/markdown.rb new file mode 100644 index 0000000..09a5697 --- /dev/null +++ b/lib/hamlit/filters/markdown.rb @@ -0,0 +1,10 @@ +module Hamlit + class Filters + class Markdown < TiltBase + def compile(node) + require 'tilt/redcarpet' if explicit_require?('markdown') + 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 index 0000000..9fa3fae --- /dev/null +++ b/lib/hamlit/filters/plain.rb @@ -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 index 0000000..423d53a --- /dev/null +++ b/lib/hamlit/filters/preserve.rb @@ -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", ' ') + 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 index 0000000..e6dccd9 --- /dev/null +++ b/lib/hamlit/filters/ruby.rb @@ -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 index 0000000..0f1d715 --- /dev/null +++ b/lib/hamlit/filters/sass.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +module Hamlit + class Filters + class Sass < TiltBase + def compile(node) + require 'tilt/sass' if explicit_require?('sass') + temple = [:multi] + temple << [:static, ""] + temple + end + end + end +end diff --git a/lib/hamlit/filters/scss.rb b/lib/hamlit/filters/scss.rb new file mode 100644 index 0000000..e74e48c --- /dev/null +++ b/lib/hamlit/filters/scss.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +module Hamlit + class Filters + class Scss < TiltBase + def compile(node) + require 'tilt/sass' if explicit_require?('scss') + temple = [:multi] + temple << [:static, ""] + 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 index 0000000..c269792 --- /dev/null +++ b/lib/hamlit/filters/text_base.rb @@ -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 index 0000000..1ab2ccb --- /dev/null +++ b/lib/hamlit/filters/tilt_base.rb @@ -0,0 +1,49 @@ +# 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?(needed_registration) + Gem::Version.new(Tilt::VERSION) >= Gem::Version.new('2.0.0') && + !Tilt.registered?(needed_registration) + 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 index 0000000..6b52638 --- /dev/null +++ b/lib/hamlit/force_escapable.rb @@ -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 index 0000000..48e3367 --- /dev/null +++ b/lib/hamlit/helpers.rb @@ -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/, ' ') + s.delete!("\r") + s + end + end +end diff --git a/lib/hamlit/html.rb b/lib/hamlit/html.rb new file mode 100644 index 0000000..ee056be --- /dev/null +++ b/lib/hamlit/html.rb @@ -0,0 +1,22 @@ +# 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 + + # This dispatcher supports Haml's "revealed" conditional comment. + def on_html_condcomment(condition, content, revealed = false) + on_html_comment [:multi, + [:static, "[#{condition}]>#{'' if revealed}"], + content, + [:static, "#{'' if revealed}" + + close = "#{'" + + 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 "" + end + + if @options.html5? + '' + else + if @options.xhtml? + if @node.value[:version] == "1.1" + '' + elsif @node.value[:version] == "5" + '' + else + case @node.value[:type] + when "strict"; '' + when "frameset"; '' + when "mobile"; '' + when "rdfa"; '' + when "basic"; '' + else '' + end + end + + elsif @options.html4? + case @node.value[:type] + when "strict"; '' + when "frameset"; '' + else '' + 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 == '"' ? """ : "'" + 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!(/"|"/, '"') + 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 index 0000000..73f507e --- /dev/null +++ b/lib/hamlit/parser/haml_error.rb @@ -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 index 0000000..9188665 --- /dev/null +++ b/lib/hamlit/parser/haml_helpers.rb @@ -0,0 +1,727 @@ +require 'hamlit/parser/haml_error' +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 = < 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] 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)}" + 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/, ' ') + 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 `
  • ` elements. + # This creates a list of the results of the block. + # For example: + # + # = list_of([['hello'], ['yall']]) do |i| + # = i[0] + # + # Produces: + # + #
  • hello
  • + #
  • yall
  • + # + # And: + # + # = list_of({:title => 'All the stuff', :description => 'A book about all the stuff.'}) do |key, val| + # %h3= key.humanize + # %p= val + # + # Produces: + # + #
  • + #

    Title

    + #

    All the stuff

    + #
  • + #
  • + #

    Description

    + #

    A book about all the stuff.

    + #
  • + # + # While: + # + # = list_of(["Home", "About", "Contact", "FAQ"], {class: "nav", role: "nav"}) do |item| + # %a{ href="#" }= item + # + # Produces: + # + # + # + # + # + # + # `[[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!#{result}! + 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 + # + # + # + # @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: + # + #

    foo

    + #

    bar

    + # baz + # + # @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: + # + #

    foo

    + #

    bar

    + # baz + # + # + # @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: + # + # (chicken) + # + # and + # + # = surround '*' do + # %strong angry + # + # Produces: + # + # *angry* + # + # @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: + # + # *Not really + # + # @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 + # here. + # + # @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 `"

    13

    \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 + # + # + # + # + # + # + #
    + # + # strong! + # + # data + # + # more_data + #
    + # + # @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] 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 = "" + 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 + # + #
    + #

    + # A (possibly) important paragraph. + #

    + #
    + # + # if `important` is truthy, and just + # + #

    + # A (possibly) important paragraph. + #

    + # + # 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 = { '&' => '&', '<' => '<', '>' => '>', '"' => '"', "'" => ''' } + + 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 + alias :is_haml? :is_haml? +end diff --git a/lib/hamlit/parser/haml_options.rb b/lib/hamlit/parser/haml_options.rb new file mode 100644 index 0000000..dbf447d --- /dev/null +++ b/lib/hamlit/parser/haml_options.rb @@ -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 `'`) 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 ``. + # 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 index 0000000..bf39d7b --- /dev/null +++ b/lib/hamlit/parser/haml_parser.rb @@ -0,0 +1,800 @@ +require 'strscan' +require 'hamlit/parser/haml_util' +require 'hamlit/parser/haml_error' + +module Hamlit + class HamlParser + include ::Hamlit::HamlUtil + + attr_reader :root + + # Designates an XHTML/XML element. + ELEMENT = ?% + + # Designates a `
    ` element with the given class. + DIV_CLASS = ?. + + # Designates a `
    ` 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 index 0000000..447ccc1 --- /dev/null +++ b/lib/hamlit/parser/haml_util.rb @@ -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 < "\"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 index 0000000..4995ca5 --- /dev/null +++ b/lib/hamlit/parser/haml_xss_mods.rb @@ -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 index 0000000..a71b4ff --- /dev/null +++ b/lib/hamlit/rails_helpers.rb @@ -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)}" + 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 index 0000000..863528b --- /dev/null +++ b/lib/hamlit/rails_template.rb @@ -0,0 +1,59 @@ +# 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, source = nil) + source ||= template.source + 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(source) + end + + def supports_streaming? + RailsTemplate.options[:streaming] + 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 index 0000000..4139d78 --- /dev/null +++ b/lib/hamlit/railtie.rb @@ -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 index 0000000..5aecb01 --- /dev/null +++ b/lib/hamlit/ruby_expression.rb @@ -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/string_splitter.rb b/lib/hamlit/string_splitter.rb new file mode 100644 index 0000000..2012b7a --- /dev/null +++ b/lib/hamlit/string_splitter.rb @@ -0,0 +1,19 @@ +require 'ripper' +require 'hamlit/ruby_expression' + +module Hamlit + module StringSplitter + # `code` param must be valid string literal + def self.compile(code) + unless Ripper.respond_to?(:lex) # truffleruby doesn't have Ripper.lex + return [[:dynamic, code]] + end + + begin + Temple::Filters::StringSplitter.compile(code) + rescue Temple::FilterError => e + raise Hamlit::InternalError.new(e.message) + end + end + end +end diff --git a/lib/hamlit/template.rb b/lib/hamlit/template.rb new file mode 100644 index 0000000..7fb93a1 --- /dev/null +++ b/lib/hamlit/template.rb @@ -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, :hamlit], + ) + + 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 index 0000000..e23ed0a --- /dev/null +++ b/lib/hamlit/utils.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module Hamlit + module Utils + # Java extension is not implemented for JRuby yet. + # TruffleRuby does not implement `rb_ary_sort_bang`, etc. + if /java/ === RUBY_PLATFORM || RUBY_ENGINE == 'truffleruby' + require 'cgi/escape' + + def self.escape_html(html) + CGI.escapeHTML(html.to_s) + end + else + require 'hamlit/hamlit' # Hamlit::Utils.escape_html + end + + 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 index 0000000..53d02ff --- /dev/null +++ b/lib/hamlit/version.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true +module Hamlit + VERSION = '2.11.0' +end diff --git a/test/haml/MIT-LICENSE b/test/haml/MIT-LICENSE new file mode 100644 index 0000000..758713a --- /dev/null +++ b/test/haml/MIT-LICENSE @@ -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 index 0000000..9255e60 --- /dev/null +++ b/test/haml/README.md @@ -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 index 0000000..e953596 --- /dev/null +++ b/test/haml/engine_test.rb @@ -0,0 +1,2108 @@ +$:.unshift __dir__ + +require_relative '../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("
    ", render(".atlantis{:style => 'ugly'}").chomp) + end + + def test_css_id_as_attribute_should_be_appended_with_underscore + assert_equal("
    ", render("#my_id{:id => '1'}").chomp) + assert_equal("
    ", render("#my_id{:id => 1}").chomp) + end + + def test_ruby_code_should_work_inside_attributes + assert_equal("

    foo

    ", render("%p{:class => 1+2} foo").chomp) + end + + def test_class_attr_with_array + assert_equal("

    foo

    \n", render("%p{:class => %w[a b]} foo")) # basic + assert_equal("

    foo

    \n", render("%p.css{:class => %w[a b]} foo")) # merge with css + assert_equal("

    foo

    \n", render("%p.css{:class => %w[css b]} foo")) # merge uniquely + assert_equal("

    foo

    \n", render("%p{:class => [%w[a b], %w[c d]]} foo")) # flatten + assert_equal("

    foo

    \n", render("%p{:class => [:a, :b] } foo")) # stringify + # [INCOMPATIBILITY] Hamlit limits boolean attributes + # assert_equal("

    foo

    \n", render("%p{:class => [nil, false] } foo")) # strip falsey + assert_equal("

    foo

    \n", render("%p{:class => [nil, false] } foo")) # strip falsey + assert_equal("

    foo

    \n", render("%p{:class => :a} foo")) # single stringify + # [INCOMPATIBILITY] Hamlit limits boolean attributes + # assert_equal("

    foo

    \n", render("%p{:class => false} foo")) # single falsey + assert_equal("

    foo

    \n", render("%p{:class => false} foo")) # single falsey + assert_equal("

    foo

    \n", render("%p(class='html'){:class => %w[a b]} foo")) # html attrs + end + + def test_id_attr_with_array + assert_equal("

    foo

    \n", render("%p{:id => %w[a b]} foo")) # basic + assert_equal("

    foo

    \n", render("%p#css{:id => %w[a b]} foo")) # merge with css + assert_equal("

    foo

    \n", render("%p{:id => [%w[a b], %w[c d]]} foo")) # flatten + assert_equal("

    foo

    \n", render("%p{:id => [:a, :b] } foo")) # stringify + # [INCOMPATIBILITY] Hamlit limits boolean attributes + # assert_equal("

    foo

    \n", render("%p{:id => [nil, false] } foo")) # strip falsey + assert_equal("

    foo

    \n", render("%p{:id => [nil, false] } foo")) # strip falsey + assert_equal("

    foo

    \n", render("%p{:id => :a} foo")) # single stringify + # [INCOMPATIBILITY] Hamlit limits boolean attributes + # assert_equal("

    foo

    \n", render("%p{:id => false} foo")) # single falsey + assert_equal("

    foo

    \n", render("%p{:id => false} foo")) # single falsey + assert_equal("

    foo

    \n", render("%p(id='html'){:id => %w[a b]} foo")) # html attrs + end + + def test_colon_in_class_attr + assert_equal("

    \n", render("%p.foo:bar/")) + end + + def test_colon_in_id_attr + assert_equal("

    \n", render("%p#foo:bar/")) + end + + def test_dynamic_attributes_with_no_content + assert_haml_ugly(< "http://" + "haml.info"} +HAML + end + + def test_attributes_with_to_s + assert_equal(<

    +

    +

    +

    +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("
    ", + # render(".no_attributes{:nil => nil}").chomp) + assert_equal("
    ", + render(".no_attributes{:nil => nil}").chomp) + end + + def test_strings_should_get_stripped_inside_tags + assert_equal("
    This should have no spaces in front of it
    ", + render(".stripped This should have no spaces in front of it").chomp) + end + + def test_one_liner_should_be_one_line + assert_equal("

    Hello

    ", 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("Hi there!\n", engine.render) + assert_equal("Hi there!\n", engine.render) + assert_equal("Hi there!\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("

    Hello World

    \n", render('%p Hello #@who', scope: scope, escape_html: false)) + assert_equal("

    \n Hello World\n

    \n", render("%p\n Hello \#@who", scope: scope, escape_html: false)) + assert_equal("

    Hello World

    \n", render('%p Hello #@who', scope: scope, escape_html: true)) + assert_equal("

    \n Hello World\n

    \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("

    Hello World

    \n", render('%p Hello #$global_var_for_testing', escape_html: false)) + assert_equal("

    \n Hello World\n

    \n", render("%p\n Hello \#$global_var_for_testing", escape_html: false)) + assert_equal("

    Hello World

    \n", render('%p Hello #$global_var_for_testing', escape_html: true)) + assert_equal("

    \n Hello World\n

    \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("

    2

    \n", render('%p #@foo', :scope => scope)) + assert_equal("

    \n 2\n

    \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("

    2

    \n", render('%p #$global_var_for_testing')) + assert_equal("

    \n 2\n

    \n", render("%p\n \#$global_var_for_testing")) + ensure + $global_var_for_testing = nil + end + + def test_escaped_interpolation + assert_equal("

    Foo & Bar & Baz

    \n", render('%p& Foo #{"&"} Bar & Baz')) + end + + def test_nil_tag_value_should_render_as_empty + assert_equal("

    \n", render("%p= nil")) + end + + def test_tag_with_failed_if_should_render_as_empty + assert_equal("

    \n", render("%p= 'Hello' if false")) + end + + def test_static_attributes_with_empty_attr + assert_equal("\n", render("%img{:src => '/foo.png', :alt => ''}")) + end + + def test_dynamic_attributes_with_empty_attr + # [INCOMPATIBILITY] Hamlit limits boolean attributes + # assert_equal("\n", render("%img{:width => nil, :src => '/foo.png', :alt => String.new}")) + assert_equal("\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(< {:hash => hash}) +
    +
    +
    +HTML +%div{hash} +.special{hash} +%div{hash} +HAML + assert_equal(hash, {:color => 'red'}) + end + + def test_ugly_semi_prerendered_tags + assert_equal(< true)) +

    +

    foo

    +

    +

    foo

    +

    foo +bar

    +

    foo +bar

    +

    +foo +

    +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("

    0

    \n

    1

    \n

    2

    \n", render("- for i in (0...3)\n %p= |\n i |")) + end + + def test_cr_newline + assert_equal("

    foo

    \n

    bar

    \n

    baz

    \n

    boom

    \n", render("%p foo\r%p bar\r\n%p baz\n\r%p boom")) + end + + def test_textareas; skip # script bug + assert_equal("\n", + render('%textarea= "Foo\n bar\n baz"')) + + assert_equal("
    Foo
      bar
       baz
    \n", + render('%pre= "Foo\n bar\n baz"')) + + assert_equal("\n", + render("%textarea #{'a' * 100}")) + + assert_equal("

    \n \n

    \n", render(<Foo bar baz +HTML +%pre + %code + :preserve + Foo + bar + baz +HAML + end + + def test_boolean_attributes + # [INCOMPATIBILITY] Hamlit limits boolean attributes + # assert_equal("

    \n", + # render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :html4)) + # assert_equal("

    \n", + # render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :xhtml)) + # + # assert_equal("

    \n", + # render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :html4)) + # assert_equal("

    \n", + # render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :xhtml)) + + assert_equal("

    \n", + render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :html4)) + assert_equal("

    \n", + render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :xhtml)) + + assert_equal("

    \n", + render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :html4)) + assert_equal("

    \n", + render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :xhtml)) + end + + def test_nuke_inner_whitespace_in_loops + assert_equal(<foobarbaz +HTML +%ul< + - for str in %w[foo bar baz] + = str +HAML + end + + def test_both_whitespace_nukes_work_together; skip # dynamic indentation + assert_equal(<Foo + Bar

    +RESULT +%p + %q><= "Foo\\nBar" +SOURCE + end + + def test_nil_option + assert_equal("

    \n", render('%p{:foo => "bar"}', :attr_wrapper => nil)) + end + + def test_comment_with_crazy_nesting + assert_equal(< 'te'+'st'} + = "foo\\nbar" +HAML + end + + def test_whitespace_nuke_with_both_newlines; skip # script bug # runtime nuke + assert_equal("

    foo

    \n", render('%p<= "\nfoo\n"')) + assert_equal(< +

    foo

    +

    +HTML +%p + %p<= "\\nfoo\\n" +HAML + end + + def test_whitespace_nuke_with_tags_and_else + assert_haml_ugly(< + foo

    +HTML +%p + foo + = " " + %a> +HAML + end + + def test_both_case_indentation_work_with_deeply_nested_code + assert_haml_ugly(< true)) += capture_haml do + foo +HAML + end + + def test_plain_equals_with_ugly + assert_equal("foo\nbar\n", render(< true)) += "foo" +bar +HAML + end + + def test_inline_if + assert_equal(<One

    +

    +

    Three

    +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(< + 2|3|4 + b-a-r +

    +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(<hi!

    +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(< "Bar", | + :b => "Baz" }[:a] | +HAML + assert_equal(< "Bar", | + :b => "Baz" }[:a] | +HAML + end + + def test_multiline_in_filter + assert_equal(< false) +#foo{:class => ''} + bar +HAML + end + + def test_escape_attrs_always; skip # attribute escape + assert_equal(< :always)) +
    + bar +
    +HTML +#foo{:class => '"<>&"'} + bar +HAML + end + + def test_escape_html + html = < true)) +&= "&" +!= "&" += "&" +HAML + + assert_equal(html, render(< true)) +&~ "&" +!~ "&" +~ "&" +HAML + + assert_equal(html, render(< true)) +& \#{"&"} +! \#{"&"} +\#{"&"} +HAML + + assert_equal(html, render(< true)) +&== \#{"&"} +!== \#{"&"} +== \#{"&"} +HAML + + tag_html = <&

    +

    &

    +

    &

    +HTML + + assert_equal(tag_html, render(< true)) +%p&= "&" +%p!= "&" +%p= "&" +HAML + + assert_equal(tag_html, render(< true)) +%p&~ "&" +%p!~ "&" +%p~ "&" +HAML + + assert_equal(tag_html, render(< true)) +%p& \#{"&"} +%p! \#{"&"} +%p \#{"&"} +HAML + + assert_equal(tag_html, render(< true)) +%p&== \#{"&"} +%p!== \#{"&"} +%p== \#{"&"} +HAML + end + + def test_new_attrs_with_hash + assert_equal("\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(<foo-end

    +

    bar-end

    +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(<Foo\n", + render('%a(href="#" rel="top") Foo')) + assert_equal("Foo\n", + render('%a(href="#") #{"Foo"}')) + + assert_equal("\n", render('%a(href="#\\"")')) + end + + def test_case_assigned_to_var + assert_equal(< 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("

    foo & bar

    \n", render("%p&= 'foo & bar'", :escape_html => false)) + end + + def test_ampersand_equals_should_escape_before_preserve; skip # script bug + assert_equal("\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("

    foo & bar

    \n", render("%p!= 'foo & bar'", :escape_html => true)) + end + + def test_static_attributes_should_be_escaped; skip # attribute escape + assert_equal("\n", + render("%img.atlantis{:style => 'ugly&stupid'}")) + assert_equal("
    foo
    \n", + render(".atlantis{:style => 'ugly&stupid'} foo")) + assert_equal("

    foo

    \n", + render("%p.atlantis{:style => 'ugly&stupid'}= 'foo'")) + assert_equal("

    \n", + render("%p.atlantis{:style => \"ugly\\nstupid\"}")) + end + + def test_dynamic_attributes_should_be_escaped; skip # script bug + assert_equal("\n", + render("%img{:width => nil, :src => '&foo.png', :alt => String.new}")) + assert_equal("

    foo

    \n", + render("%p{:width => nil, :src => '&foo.png', :alt => String.new} foo")) + assert_equal("
    foo
    \n", + render("%div{:width => nil, :src => '&foo.png', :alt => String.new}= 'foo'")) + assert_equal("\n", + render("%img{:width => nil, :src => \"foo\\n.png\", :alt => String.new}")) + end + + def test_string_double_equals_should_be_escaped + assert_equal("

    4&<

    \n", render("%p== \#{2+2}&\#{'<'}", :escape_html => true)) + assert_equal("

    4&<

    \n", render("%p== \#{2+2}&\#{'<'}", :escape_html => false)) + end + + def test_escaped_inline_string_double_equals + assert_equal("

    4&<

    \n", render("%p&== \#{2+2}&\#{'<'}", :escape_html => true)) + assert_equal("

    4&<

    \n", render("%p&== \#{2+2}&\#{'<'}", :escape_html => false)) + end + + def test_unescaped_inline_string_double_equals + assert_equal("

    4&<

    \n", render("%p!== \#{2+2}&\#{'<'}", :escape_html => true)) + assert_equal("

    4&<

    \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("

    4&<

    \n", render("%p \#{2+2}&\#{'<'}", :escape_html => true)) + assert_equal("

    4&<

    \n", render("%p \#{2+2}&\#{'<'}", :escape_html => false)) + end + + def test_escaped_inline_string_interpolation + assert_equal("

    4&<

    \n", render("%p& \#{2+2}&\#{'<'}", :escape_html => true)) + assert_equal("

    4&<

    \n", render("%p& \#{2+2}&\#{'<'}", :escape_html => false)) + end + + def test_unescaped_inline_string_interpolation + assert_equal("

    4&<

    \n", render("%p! \#{2+2}&\#{'<'}", :escape_html => true)) + assert_equal("

    4&<

    \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("<br>\n", render('&#{"
    "}')) + assert_equal("<br>\n", render('%span&#{"
    "}')) + 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("
    \n", render('!#{"
    "}')) + assert_equal("
    \n", render('%span!#{"
    "}')) + 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("

    foo & bar

    \n", render("%p= 'foo & bar'", :escape_html => true)) + assert_equal("

    foo & bar

    \n", render("%p= 'foo & bar'", :escape_html => false)) + end + + def test_script_ending_in_comment_should_render_when_html_is_escaped + assert_equal("foo&bar\n", render("= 'foo&bar' #comment", :escape_html => true)) + end + + def test_script_with_if_shouldnt_output + assert_equal(<foo

    +

    +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("
    \n", render("#foo{:yes => 'no'}/", :suppress_eval => true)) + assert_equal("
    \n", render("#foo{:yes => 'no', :call => a_function() }/", :suppress_eval => true)) + assert_equal("
    \n", render("%div[1]/", :suppress_eval => true)) + assert_equal("", render(":ruby\n Kernel.puts 'hello'", :suppress_eval => true)) + end + + def test_doctypes + assert_equal('', + render('!!!', :format => :html5).strip) + assert_equal('', render('!!! 5').strip) + assert_equal('', + render('!!! strict', :format => :xhtml).strip) + assert_equal('', + render('!!! frameset', :format => :xhtml).strip) + assert_equal('', + render('!!! mobile', :format => :xhtml).strip) + assert_equal('', + render('!!! basic', :format => :xhtml).strip) + assert_equal('', + render('!!! transitional', :format => :xhtml).strip) + assert_equal('', + render('!!!', :format => :xhtml).strip) + assert_equal('', + render('!!! strict', :format => :html4).strip) + assert_equal('', + render('!!! frameset', :format => :html4).strip) + assert_equal('', + render('!!! transitional', :format => :html4).strip) + assert_equal('', + render('!!!', :format => :html4).strip) + end + + def test_attr_wrapper; skip # options + assert_equal("

    \n", render("%p{ :strange => 'attrs'}", :attr_wrapper => '*')) + assert_equal("

    \n", render("%p{ :escaped => 'quo\"te'}", :attr_wrapper => '"')) + assert_equal("

    \n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"')) + assert_equal("

    \n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"')) + assert_equal("\n", render("!!! XML", :attr_wrapper => '"', :format => :xhtml)) + end + + def test_autoclose_option + assert_equal("\n", render("%flaz{:foo => 'bar'}", :autoclose => ["flaz"])) + assert_equal(< [/^flaz/])) + + + +HTML +%flaz +%flaznicate +%flan +HAML + end + + def test_attrs_parsed_correctly; skip # attribute escape + assert_equal("

    biddly='bar => baz'>

    \n", render("%p{'boom=>biddly' => 'bar => baz'}")) + assert_equal("

    \n", render("%p{'foo,bar' => 'baz, qux'}")) + assert_equal("

    \n", render("%p{ :escaped => \"quo\\nte\"}")) + assert_equal("

    \n", render("%p{ :escaped => \"quo\#{2 + 2}te\"}")) + end + + def test_correct_parsing_with_brackets; skip # script bug + assert_equal("

    {tada} foo

    \n", render("%p{:class => 'foo'} {tada} foo")) + assert_equal("

    deep {nested { things }}

    \n", render("%p{:class => 'foo'} deep {nested { things }}")) + assert_equal("

    {a { d

    \n", render("%p{{:class => 'foo'}, :class => 'bar'} {a { d")) + assert_equal("

    a}

    \n", render("%p{:foo => 'bar'} a}")) + + foo = [] + foo[0] = Struct.new('Foo', :id).new + assert_equal("

    New User]

    \n", + render("%p[foo[0]] New User]", :locals => {:foo => foo})) + assert_equal("

    New User]

    \n", + render("%p[foo[0], :prefix] New User]", :locals => {:foo => foo})) + + foo[0].id = 1 + assert_equal("

    New User]

    \n", + render("%p[foo[0]] New User]", :locals => {:foo => foo})) + assert_equal("

    New User]

    \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("

    nil

    \n", render("%p{ :attr => nil } nil")) + assert_equal("

    nil

    \n", render("%p{ :attr => x } nil", :locals => {:x => nil})) + end + + def test_nil_id_with_syntactic_id + assert_equal("

    nil

    \n", render("%p#foo{:id => nil} nil")) + assert_equal("

    nil

    \n", render("%p#foo{{:id => 'bar'}, :id => nil} nil")) + assert_equal("

    nil

    \n", render("%p#foo{{:id => nil}, :id => 'bar'} nil")) + end + + def test_nil_class_with_syntactic_class + assert_equal("

    nil

    \n", render("%p.foo{:class => nil} nil")) + assert_equal("

    nil

    \n", render("%p.bar.foo{:class => nil} nil")) + assert_equal("

    nil

    \n", render("%p.foo{{:class => 'bar'}, :class => nil} nil")) + assert_equal("

    nil

    \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("

    \n", render('%p{:a => "b#{1 + 1}c"}')) + assert_equal("

    \n", render("%p{:a => 'b' + (1 + 1).to_s + 'c'}")) + end + + def test_dynamic_attrs_with_self_closed_tag + assert_equal("\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("\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("\nconcatted\n", + render("/ Hello \#{1 + 1}\n- haml_concat 'concatted'")) + end + + def test_balanced_conditional_comments + assert_equal("\n", + render("/[if !(IE 6)|(IE 7)] Bracket: ]")) + end + + def test_downlevel_revealed_conditional_comments; skip + assert_equal(" A comment \n", + render("/![if !IE] A comment")) + end + + def test_downlevel_revealed_conditional_comments_block + assert_equal("\nA comment\n\n", + render("/![if !IE]\n A comment")) + end + + def test_local_assigns_dont_modify_class + assert_haml_ugly("= foo", :locals => {:foo => 'bar'}) + assert_nil(defined?(foo)) + end + + def test_object_ref_with_nil_id; skip # object reference + user = User.new + assert_equal("

    New User

    \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("

    New User

    \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("

    My Thing

    \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("

    CPK Record

    \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("

    1

    \n
    3
    \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("

    1

    \n
    3
    \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("
    \n
    \n

    hello world

    \n
    \n
    \n", + render("#outer\n #inner\n %p hello world", :ugly => true)) + + assert_equal("

    #{'s' * 75}

    \n", + render("%p #{'s' * 75}", :ugly => true)) + + assert_equal("

    #{'s' * 75}

    \n", + render("%p= 's' * 75", :ugly => true)) + end + + def test_remove_whitespace_true; skip # options + assert_equal("

    hello world

    ", + render("#outer\n #inner\n %p hello world", :remove_whitespace => true)) + assert_equal("

    hello world

    foo   bar\nbaz

    ", render(< true)) +%p + hello world + %pre + foo bar + baz +HAML + assert_equal("
    foo bar
    ", + render('%div foo bar', :remove_whitespace => true)) + end + + def test_auto_preserve_unless_ugly; skip # preserve + assert_equal("
    foo
    bar
    \n", render('%pre="foo\nbar"')) + assert_equal("
    foo\nbar
    \n", render("%pre\n foo\n bar")) + assert_equal("
    foo\nbar
    \n", render('%pre="foo\nbar"', :ugly => true)) + assert_equal("
    foo\nbar
    \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("
    \n", render("%a{:b => 'a => b'}", :suppress_eval => true)) + assert_equal("\n", render("%a{:b => 'a, b'}", :suppress_eval => true)) + assert_equal("\n", render('%a{:b => "a\tb"}', :suppress_eval => true)) + assert_equal("\n", render('%a{:b => "a\\#{foo}b"}', :suppress_eval => true)) + assert_equal("\n", render("%a{:b => '#f00'}", :suppress_eval => true)) + end + + def test_dynamic_hashes_with_suppress_eval; skip # options + assert_equal("\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("\n", engine.to_html) + ensure + $global_var_for_testing = nil + end + + def test_utf8_attrs + assert_equal("\n", render("%a{:href => 'héllo'}")) + assert_equal("\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 "
    \n", render(".foo", :format => :html4) + end + + def test_html_doesnt_add_slash_to_self_closing_tags + assert_equal "\n", render("%a/", :format => :html4) + assert_equal "\n", render("%a{:foo => 1 + 1}/", :format => :html4) + assert_equal "\n", render("%meta", :format => :html4) + assert_equal "\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 %{\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 %{\n}, render('!!!', :format => :html5) + end + + # HTML5 custom data attributes + def test_html5_data_attributes_without_hyphenation; skip # hyphenate + assert_equal("
    \n", + render("%div{:data => {:author_id => 123, :foo => 'bar', :biz => 'baz'}}", + :hyphenate_data_attrs => false)) + + assert_equal("
    \n", + render("%div{:data => {:one_plus_one => 1+1}}", + :hyphenate_data_attrs => false)) + + assert_equal("
    \n", + render(%{%div{:data => {:foo => %{Here's a "quoteful" string.}}}}, + :hyphenate_data_attrs => false)) #' + end + + def test_html5_data_attributes_with_hyphens + assert_equal("
    \n", + render("%div{:data => {:foo_bar => 'blip'}}")) + assert_equal("
    \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("
    \n", + render("%div{:aria => {:foo => 'blip'}}")) + assert_equal("
    \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{
    \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("
    \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("
    \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("
    \n", + render("%div{:data => {:foo => 'first'}, 'data-foo' => 'second'}")) + assert_equal("
    \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("
    \n", + render("%div{data_hash, :data => {:foo => 'blip', :brat => 'wurst'}}", scope: obj)) + assert_equal("
    \n", + render("%div{data_hash, 'data-foo' => 'blip'}", scope: obj)) + assert_equal("
    \n", + render("%div{data_hash, :data => 'dat'}", scope: obj)) + assert_equal("
    \n", + render("%div{data_val, :data => {:foo => 'blip', :brat => 'wurst'}}", scope: obj)) + end + + def test_html5_data_attributes_with_identical_attribute_values + assert_equal("
    \n", + render("%div{:data => {:x => 50, :y => 50}}")) + end + + def test_xml_doc_using_html5_format_and_mime_type; skip # mime_type + assert_equal(< :html5, :mime_type => 'text/xml' })) + + + +
    +
    +XML +!!! XML +%root + %element/ + %hr +HAML + end + + def test_xml_doc_using_html4_format_and_mime_type; skip # mime_type + assert_equal(< :html4, :mime_type => 'text/xml' })) + + + +
    +
    +XML +!!! XML +%root + %element/ + %hr +HAML + end + + # New attributes + + def test_basic_new_attributes + assert_equal("bar\n", render("%a() bar")) + assert_equal("bar\n", render("%a(href='foo') bar")) + assert_equal("baz\n", render(%q{%a(b="c" c='d' d="e") baz})) + end + + def test_new_attribute_ids; skip # object reference + assert_equal("
    \n", render("#foo(id='bar')")) + assert_equal("
    \n", render("#foo{:id => 'bar'}(id='baz')")) + assert_equal("
    \n", render("#foo(id='baz'){:id => 'bar'}")) + foo = User.new(42) + assert_equal("
    \n", + render("#foo(id='baz'){:id => 'bar'}[foo]", :locals => {:foo => foo})) + assert_equal("
    \n", + render("#foo(id='baz')[foo]{:id => 'bar'}", :locals => {:foo => foo})) + assert_equal("
    \n", + render("#foo[foo](id='baz'){:id => 'bar'}", :locals => {:foo => foo})) + assert_equal("
    \n", + render("#foo[foo]{:id => 'bar'}(id='baz')", :locals => {:foo => foo})) + end + + def test_new_attribute_classes; skip # object reference + assert_equal("
    \n", render(".foo(class='bar')")) + assert_equal("
    \n", render(".foo{:class => 'bar'}(class='baz')")) + assert_equal("
    \n", render(".foo(class='baz'){:class => 'bar'}")) + foo = User.new(42) + assert_equal("
    \n", + render(".foo(class='baz'){:class => 'bar'}[foo]", :locals => {:foo => foo})) + assert_equal("
    \n", + render(".foo[foo](class='baz'){:class => 'bar'}", :locals => {:foo => foo})) + assert_equal("
    \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("bar\n", render("%a(href) bar", :format => :xhtml)) + assert_equal("bar\n", render("%a(href bar='baz') bar", :format => :html5)) + assert_equal("bar\n", render("%a(href=true) bar")) + assert_equal("bar\n", render("%a(href=false) bar")) + end + + def test_new_attribute_parsing; skip # attribute escape + assert_equal("bar\n", render("%a(a2=b2) bar", :locals => {:b2 => 'b2'})) + assert_equal(%Q{bar\n}, render(%q{%a(a="#{'foo"bar'}") bar})) #' + assert_equal(%Q{bar\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #' + assert_equal(%Q{bar\n}, render(%q{%a(a='foo"bar') bar})) + assert_equal(%Q{bar\n}, render(%q{%a(a="foo'bar") bar})) + assert_equal("bar\n", render("%a(a:b='foo') bar")) + assert_equal("bar\n", render("%a(a = 'foo' b = 'bar') bar")) + assert_equal("bar\n", render("%a(a = foo b = bar) bar", :locals => {:foo => 'foo', :bar => 'bar'})) + assert_equal("(b='bar')\n", render("%a(a='foo')(b='bar')")) + assert_equal("baz\n", render("%a(a='foo)bar') baz")) + assert_equal("baz\n", render("%a( a = 'foo' ) baz")) + end + + def test_new_attribute_escaping; skip # attribute escape + assert_equal(%Q{bar\n}, render(%q{%a(a="foo \" bar") bar})) + assert_equal(%Q{bar\n}, render(%q{%a(a="foo \\\\\" bar") bar})) + + assert_equal(%Q{bar\n}, render(%q{%a(a='foo \' bar') bar})) + assert_equal(%Q{bar\n}, render(%q{%a(a='foo \\\\\' bar') bar})) + + assert_equal(%Q{bar\n}, render(%q{%a(a="foo \\\\ bar") bar})) + assert_equal(%Q{bar\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(<foo

    +HTML +- foo = ["bar", + "baz", + "bang"] += foo.join(", ") +%p foo +HAML + end + + def test_loud_ruby_multiline + assert_equal(<foo

    +

    bar

    +HTML += ["bar", + "baz", + "bang"].join(", ") +%p foo +%p bar +HAML + end + + def test_ruby_multiline_with_punctuated_methods_is_continuation + assert_equal(<foo

    +

    bar

    +HTML += ["bar", + " ".strip!, + "".empty?, + "bang"].join(", ") +%p foo +%p bar +HAML + end + + def test_ruby_character_literals_are_not_continuation + html = ",\n,\n

    foo

    \n" + assert_equal(html, render(<foo

    +

    bar

    +HTML +&= ["bar<", + "baz", + "bang"].join(", ") +%p foo +%p bar +HAML + end + + def test_unescaped_loud_ruby_multiline + assert_equal(< true)) +bar<, baz, bang +

    foo

    +

    bar

    +HTML +!= ["bar<", + "baz", + "bang"].join(", ") +%p foo +%p bar +HAML + end + + def test_flattened_loud_ruby_multiline + assert_equal(<bar baz bang +

    foo

    +

    bar

    +HTML +~ "
    " + ["bar",
    +             "baz",
    +             "bang"].join("\\n") + "
    " +%p foo +%p bar +HAML + end + + def test_loud_ruby_multiline_with_block; skip # block script + assert_equal(<foo

    +

    bar

    +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(<foo

    +

    bar

    +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(<foo, bar, baz

    +

    foo

    +

    bar

    +HTML +%p= ["foo", + "bar", + "baz"].join(", ") +%p foo +%p bar +HAML + end + + def test_escaped_ruby_multiline_in_tag; skip # script bug + assert_equal(<foo<, bar, baz

    +

    foo

    +

    bar

    +HTML +%p&= ["foo<", + "bar", + "baz"].join(", ") +%p foo +%p bar +HAML + end + + def test_unescaped_ruby_multiline_in_tag + assert_equal(< true)) +

    foo<, bar, baz

    +

    foo

    +

    bar

    +HTML +%p!= ["foo<", + "bar", + "baz"].join(", ") +%p foo +%p bar +HAML + end + + def test_ruby_multiline_with_normal_multiline + assert_equal(<foo

    +

    bar

    +HTML += "foo" + | + "bar" + | + ["bar", | + "baz", + "bang"].join(", ") +%p foo +%p bar +HAML + end + + def test_ruby_multiline_after_filter + assert_equal(<foo

    +

    bar

    +HTML +:plain + foo + bar += ["bar", + "baz", + "bang"].join(", ") +%p foo +%p bar +HAML + end + + # Encodings + + def test_utf_8_bom; # encoding + assert_equal < +

    baz

    +
    +HTML +\xEF\xBB\xBF.foo + %p baz +HAML + end + + def test_default_encoding + assert_equal(Encoding.find("utf-8"), render(< "ascii-8bit")) +

    bâr

    +

    föö

    +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 # encoding + render("foo\nbar\nb\xFEaz".force_encoding("utf-8")) + assert(false, "Expected exception") + rescue Hamlit::Error => e + 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(<bâr

    +

    föö

    +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 +%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 "

    ", result + end + + private + + def assert_valid_encoding_comment(comment) + assert_renders_encoded(<ЖЛЫ

    +

    тАЬ

    +HTML +#{comment} +%p ЖЛЫ +%p тАЬ +HAML + end + + def assert_converts_template_properly + engine = Haml::Engine.new(< "macRoman") +%p bâr +%p föö +HAML + assert_encoded_equal(<bâr

    +

    föö

    +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 if RUBY_ENGINE != 'truffleruby' # truffleruby cannot run Haml diff --git a/test/haml/erb/_av_partial_1.erb b/test/haml/erb/_av_partial_1.erb new file mode 100644 index 0000000..0c836a1 --- /dev/null +++ b/test/haml/erb/_av_partial_1.erb @@ -0,0 +1,12 @@ +

    This is a pretty complicated partial

    +
    +

    It has several nested partials,

    +
      + <% 5.times do %> +
    • + Partial: + <% @nesting = 5 %> + <%= render :partial => 'erb/av_partial_2' %> + <% end %> +
    +
    diff --git a/test/haml/erb/_av_partial_2.erb b/test/haml/erb/_av_partial_2.erb new file mode 100644 index 0000000..189d858 --- /dev/null +++ b/test/haml/erb/_av_partial_2.erb @@ -0,0 +1,8 @@ +<% @nesting -= 1 %> +
    +

    This is a crazy deep-nested partial.

    +

    Nesting level <%= @nesting %>

    + <% if @nesting > 0 %> + <%= render :partial => 'erb/av_partial_2' %> + <% end %> +
    diff --git a/test/haml/erb/action_view.erb b/test/haml/erb/action_view.erb new file mode 100644 index 0000000..389ffe9 --- /dev/null +++ b/test/haml/erb/action_view.erb @@ -0,0 +1,62 @@ + + + + Hampton Catlin Is Totally Awesome + + + +

    + This is very much like the standard template, + except that it has some ActionView-specific stuff. + It's only used for benchmarking. +

    +
    + <%= render :partial => 'erb/av_partial_1' %> +
    + +
    + 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 %> +
    + <% 120.times do |number| -%> + <%= number %> + <% end -%> +
    <%= " Quotes should be loved! Just like people!" %>
    + Wow. +

    + <%= "Holy cow " + + "multiline " + + "tags! " + + "A pipe (|) even!" %> + <%= [1, 2, 3].collect { |n| "PipesIgnored|" } %> + <%= [1, 2, 3].collect { |n| + n.to_s + }.join("|") %> +

    +
    + <% foo = String.new + foo << "this" + foo << " shouldn't" + foo << " evaluate" %> + <%= foo + "but now it should!" %> + <%# Woah crap a comment! %> +
    +
      + <% ('a'..'f').each do |a|%> +
    • <%= a %> + <% end %> +
      <%= @should_eval = "with this text" %>
      + <%= [ 104, 101, 108, 108, 111 ].map do |byte| + byte.chr + end %> + + + diff --git a/test/haml/erb/standard.erb b/test/haml/erb/standard.erb new file mode 100644 index 0000000..0cc8ea7 --- /dev/null +++ b/test/haml/erb/standard.erb @@ -0,0 +1,55 @@ + + + + Hampton Catlin Is Totally Awesome + + + + +
      + 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 %> +
      + <% 120.times do |number| -%> + <%= number %> + <% end -%> +
      <%= " Quotes should be loved! Just like people!" %>
      + Wow. +

      + <%= "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 %> +
      + <% foo = String.new + foo << "this" + foo << " shouldn't" + foo << " evaluate" %> + <%= foo + "but now it should!" %> + <%# Woah crap a comment! %> +
      +
        + <% ('a'..'f').each do |a|%> +
      • <%= a %>
      • + <% end %> +
        <%= @should_eval = "with this text" %>
        + <%= "foo".each_line do |line| + nil + end %> + + + diff --git a/test/haml/filters_test.rb b/test/haml/filters_test.rb new file mode 100644 index 0000000..933c187 --- /dev/null +++ b/test/haml/filters_test.rb @@ -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")) + 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; skip + assert_haml_ugly(":plain\n \#{''}") + end + +end if RUBY_ENGINE != 'truffleruby' # truffleruby does not implement Ripper.lex + +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("<img>\n", render(":erb\n <%= '' %>")) + assert_equal("\n", render(":erb\n <%= ''.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 = "\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 = "\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 = "\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 = "\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 = "\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('//', out) + end +end + +class CSSFilterTest < Haml::TestCase + test "should wrap output in CDATA and a CSS tag when output is XHTML" do + html = "\n" + haml = ":css\n foo" + assert_equal(html, render(haml, :format => :xhtml)) + end + + test "should not include type in HTML 5 output" do + html = "\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 = "\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 = "\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 = "\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('', out) + end +end + +class CDATAFilterTest < Haml::TestCase + test "should wrap output in CDATA tag" do + html = "\n" + haml = ":cdata\n foo" + assert_equal(html, render(haml)) + end +end + +class EscapedFilterTest < Haml::TestCase + test "should escape ampersands" do + html = "&\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 index 0000000..2fbf0ff --- /dev/null +++ b/test/haml/gemfiles/.bundle/config @@ -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 index 0000000..9769d16 --- /dev/null +++ b/test/haml/gemfiles/Gemfile.rails-4.0.x @@ -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 index 0000000..23af5b1 --- /dev/null +++ b/test/haml/gemfiles/Gemfile.rails-4.1.x @@ -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 index 0000000..fa56c67 --- /dev/null +++ b/test/haml/gemfiles/Gemfile.rails-4.2.x @@ -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 index 0000000..5a8e332 --- /dev/null +++ b/test/haml/haml-spec/LICENSE @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + 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 index 0000000..3366c7b --- /dev/null +++ b/test/haml/haml-spec/README.md @@ -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 index 0000000..601bc73 --- /dev/null +++ b/test/haml/haml-spec/Rakefile @@ -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 index 0000000..4589fe9 --- /dev/null +++ b/test/haml/haml-spec/tests.yml @@ -0,0 +1,538 @@ +--- +headers: + an XHTML XML prolog: + haml: "!!! XML" + html: "" + config: + format: xhtml + an XHTML default (transitional) doctype: + haml: "!!!" + html: + config: + format: xhtml + an XHTML 1.1 doctype: + haml: "!!! 1.1" + html: + config: + format: xhtml + an XHTML 1.2 mobile doctype: + haml: "!!! mobile" + html: + config: + format: xhtml + an XHTML 1.1 basic doctype: + haml: "!!! basic" + html: + config: + format: xhtml + an XHTML 1.0 frameset doctype: + haml: "!!! frameset" + html: + config: + format: xhtml + an HTML 5 doctype with XHTML syntax: + haml: "!!! 5" + html: "" + config: + format: xhtml + an HTML 5 XML prolog (silent): + haml: "!!! XML" + html: '' + config: + format: html5 + an HTML 5 doctype: + haml: "!!!" + html: "" + config: + format: html5 + an HTML 4 XML prolog (silent): + haml: "!!! XML" + html: '' + config: + format: html4 + an HTML 4 default (transitional) doctype: + haml: "!!!" + html: + config: + format: html4 + an HTML 4 frameset doctype: + haml: "!!! frameset" + html: + config: + format: html4 + an HTML 4 strict doctype: + haml: "!!! strict" + html: + config: + format: html4 +basic Haml tags and CSS: + a simple Haml tag: + haml: "%p" + html: "

        " + a self-closing tag (XHTML): + haml: "%meta" + html: "" + config: + format: xhtml + a self-closing tag (HTML4): + haml: "%meta" + html: "" + config: + format: html4 + a self-closing tag (HTML5): + haml: "%meta" + html: "" + config: + format: html5 + a self-closing tag ('/' modifier + XHTML): + haml: "%zzz/" + html: "" + config: + format: xhtml + a self-closing tag ('/' modifier + HTML5): + haml: "%zzz/" + html: "" + config: + format: html5 + a tag with a CSS class: + haml: "%p.class1" + html: "

        " + a tag with multiple CSS classes: + haml: "%p.class1.class2" + html: "

        " + a tag with a CSS id: + haml: "%p#id1" + html: "

        " + a tag with multiple CSS id's: + haml: "%p#id1#id2" + html: "

        " + a tag with a class followed by an id: + haml: "%p.class1#id1" + html: "

        " + a tag with an id followed by a class: + haml: "%p#id1.class1" + html: "

        " + an implicit div with a CSS id: + haml: "#id1" + html: "
        " + an implicit div with a CSS class: + haml: ".class1" + html: "
        " + multiple simple Haml tags: + haml: |- + %div + %div + %p + html: |- +
        +
        +

        +
        +
        +tags with unusual HTML characters: + a tag with colons: + haml: "%ns:tag" + html: "" + a tag with underscores: + haml: "%snake_case" + html: "" + a tag with dashes: + haml: "%dashed-tag" + html: "" + a tag with camelCase: + haml: "%camelCase" + html: "" + a tag with PascalCase: + haml: "%PascalCase" + html: "" +tags with unusual CSS identifiers: + an all-numeric class: + haml: ".123" + html: "
        " + a class with underscores: + haml: ".__" + html: "
        " + a class with dashes: + haml: ".--" + html: "
        " +tags with inline content: + Inline content simple tag: + haml: "%p hello" + html: "

        hello

        " + Inline content tag with CSS: + haml: "%p.class1 hello" + html: "

        hello

        " + Inline content multiple simple tags: + haml: |- + %div + %div + %p text + html: |- +
        +
        +

        text

        +
        +
        +tags with nested content: + Nested content simple tag: + haml: |- + %p + hello + html: |- +

        + hello +

        + Nested content tag with CSS: + haml: |- + %p.class1 + hello + html: |- +

        + hello +

        + Nested content multiple simple tags: + haml: |- + %div + %div + %p + text + html: |- +
        +
        +

        + text +

        +
        +
        +tags with HTML-style attributes: + HTML-style one attribute: + haml: "%p(a='b')" + html: "

        " + HTML-style multiple attributes: + haml: "%p(a='b' c='d')" + html: "

        " + HTML-style attributes separated with newlines: + haml: |- + %p(a='b' + c='d') + html: "

        " + HTML-style interpolated attribute: + haml: '%p(a="#{var}")' + html: "

        " + locals: + var: value + HTML-style 'class' as an attribute: + haml: "%p(class='class1')" + html: "

        " + HTML-style tag with a CSS class and 'class' as an attribute: + haml: "%p.class2(class='class1')" + html: "

        " + HTML-style tag with 'id' as an attribute: + haml: "%p(id='1')" + html: "

        " + HTML-style tag with a CSS id and 'id' as an attribute: + haml: "%p#id(id='1')" + html: "

        " + HTML-style tag with a variable attribute: + haml: "%p(class=var)" + html: "

        " + locals: + var: hello + HTML-style tag with a CSS class and 'class' as a variable attribute: + haml: ".hello(class=var)" + html: "
        " + locals: + var: world + HTML-style tag multiple CSS classes (sorted correctly): + haml: ".z(class=var)" + html: "
        " + locals: + var: a + HTML-style tag with an atomic attribute: + haml: "%a(flag)" + html: "" +tags with Ruby-style attributes: + Ruby-style one attribute: + haml: "%p{:a => 'b'}" + html: "

        " + optional: true + Ruby-style attributes hash with whitespace: + haml: "%p{ :a => 'b' }" + html: "

        " + optional: true + Ruby-style interpolated attribute: + haml: '%p{:a =>"#{var}"}' + html: "

        " + optional: true + locals: + var: value + Ruby-style multiple attributes: + haml: "%p{ :a => 'b', 'c' => 'd' }" + html: "

        " + optional: true + Ruby-style attributes separated with newlines: + haml: |- + %p{ :a => 'b', + 'c' => 'd' } + html: "

        " + optional: true + Ruby-style 'class' as an attribute: + haml: "%p{:class => 'class1'}" + html: "

        " + optional: true + Ruby-style tag with a CSS class and 'class' as an attribute: + haml: "%p.class2{:class => 'class1'}" + html: "

        " + optional: true + Ruby-style tag with 'id' as an attribute: + haml: "%p{:id => '1'}" + html: "

        " + optional: true + Ruby-style tag with a CSS id and 'id' as an attribute: + haml: "%p#id{:id => '1'}" + html: "

        " + optional: true + Ruby-style tag with a CSS id and a numeric 'id' as an attribute: + haml: "%p#id{:id => 1}" + html: "

        " + optional: true + Ruby-style tag with a variable attribute: + haml: "%p{:class => var}" + html: "

        " + optional: true + locals: + var: hello + Ruby-style tag with a CSS class and 'class' as a variable attribute: + haml: ".hello{:class => var}" + html: "
        " + optional: true + locals: + var: world + Ruby-style tag multiple CSS classes (sorted correctly): + haml: ".z{:class => var}" + html: "
        " + 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: "" + a nested markup comment: + haml: |- + / + comment + comment2 + html: |- + +conditional comments: + a conditional comment: + haml: |- + /[if IE] + %p a + html: |- + +internal filters: + content in an 'escaped' filter: + haml: |- + :escaped + <&"> + html: "<&">" + content in a 'preserve' filter: + haml: |- + :preserve + hello + + %p + html: |- + hello +

        + content in a 'plain' filter: + haml: |- + :plain + hello + + %p + html: |- + hello +

        + content in a 'css' filter (XHTML): + haml: |- + :css + hello + + %p + html: |- + +

        + config: + format: xhtml + content in a 'javascript' filter (XHTML): + haml: |- + :javascript + a(); + %p + html: |- + +

        + config: + format: xhtml + content in a 'css' filter (HTML): + haml: |- + :css + hello + + %p + html: |- + +

        + config: + format: html5 + content in a 'javascript' filter (HTML): + haml: |- + :javascript + a(); + %p + html: |- + +

        + config: + format: html5 +Ruby-style interpolation: + interpolation inside inline content: + haml: "%p #{var}" + html: "

        value

        " + optional: true + locals: + var: value + no interpolation when escaped: + haml: "%p \\#{var}" + html: "

        #{var}

        " + optional: true + locals: + var: value + interpolation when the escape character is escaped: + haml: "%p \\\\#{var}" + html: "

        \\value

        " + 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: "<"&>" + code following '=' when escape_haml is set to true: + haml: = '<"&>' + html: "<"&>" + 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: "" + config: + format: xhtml + boolean attribute with HTML: + haml: "%input(checked=true)" + html: "" + config: + format: html5 +whitespace preservation: + following the '~' operator: + haml: ~ "Foo\n
        Bar\nBaz
        " + html: |- + Foo +
        Bar
        Baz
        + optional: true + inside a textarea tag: + haml: |- + %textarea + hello + hello + html: |- + + inside a pre tag: + haml: |- + %pre + hello + hello + html: |- +
        hello
        +      hello
        +whitespace removal: + a tag with '>' appended and inline content: + haml: |- + %li hello + %li> world + %li again + html: "
      • hello
      • world
      • again
      • " + a tag with '>' appended and nested content: + haml: |- + %li hello + %li> + world + %li again + html: |- +
      • hello
      • + world +
      • again
      • + a tag with '<' appended: + haml: |- + %p< + hello + world + html: |- +

        hello + world

        diff --git a/test/haml/haml-spec/ugly_test.rb b/test/haml/haml-spec/ugly_test.rb new file mode 100644 index 0000000..05f968b --- /dev/null +++ b/test/haml/haml-spec/ugly_test.rb @@ -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 = { 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{} + 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{} + 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{} + 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{} + 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{} + 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{} + 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{} + 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{} + 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{} + 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{} + 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{} + 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{

        } + 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{} + 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{} + 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{} + 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{} + 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{} + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{
        } + 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{
        } + 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{
        +
        +

        +
        +
        } + 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{} + 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{} + 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{} + 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{} + 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{} + 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{
        } + 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{
        } + 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{
        } + 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{

        hello

        } + 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{

        hello

        } + 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{
        +
        +

        text

        +
        +
        } + 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{

        + hello +

        } + 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{

        + hello +

        } + 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{
        +
        +

        + text +

        +
        +
        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{
        } + 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{
        } + 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{} + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{

        } + 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{
        } + 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{
        } + 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{} + 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{} + 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{} + 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{<&">} + 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 +

        } + 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 +

        } + 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{ +

        } + 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{ +

        } + 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{ +

        } + 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{ +

        } + 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{

        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 + + def test_no_interpolation_when_escaped + haml = %q{%p \#{var}} + _html = %q{

        #{var}

        } + 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{

        \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 + + 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{<"&>} + 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_eq_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 + + def test_code_following_neq_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{} + 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{} + 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
        Bar\nBaz
        "} + _html = %q{Foo +
        Bar
        Baz
        } + 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{} + 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{
        hello
        +hello
        } + 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{
      • hello
      • world
      • again
      • } + 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{
      • hello
      • + world +
      • again
      • } + 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{

        hello +world

        } + 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 if RUBY_ENGINE != 'truffleruby' # truffleruby cannot run Haml diff --git a/test/haml/helper_test.rb b/test/haml/helper_test.rb new file mode 100644 index 0000000..a4d7425 --- /dev/null +++ b/test/haml/helper_test.rb @@ -0,0 +1,699 @@ +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) do + def nested_tag + content_tag(:span) {content_tag(:div) {"something"}} + end + + def wacky_form + form_tag("/foo") {"bar"} + end + + def compiled_method_container + self.class + end + end.new(ActionView::LookupContext.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 "

        " + end + def @base.render_something_with_haml_tag_and_concat + haml_tag 'p' do + haml_concat '' + 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("<p>\n

        \n <foo>\n

        \n<p>\n", output) + end + + def test_with_raw_haml_concat; skip + haml = <&" +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 Bar", Haml::Helpers.flatten("Foo\nBar")) + + assert_equal("Hello World! YOU ARE FLAT? OMGZ!", + Haml::Helpers.flatten("Hello\nWorld!\nYOU ARE \rFLAT?\n\rOMGZ!")) + end + + def test_list_of_should_render_correctly; skip + assert_equal("
      • 1
      • \n
      • 2
      • \n", render("= list_of([1, 2]) do |i|\n = i")) + assert_equal("
      • [1]
      • \n", render("= list_of([[1]]) do |i|\n = i.inspect")) + assert_equal("
      • \n

        Fee

        \n

        A word!

        \n
      • \n
      • \n

        Fi

        \n

        A word!

        \n
      • \n
      • \n

        Fo

        \n

        A word!

        \n
      • \n
      • \n

        Fum

        \n

        A word!

        \n
      • \n", + render("= list_of(['Fee', 'Fi', 'Fo', 'Fum']) do |title|\n %h1= title\n %p A word!")) + assert_equal("
      • 1
      • \n
      • 2
      • \n", render("= list_of([1, 2], {:c => 3}) do |i|\n = i")) + assert_equal("
      • [1]
      • \n", render("= list_of([[1]], {:c => 3}) do |i|\n = i.inspect")) + assert_equal("
      • \n

        Fee

        \n

        A word!

        \n
      • \n
      • \n

        Fi

        \n

        A word!

        \n
      • \n
      • \n

        Fo

        \n

        A word!

        \n
      • \n
      • \n

        Fum

        \n

        A word!

        \n
      • \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") =~ /#/) + 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("

        tabbed

        \n", render("- buffer.tabulation=5\n%p tabbed")) + end + + def test_with_tabs; skip + assert_equal(< "<%= flatten('Foo\\nBar') %>") + rescue NoMethodError, ActionView::Template::Error + proper_behavior = true + end + assert(proper_behavior) + + begin + action_view_instance.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(<]+>Foo bar baz\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 Bar Baz 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 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 ' 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 ' Foo', match_data[2] + end + + def test_capture_haml; skip + assert_equal(<13

        \\n" +HTML +- (foo = capture_haml(13) do |a| + %p= a +- end) += foo.inspect +HAML + end + + def test_content_tag_block; skip + assert_equal(<

        bar

        +bar +
    +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(< :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("

    \n", render("- haml_tag 'p#some_id'")) + end + + def test_haml_tag_name_attribute_with_colon_id; skip + assert_equal("

    \n", render("- haml_tag 'p#some:id'")) + end + + def test_haml_tag_without_name_but_with_id; skip + assert_equal("
    \n", render("- haml_tag '#some_id'")) + end + + def test_haml_tag_without_name_but_with_class; skip + assert_equal("
    \n", render("- haml_tag '.foo'")) + end + + def test_haml_tag_without_name_but_with_colon_class; skip + assert_equal("
    \n", render("- haml_tag '.foo:bar'")) + end + + def test_haml_tag_name_with_id_and_class; skip + assert_equal("

    \n", render("- haml_tag 'p#some_id.foo'")) + end + + def test_haml_tag_name_with_class; skip + assert_equal("

    \n", render("- haml_tag 'p.foo'")) + end + + def test_haml_tag_name_with_class_and_id; skip + assert_equal("

    \n", render("- haml_tag 'p.foo#some_id'")) + end + + def test_haml_tag_name_with_id_and_multiple_classes; skip + assert_equal("

    \n", render("- haml_tag 'p#some_id.foo.bar'")) + end + + def test_haml_tag_name_with_multiple_classes_and_id; skip + assert_equal("

    \n", render("- haml_tag 'p.foo.bar#some_id'")) + end + + def test_haml_tag_name_and_attribute_classes_merging_with_id; skip + assert_equal("

    \n", render("- haml_tag 'p#some_id.foo', :class => 'bar'")) + end + + def test_haml_tag_name_and_attribute_classes_merging; skip + assert_equal("

    \n", render("- haml_tag 'p.foo', :class => 'bar'")) + end + + def test_haml_tag_name_merges_id_and_attribute_id; skip + assert_equal("

    \n", render("- haml_tag 'p#foo', :id => 'bar'")) + end + + def test_haml_tag_attribute_html_escaping; skip + assert_equal("

    baz

    \n", render("%p{:id => 'foo&bar'} baz", :escape_html => true)) + end + + def test_haml_tag_autoclosed_tags_are_closed_xhtml; skip + assert_equal("
    \n", render("- haml_tag :br, :class => 'foo'", :format => :xhtml)) + end + + def test_haml_tag_autoclosed_tags_are_closed_html; skip + assert_equal("
    \n", render("- haml_tag :br, :class => 'foo'", :format => :html5)) + end + + def test_haml_tag_with_class_array; skip + assert_equal("

    foo

    \n", render("- haml_tag :p, 'foo', :class => %w[a b]")) + assert_equal("

    foo

    \n", render("- haml_tag 'p.c.d', 'foo', :class => %w[a b]")) + end + + def test_haml_tag_with_id_array; skip + assert_equal("

    foo

    \n", render("- haml_tag :p, 'foo', :id => %w[a b]")) + assert_equal("

    foo

    \n", render("- haml_tag 'p#c', 'foo', :id => %w[a b]")) + end + + def test_haml_tag_with_data_hash; skip + assert_equal("

    foo

    \n", + render("- haml_tag :p, 'foo', :data => {:foo => 'bar', :baz => true}")) + end + + def test_haml_tag_non_autoclosed_tags_arent_closed; skip + assert_equal("

    \n", render("- haml_tag :p")) + end + + def test_haml_tag_renders_text_on_a_single_line; skip + assert_equal("

    #{'a' * 100}

    \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("

    \n", render("- haml_tag :p, :/", :format => :xhtml)) + assert_equal("

    \n", render("- haml_tag :p, :/", :format => :html5)) + assert_equal("

    kumquat

    \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(< + foo + bar + baz +

    +HTML +- haml_tag :p, "foo\\nbar\\nbaz" +HAML + end + + def test_haml_concat_inside_haml_tag_escaped_with_xss; skip + assert_equal("

    \n <>&\n

    \n", render(<&" +HAML + end + + def test_haml_concat_with_multiline_string; skip + assert_equal(< + foo + bar + baz +

    +HTML +%p + - haml_concat "foo\\nbar\\nbaz" +HAML + end + + def test_haml_tag_with_ugly; skip + assert_equal(< true)) +

    +Hi! +

    +HTML +- haml_tag :p do + - haml_tag :strong, "Hi!" +HAML + end + + def test_haml_tag_if_positive; skip + assert_equal(< +

    A para

    +
    +HTML +- haml_tag_if true, '.conditional' do + %p A para +HAML + end + + def test_haml_tag_if_positive_with_attributes; skip + assert_equal(< +

    A para

    +
    +HTML +- haml_tag_if true, '.conditional', {:foo => 'bar'} do + %p A para +HAML + end + + def test_haml_tag_if_negative; skip + assert_equal(<A para

    +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 = "
    MyDiv
    \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(< "/") do + %b Bold! +HAML + + # see if Bold is within form tags: + assert_match(/.*Bold!<\/b>.*<\/form>/m, template.render(HomemadeViewContext.new, {})) + end + + def test_find_and_preserve_with_block; skip + assert_equal("
    Foo
    Bar
    \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("
    Foo\nBar
    \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("
    Foo
    Bar
    Foo 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("

    \n Blah\n

    \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 "
    something
    ", render("= nested_tag", :action_view).strip + end + + def test_error_return; skip + assert_raises(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 "

    some tag content

    ", ActsLikeTag.new.to_s + end + + def test_capture_with_nuke_outer; skip + assert_equal "
    \n*
    hi there!
    \n", render(< hi there! +HAML + + assert_equal "
    \n*
    hi there!
    \n", render(< hi there! +HAML + end + + def test_html_escape + assert_equal ""><&", 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 ""><&\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 ""><&", Haml::Helpers.escape_once('"><&') + end + + def test_escape_once_leaves_entity_references + assert_equal ""><&  ", Haml::Helpers.escape_once('"><&  ') + end + + def test_escape_once_leaves_numeric_references; skip + assert_equal ""><&  ", Haml::Helpers.escape_once('"><&  ') #decimal + assert_equal ""><&  ", Haml::Helpers.escape_once('"><&  ') #hexadecimal + end + + def test_escape_once_encoding + old_stderr, $stderr = $stderr, StringIO.new + string = "\"><&\u00e9  " + assert_equal ""><&\u00e9  ", 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("\n", + render("%html{html_attrs}", :format => :xhtml)) + end + + def test_html_attrs_html4; skip + assert_equal("\n", + render("%html{html_attrs}", :format => :html4)) + end + + def test_html_attrs_html5; skip + assert_equal("\n", + render("%html{html_attrs}", :format => :html5)) + end + + def test_html_attrs_xhtml_other_lang; skip + assert_equal("\n", + render("%html{html_attrs('es-AR')}", :format => :xhtml)) + end + + def test_html_attrs_html4_other_lang; skip + assert_equal("\n", + render("%html{html_attrs('es-AR')}", :format => :html4)) + end + + def test_html_attrs_html5_other_lang; skip + assert_equal("\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 diff --git a/test/haml/markaby/standard.mab b/test/haml/markaby/standard.mab new file mode 100644 index 0000000..aff8641 --- /dev/null +++ b/test/haml/markaby/standard.mab @@ -0,0 +1,52 @@ +self << '' +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 index 0000000..805f8ca --- /dev/null +++ b/test/haml/mocks/article.rb @@ -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 index 0000000..63bbd22 --- /dev/null +++ b/test/haml/results/content_for_layout.xhtml @@ -0,0 +1,12 @@ + + + + +
    + Lorem ipsum dolor sit amet +
    +
    + Lorem ipsum dolor sit amet +
    + + diff --git a/test/haml/results/eval_suppressed.xhtml b/test/haml/results/eval_suppressed.xhtml new file mode 100644 index 0000000..fb7bd33 --- /dev/null +++ b/test/haml/results/eval_suppressed.xhtml @@ -0,0 +1,9 @@ +

    +

    +

    Me!

    +
    +

    All

    +
    +

    This

    +Should render +
    diff --git a/test/haml/results/helpers.xhtml b/test/haml/results/helpers.xhtml new file mode 100644 index 0000000..1d6d8fa --- /dev/null +++ b/test/haml/results/helpers.xhtml @@ -0,0 +1,72 @@ +&&&&&&&&&&& +
    +

    Title

    +

    +Woah this is really crazy +I mean wow, +man. +

    +
    + +
    +

    Title

    +

    +Woah this is really crazy +I mean wow, +man. +

    +
    + +
    +

    Title

    +

    +Woah this is really crazy +I mean wow, +man. +

    +
    + +

    foo

    +

    reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally loooooooooooooooooong

    +
    +
    +
    +

    Big!

    +

    Small

    + +
    +
    +

    foo

    +

    bar

    +
    +
    +(parentheses!) +
    +*Not really +click +here. +

    baz

    +

    boom

    +foo +
  • google
  • +foo +

    +bar +
    +boom +baz +boom, again +

    + + + + + +
    +strong! +data + +more_data +
    +
    +
    diff --git a/test/haml/results/helpful.xhtml b/test/haml/results/helpful.xhtml new file mode 100644 index 0000000..93c05a5 --- /dev/null +++ b/test/haml/results/helpful.xhtml @@ -0,0 +1,13 @@ +
    +

    Hello

    +
    World
    +
    +
    id
    +
    class
    +
    id class
    +
    boo
    +
    moo
    +
    foo
    + +Boo + diff --git a/test/haml/results/just_stuff.xhtml b/test/haml/results/just_stuff.xhtml new file mode 100644 index 0000000..3ce2adf --- /dev/null +++ b/test/haml/results/just_stuff.xhtml @@ -0,0 +1,68 @@ + + + + + +Boo! +Embedded? false! +Embedded? true! +Embedded? true! +Embedded? twice! true! +Embedded? one af"t"er another! +

    Embedded? false!

    +

    Embedded? true!

    +

    Embedded? true!

    +

    Embedded? twice! true!

    +

    Embedded? one af"t"er another!

    +stuff followed by whitespace +block with whitespace +

    +Escape +- character +%p foo +yee\ha + don't lstrip me +

    + + +

    class attribute should appear!

    +

    this attribute shouldn't appear

    + + + +testtest +
    + + +
    + + +
    +Nested content +
    +

    Blah

    +

    Blah

    +

    Blah

    +

    Blump

    +

    Whee

    +Woah inner quotes +

    +

    + +hello +

    + +
    + diff --git a/test/haml/results/list.xhtml b/test/haml/results/list.xhtml new file mode 100644 index 0000000..c481811 --- /dev/null +++ b/test/haml/results/list.xhtml @@ -0,0 +1,12 @@ +!Not a Doctype! +
      +
    • a
    • +
    • b
    • +
    • c
    • +
    • d
    • +
    • e
    • +
    • f
    • +
    • g
    • +
    • h
    • +
    • i
    • +
    diff --git a/test/haml/results/nuke_inner_whitespace.xhtml b/test/haml/results/nuke_inner_whitespace.xhtml new file mode 100644 index 0000000..4a7bf83 --- /dev/null +++ b/test/haml/results/nuke_inner_whitespace.xhtml @@ -0,0 +1,40 @@ +

    +Foo +

    +

    +Foo +

    +

    +Foo +Bar +

    +

    +Foo +Bar +

    +

    +Foo +Bar +

    +

    +Foo +Bar +

    +

    +

    +Foo +Bar +
    +

    +

    +

    +Foo +Bar +
    +

    +

    +foo + +bar + +

    diff --git a/test/haml/results/nuke_outer_whitespace.xhtml b/test/haml/results/nuke_outer_whitespace.xhtml new file mode 100644 index 0000000..913dc10 --- /dev/null +++ b/test/haml/results/nuke_outer_whitespace.xhtml @@ -0,0 +1,140 @@ +

    +

    +Foo +

    +

    +

    +

    +Foo +

    +

    +

    +

    Foo

    +

    +

    +

    Foo

    +

    +

    +

    +Foo +

    +

    +

    +

    +Foo +

    +

    +

    +

    Foo

    +

    +

    +

    Foo

    +

    +

    +

    +Foo +Bar +

    +

    +

    +

    +Foo +Bar +

    +

    +

    +

    Foo +Bar

    +

    +

    +

    Foo +Bar

    +

    +

    +

    +foo +Foo +bar +

    +

    +

    +

    +foo +Foo +bar +

    +

    +

    +

    +fooFoobar +

    +

    +

    +

    +fooFoobar +

    +

    +

    +

    +foo +Foo +bar +

    +

    +

    +

    +foo +Foo +bar +

    +

    +

    +

    +fooFoobar +

    +

    +

    +

    +fooFoobar +

    +

    +

    +

    +foo +Foo +Bar +bar +

    +

    +

    +

    +foo +Foo +Bar +bar +

    +

    +

    +

    +fooFoo +Barbar +

    +

    +

    +

    +fooFoo +Barbar +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    diff --git a/test/haml/results/original_engine.xhtml b/test/haml/results/original_engine.xhtml new file mode 100644 index 0000000..e277475 --- /dev/null +++ b/test/haml/results/original_engine.xhtml @@ -0,0 +1,20 @@ + + + +Stop. haml time +
    +

    This is a title!

    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit

    +

    Cigarettes!

    +

    Man alive!

    +
      +
    • Slippers
    • +
    • Shoes
    • +
    • Bathrobe
    • +
    • Coffee
    • +
    +
    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/results/partial_layout.xhtml b/test/haml/results/partial_layout.xhtml new file mode 100644 index 0000000..6a60101 --- /dev/null +++ b/test/haml/results/partial_layout.xhtml @@ -0,0 +1,6 @@ +

    Partial layout used with for block:

    +
    +

    This is inside a partial layout

    +

    Some content within a layout

    + +
    diff --git a/test/haml/results/partial_layout_erb.xhtml b/test/haml/results/partial_layout_erb.xhtml new file mode 100644 index 0000000..fe433e9 --- /dev/null +++ b/test/haml/results/partial_layout_erb.xhtml @@ -0,0 +1,6 @@ +

    Partial layout used with for block:

    +
    +

    This is inside a partial layout

    +Some content within a layout + +
    diff --git a/test/haml/results/partials.xhtml b/test/haml/results/partials.xhtml new file mode 100644 index 0000000..675c526 --- /dev/null +++ b/test/haml/results/partials.xhtml @@ -0,0 +1,22 @@ +

    +@foo = +value one +

    +

    +@foo = +value two +

    +

    + @foo = + value two +

    +Toplevel? false +

    + @foo = + value three +

    + +

    +@foo = +value three +

    diff --git a/test/haml/results/render_layout.xhtml b/test/haml/results/render_layout.xhtml new file mode 100644 index 0000000..9712bb5 --- /dev/null +++ b/test/haml/results/render_layout.xhtml @@ -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 index 0000000..cd7b86f --- /dev/null +++ b/test/haml/results/silent_script.xhtml @@ -0,0 +1,74 @@ +
    +

    I can count!

    +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +

    I know my ABCs!

    +
      +
    • a
    • +
    • b
    • +
    • c
    • +
    • d
    • +
    • e
    • +
    • f
    • +
    • g
    • +
    • h
    • +
    • i
    • +
    • j
    • +
    • k
    • +
    • l
    • +
    • m
    • +
    • n
    • +
    • o
    • +
    • p
    • +
    • q
    • +
    • r
    • +
    • s
    • +
    • t
    • +
    • u
    • +
    • v
    • +
    • w
    • +
    • x
    • +
    • y
    • +
    • z
    • +
    +

    I can catch errors!

    +Oh no! "foo" happened! +

    +"false" is: +false +

    +Even! +Odd! +Even! +Odd! +Even! +
    +
    +foobar +
    +0 +1 +2 +3 +4 +
    +

    boom

    +
    diff --git a/test/haml/results/standard.xhtml b/test/haml/results/standard.xhtml new file mode 100644 index 0000000..7d7c6e4 --- /dev/null +++ b/test/haml/results/standard.xhtml @@ -0,0 +1,159 @@ + + + +Hampton Catlin Is Totally Awesome + + + + +
    +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 +
    +
    Quotes should be loved! Just like people!
    +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.| +

    +Holy cow multiline tags! A pipe (|) even! +PipesIgnored|PipesIgnored|PipesIgnored| +1|2|3 +

    +
    +this shouldn't evaluate but now it should! +
    +
      +
    • a
    • +
    • b
    • +
    • c
    • +
    • d
    • +
    • e
    • +
    • f
    • +
    +
    with this text
    +foo + + diff --git a/test/haml/results/tag_parsing.xhtml b/test/haml/results/tag_parsing.xhtml new file mode 100644 index 0000000..a575d86 --- /dev/null +++ b/test/haml/results/tag_parsing.xhtml @@ -0,0 +1,23 @@ +
    +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +
    +
    +

    +
    a
    +
    b
    +
    c
    +
    d
    +
    e
    +
    f
    +
    g
    +
    diff --git a/test/haml/results/very_basic.xhtml b/test/haml/results/very_basic.xhtml new file mode 100644 index 0000000..25f83eb --- /dev/null +++ b/test/haml/results/very_basic.xhtml @@ -0,0 +1,5 @@ + + + + + diff --git a/test/haml/results/whitespace_handling.xhtml b/test/haml/results/whitespace_handling.xhtml new file mode 100644 index 0000000..8c86da8 --- /dev/null +++ b/test/haml/results/whitespace_handling.xhtml @@ -0,0 +1,94 @@ +
    +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    +Foo bar +<pre>foo bar</pre> +<pre>foo +bar</pre> +

    <pre>foo +bar</pre>

    +

    foo +bar

    +
    +
    +13 +<textarea> +a +</textarea> +<textarea> +a +</textarea>
    +
    +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    +Foo bar +
    foo bar
    +
    foo
    bar
    +

    foo
    bar

    +

    foo +bar

    +
                                                     ___
                                                  ,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!
    +This should! + +
    +
    +13 +
    +
           __     ______        __               ______
    .----.|  |--.|__    |.----.|  |--..--------.|  __  |
    |  __||     ||__    ||  __||    < |        ||  __  |
    |____||__|__||______||____||__|__||__|__|__||______|
    +
    foo
    +bar
    diff --git a/test/haml/template_test.rb b/test/haml/template_test.rb new file mode 100644 index 0000000..0e58119 --- /dev/null +++ b/test/haml/template_test.rb @@ -0,0 +1,371 @@ +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' } + + context = ActionView::LookupContext.new(TEMPLATE_PATH) + base = ActionView::Base.new(context, 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 + + def base.compiled_method_container() self.class; 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 = "

    This is part of the broken view.

    \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", scope: @base) + + @base.instance_eval("@author = 'Hampton Catlin'") + assert_haml_ugly(".author= @author", scope: @base) + + @base.instance_eval("@author = 'Hampton'") + assert_haml_ugly("= @author", scope: @base) + + @base.instance_eval("@author = 'Catlin'") + assert_haml_ugly("= @author", scope: @base) + 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(< true)) +

    +foo +baz +

    +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 = < e + assert_match(/^\(haml\):5/, e.backtrace[0]) + else + assert false + end + end + + def test_form_builder_label_with_block; skip + output = render(< :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 & 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 & Bar\n", render('Foo #{"&"} Bar', :action_view)) + end + + def test_xss_protection_in_attributes; skip + assert_equal("
    \n", render('%div{ "data-html" => "bar" }', :action_view)) + end + + def test_xss_protection_in_attributes_with_safe_strings; skip + assert_equal("
    \n", render('%div{ "data-html" => "bar".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 & 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 & 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(< +
      +
    • Content!
    • +
    +
    +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(< '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 index 0000000..02aa9d0 --- /dev/null +++ b/test/haml/templates/_av_partial_1_ugly.haml @@ -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 index 0000000..e7d2008 --- /dev/null +++ b/test/haml/templates/_av_partial_2.haml @@ -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 index 0000000..0b854fc --- /dev/null +++ b/test/haml/templates/_av_partial_2_ugly.haml @@ -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 index 0000000..91c839d --- /dev/null +++ b/test/haml/templates/_layout.erb @@ -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 index 0000000..7cf538b --- /dev/null +++ b/test/haml/templates/_layout_for_partial.haml @@ -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 index 0000000..756b54b --- /dev/null +++ b/test/haml/templates/_partial.haml @@ -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 index 0000000..896b975 --- /dev/null +++ b/test/haml/templates/_text_area.haml @@ -0,0 +1,3 @@ +.text_area_test_area + ~ "" += "" diff --git a/test/haml/templates/_text_area_helper.html.haml b/test/haml/templates/_text_area_helper.html.haml new file mode 100644 index 0000000..f70d044 --- /dev/null +++ b/test/haml/templates/_text_area_helper.html.haml @@ -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 index 0000000..a90f423 --- /dev/null +++ b/test/haml/templates/action_view.haml @@ -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 index 0000000..9e441a3 --- /dev/null +++ b/test/haml/templates/action_view_ugly.haml @@ -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 index 0000000..57c1734 --- /dev/null +++ b/test/haml/templates/breakage.haml @@ -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 index 0000000..e9d7e6d --- /dev/null +++ b/test/haml/templates/content_for_layout.haml @@ -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 index 0000000..1e3c034 --- /dev/null +++ b/test/haml/templates/eval_suppressed.haml @@ -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 index 0000000..c4be300 --- /dev/null +++ b/test/haml/templates/helpers.haml @@ -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 index 0000000..3e44a50 --- /dev/null +++ b/test/haml/templates/helpful.haml @@ -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 index 0000000..0dd82c7 --- /dev/null +++ b/test/haml/templates/just_stuff.haml @@ -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 index 0000000..40a80e6 --- /dev/null +++ b/test/haml/templates/list.haml @@ -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 index 0000000..8eebd41 --- /dev/null +++ b/test/haml/templates/nuke_inner_whitespace.haml @@ -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 index 0000000..1e2a7f5 --- /dev/null +++ b/test/haml/templates/nuke_outer_whitespace.haml @@ -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 index 0000000..df31a5a --- /dev/null +++ b/test/haml/templates/original_engine.haml @@ -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 index 0000000..a463ea1 --- /dev/null +++ b/test/haml/templates/partial_layout.haml @@ -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 index 0000000..7f88377 --- /dev/null +++ b/test/haml/templates/partial_layout_erb.erb @@ -0,0 +1,4 @@ +

    Partial layout used with for block:

    +<%= 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 index 0000000..327d90d --- /dev/null +++ b/test/haml/templates/partialize.haml @@ -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 index 0000000..d74f4b4 --- /dev/null +++ b/test/haml/templates/partials.haml @@ -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 index 0000000..549742b --- /dev/null +++ b/test/haml/templates/render_layout.haml @@ -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 index 0000000..2df83e8 --- /dev/null +++ b/test/haml/templates/silent_script.haml @@ -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 index 0000000..c1d4866 --- /dev/null +++ b/test/haml/templates/standard.haml @@ -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 index 0000000..c1d4866 --- /dev/null +++ b/test/haml/templates/standard_ugly.haml @@ -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 index 0000000..f142ebb --- /dev/null +++ b/test/haml/templates/tag_parsing.haml @@ -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 index 0000000..93396b9 --- /dev/null +++ b/test/haml/templates/very_basic.haml @@ -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 index 0000000..f459e75 --- /dev/null +++ b/test/haml/templates/whitespace_handling.haml @@ -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" + ~ "
    foo bar
    " + ~ "
    foo\nbar
    " + %p~ "
    foo\nbar
    " + %p~ "foo\nbar" +.foo + ~ 13 + ~ "".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("
    foo bar
    ") + = find_and_preserve("
    foo\nbar
    ") + %p= find_and_preserve("
    foo\nbar
    ") + %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 index 0000000..c1aa69f --- /dev/null +++ b/test/haml/templates/with_bom.haml @@ -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 index 0000000..59997ad --- /dev/null +++ b/test/hamlit/attribute_parser_test.rb @@ -0,0 +1,101 @@ +describe Hamlit::AttributeParser do + describe '.parse' do + def assert_parse(expected, haml) + actual = Hamlit::AttributeParser.parse(haml) + if expected.nil? + assert_nil actual + else + assert_equal expected, actual + end + 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 if RUBY_ENGINE != 'truffleruby' # truffleruby doesn't have Ripper.lex +end diff --git a/test/hamlit/cli_test.rb b/test/hamlit/cli_test.rb new file mode 100644 index 0000000..02bf398 --- /dev/null +++ b/test/hamlit/cli_test.rb @@ -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/dynamic_merger_test.rb b/test/hamlit/dynamic_merger_test.rb new file mode 100644 index 0000000..9af00d8 --- /dev/null +++ b/test/hamlit/dynamic_merger_test.rb @@ -0,0 +1,57 @@ +describe Hamlit::DynamicMerger do + describe '#call' do + def assert_compile(expected, input) + actual = Hamlit::DynamicMerger.new.compile(input) + assert_equal expected, actual + end + + def assert_noop(input) + actual = Hamlit::DynamicMerger.new.compile(input) + assert_equal input, actual + end + + def strlit(body) + "%Q\0#{body}\0" + end + + specify { assert_compile([:static, 'foo'], [:multi, [:static, 'foo']]) } + specify { assert_compile([:dynamic, 'foo'], [:multi, [:dynamic, 'foo']]) } + specify { assert_noop([:multi, [:static, 'foo'], [:newline]]) } + specify { assert_noop([:multi, [:dynamic, 'foo'], [:newline]]) } + specify { assert_noop([:multi, [:static, "foo\n"], [:newline]]) } + specify { assert_noop([:multi, [:static, 'foo'], [:dynamic, "foo\n"], [:newline]]) } + specify { assert_noop([:multi, [:static, "foo\n"], [:dynamic, 'foo'], [:newline]]) } + specify do + assert_compile([:dynamic, strlit("\#{foo}foo\n")], + [:multi, [:dynamic, 'foo'], [:static, "foo\n"], [:newline]]) + end + specify do + assert_compile([:multi, + [:dynamic, strlit("\#{foo}foo\n\n")], + [:newline], [:code, 'foo'], + ], + [:multi, + [:dynamic, 'foo'], [:static, "foo\n\n"], [:newline], [:newline], + [:newline], [:code, 'foo'], + ]) + end + specify do + assert_compile([:multi, + [:dynamic, strlit("\#{foo}foo\n")], + [:code, 'bar'], + [:dynamic, strlit("\#{foo}foo\n")], + ], + [:multi, + [:dynamic, 'foo'], [:static, "foo\n"], [:newline], + [:code, 'bar'], + [:dynamic, 'foo'], [:static, "foo\n"], [:newline], + ]) + end + specify do + assert_compile([:multi, [:newline], [:dynamic, strlit("foo\n\#{foo}")]], + [:multi, [:newline], [:newline], [:static, "foo\n"], [:dynamic, 'foo']]) + end + specify { assert_compile([:static, "\n"], [:multi, [:static, "\n"]]) } + specify { assert_compile([:newline], [:multi, [:newline]]) } + end +end diff --git a/test/hamlit/engine/attributes_test.rb b/test/hamlit/engine/attributes_test.rb new file mode 100644 index 0000000..e43fefa --- /dev/null +++ b/test/hamlit/engine/attributes_test.rb @@ -0,0 +1,350 @@ +require_relative '../../test_helper' + +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 }|) } + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.4.0') + it { assert_haml(%q|%p{class: "a #{"1".concat "2", "3"}"} foo|) } + end + + 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|
    \n|, %q|#a{ id: [] }|) } + it { assert_render(%Q|
    \n|, %q|%div{ id: [nil, false] }|) } + it { assert_render(%Q|
    \n|, %q|#d#c{ id: [] }(id=id)|, locals: { id: 'a' }) } + it { assert_render(%Q|
    \n|, %q|%div{ id: nil }|) } + it { assert_render(%Q|\n|, %q|%input{ id: false }|) } + it { assert_render(%Q|\n|, %q|%input{ id: val }|, locals: { val: false }) } + it { assert_render(%Q|\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|
    \n|, %q|%div{ class: nil }|) } + it { assert_render(%Q|
    \n|, %q|%div{ class: false }|) } + it { assert_render(%Q|
    \n|, %q|%div{ class: false }|) } + it { assert_render(%Q|
    \n|, %q|%div{ class: val }|, locals: { val: false }) } + it { assert_render(%Q|
    \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=' }|) } + 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|\n|, %q|%a{ href: "'\"" }|) } + it { assert_render(%Q|\n|, %q|%input{ value: nil }|) } + it { assert_render(%Q|\n|, %q|%input{ value: false }|) } + it { assert_render(%Q|\n|, %q|%input{ value: val }|, locals: { val: false }) } + it { assert_render(%Q|\n|, %q|%input{ hash }|, locals: { hash: { value: false } }) } + it do + assert_render(%Q|
    \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|\n|, %q|%a[foo]|, locals: { foo: TestObject.new(10) }) } + it { assert_render(%Q|\n|, %q|%a[foo, nil]|, locals: { foo: TestObject.new(10) }) } + it { assert_render(%Q|\n|, %q|%a[foo]|, locals: { foo: TestObject.new(nil) }) } + it { assert_render(%Q|\n|, %q|%a[foo, 'pre']|, locals: { foo: TestObject.new(10) }) } + it { assert_render(%Q|
    \n|, %q|.static#static[TestObject.new(10)]|) } + it { assert_render(%Q|
    \n|, %q|.static#static[nil]|) } + it do + assert_render( + %Q|\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|\n|, %q|%a{ href: '/' }|) } + it { assert_render(%Q|\n|, %q|%a{ href: '/' }|, attr_quote: ?') } + it { assert_render(%Q|\n|, %q|%a{ href: '/' }|, attr_quote: ?*) } + + it { assert_render(%Q|\n|, %q|%a{ id: '/' }|, attr_quote: ?") } + it { assert_render(%Q|\n|, %q|%a{ id: val }|, attr_quote: ?", locals: { val: '/' }) } + it { assert_render(%Q|\n|, %q|%a{ hash }|, attr_quote: ?", locals: { hash: { id: '/' } }) } + + it { assert_render(%Q|\n|, %q|%a{ class: '/' }|, attr_quote: ?") } + it { assert_render(%Q|\n|, %q|%a{ class: val }|, attr_quote: ?", locals: { val: '/' }) } + it { assert_render(%Q|\n|, %q|%a{ hash }|, attr_quote: ?", locals: { hash: { class: '/' } }) } + + it { assert_render(%Q|\n|, %q|%a{ data: '/' }|, attr_quote: ?") } + it { assert_render(%Q|\n|, %q|%a{ data: val }|, attr_quote: ?", locals: { val: '/' }) } + it { assert_render(%Q|\n|, %q|%a{ data: { url: '/' } }|, attr_quote: ?") } + it { assert_render(%Q|\n|, %q|%a{ data: val }|, attr_quote: ?", locals: { val: { url: '/' } }) } + it { assert_render(%Q|\n|, %q|%a{ hash }|, attr_quote: ?", locals: { hash: { data: { url: '/' } } }) } + + it { assert_render(%Q|\n|, %q|%a{ disabled: '/' }|, attr_quote: ?") } + it { assert_render(%Q|\n|, %Q|%a{ disabled: val }|, attr_quote: ?", locals: { val: '/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, attr_quote: ?", locals: { hash: { disabled: '/' } }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, attr_quote: ?", format: :xhtml, locals: { hash: { disabled: true } }) } + + it { assert_render(%Q|\n|, %q|%a{ href: '/' }|, attr_quote: ?") } + it { assert_render(%Q|\n|, %q|%a{ href: val }|, attr_quote: ?", locals: { val: '/' }) } + it { assert_render(%Q|\n|, %q|%a{ hash }|, attr_quote: ?", locals: { hash: { href: '/' } }) } + end + + describe 'escape_attrs' do + it { assert_render(%Q|\n|, %q|%a{ id: '&<>"/' }|, escape_attrs: false) } + it { assert_render(%Q|\n|, %Q|%a{ id: val }|, escape_attrs: false, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: false, locals: { hash: { id: '&<>"/' } }) } + it { assert_render(%Q|\n|, %q|%a{ id: '&<>"/' }|, escape_attrs: true) } + it { assert_render(%Q|\n|, %Q|%a{ id: val }|, escape_attrs: true, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: true, locals: { hash: { id: '&<>"/' } }) } + + it { assert_render(%Q|\n|, %q|%a{ class: '&<>"/' }|, escape_attrs: false) } + it { assert_render(%Q|\n|, %Q|%a{ class: val }|, escape_attrs: false, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: false, locals: { hash: { class: '&<>"/' } }) } + it { assert_render(%Q|\n|, %q|%a{ class: '&<>"/' }|, escape_attrs: true) } + it { assert_render(%Q|\n|, %Q|%a{ class: val }|, escape_attrs: true, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: true, locals: { hash: { class: '&<>"/' } }) } + + it { assert_render(%Q|\n|, %q|%a{ data: '&<>"/' }|, escape_attrs: false) } + it { assert_render(%Q|\n|, %Q|%a{ data: val }|, escape_attrs: false, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: false, locals: { hash: { data: '&<>"/' } }) } + it { assert_render(%Q|\n|, %q|%a{ data: '&<>"/' }|, escape_attrs: true) } + it { assert_render(%Q|\n|, %Q|%a{ data: val }|, escape_attrs: true, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: true, locals: { hash: { data: '&<>"/' } }) } + + it { assert_render(%Q|\n|, %q|%a{ disabled: '&<>"/' }|, escape_attrs: false) } + it { assert_render(%Q|\n|, %Q|%a{ disabled: val }|, escape_attrs: false, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: false, locals: { hash: { disabled: '&<>"/' } }) } + it { assert_render(%Q|\n|, %q|%a{ disabled: '&<>"/' }|, escape_attrs: true) } + it { assert_render(%Q|\n|, %Q|%a{ disabled: val }|, escape_attrs: true, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: true, locals: { hash: { disabled: '&<>"/' } }) } + + it { assert_render(%Q|\n|, %q|%a{ href: '&<>"/' }|, escape_attrs: false) } + it { assert_render(%Q|\n|, %Q|%a{ href: val }|, escape_attrs: false, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: false, locals: { hash: { href: '&<>"/' } }) } + it { assert_render(%Q|\n|, %q|%a{ href: '&<>"/' }|, escape_attrs: true) } + it { assert_render(%Q|\n|, %Q|%a{ href: val }|, escape_attrs: true, locals: { val: '&<>"/' }) } + it { assert_render(%Q|\n|, %Q|%a{ hash }|, escape_attrs: true, locals: { hash: { href: '&<>"/' } }) } + end + + describe 'format' do + it { assert_render(%Q|\n|, %q|%a{ disabled: true }|, format: :html) } + it { assert_render(%Q|\n|, %q|%a{ disabled: val }|, format: :html, locals: { val: true }) } + it { assert_render(%Q|\n|, %q|%a{ hash }|, format: :html, locals: { hash: { disabled: true } }) } + it { assert_render(%Q|\n|, %q|%a{ disabled: true }|, format: :xhtml) } + it { assert_render(%Q|\n|, %q|%a{ disabled: val }|, format: :xhtml, locals: { val: true }) } + it { assert_render(%Q|\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 index 0000000..a1e0aa7 --- /dev/null +++ b/test/hamlit/engine/comment_test.rb @@ -0,0 +1,75 @@ +describe Hamlit::Engine do + include RenderHelper + + describe 'comment' do + it 'renders html comment' do + assert_render(%Q|\n|, '/ comments') + end + + it 'strips html comment ignoring around spcaes' do + assert_render(%Q|\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) + + + 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) + + HTML + /[[if IE]] + %span hello + world + HAML + end + it 'renders conditional comment' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + 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 index 0000000..2b79a6e --- /dev/null +++ b/test/hamlit/engine/doctype_test.rb @@ -0,0 +1,21 @@ +describe Hamlit::Engine do + include RenderHelper + + describe 'doctype' do + it 'renders html5 doctype' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + HTML + !!! + HAML + end + + it 'renders xml doctype' do + assert_render(<<-HTML.unindent, <<-HAML.unindent, format: :xhtml) + + 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 index 0000000..d91b2e3 --- /dev/null +++ b/test/hamlit/engine/indent_test.rb @@ -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) +

    + +

    + HTML + %p + \t%a + HAML + end + + it 'accepts N-space indentation' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +

    + + foo + +

    + HTML + %p + %span + foo + HAML + end + + it 'accepts N-tab indentation' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +

    + + foo + +

    + 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 index 0000000..40510ee --- /dev/null +++ b/test/hamlit/engine/multiline_test.rb @@ -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) + +
    + 12 +
    +
    + 3 +
    +
    + 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 index 0000000..f2b6621 --- /dev/null +++ b/test/hamlit/engine/new_attribute_test.rb @@ -0,0 +1,101 @@ +describe Hamlit::Engine do + include RenderHelper + + describe 'new attributes' do + it 'renders attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +

    bar

    + HTML + %p(class='foo') bar + HAML + end + + it 'renders multiple attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +

    bar

    + HTML + %p(a=1 b=2) bar + HAML + end + + it 'renders hyphenated attributes properly' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +

    bar

    + HTML + %p(data-foo='bar') bar + HAML + end + + it 'renders multiply hyphenated attributes properly' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +

    bar

    + 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) + + + + HTML + %a(title="'") + %a(title = "'\"") + %a(href='/search?foo=bar&hoge=') + HAML + end + + it 'escapes attribute values on dynamic attributes' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + + HTML + - title = "'\"" + - href = '/search?foo=bar&hoge=' + %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) +
    + HTML + .item(class='first') + HAML + end + + it 'does not generate double classes for a variable' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + 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) +
    + HTML + #item(id='first') + HAML + end + + it 'concatenates ids with underscore for a variable' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + 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 index 0000000..9ee2428 --- /dev/null +++ b/test/hamlit/engine/old_attribute_test.rb @@ -0,0 +1,476 @@ +require_relative '../../test_helper' + +describe Hamlit::Engine do + include RenderHelper + + describe 'old attributes' do + it 'renders attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + bar + HTML + %span{class: 'foo'} bar + HAML + end + + it 'renders attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + bar + HTML + %span{ data: 2 } bar + HAML + end + + it 'renders attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + bar + HTML + %span{ :class => 'foo' } bar + HAML + end + + it 'renders attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + bar + HTML + %span{ :class => 'foo', id: 'bar' } bar + HAML + end + + it 'renders attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + bar + HTML + %span{ :'data-disabled' => true } bar + HAML + end + + it 'accepts method call including comma' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + 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) + bar + HTML + %span{ class: 'foo' } bar + HAML + end + + it 'renders multi-byte chars as static attribute value' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + こんにちは + HTML + %img{ alt: 'こんにちは' } + HAML + end + + it 'sorts static attributes by name' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + + 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) + + HTML + - hash = { foo: 'bar' } + %span{ hash } + HAML + end + + it 'renders multiples hashes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + 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) + + HTML + - h1 = { a: 'b' } + - h2 = { c: 'd' } + - h3 = { e: 'f' } + %span{ h1, h2, h3, g: 'h', i: 'j' } + HAML + end + + it 'does not crash when nil is given' do + if /java/ === RUBY_PLATFORM + skip 'maybe due to Ripper of JRuby' + end + if RUBY_ENGINE == 'truffleruby' + skip 'truffleruby raises NoMethodError' + end + + assert_raises ArgumentError do + render_hamlit("%div{ nil }") + end + end + end + + describe 'joinable attributes' do + it 'joins class with a space' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +

    +

    +

    + 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) +
    +
    +
    +
    + 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) +

    +

    +

    + 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) + + 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) + + + + + + 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) +
    +
    +
    +
    +
    +
    +
    +
    + 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) + + + + + + + + + + + + + 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) + + + + HTML + %a{title: "'"} + %a{title: "'\""} + %a{href: '/search?foo=bar&hoge='} + HAML + end + + it 'escapes attribute values on dynamic attributes' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + + HTML + - title = "'\"" + - href = '/search?foo=bar&hoge=' + %a{title: title} + %a{href: href} + HAML + end + + it 'escapes attribute values on hash attributes' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + + HTML + - title = { title: "'\"" } + - href = { href: '/search?foo=bar&hoge=' } + %a{ title } + %a{ href } + HAML + end + end + + describe 'nested data attributes' do + it 'renders data attribute by hash' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + HTML + - hash = { bar: 'baz' } + %span.foo{ data: hash } + HAML + end + + it 'renders true attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + bar + HTML + %span{ data: { disabled: true } } bar + HAML + end + + it 'renders nested hash whose value is variable' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + bar + 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) +
    + HTML + %div{ data: { raw_src: 'foo' } } + HAML + end + + it 'changes an underscore in a nested dynamic attribute' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + HTML + - hash = { raw_src: 'foo' } + %div{ data: hash } + HAML + end + end + + describe 'nested aria attributes' do + it 'renders aria attribute by hash' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + HTML + - hash = { bar: 'baz' } + %span.foo{ aria: hash } + HAML + end + + it 'renders true attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + bar + HTML + %span{ aria: { disabled: true } } bar + HAML + end + + it 'renders nested hash whose value is variable' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + bar + HTML + - hash = { disabled: true } + %span{ aria: hash } bar + HAML + end + + it 'changes an underscore in a nested key to a hyphen' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + HTML + %div{ aria: { raw_src: 'foo' } } + HAML + end + + it 'changes an underscore in a nested dynamic attribute' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + HTML + - hash = { raw_src: 'foo' } + %div{ aria: hash } + HAML + end + end if RUBY_ENGINE != 'truffleruby' # aria attribute is not working in truffleruby + + describe 'element class with attribute class' do + it 'does not generate double classes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + HTML + .item{ class: 'first' } + HAML + end + + it 'does not generate double classes for a variable' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + HTML + - val = 'val' + .element{ class: val } + HAML + end + + it 'does not generate double classes for hash attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + 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) +
    + HTML + #item{ id: 'first' } + HAML + end + + it 'does not generate double ids for a variable' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + HTML + - val = 'first' + #item{ id: val } + HAML + end + + it 'does not generate double ids for hash attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + 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) +
    + 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) + + HTML + %meta{ content: 'IE=edge', 'http-equiv': 'X-UA-Compatible' } + HAML + end + + it 'renders dynamic attributes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + 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 index 0000000..eda58a0 --- /dev/null +++ b/test/hamlit/engine/script_test.rb @@ -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 + 12 + 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) + + ## + ["#", "#"] + HTML + = # comment_only + = '#' + "#" # = 3 # + = ['#', + "#"] # comment + HAML + end + + it 'renders multi-lines script' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + 3 + 4 / 2 + -1 + + 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) + + 0 + 1 + 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 + - obj = Object.new; def obj.foo(a, b); a + b; end + = obj.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) + <"&> + <"&> + HTML + &= '<"&>' + &= '<"&>'.tap do |str| + -# no operation + HAML + end + + it 'regards ~ operator as =' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + <code>hello + world</code> + HTML + ~ "hello\nworld" + 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|3\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 index 0000000..098eaf9 --- /dev/null +++ b/test/hamlit/engine/silent_script_test.rb @@ -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) + + ok + + HTML + %span + - if false + ng + - else + ok + HAML + end + + it 'renders empty elsif statement' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + + HTML + %span + - if false + - elsif false + HAML + end + + it 'renders empty else statement' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + + HTML + %span + - if false + ng + - else + HAML + end + + it 'renders empty when statement' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + + 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") + [", "] + HTML + end + + it 'accepts illegal indent in continuing code' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + +
    + 3 +
    +
    + HTML + %span + %div + - obj = Object.new; def obj.foo(a, b); a + b; end + - num = obj.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 index 0000000..821447c --- /dev/null +++ b/test/hamlit/engine/tag_test.rb @@ -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) + hello + HTML + %span hello + HAML + end + + it 'accepts multi-line =' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + o + HTML + %span= 'hello'.gsub('hell', + '') + HAML + end + + it 'renders multi-line tag' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + hello + + HTML + %span + hello + HAML + end + + it 'renders a nested tag' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + + hello + + + world + + + HTML + %span + %b + hello + %i + %small world + HAML + end + + it 'renders multi-line texts' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + + hello + world + + + HTML + %span + %b + hello + world + HAML + end + + it 'ignores empty lines' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + + hello + + + HTML + %span + + %b + + hello + + HAML + end + + it 'renders classes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + hello + HTML + %span.foo-1.bar_A hello + HAML + end + + it 'renders ids only last one' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + hello + + HTML + %span#Bar_0#bar- + hello + HAML + end + + it 'renders ids and classes' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + hello + HTML + %span#a.b#c.d hello + HAML + end + + it 'renders implicit div tag starting with id' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + HTML + #hello.world + HAML + end + + it 'renders implicit div tag starting with class' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    + foo +
    + HTML + .world#hello + foo + HAML + end + + it 'renders large-case tag' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + foo + + HTML + %SPAN + foo + HAML + end + + it 'renders h1 tag' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +

    foo

    + 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) + raise 'a' + HTML + %span- raise 'a' + HAML + end + + it 'renders a text just after attributes' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + a + HTML + %span{a: 2}a + HAML + end + + it 'strips a text' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + foo + HTML + %span foo + HAML + end + + it 'ignores spaces after tag' do + assert_render(<<-HTML.unindent, "%span \n a") + + a + + HTML + end + + it 'parses self-closing tag' do + assert_render(<<-HTML.unindent, <<-HAML.unindent, format: :xhtml) +
    +
    + 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 index 0000000..178b1b1 --- /dev/null +++ b/test/hamlit/engine/text_test.rb @@ -0,0 +1,212 @@ +describe Hamlit::Engine do + include RenderHelper + + describe 'text' do + it 'renders string interpolation' do + skip 'escape is not working well in truffleruby' if RUBY_ENGINE == 'truffleruby' + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + a3aa" ["1", 2] b " ! + a{:a=>3} + + HTML + #{ "a#{3}a" }a" #{["1", 2]} b " ! + a#{{ a: 3 }} + + 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 + skip 'escape is not working well in truffleruby' if RUBY_ENGINE == 'truffleruby' + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + = + = + + <a> + HTML + === + == = + == + == #{''} + HAML + end + + it 'renders !== operator' do + skip 'escape is not working well in truffleruby' if RUBY_ENGINE == 'truffleruby' + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + <a> + + = + = + HTML + == #{''} + !== #{''} + !=== + !== = + 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) +
    + foo +
    - bar
    +
    - baz
    +
    + HTML + %div + foo + .test - bar + .test - baz + HAML + end + + describe 'inline operator' do + it 'renders ! operator' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + + + HTML + %span!#{''} + %span! #{''} + ! #{''} + HAML + end + + it 'renders & operator' do + skip 'escape is not working well in truffleruby' if RUBY_ENGINE == 'truffleruby' + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + <nyaa> + <nyaa> + <nyaa> + HTML + %span& #{''} + %span&#{''} + & #{''} + HAML + end + + it 'renders !, & operator right before a non-space character' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) +   +   + !hello + !hello + HTML +   + \  + !hello + \!hello + HAML + end + + it 'renders &, ! operator inside a tag' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +   + nbsp; + nbsp; + !hello + hello + hello + HTML + %span   + %span  + %span& nbsp; + %span !hello + %span!hello + %span! hello + HAML + end + + it 'does not accept backslash operator' do + assert_render(<<-'HTML'.unindent, <<-'HAML'.unindent) + \ foo + HTML + %span\ foo + HAML + end + + it 'renders != operator' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + HTML + %span!= '' + HAML + end + + it 'renders !== operator' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + + + + HTML + %span!==#{''} + %span!== #{''} + !==#{''} + !== #{''} + HAML + end + + it 'renders &= operator' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + <nyaa> + HTML + %span&= '' + HAML + end + + it 'renders &== operator' do + skip 'escape is not working well in truffleruby' if RUBY_ENGINE == 'truffleruby' + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + = + = + <p> + HTML + &=== + &== = + &== #{'

    '} + HAML + end + + it 'renders ~ operator' do + assert_render(<<-HTML.unindent, <<-HAML.unindent, escape_html: false) + 1 + 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<b>c\n", 'a#{""}c') } if RUBY_ENGINE != 'truffleruby' # escape is not working in truffleruby + end + end +end diff --git a/test/hamlit/engine/whitespace_test.rb b/test/hamlit/engine/whitespace_test.rb new file mode 100644 index 0000000..20ebd00 --- /dev/null +++ b/test/hamlit/engine/whitespace_test.rb @@ -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) + ab + c + d + + e + + f + 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) + a + b + + c + + HTML + %span a + - if true + %span> + b + %span + c + HAML + end + + it 'removes whitespaces inside block script' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + foofoo2bar + 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) +

    foofoofoo
    + HTML + .bar< + - 3.times do + = 'foo' + HAML + end + + it 'removes whitespace inside script recursively' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    bar1bar1bar1bar12
    + 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) +
    helloworld
    + HTML + %div< + #{'hello'} + world + HAML + end + + it 'removes whitespace inside script inside silent script' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) +
    12
    + HTML + .bar< + - 1.times do + = '1' + = '2' + HAML + end + + it 'does not nuke internal recursively' do + assert_render(%Q|
    \nhello\n
    |, <<-HAML.unindent) + %div>< + %span> + hello + HAML + end + + it 'does not nuke inside script' do + assert_render(%Q|
    \nhello\n1
    |, <<-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 index 0000000..f1c464c --- /dev/null +++ b/test/hamlit/error_test.rb @@ -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 index 0000000..f9497a0 --- /dev/null +++ b/test/hamlit/filters/cdata_test.rb @@ -0,0 +1,27 @@ +describe Hamlit::Filters do + include RenderHelper + + describe '#compile' do + it 'renders cdata' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + HTML + :cdata + foo bar + HAML + end + + it 'parses string interpolation' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + 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 index 0000000..d4dbb5c --- /dev/null +++ b/test/hamlit/filters/coffee_test.rb @@ -0,0 +1,62 @@ +describe Hamlit::Filters do + include RenderHelper + + describe '#compile' do + it 'renders coffee filter' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + HTML + :coffee + foo = -> + alert('hello') + HAML + end + + it 'renders coffeescript filter' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + HTML + :coffeescript + foo = -> + alert('hello') + HAML + end + + it 'renders coffeescript filter' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + HTML + :coffee + foo = -> + alert("#{'<&>'}") + HAML + end + end unless /java/ === RUBY_PLATFORM # execjs is not working with Travis JRuby environment +end diff --git a/test/hamlit/filters/css_test.rb b/test/hamlit/filters/css_test.rb new file mode 100644 index 0000000..8e3ad29 --- /dev/null +++ b/test/hamlit/filters/css_test.rb @@ -0,0 +1,35 @@ +describe Hamlit::Filters do + include RenderHelper + + describe '#compile' do + it 'renders css' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + HTML + :css + .foo { + width: 100px; + } + HAML + end + + it 'parses string interpolation' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + 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 index 0000000..baf3834 --- /dev/null +++ b/test/hamlit/filters/erb_test.rb @@ -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 index 0000000..2a0b4b0 --- /dev/null +++ b/test/hamlit/filters/javascript_test.rb @@ -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 + + after + HTML + before + :javascript + after + HAML + end + + it 'compiles javascript filter' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + before + + after + HTML + before + :javascript + alert('hello'); + after + HAML + end + + it 'accepts illegal indentation' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + + HTML + :javascript + if { + alert('hello'); + } + :javascript + if { + alert('hello'); + } + HAML + end + + it 'accepts illegal indentation' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + HTML + :javascript + if { + alert('a'); + } + HAML + end + + it 'parses string interpolation' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + 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 index 0000000..bb84f9f --- /dev/null +++ b/test/hamlit/filters/markdown_test.rb @@ -0,0 +1,42 @@ +describe Hamlit::Filters do + include RenderHelper + + describe '#compile' do + it 'renders markdown filter' do + if /java/ === RUBY_PLATFORM && !system('which pandoc > /dev/null') + skip 'pandoc is required to test :markdown filter' + end + + assert_render(<<-HTML.unindent, <<-HAML.unindent) +

    Hamlit

    + +

    Yet another haml implementation

    + + HTML + :markdown + # Hamlit + Yet another haml implementation + HAML + end + + it 'renders markdown filter with string interpolation' do + if /java/ === RUBY_PLATFORM && !system('which pandoc > /dev/null') + skip 'pandoc is required to test :markdown filter' + end + + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) +

    + +

    <&> + Yet another haml implementation

    + + HTML + - project = '' + :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 index 0000000..78e4a64 --- /dev/null +++ b/test/hamlit/filters/plain_test.rb @@ -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) + + 5 + HTML + :coffee + jQuery ($) -> + console.log('#{__LINE__}') + console.log('#{__LINE__}') + = __LINE__ + HAML + end + + it 'renders dynamic filter' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + 5 + HTML + :coffee + jQuery ($) -> + console.log('3') + console.log('4') + = __LINE__ + HAML + end + end unless /java/ === RUBY_PLATFORM # execjs is not working with Travis JRuby environment + + describe 'css filter' do + it 'renders static filter' do + assert_render(<<-HTML.unindent, <<-HAML.unindent) + + 6 + HTML + :css + body { + width: 3px; + height: 4px; + } + = __LINE__ + HAML + end + + it 'renders dynamic filter' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + 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) + + 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) + + 5 + HTML + :javascript + console.log("2"); + console.log("3"); + + = __LINE__ + HAML + end + + it 'renders dynamic filter' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + + 5 + HTML + :javascript + console.log("#{__LINE__}"); + console.log("#{__LINE__}"); + + = __LINE__ + HAML + end + end unless /java/ === RUBY_PLATFORM # execjs is not working with Travis JRuby environment + end + + describe 'dynamic merger' do + it 'renders optimized string' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + foo1 + 2 + 3bar + 5baz + HTML + foo#{__LINE__} + #{__LINE__} + #{__LINE__}bar + - 1.to_s + #{__LINE__}baz + HAML + end + end +end if RUBY_ENGINE != 'truffleruby' # negetive line numbers are broken in truffleruby diff --git a/test/hamlit/optimization_test.rb b/test/hamlit/optimization_test.rb new file mode 100644 index 0000000..303a97e --- /dev/null +++ b/test/hamlit/optimization_test.rb @@ -0,0 +1,47 @@ +require_relative '../test_helper' + +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|\n1\n|) + end + + it 'renders inline static script statically' do + haml = %|%span= 1| + assert_equal true, compiled_code(haml).include?(%|1|) + 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?(%|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 if RUBY_ENGINE != 'truffleruby' # truffleruby does not implement major Ripper features diff --git a/test/hamlit/rails_template_test.rb b/test/hamlit/rails_template_test.rb new file mode 100644 index 0000000..92b0d22 --- /dev/null +++ b/test/hamlit/rails_template_test.rb @@ -0,0 +1,166 @@ +# 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 = Class.new(ActionView::Base) do + def compiled_method_container + self.class + end + end.new(ActionView::LookupContext.new('')) + base.render(inline: haml, type: :haml) + end + + specify 'html escape' do + assert_equal %Q|<script>alert("a");</script>\n|, render(<<-HAML.unindent) + = '' + HAML + assert_equal %Q|\n|, render(<<-HAML.unindent) + = ''.html_safe + HAML + skip 'escape is not working well in truffleruby' if RUBY_ENGINE == 'truffleruby' + assert_equal %Q|<script>alert("a");</script>\n|, render(<<-'HAML'.unindent) + #{''} + HAML + assert_equal %Q|\n|, render(<<-'HAML'.unindent) + #{''.html_safe} + HAML + end + + specify 'attribute escape' do + assert_equal %Q|
    \n|, render(<<-HAML.unindent) + %a{ href: '' } + HAML + assert_equal %Q|\n|, render(<<-HAML.unindent) + %a{ href: '