From: Pirate Praveen Date: Fri, 27 Aug 2021 12:57:59 +0000 (+0100) Subject: Import ruby-hamlit_2.15.1.orig.tar.gz X-Git-Tag: archive/raspbian/2.15.1-2+rpi1~5 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=c2b3b6494834db25dff6b59888c2852f9226706c;p=ruby-hamlit.git Import ruby-hamlit_2.15.1.orig.tar.gz [dgit import orig ruby-hamlit_2.15.1.orig.tar.gz] --- c2b3b6494834db25dff6b59888c2852f9226706c diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..36c8bcb --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: k0kubun diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 0000000..29ca59f --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,46 @@ +name: bench +on: + push: + branches: + - master + pull_request: + types: + - opened + - synchronize + - reopened + schedule: + - cron: "00 15 * * *" # 7:00 PST (-8), 8:00 PDT (-7) +jobs: + bench: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - slim: 1 + - template: 'benchmark/boolean_attribute.haml,benchmark/class_attribute.haml,benchmark/id_attribute.haml,benchmark/data_attribute.haml,benchmark/common_attribute.haml' + - 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' + - template: 'benchmark/etc/attribute_builder.haml' + - template: 'benchmark/etc/static_analyzer.haml' + - template: 'benchmark/etc/string_interpolation.haml' + - template: 'test/haml/templates/standard.haml' + compile: 1 + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.0 + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-${{ matrix.ruby }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: ${{ runner.os }}-gems- + - run: sudo apt-get update && sudo apt-get install -y nodejs libxslt-dev # nodejs for execjs, libxslt for TruffleRuby nokogiri + - name: bundle install + run: bundle config path vendor/bundle && bundle install -j$(nproc) --retry 3 + - run: bundle exec rake bench + env: + SLIM_BENCH: ${{ matrix.slim }} + TEMPLATE: ${{ matrix.template }} + COMPILE: ${{ matrix.compile }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..074eded --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: test +on: + push: + branches: + - master + pull_request: + types: + - opened + - synchronize + - reopened + schedule: + - cron: "00 15 * * *" # 7:00 PST (-8), 8:00 PDT (-7) +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: + - '2.5' + - '2.6' + - '2.7' + - '3.0' + - jruby + - truffleruby-head + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-${{ matrix.ruby }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: ${{ runner.os }}-gems- + - run: sudo apt-get update && sudo apt-get install -y nodejs libxslt-dev # nodejs for execjs, libxslt for TruffleRuby nokogiri + - name: bundle install + run: bundle config path vendor/bundle && bundle install -j$(nproc) --retry 3 + - run: bundle exec rake test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f20e4cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +/haml/ +.sass-cache +.ruby-version +*.bundle +*.so +*.su +*.a +*.o +*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ee39fad --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,826 @@ +# 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.15.1](https://github.com/k0kubun/hamlit/compare/v2.15.0...v2.15.1) - 2021-07-23 + +### Fixed + +- Remove `benchmark` from the gem package to reduce its size [#186](https://github.com/k0kubun/hamlit/issues/186) + *Thanks to @pocke* + +## [2.15.0](https://github.com/k0kubun/hamlit/compare/v2.14.6...v2.15.0) - 2021-04-12 + +### Added + +- Always use Hamlit when both haml.gem and hamlit.gem are installed in Rails + *Thanks to @igor-drozdov* + +## [2.14.6](https://github.com/k0kubun/hamlit/compare/v2.14.5...v2.14.6) - 2021-03-23 + +### Fixed + +- Optimize v2.14.5's implementation a little + +## [2.14.5](https://github.com/k0kubun/hamlit/compare/v2.14.4...v2.14.5) - 2021-03-23 + +### Added + +- Support `config.action_view.annotate_rendered_view_with_filenames = true` of Rails 6.1 + *Thanks to @kirin121* + +## [2.14.4](https://github.com/k0kubun/hamlit/compare/v2.14.3...v2.14.4) - 2021-02-01 + +### Fixed + +- Prevent another SEGV in a C extension after `GC.compact` [#177](https://github.com/k0kubun/hamlit/issues/177) + *Thanks to @stanhu* + +## [2.14.3](https://github.com/k0kubun/hamlit/compare/v2.14.2...v2.14.3) - 2021-01-24 + +### Fixed + +- Ensure the Rails initializer is called before `:load_config_initializers` [#176](https://github.com/k0kubun/hamlit/issues/176) + *Thanks to @sunny* + +## [2.14.2](https://github.com/k0kubun/hamlit/compare/v2.14.1...v2.14.2) - 2021-01-21 + +### Fixed + +- Prevent SEGV in a C extension after `GC.compact` [#171](https://github.com/k0kubun/hamlit/issues/171) + *Thanks to @stanhu* + +## [2.14.1](https://github.com/k0kubun/hamlit/compare/v2.14.0...v2.14.1) - 2021-01-07 + +### Added + +- Add `-c` option to `hamlit compile` that works like `haml -c` [#166](https://github.com/k0kubun/hamlit/issues/166) + *Thanks to @knightq* + +## [2.14.0](https://github.com/k0kubun/hamlit/compare/v2.13.2...v2.14.0) - 2021-01-07 + +### Changed + +- CLI changes + - Remove `-c` shorthand of `--color`. + - Make `--color` default. Please use `--no-color` to disable it. + - `--color` uses IRB instead of Pry for syntax highlight. + - Syntax highlight of `hamlit compile` is enabled only with IRB of Ruby 2.7+. + - Syntax highlight of `hamlit parse` / `hamlit temple` is enabled only with IRB of Ruby 3.1+. + +## [2.13.2](https://github.com/k0kubun/hamlit/compare/v2.13.1...v2.13.2) - 2020-12-27 + +### Added + +- Speed up `hamlit` commands [#166](https://github.com/k0kubun/hamlit/issues/166) + *Thanks to @knightq* + +## [2.13.1](https://github.com/k0kubun/hamlit/compare/v2.13.0...v2.13.1) - 2020-12-27 + +### Added + +- Support [multiline attributes](https://github.com/haml/haml/pull/1043) of Haml 5.2.1. + +## [2.13.0](https://github.com/k0kubun/hamlit/compare/v2.12.0...v2.13.0) - 2020-10-02 + +### Added + +- Support `--enable-frozen-string-literal` [#162](https://github.com/k0kubun/hamlit/issues/162). + *Thanks to @aliismayilov* + +### Changed + +- Upgrade the Haml parser from Haml 4.0 to 5.2 [#163](https://github.com/k0kubun/hamlit/issues/163). + - Allow `@` as tag's class name. + - Fix NameError on an `InvalidAttributeNameError` reference introduced at Hamlit v2.12.0. + - You can no longer specify `ugly` option, which has had no effect. + +## [2.12.0](https://github.com/k0kubun/hamlit/compare/v2.11.1...v2.12.0) - 2020-09-30 + +### Changed + +- Class names are no longer ordered alphabetically. + *Thanks to @aliismayilov* + - This is compatible with [Haml 5.2](https://github.com/haml/haml/blob/v5.2.0/CHANGELOG.md#52) + +## [2.11.1](https://github.com/k0kubun/hamlit/compare/v2.11.0...v2.11.1) - 2020-08-25 + +### Fixed + +- Fix a line number on an error after filters like preserve, plain, and ruby. + *Thanks to @rgisiger* + +## [2.11.0](https://github.com/k0kubun/hamlit/compare/v2.10.1...v2.11.0) - 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..9515326 --- /dev/null +++ b/Gemfile @@ -0,0 +1,24 @@ +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 + +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' + gem 'faml' + gem 'stackprof' + end +end diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..bb938b0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,47 @@ +Copyright (c) 2006-2019 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. + +=== + +The above license is applied to lib/hamlit/parser/*.rb and test/haml/*. +Everything else is licensed under: + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3211b67 --- /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) +[![test](https://github.com/k0kubun/hamlit/workflows/test/badge.svg)](https://github.com/k0kubun/hamlit/actions?query=workflow%3Atest) + +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 **1.94x 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/4.1.0/benchmarks/run-benchmarks.rb) for fairness. ([Result on Travis](https://travis-ci.org/github/k0kubun/hamlit/jobs/732178446)) + +Hamlit Benchmark + +``` + hamlit v2.13.0: 247404.4 i/s + erubi v1.9.0: 244356.4 i/s - 1.01x slower + slim v4.1.0: 238254.3 i/s - 1.04x slower + faml v0.8.1: 197293.2 i/s - 1.25x slower + haml v5.2.0: 127834.4 i/s - 1.94x 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..73d9baa --- /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/graph/graph.key b/benchmark/graph/graph.key new file mode 100644 index 0000000..8e73b78 Binary files /dev/null and b/benchmark/graph/graph.key differ diff --git a/benchmark/graph/graph.png b/benchmark/graph/graph.png new file mode 100644 index 0000000..d6ca85b Binary files /dev/null and b/benchmark/graph/graph.png differ 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/bin/update-haml b/bin/update-haml new file mode 100755 index 0000000..2e49933 --- /dev/null +++ b/bin/update-haml @@ -0,0 +1,125 @@ +#!/usr/bin/env ruby +require 'fileutils' +require 'tmpdir' + +HAML_REPO = 'haml/haml' +HAML_VERSION = '5.2.1' + +module GitHubFetcher + def self.fetch(repo, tag:, path:) + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + url = "https://github.com/#{repo}/archive/#{tag}.tar.gz" + system("curl -L --fail --retry 3 --retry-delay 1 #{url} -o - | tar zxf -") + FileUtils.mv("#{File.basename(repo)}-#{tag.sub(/\Av/, '')}", path) + end + end + end +end + +class LicenseBuilder + DELIMITER = "\n===" + + def initialize(haml_license:, hamlit_license:) + @haml_license = haml_license + @hamlit_license = hamlit_license + end + + def build + license = [ + File.read(@haml_license), + File.read(@hamlit_license).split(DELIMITER, 2).last, + ].join(DELIMITER) + File.write(@hamlit_license, license) + end +end + +# Generate lib/hamlit/parser from haml +class HamlitParserBuilder + TARGET_FILES = [ + 'attribute_builder.rb', + 'buffer.rb', + 'error.rb', + 'helpers.rb', + 'options.rb', + 'temple_engine.rb', + # TODO: make the upstream sharable first + # 'parser.rb', + 'util.rb', + 'helpers/xss_mods.rb', + ] + + # Classes which are just referenced by Options and not really used by the parser + DUMMY_CLASSES = { + 'compiler.rb' => 'Compiler', + 'escapable.rb' => 'Escapable', + 'generator.rb' => 'Generator', + } + + def initialize(haml:, hamlit_parser:) + @haml = haml + @hamlit_parser = hamlit_parser + end + + def build + TARGET_FILES.each do |file| + src_path = File.join(@haml, file) + dest_path = File.join(@hamlit_parser, "haml_#{file}") + + FileUtils.mkdir_p(File.dirname(dest_path)) + FileUtils.cp(src_path, dest_path) + + src = File.read(dest_path) + patch_source!(src, file: file) + File.write(dest_path, src) + end + + DUMMY_CLASSES.each do |file, klass| + dest_path = File.join(@hamlit_parser, "haml_#{file}") + src = "class Hamlit::Haml#{klass}; end\n" + File.write(dest_path, src) + end + end + + private + + def patch_source!(src, file:) + # Use Hamlit::HamlFoo instead of Haml::Foo + src.gsub!(/^module Haml\n((?: #[^\n]+\n)*) (module|class) ([^ ]+)/, "module Hamlit\n\\1 \\2 Haml\\3") + src.gsub!(/\bHaml::/, 'Hamlit::Haml') + + # Prefix Haml to references without Haml:: + src.gsub!(/\b(AttributeBuilder|Error|InvalidAttributeNameError|Options|Parser|SyntaxError)\./, 'Haml\0') + src.gsub!(/\brescue SyntaxError /, 'rescue HamlSyntaxError ') + + # Hamlit should not rely on Haml + src.gsub!(/^require 'haml\/([^']+)'/, "require 'hamlit/parser/haml_\\1'") + + case file + when 'error.rb' + src.gsub!(/^ class ([^ ]+) < ([^ ]+);/, ' class Haml\1 < Haml\2;') + when 'helpers.rb' + src.gsub!(/^ def is_haml\?\n false\n end/m) { |str| str.gsub(/^ /, ' # ') } # not needed for the parser + when 'options.rb' + src.gsub!(/\.is_a\?\(Options\)/, '.is_a?(HamlOptions)') + when 'temple_engine.rb' + src.gsub!(/\buse (Generator|Escapable)$/, 'use Haml\1') + end + end +end + +FileUtils.rm_rf(haml = File.expand_path('../haml', __dir__)) +GitHubFetcher.fetch(HAML_REPO, tag: HAML_VERSION, path: haml) + +hamlit = File.expand_path('..', __dir__) +LicenseBuilder.new( + haml_license: File.join(haml, 'MIT-LICENSE'), + hamlit_license: File.join(hamlit, 'LICENSE.txt'), +).build + +hamlit_parser = File.join(hamlit, 'lib/hamlit/parser') +# TODO: FileUtils.rm_rf(hamlit_parser = File.join(hamlit, 'lib/hamlit/parser')) +HamlitParserBuilder.new( + haml: File.join(haml, 'lib/haml'), + hamlit_parser: hamlit_parser, +).build 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..06bbbd5 --- /dev/null +++ b/ext/hamlit/hamlit.c @@ -0,0 +1,537 @@ +#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_xhtml; + +static VALUE str_aria, str_data, str_equal, str_hyphen, str_space, str_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_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; + 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 boolean_attributes, 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, boolean_attributes)) { + 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, 5, UNLIMITED_ARGUMENTS); + rb_scan_args(argc - 5, argv + 5, "*", &array); + + return hamlit_build(argv[0], argv[1], argv[2], argv[3], argv[4], 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_xhtml = rb_intern("xhtml"); + + // Consider using rb_interned_str() once we stop supporting Ruby 2.7. + rb_gc_register_mark_object(str_aria = rb_obj_freeze(rb_str_new_cstr("aria"))); + rb_gc_register_mark_object(str_data = rb_obj_freeze(rb_str_new_cstr("data"))); + rb_gc_register_mark_object(str_equal = rb_obj_freeze(rb_str_new_cstr("="))); + rb_gc_register_mark_object(str_hyphen = rb_obj_freeze(rb_str_new_cstr("-"))); + rb_gc_register_mark_object(str_space = rb_obj_freeze(rb_str_new_cstr(" "))); + rb_gc_register_mark_object(str_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..bce4e31 --- /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|benchmark)/}) } + 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' + 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..ce7c5ca --- /dev/null +++ b/lib/hamlit.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +require 'hamlit/engine' +require 'hamlit/error' +require 'hamlit/version' +require 'hamlit/template' + +if File.basename($0) != 'hamlit' + begin + require 'rails' + require 'hamlit/railtie' + rescue LoadError + end +end diff --git a/lib/hamlit/ambles.rb b/lib/hamlit/ambles.rb new file mode 100644 index 0000000..3036a82 --- /dev/null +++ b/lib/hamlit/ambles.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module Hamlit + class Ambles < Temple::Filter + define_options :preamble, :postamble + + def initialize(*) + super + @preamble = options[:preamble] + @postamble = options[:postamble] + end + + def call(ast) + ret = [:multi] + ret << [:static, @preamble] if @preamble + ret << ast + ret << [:static, @postamble] if @postamble + ret + end + end +end diff --git a/lib/hamlit/attribute_builder.rb b/lib/hamlit/attribute_builder.rb new file mode 100644 index 0000000..0893083 --- /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, boolean_attributes, 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).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).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..ac7cbcb --- /dev/null +++ b/lib/hamlit/attribute_compiler.rb @@ -0,0 +1,128 @@ +# 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[:dynamic_attributes].new, node.value[:dynamic_attributes].old].compact.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 = [] + attrs.unshift(node.value[:attributes].inspect) if node.value[:attributes] != {} + + args = [ + @escape_attrs.inspect, "#{@quote.inspect}.freeze", @format.inspect, + '::Hamlit::AttributeBuilder::BOOLEAN_ATTRIBUTES', node.value[:object_ref], + ] + attrs + [:html, :attrs, [:dynamic, "::Hamlit::AttributeBuilder.build(#{args.join(', ')}, #{node.value[:dynamic_attributes].to_literal})"]] + 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..106c882 --- /dev/null +++ b/lib/hamlit/cli.rb @@ -0,0 +1,154 @@ +# 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, binding, file) + end + + desc 'compile HAML', 'Show compile result' + option :actionview, type: :boolean, default: false, aliases: %w[-a] + option :color, type: :boolean, default: true + option :check, type: :boolean, default: false, aliases: %w[-c] + def compile(file) + code = generate_code(file) + if options[:check] + if error = validate_ruby(code, file) + abort error.message.split("\n").first + end + puts "Syntax OK" + return + end + puts_code(code, color: options[:color]) + end + + desc 'temple HAML', 'Show temple intermediate expression' + option :color, type: :boolean, default: true + def temple(file) + pp_object(generate_temple(file), color: options[:color]) + end + + desc 'parse HAML', 'Show parse result' + option :color, type: :boolean, default: true + 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: true) + begin + require 'irb/color' + rescue LoadError + color = false + end + if color + puts IRB::Color.colorize_code(code) + else + puts code + end + end + + # Enable colored pretty printing only for development environment. + def pp_object(arg, color: true) + begin + require 'irb/color_printer' + rescue LoadError + color = false + end + if color + IRB::ColorPrinter.pp(arg) + else + require 'pp' + pp(arg) + end + end + + def validate_ruby(code, file) + begin + eval("BEGIN {return nil}; #{code}", binding, file) + rescue ::SyntaxError # Not to be confused with Hamlit::SyntaxError + $! + 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..d408b0d --- /dev/null +++ b/lib/hamlit/compiler/children_compiler.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true +require 'hamlit/temple_line_counter' + +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 << moving_lineno(n) { block.call(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 + end + + def moving_lineno(node, &block) + # before: As they may have children, we need to increment lineno before compilation. + case node.type + when :script, :silent_script + @lineno += 1 + when :tag + [node.value[:dynamic_attributes].new, node.value[:dynamic_attributes].old].compact.each do |attribute_hash| + @lineno += attribute_hash.count("\n") + end + @lineno += 1 if node.children.empty? && node.value[:parse] + end + + temple = block.call # compile + + # after: filter may not have children, and for some dynamic filters we can't predict the number of lines. + case node.type + when :filter + @lineno += TempleLineCounter.count_lines(temple) + end + + temple + 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..f452e04 --- /dev/null +++ b/lib/hamlit/compiler/comment_compiler.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +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..886e33b --- /dev/null +++ b/lib/hamlit/engine.rb @@ -0,0 +1,40 @@ +# 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' +require 'hamlit/ambles' + +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 + use Ambles + 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..9c37f51 --- /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.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..48bde2e --- /dev/null +++ b/lib/hamlit/filters/markdown.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +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..f98a4f2 --- /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.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..f29742b --- /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.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..cb7e1d4 --- /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.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, "#{'\n" + options[:postamble] = "\n" + 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_helpers/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..ff1e5ab --- /dev/null +++ b/lib/hamlit/railtie.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +require 'rails' + +module Hamlit + class Railtie < ::Rails::Railtie + initializer :hamlit, before: :load_config_initializers do |app| + # Load haml/plugin first to override if available + begin + require 'haml/plugin' + rescue LoadError + end + 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..6366fd5 --- /dev/null +++ b/lib/hamlit/string_splitter.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +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/temple_line_counter.rb b/lib/hamlit/temple_line_counter.rb new file mode 100644 index 0000000..084d69f --- /dev/null +++ b/lib/hamlit/temple_line_counter.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +module Hamlit + # A module to count lines of expected code. This would be faster than actual code generation + # and counting newlines in it. + module TempleLineCounter + class UnexpectedExpression < StandardError; end + + def self.count_lines(exp) + type, *args = exp + case type + when :multi + args.map { |a| count_lines(a) }.reduce(:+) || 0 + when :dynamic, :code + args.first.count("\n") + when :static + 0 # It has not real newline "\n" but escaped "\\n". + when :case + arg, *cases = args + arg.count("\n") + cases.map do |cond, e| + (cond == :else ? 0 : cond.count("\n")) + count_lines(e) + end.reduce(:+) + when :escape + count_lines(args[1]) + when :newline + 1 + else + raise UnexpectedExpression.new("[HAML BUG] Unexpected Temple expression '#{type}' is given!") + end + end + end +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..8543b94 --- /dev/null +++ b/lib/hamlit/version.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true +module Hamlit + VERSION = '2.15.1' +end diff --git a/test/haml/engine_test.rb b/test/haml/engine_test.rb new file mode 100644 index 0000000..83d80e9 --- /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'}" => error(:unbalanced_brackets), + "%p{:foo => 'bar}" => error(:unbalanced_brackets), + "%p{:foo => 'bar\"}" => error(:unbalanced_brackets), + # 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".dup.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..6eccfe8 --- /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..5c9e412 --- /dev/null +++ b/test/haml/helper_test.rb @@ -0,0 +1,698 @@ +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(''), {}, 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..f18cbdb --- /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..50b0a97 --- /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, ActionController::Base.new) + + # 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(template: 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..c5fd5e2 --- /dev/null +++ b/test/hamlit/engine/attributes_test.rb @@ -0,0 +1,361 @@ +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", "2", "3"].join}"} 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' }) } + + it { assert_haml(<<-HAML.unindent) } + .haml#info{ + "data": { + "content": "/:|}", + "haml-info": { + "url": "https://haml.info", + } + } + } Haml + HAML + 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..f7cbb55 --- /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..c727eab --- /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 + + describe 'plain filter' do + it 'renders line numbers with an empty line correctly' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + hello + 4 + HTML + :plain + hello + + = __LINE__ + HAML + end + + it 'renders line numbers with a script line correctly' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + hello + 3 + 4 + HTML + :plain + hello + = 3 + = __LINE__ + HAML + end + + it 'renders line numbers with interpolation' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + hello + + 3 + 4 + HTML + :plain + hello#{} + = 3 + = __LINE__ + HAML + end + end + + describe 'preserve filter' do + it 'renders line numbers correctly' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + hello + 4 + HTML + :preserve + hello + + = __LINE__ + HAML + end + end + + describe 'ruby filter' do + it 'renders line numbers correctly' do + assert_render(<<-HTML.unindent, <<-'HAML'.unindent) + 4 + HTML + :ruby + _ = 1 + + = __LINE__ + HAML + end + end + 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..59b18e8 --- /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(''), {}, ActionController::Base.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: '