├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── REFERENCE.md ├── hamlpy ├── __init__.py ├── apps.py ├── compiler.py ├── hamlpy_watcher.py ├── jinja.py ├── parser │ ├── __init__.py │ ├── attributes.py │ ├── core.py │ ├── elements.py │ ├── filters.py │ ├── nodes.py │ └── utils.py ├── template │ ├── __init__.py │ ├── loaders.py │ ├── templatize.py │ └── utils.py ├── test │ ├── __init__.py │ ├── settings.py │ ├── templates │ │ ├── allIfTypesTest.hamlpy │ │ ├── allIfTypesTest.html │ │ ├── classIdMixtures.hamlpy │ │ ├── classIdMixtures.html │ │ ├── djangoBase.hamlpy │ │ ├── djangoBase.html │ │ ├── djangoCombo.hamlpy │ │ ├── djangoCombo.html │ │ ├── filterMultilineIgnore.hamlpy │ │ ├── filterMultilineIgnore.html │ │ ├── filters.hamlpy │ │ ├── filters.html │ │ ├── filtersMarkdown.hamlpy │ │ ├── filtersMarkdown.html │ │ ├── filtersPygments.hamlpy │ │ ├── filtersPygments.html │ │ ├── hamlComments.hamlpy │ │ ├── hamlComments.html │ │ ├── implicitDivs.hamlpy │ │ ├── implicitDivs.html │ │ ├── multiLineDict.hamlpy │ │ ├── multiLineDict.html │ │ ├── nestedComments.hamlpy │ │ ├── nestedComments.html │ │ ├── nestedDjangoTags.hamlpy │ │ ├── nestedDjangoTags.html │ │ ├── nestedIfElseBlocks.hamlpy │ │ ├── nestedIfElseBlocks.html │ │ ├── nukeInnerWhiteSpace.hamlpy │ │ ├── nukeInnerWhiteSpace.html │ │ ├── nukeOuterWhiteSpace.hamlpy │ │ ├── nukeOuterWhiteSpace.html │ │ ├── selfClosingDjango.hamlpy │ │ ├── selfClosingDjango.html │ │ ├── selfClosingTags.hamlpy │ │ ├── selfClosingTags.html │ │ ├── simple.hamlpy │ │ ├── simple.html │ │ ├── whitespacePreservation.hamlpy │ │ └── whitespacePreservation.html │ ├── test_attributes.py │ ├── test_compiler.py │ ├── test_elements.py │ ├── test_jinja.py │ ├── test_loader.py │ ├── test_nodes.py │ ├── test_parser.py │ ├── test_templates.py │ ├── test_templatize.py │ ├── test_views.py │ └── test_watcher.py └── views │ ├── __init__.py │ └── generic │ └── __init__.py ├── poetry.lock ├── pyproject.toml └── setup.cfg /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Test 6 | strategy: 7 | matrix: 8 | python-version: ['3.9.x', '3.10.x', '3.11.x'] 9 | django-version: ['4.0.10', '4.1.9', '4.2.1'] 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | 15 | - name: Install Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | 20 | - name: Initialize environment 21 | run: | 22 | python -m pip install -U pip poetry 23 | poetry install 24 | poetry run pip install -q django==${{ matrix.django-version }} 25 | 26 | - name: Run pre-test checks 27 | run: | 28 | poetry run black --check hamlpy 29 | poetry run ruff hamlpy 30 | poetry run isort hamlpy 31 | 32 | - name: Run tests 33 | run: poetry run py.test --cov-report=xml --cov=hamlpy hamlpy 34 | 35 | - name: Upload coverage 36 | if: success() 37 | uses: codecov/codecov-action@v3 38 | with: 39 | token: ${{ secrets.CODECOV_TOKEN }} 40 | fail_ci_if_error: true 41 | 42 | release: 43 | name: Release 44 | needs: [test] 45 | if: startsWith(github.ref, 'refs/tags/') 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout code 49 | uses: actions/checkout@v3 50 | 51 | - name: Install Python 52 | uses: actions/setup-python@v4 53 | with: 54 | python-version: '3.9.x' 55 | 56 | - name: Publish release 57 | run: | 58 | python -m pip install -U pip poetry 59 | poetry build 60 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} 61 | poetry publish 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .cache 3 | .coverage 4 | .project 5 | .pydevproject 6 | .DS_Store 7 | .vscode 8 | .idea/ 9 | .tox/ 10 | .watcher_test/ 11 | .venv 12 | coverage.xml 13 | build/ 14 | htmlcov/ 15 | env/ 16 | dist/ 17 | django_hamlpy.egg-info/ 18 | fabric 19 | fabfile.* 20 | deploy 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v1.7.0 (2023-05-12) 2 | ------------------------- 3 | * Add smart_quotes option to replace conflicting quotes in attribute values with ' or " 4 | 5 | v1.6.0 (2023-05-11) 6 | ------------------------- 7 | * Add support for named end blocks 8 | * Fix using double quotes as attribute wrapper 9 | * Update required python version to 3.9 10 | 11 | v1.5.0 (2023-03-03) 12 | ------------------------- 13 | * Update .gitignore 14 | * Merge pull request #95 from nyaruka/fix-app-config 15 | * Test in python 3.11 as well 16 | * Run black 17 | * Update pygments to 2.14.0 18 | * Add apps submodule 19 | * Update deps for code styles 20 | * Update README.md 21 | 22 | v1.4.4 23 | ---------- 24 | * Loosen regex dependency 25 | 26 | v1.4.3 27 | ---------- 28 | * Fix Django requirement to properly support 3.x 29 | 30 | v1.4.2 31 | ---------- 32 | * Switch to new release system 33 | 34 | 1.4.1 (2020-12-08) 35 | ================ 36 | 37 | * Allow colons inside class names 38 | 39 | 1.3 (2020-09-21) 40 | ================ 41 | 42 | * Add support for markdown extensions (https://github.com/nyaruka/django-hamlpy/pull/78) 43 | * Drop support for Django 1.x, test on 2.x and 3.x 44 | 45 | 1.1.1 (2017-05-29) 46 | =================== 47 | 48 | * Fix patching makemessages on Django 1.11 49 | 50 | 1.1 (2017-03-20) 51 | =================== 52 | 53 | * Add support for more doc types (default is HTML5) (https://github.com/nyaruka/django-hamlpy/pull/63) 54 | * Add format (html4/html5/xhtml) and escape_attrs (True/False) compiler options 55 | * Don't use empty tag syntax or CDATA sections (can be overridden) in HTML5 format 56 | * Fix patching of templatize for makemessages (https://github.com/nyaruka/django-hamlpy/pull/65) 57 | * Add support for Django 1.11 betas (https://github.com/nyaruka/django-hamlpy/pull/62) 58 | * Fix use of plural tag within blocktrans tag (https://github.com/nyaruka/django-hamlpy/pull/57) 59 | * Add less, sass and escaped filters (https://github.com/nyaruka/django-hamlpy/pull/55) 60 | 61 | 1.0.1 (2017-01-26) 62 | =================== 63 | 64 | * Add :preserve filter (https://github.com/nyaruka/django-hamlpy/pull/49) 65 | * Fix filter lookup to ignore trailing whitespace 66 | 67 | 1.0 (2016-12-14) 68 | =================== 69 | 70 | This is the first major release and there are some potentially breaking changes noted below. 71 | 72 | * Refactor of parsing code giving ~40% performance improvement 73 | * Added support for HTML style attribute dictionaries, e.g. `%span(foo="bar")` 74 | * Improved error reporting from parser to help find problems in your templates 75 | * Fixed attribute values not being able to include braces (https://github.com/nyaruka/django-hamlpy/issues/39) 76 | * Fixed attribute values which are Haml not being able to have blank lines (https://github.com/nyaruka/django-hamlpy/issues/41) 77 | * Fixed sequential `with` tags ended up nested (https://github.com/nyaruka/django-hamlpy/issues/23) 78 | * Fixed templatize patching for Django 1.9+ 79 | * Changed support for `={..}` style expressions to be disabled by default (https://github.com/nyaruka/django-hamlpy/issues/16) 80 | * Removed support for `=#` comment syntax 81 | * Project now maintained by Nyaruka (https://github.com/nyaruka) 82 | 83 | Breaking Changes 84 | ---------------- 85 | 86 | * Support for `={...}` variable substitutions is deprecated and disabled by default, but can be enabled by setting 87 | `HAMLPY_DJANGO_INLINE_STYLE` to `True` if you are using the template loaders, or specifying --django-inline if you are 88 | using the watcher script. The preferred syntax for variable substitutions is `#{...}` as this is actual Haml and is 89 | less likely conflict with other uses of the `=` character. 90 | * The `=# ...` comment syntax is no longer supported. This is not Haml and was never documented anywhere. You should use 91 | the `-# ...` syntax instead. 92 | * Any line beginning with a colon is interpreted as a filter, so if this is not the case, you should escape the colon, 93 | e.g. `\:not-a-filter ` 94 | 95 | 0.86.1 (2016-11-15) 96 | =================== 97 | 98 | * Fixed some incorrect relative imports #21 by @Kangaroux 99 | 100 | 0.86 (2016-11-11) 101 | ================= 102 | 103 | * Add call and macro tags to the self-closing dict by @andreif 104 | * Remove django 1.1 support by @rowanseymour 105 | * Switch to a real parser instead of eval by @rowanseymour 106 | * Improve tests and code quality by @rowanseymour 107 | * Add performance tests by @rowanseymour 108 | * Tag names can include a "-" (for angular for example) by @rowanseymour 109 | * Don't uses tox anymore for testing by @rowanseymour 110 | * Classes shorthand can come before a id one by @rowanseymour 111 | * Added co-maintainership guidelines by @psycojoker 112 | 113 | 0.85 (2016-10-13) 114 | ================= 115 | 116 | * Python 3 support by @rowanseymour https://github.com/Psycojoker/django-hamlpy/pull/1 117 | * Attributes now output by alphabetic order (this was needed to have deterministic tests) by @rowanseymour https://github.com/Psycojoker/django-hamlpy/pull/2 118 | 119 | 0.84 (2016-09-25) 120 | ================= 121 | 122 | * Add support for boolean attributes - see [Attributes without values (Boolean attributes)](http://github.com/psycojoker/django-hamlpy/blob/master/reference.md#attributes-without-values-boolean-attributes) 123 | * Add Django class based generic views that looks for `*.haml` and `*.hamlpy` templates in additions of the normal ones (https://github.com/Psycojoker/django-hamlpy#class-based-generic-views) 124 | 125 | 0.83 (2016-09-23) 126 | ================= 127 | 128 | * New fork since sadly hamlpy isn't maintained anymore 129 | * Pypi release have been renamed "django-hamlpy" instead of "hamlpy" 130 | * Added Django 1.9 compatibility (https://github.com/jessemiller/HamlPy/pull/166) 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 Jesse Miller 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [![Build Status](https://github.com/nyaruka/django-hamlpy/workflows/CI/badge.svg)](https://github.com/nyaruka/django-hamlpy/actions?query=workflow%3ACI) 4 | [![Coverage Status](https://codecov.io/gh/nyaruka/django-hamlpy/branch/main/graph/badge.svg)](https://codecov.io/gh/nyaruka/django-hamlpy) 5 | [![PyPI Release](https://img.shields.io/pypi/v/django-hamlpy.svg)](https://pypi.python.org/pypi/django-hamlpy/) 6 | 7 | Why type: 8 | 9 | ```html 10 | 13 | ``` 14 | 15 | when you can just type: 16 | 17 | ```haml 18 | .left#banner 19 | Greetings! 20 | ``` 21 | 22 | ... and do something more fun with all the time you save not typing angle brackets and remembering to close tags? 23 | 24 | The syntax above is [Haml](https://haml.info) - a templating language used extensively in the Ruby on Rails 25 | community. This library lets Django developers use a Haml like syntax in their templates. It's not a template engine in 26 | itself, but simply a compiler which will convert "HamlPy" files into templates that Django can understand. 27 | 28 | This project is a fork of the no longer maintained [HamlPy](https://github.com/jessemiller/HamlPy). It introduces 29 | Python 3 support, support for new Django versions, and a host of new features and bug fixes. Note that the package name 30 | is now *django-hamlpy*. 31 | 32 | ## Installing 33 | 34 | The latest stable version can be installed using [pip](http://pypi.python.org/pypi/pip/): 35 | 36 | pip install django-hamlpy 37 | 38 | And the latest development version can be installed directly from GitHub: 39 | 40 | pip install git+https://github.com/nyaruka/django-hamlpy 41 | 42 | **NOTE:** If you run into build errors, then you may need to install [python's development package](http://stackoverflow.com/a/21530768/2896976). 43 | 44 | ## Syntax 45 | 46 | Almost all of the syntax of Haml is preserved. 47 | 48 | ```haml 49 | #profile(style="width: 200px") 50 | .left.column 51 | #date 2010/02/18 52 | #address Toronto, ON 53 | .right.column< 54 | #bio Jesse Miller 55 | ``` 56 | 57 | turns into: 58 | 59 | ```htmldjango 60 |
61 |
62 |
2010/02/18
63 |
Toronto, ON
64 |
65 |
Jesse Miller
66 |
67 | ``` 68 | 69 | The main difference is instead of interpreting Ruby, or even Python we instead can create Django tags and variables. For 70 | example: 71 | 72 | ```haml 73 | %ul#athletes 74 | - for athlete in athlete_list 75 | %li.athlete{'id': 'athlete_#{ athlete.pk }'}= athlete.name 76 | ``` 77 | 78 | becomes... 79 | 80 | ```htmldjango 81 | 86 | ``` 87 | 88 | ## Usage 89 | 90 | There are two different ways to use this library. 91 | 92 | ### Option 1: Template loaders 93 | 94 | These are Django template loaders which will convert any templates with `.haml` or `.hamlpy` extensions to regular 95 | Django templates whenever they are requested by a Django view. To use them, add them to the list of template loaders in 96 | your Django settings, e.g. 97 | 98 | ```python 99 | TEMPLATES=[ 100 | { 101 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 102 | 'DIRS': ['./templates'], 103 | 'OPTIONS': { 104 | 'loaders': ( 105 | 'hamlpy.template.loaders.HamlPyFilesystemLoader', 106 | 'hamlpy.template.loaders.HamlPyAppDirectoriesLoader', 107 | ... 108 | ), 109 | } 110 | } 111 | ] 112 | ``` 113 | 114 | Ensure they are listed before the standard Django template loaders or these loaders will try to process your Haml 115 | templates. 116 | 117 | #### Template caching 118 | 119 | You can use these loaders with template caching - just add `django.template.loaders.cached.Loader` to your list of 120 | loaders, e.g. 121 | 122 | ```python 123 | 'loaders': ( 124 | ('django.template.loaders.cached.Loader', ( 125 | 'hamlpy.template.loaders.HamlPyFilesystemLoader', 126 | 'hamlpy.template.loaders.HamlPyAppDirectoriesLoader', 127 | ... 128 | )), 129 | ) 130 | ``` 131 | 132 | #### Settings 133 | 134 | You can configure the Haml compiler with the following Django settings: 135 | 136 | * `HAMLPY_ATTR_WRAPPER` -- The character that should wrap element attributes. Defaults to `'` (an apostrophe). 137 | * `HAMLPY_DJANGO_INLINE_STYLE` -- Whether to support `={...}` syntax for inline variables in addition to `#{...}`. 138 | Defaults to `False`. 139 | 140 | ### Option 2: Watcher 141 | 142 | The library can also be used as a stand-alone program. There is a watcher script which will monitor Haml files in a 143 | given directory and convert them to HTML as they are edited. 144 | 145 | ``` 146 | usage: hamlpy_watcher.py [-h] [-v] [-i EXT [EXT ...]] [-ext EXT] [-r S] 147 | [--tag TAG] [--attr-wrapper {",'}] [--django-inline] 148 | [--jinja] [--once] 149 | input_dir [output_dir] 150 | 151 | positional arguments: 152 | input_dir Folder to watch 153 | output_dir Destination folder 154 | 155 | optional arguments: 156 | -h, --help show this help message and exit 157 | -v, --verbose Display verbose output 158 | -i EXT [EXT ...], --input-extension EXT [EXT ...] 159 | The file extensions to look for. 160 | -ext EXT, --extension EXT 161 | The output file extension. Default is .html 162 | -r S, --refresh S Refresh interval for files. Default is 3 seconds. 163 | Ignored if the --once flag is set. 164 | --tag TAG Add self closing tag. eg. --tag macro:endmacro 165 | --attr-wrapper {",'} The character that should wrap element attributes. 166 | This defaults to ' (an apostrophe). 167 | --django-inline Whether to support ={...} syntax for inline variables 168 | in addition to #{...} 169 | --jinja Makes the necessary changes to be used with Jinja2. 170 | --once Runs the compiler once and exits on completion. 171 | Returns a non-zero exit code if there were any compile 172 | errors. 173 | ``` 174 | 175 | ### Create message files for translation 176 | 177 | HamlPy must first be included in Django's list of apps, i.e. 178 | 179 | ```python 180 | INSTALLED_APPS = [ 181 | ... 182 | 'hamlpy' 183 | ... 184 | ] 185 | ``` 186 | 187 | Then just include your Haml templates along with all the other files which contain translatable strings, e.g. 188 | 189 | ```bash 190 | python manage.py makemessages --extension haml,html,py,txt 191 | ``` 192 | 193 | ## Reference 194 | 195 | Check out the [reference](http://github.com/nyaruka/django-hamlpy/blob/master/REFERENCE.md) file for the complete syntax 196 | reference and more examples. 197 | 198 | ## Class Based Views 199 | 200 | This library also provides [the same class based generic views than django](https://docs.djangoproject.com/en/1.10/topics/class-based-views/generic-display/) with the enhancement that they start by looking for templates endings with `*.haml` and `*.hamlpy` in addition to their default templates. Apart from that, they are exactly the same class based generic views. For example: 201 | 202 | ```python 203 | from hamlpy.views.generic import DetailView, ListView 204 | from my_app.models import SomeModel 205 | 206 | # will look for the templates `my_app/somemodel_detail.haml`, 207 | # `my_app/somemodel_detail.hamlpy` and `my_app/somemodel_detail.html` 208 | DetailView.as_view(model=SomeModel) 209 | 210 | # will look for the templates `my_app/somemodel_list.haml`, 211 | # `my_app/somemodel_list.hamlpy` and `my_app/somemodel_list.html` 212 | ListView.as_view(model=SomeModel) 213 | ``` 214 | 215 | The available view classes are: 216 | 217 | Display views: 218 | 219 | * [DetailView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#detailview) 220 | * [ListView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#listview) 221 | 222 | Edit views: 223 | 224 | * [CreateView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#createview) 225 | * [UpdateView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#updateview) 226 | * [DeleteView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#deleteview) 227 | 228 | Date related views: 229 | 230 | * [DateDetailView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#datedetailview) 231 | * [ArchiveIndexView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#archiveindexview) 232 | * [YearArchiveView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#yeararchiveview) 233 | * [MonthArchiveView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#montharchiveview) 234 | * [WeekArchiveView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#weekarchiveview) 235 | * [DayArchiveView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#dayarchiveview) 236 | * [TodayArchiveView](https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-display/#todayarchiveview) 237 | 238 | All views are importable from `hamlpy.views.generic` and are built using the `HamlExtensionTemplateView` mixin which you 239 | can use to create your own custom Haml-using views. For example: 240 | 241 | ```python 242 | from hamlpy.views.generic import HamlExtensionTemplateView 243 | 244 | class MyNewView(HamlExtensionTemplateView, ParentViewType): 245 | pass 246 | ``` 247 | 248 | **Note**: `HamlExtensionTemplateView` *needs* to be first in the inheritance list. 249 | 250 | ## Contributing 251 | 252 | We're always happy to have contributions to this project. To get started you'll need to clone the project and install 253 | the dependencies: 254 | 255 | poetry install 256 | 257 | Please write tests for any new features and always ensure the current tests pass. To run the tests, use: 258 | 259 | py.test hamlpy 260 | 261 | To run the performance test, use: 262 | 263 | python -m hamlpy.test.test_templates 264 | -------------------------------------------------------------------------------- /REFERENCE.md: -------------------------------------------------------------------------------- 1 | # HamlPy Reference 2 | 3 | # Table of Contents 4 | 5 | - [Plain Text](#plain-text) 6 | - [Doctype](#doctype) 7 | - [HTML Elements](#html-elements) 8 | - [Element Name: %](#element-name-) 9 | - [Attributes: {} or ()](#attributes-) 10 | - [Attributes without values (Boolean attributes)](#attributes-without-values-boolean-attributes) 11 | - ['class' and 'id' attributes](#class-and-id-attributes) 12 | - [Class and ID: . and #](#class-and-id--and-) 13 | - [Implicit div elements](#implicit-div-elements) 14 | - [Self-Closing Tags: /](#self-closing-tags-) 15 | - [Comments](#comments) 16 | - [HTML Comments /](#html-comments-) 17 | - [Conditional Comments /[]](#conditional-comments-) 18 | - [HamlPy Comments: -#](#hamlpy-comments--) 19 | - [Django Specific Elements](#django-specific-elements) 20 | - [Django Variables: =](#django-variables-) 21 | - [Inline Django Variables: ={...}](#inline-django-variables-) 22 | - [Django Tags: -](#django-tags--) 23 | - [Tags within attributes:](#tags-within-attributes) 24 | - [Whitespace removal](#whitespace-removal) 25 | - [Filters](#filters) 26 | - [:plain](#plain) 27 | - [:javascript](#javascript) 28 | - [:coffeescript or :coffee](#coffeescript-or-coffee) 29 | - [:cdata](#cdata) 30 | - [:css](#css) 31 | - [:stylus](#stylus) 32 | - [:markdown](#markdown) 33 | - [:highlight](#highlight) 34 | - [:python](#python) 35 | 36 | ## Plain Text 37 | 38 | Any line that is not interpreted as something else will be taken as plain text and outputted unmodified. For example: 39 | 40 | ```haml 41 | %gee 42 | %whiz 43 | Wow this is cool! 44 | ``` 45 | 46 | is compiled to: 47 | 48 | ```htmldjango 49 | 50 | 51 | Wow this is cool! 52 | 53 | 54 | ``` 55 | 56 | ## Doctype 57 | 58 | You can specify a specific doctype after the !!! The following doctypes are supported: 59 | 60 | * `!!!`: XHTML 1.0 Transitional 61 | * `!!! Strict`: XHTML 1.0 Strict 62 | * `!!! Frameset`: XHTML 1.0 Frameset 63 | * `!!! 5`: XHTML 5 64 | * `!!! 1.1`: XHTML 1.1 65 | * `!!! XML`: XML prolog 66 | 67 | ## HTML Elements 68 | 69 | ### Element Name: % 70 | 71 | The percent character placed at the beginning of the line will then be followed by the name of the element, then optionally modifiers (see below), a space, and text to be rendered inside the element. It creates an element in the form of . For example: 72 | 73 | ```haml 74 | %one 75 | %two 76 | %three Hey there 77 | ``` 78 | 79 | is compiled to: 80 | 81 | ```htmldjango 82 | 83 | 84 | Hey there 85 | 86 | 87 | ``` 88 | 89 | Any string is a valid element name and an opening and closing tag will automatically be generated. 90 | 91 | ### Attributes: {} or () 92 | 93 | Haml has three different styles for attributes which are all supported. For example: 94 | 95 | ```haml 96 | %html{:xmlns => 'http://www.w3.org/1999/xhtml', :lang => 'en'} 97 | 98 | %html{'xmlns': 'http://www.w3.org/1999/xhtml', 'lang': 'en'} 99 | 100 | %html(xmlns='http://www.w3.org/1999/xhtml' lang='en') 101 | ``` 102 | 103 | are all compiled to: 104 | 105 | ```htmldjango 106 | 107 | ``` 108 | 109 | Long attribute dictionaries can be separated into multiple lines: 110 | 111 | ```haml 112 | %script(type='text/javascript' charset='utf-8' 113 | href='/long/url/to/javascript/resource.js') 114 | ``` 115 | 116 | #### Attributes without values (Boolean attributes) 117 | 118 | Attributes without values can be specified using singe variable. For example: 119 | 120 | ```haml 121 | %input(type='checkbox' value='Test' checked) 122 | ``` 123 | 124 | is compiled to: 125 | 126 | ```htmldjango 127 | 128 | ``` 129 | 130 | Alternatively the values can be specified using Python's ```None``` keyword (without quotes). For example: 131 | 132 | ```haml 133 | %input(type='checkbox' value='Test' checked=None) 134 | ``` 135 | 136 | is compiled to: 137 | 138 | ```htmldjango 139 | 140 | ``` 141 | 142 | 143 | #### 'class' and 'id' attributes 144 | 145 | The 'class' and 'id' attributes can also be specified as a Python tuple whose elements will be joined together. A 'class' tuple will be joined with " " and an 'id' tuple is joined with "_". For example: 146 | 147 | ```haml 148 | %div{'id': ('article', '3'), 'class': ('newest', 'urgent')} Content 149 | ``` 150 | 151 | is compiled to: 152 | 153 | ```htmldjango 154 |
Content
155 | ``` 156 | 157 | ### Class and ID: . and # 158 | 159 | The period and pound sign are borrowed from CSS. They are used as shortcuts to specify the class and id attributes of an element, respectively. Multiple class names can be specified by chaining class names together with periods. They are placed immediately after a tag and before an attribute dictionary. For example: 160 | 161 | ```haml 162 | %div#things 163 | %span#rice Chicken Fried 164 | %p.beans{'food':'true'} The magical fruit 165 | %h1#id.class.otherclass La La La 166 | ``` 167 | 168 | is compiled to: 169 | 170 | ```htmldjango 171 |
172 | Chiken Fried 173 |

The magical fruit

174 |

La La La

175 |
176 | ``` 177 | 178 | And, 179 | 180 | ```haml 181 | %div#content 182 | %div.articles 183 | %div.article.title Doogie Howser Comes Out 184 | %div.article.date 2006-11-05 185 | %div.article.entry 186 | Neil Patrick Harris would like to dispel any rumors that he is straight 187 | 188 | is compiled to: 189 | ``` 190 | 191 | ```htmldjango 192 |
193 |
194 |
Doogie Howser Comes Out
195 |
2006-11-05
196 |
197 | Neil Patrick Harris would like to dispel any rumors that he is straight 198 |
199 |
200 |
201 | ``` 202 | 203 | These shortcuts can be combined with the attribute dictionary and they will be combined as if they were all put inside a list. For example: 204 | 205 | ```haml 206 | %div#Article.article.entry{'id':'1', 'class':'visible'} Booyaka 207 | ``` 208 | 209 | is equivalent to: 210 | 211 | ```haml 212 | %div{'id':['Article','1'], 'class':['article','entry','visible']} Booyaka 213 | ``` 214 | 215 | and would compile to: 216 | 217 | ```htmldjango 218 |
Booyaka
219 | ``` 220 | 221 | #### Implicit div elements 222 | 223 | Because divs are used so often, they are the default element. If you only define a class and/or id using `.` or `#` then the %div will be implied. For example: 224 | 225 | ```haml 226 | #collection 227 | .item 228 | .description What a cool item! 229 | ``` 230 | 231 | will compile to: 232 | 233 | ```htmldjango 234 |
235 |
236 |
What a cool item!
237 |
238 |
239 | ``` 240 | 241 | ### Self-Closing Tags: / 242 | 243 | The forward slash character, when placed at the end of a tag definition, causes the tag to be self-closed. For example: 244 | 245 | ```haml 246 | %br/ 247 | %meta{'http-equiv':'Content-Type', 'content':'text/html'}/ 248 | ``` 249 | 250 | will compile to: 251 | 252 | ```htmldjango 253 |
254 | 255 | ``` 256 | 257 | Some tags are automatically closed, as long as they have no content. `meta, img, link, script, br` and `hr` tags are automatically closed. For example: 258 | 259 | ```haml 260 | %br 261 | %meta{'http-equiv':'Content-Type', 'content':'text/html'} 262 | ``` 263 | 264 | will compile to: 265 | 266 | ```htmldjango 267 |
268 | 269 | ``` 270 | 271 | ## Comments 272 | 273 | There are two types of comments supported: those that show up in the HTML and those that don't. 274 | 275 | ### HTML Comments / 276 | 277 | The forward slash character, when placed at the beginning of a line, wraps all the text after it in an HTML comment. For example: 278 | 279 | ```haml 280 | %peanutbutterjelly 281 | / This is the peanutbutterjelly element 282 | I like sandwiches! 283 | ``` 284 | 285 | is compiled to: 286 | 287 | ```htmldjango 288 | 289 | 290 | I like sandwiches! 291 | 292 | ``` 293 | 294 | The forward slash can also wrap indented sections of code. For example: 295 | 296 | ```haml 297 | / 298 | %p This doesn't render 299 | %div 300 | %h1 Because it's commented out! 301 | ``` 302 | 303 | is compiled to: 304 | 305 | ```htmldjango 306 | 312 | ``` 313 | 314 | ### Conditional Comments /[] 315 | 316 | You can use [Internet Explorer conditional comments](http://www.quirksmode.org/css/condcom.html) by enclosing the condition in square brackets after the /. For example: 317 | 318 | ```haml 319 | /[if IE] 320 | %h1 Get a better browser 321 | ``` 322 | 323 | is compiled to: 324 | 325 | ```htmldjango 326 | 329 | ``` 330 | 331 | ### HamlPy Comments: -# 332 | 333 | The hyphen followed immediately by the pound sign signifies a silent comment. Any text following this isn't rendered during compilation at all. For example: 334 | 335 | ```haml 336 | %p foo 337 | -# Some comment 338 | %p bar 339 | ``` 340 | 341 | is compiled to: 342 | 343 | ```htmldjango 344 |

foo

345 |

bar

346 | ``` 347 | 348 | ## Django Specific Elements 349 | 350 | The key difference in HamlPy from Haml is the support for Django elements. The syntax for ruby evaluation is borrowed from Haml and instead outputs Django tags and variables. 351 | 352 | ### Django Variables: = 353 | 354 | A line starting with an equal sign followed by a space and then content is evaluated as a Django variable. For example: 355 | 356 | ```haml 357 | .article 358 | .preview 359 | = story.teaser 360 | ``` 361 | 362 | is compiled to: 363 | 364 | ```htmldjango 365 |
366 |
367 | {{ story.teaser }} 368 |
369 |
370 | ``` 371 | 372 | A Django variable can also be used as content for any HTML element by placing an equals sign as the last character before the space and content. For example: 373 | 374 | ```haml 375 | %h2 376 | %a{'href':'stories/1'}= story.teaser 377 | ``` 378 | 379 | is compiled to: 380 | 381 | ```htmldjango 382 |

383 | {{ story.teaser }} 384 |

385 | ``` 386 | 387 | ### Inline Django Variables: #{...} 388 | 389 | You can also use inline variables using the `#{...}` syntax. For example: 390 | 391 | ```haml 392 | Hello #{name}, how are you today? 393 | ``` 394 | 395 | is compiled to 396 | 397 | ```htmldjango 398 | Hello {{ name }}, how are you today? 399 | ``` 400 | 401 | Inline variables can also be used in an element's attribute values. For example: 402 | 403 | ```haml 404 | %a{'title':'Hello #{name}, how are you?'} Hello 405 | ``` 406 | 407 | is compiled to: 408 | 409 | ```htmldjango 410 | Hello 411 | ``` 412 | 413 | Inline variables can be escaped by placing a `\` before them. For example: 414 | 415 | ```haml 416 | Hello \#{name} 417 | ``` 418 | 419 | is compiled to: 420 | 421 | ```htmldjango 422 | Hello #{name} 423 | ``` 424 | 425 | Django style `={...}` syntax is also optionally supported. If you are using the template loader 426 | then ensure `HAMLPY_DJANGO_INLINE_STYLE` is `True`, and the two syntaxes can then be used interchangeably. 427 | 428 | 429 | 430 | ### Django Tags: - 431 | 432 | The hypen character at the start of the line followed by a space and a Django tag will be inserted as a Django tag. For example: 433 | 434 | ```haml 435 | - block content 436 | %h1= section.title 437 | 438 | - for dog in dog_list 439 | %h2 440 | = dog.name 441 | ``` 442 | 443 | is compiled to: 444 | 445 | ```htmldjango 446 | {% block content %} 447 |

{{ section.title }}

448 | 449 | {% for dog in dog_list %} 450 |

451 | {{ dog.name }} 452 |

453 | {% endfor %} 454 | {% endblock %} 455 | ``` 456 | 457 | 458 | Notice that block, for, if and else, as well as ifequal, ifnotequal, ifchanged and 'with' are all automatically closed. Using endfor, endif, endifequal, endifnotequal, endifchanged or endblock will throw an exception. 459 | 460 | #### Tags within attributes: 461 | 462 | This is not yet supported: `%div{'attr':"- firstof var1 var2 var3"}` will not insert the `{% ... %}`. 463 | 464 | The workaround is to insert actual django template tag code into the haml. For example: 465 | 466 | ```haml 467 | %a{'href': "{% url socialauth_begin 'github' %}"} Login with Github 468 | ``` 469 | 470 | is compiled to: 471 | 472 | ```htmldjango 473 | Login with Github 474 | ``` 475 | 476 | 477 | ### Whitespace removal 478 | 479 | Sometimes we want to remove whitespace inside or around an element, usually to fix the spacing problem with inline-block elements (see "The Enormous Drawback" section of [this article](http://robertnyman.com/2010/02/24/css-display-inline-block-why-it-rocks-and-why-it-sucks/) for more details). 480 | 481 | To remove leading and trailing spaces **inside** a node ("inner whitespace removal"), use the `<` character after an element. For example, this: 482 | 483 | ```haml 484 | %div 485 | %pre< 486 | = Foo 487 | ``` 488 | 489 | is compiled to: 490 | 491 | ```htmldjango 492 |
493 |
{{ Foo }}
494 |
495 | ``` 496 | 497 | To remove leading and trailing spaces **around** a node ("outer whitespace removal"), use the `>` character after an element. For example, this: 498 | 499 | ```haml 500 | %li Item one 501 | %li> Item two 502 | %li Item three 503 | ``` 504 | 505 | is compiled to: 506 | 507 | ```htmldjango 508 |
  • Item one
  • Item two
  • Item three
  • 509 | ``` 510 | 511 | ## Filters 512 | 513 | ### :plain 514 | 515 | Does not parse the filtered text. This is useful for large blocks of text without HTML tags, when you don’t want lines starting with . or - to be parsed. 516 | 517 | ### :javascript 518 | 519 | Surrounds the filtered text with <script type="text/javascript"> and CDATA tags. Useful for including inline Javascript. 520 | 521 | ### :coffeescript or :coffee 522 | 523 | Surrounds the filtered text with <script type="text/coffeescript"> and CDATA tags. Useful for including inline Coffeescript. 524 | 525 | ### :cdata 526 | 527 | Surrounds the filtered text with CDATA tags. 528 | 529 | ### :css 530 | 531 | Surrounds the filtered text with <style type="text/css"> and CDATA tags. Useful for including inline CSS. 532 | 533 | ### :stylus 534 | 535 | Surrounds the filtered text with <style type="text/stylus"> and CDATA tags. Useful for including inline Stylus. 536 | 537 | ### :markdown 538 | 539 | Converts the filter text from Markdown to HTML, using the Python [Markdown library](http://freewisdom.org/projects/python-markdown/). 540 | 541 | You can also specify the enabled extensions using the setting HAMLPY_MARKDOWN_EXTENSIONS and assigning a list of valid extensions to it. See [Markdown library extensions documentation](https://python-markdown.github.io/extensions/) for available extensions. For example: 542 | 543 | ```python 544 | HAMLPY_MARKDOWN_EXTENSIONS = ['extras'] 545 | ``` 546 | 547 | ### :highlight 548 | 549 | This will output the filtered text with syntax highlighting using [Pygments](http://pygments.org). 550 | 551 | For syntax highlighting to work correctly, you will also need to generate or include a Pygments CSS file. See 552 | the section ["Generating styles"](http://pygments.org/docs/cmdline/#generating-styles) in the Pygments 553 | documentation for more information. 554 | 555 | ### :python 556 | 557 | Execute the filtered text as python and output the result in the file. For example: 558 | 559 | ```haml 560 | :python 561 | for i in range(0, 5): 562 | print "

    item %s

    " % i 563 | ``` 564 | 565 | is compiled to: 566 | 567 | ```htmldjango 568 |

    item 0

    569 |

    item 1

    570 |

    item 2

    571 |

    item 3

    572 |

    item 4

    573 | ``` 574 | 575 | -------------------------------------------------------------------------------- /hamlpy/__init__.py: -------------------------------------------------------------------------------- 1 | HAML_EXTENSIONS = ("haml", "hamlpy") 2 | -------------------------------------------------------------------------------- /hamlpy/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class Config(AppConfig): 5 | name = "hamlpy" 6 | 7 | def ready(self): 8 | # patch Django's templatize method 9 | from .template import templatize # noqa 10 | 11 | 12 | default_app_config = "hamlpy.Config" 13 | -------------------------------------------------------------------------------- /hamlpy/compiler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import warnings 4 | 5 | import regex 6 | 7 | from hamlpy.parser.core import Stream 8 | from hamlpy.parser.nodes import Node, read_node 9 | 10 | 11 | class Options(object): 12 | HTML4 = "html4" 13 | HTML5 = "html5" 14 | XHTML = "xhtml" 15 | 16 | def __init__(self, **kwargs): 17 | # standard Haml options 18 | self.attr_wrapper = "'" # how to render attribute values, e.g. foo='bar' 19 | self.format = self.HTML5 # HTML4, HTML5 or XHTML 20 | self.escape_attrs = False # escape HTML-sensitive characters in attribute values 21 | self.cdata = False # wrap CSS, Javascript etc content in CDATA section 22 | 23 | # implementation specific options 24 | self.django_inline_style = False # support both #{...} and ={...} 25 | self.tag_config = "django" # Django vs Jinja2 tags 26 | self.custom_self_closing_tags = {} # additional self-closing tags 27 | self.endblock_names = False # include block name on endblock closing tags 28 | self.smart_quotes = False 29 | self.debug_tree = False 30 | 31 | for k, v in kwargs.items(): 32 | setattr(self, k, v) 33 | 34 | if self.django_inline_style: # pragma: no cover 35 | warnings.warn("Support for ={..} style variables is deprecated", DeprecationWarning) 36 | 37 | self.cdata = self._cdata 38 | 39 | @property 40 | def html4(self): 41 | return self.format == self.HTML4 42 | 43 | @property 44 | def html5(self): 45 | return self.format == self.HTML5 46 | 47 | @property 48 | def html(self): 49 | return self.html4 or self.html5 50 | 51 | @property 52 | def xhtml(self): 53 | return self.format == self.XHTML 54 | 55 | @property 56 | def _cdata(self): 57 | return self.xhtml or self.cdata 58 | 59 | 60 | class Compiler: 61 | TAG_CONFIGS = { 62 | "django": { 63 | "self_closing": { 64 | "for": "endfor", 65 | "if": "endif", 66 | "ifchanged": "endifchanged", 67 | "ifequal": "endifequal", 68 | "ifnotequal": "endifnotequal", 69 | "block": "endblock", 70 | "filter": "endfilter", 71 | "autoescape": "endautoescape", 72 | "with": "endwith", 73 | "blocktrans": "endblocktrans", 74 | "spaceless": "endspaceless", 75 | "comment": "endcomment", 76 | "cache": "endcache", 77 | "localize": "endlocalize", 78 | "call": "endcall", 79 | "macro": "endmacro", 80 | "compress": "endcompress", 81 | }, 82 | "may_contain": { 83 | "blocktrans": ["plural"], 84 | "if": ["else", "elif"], 85 | "ifchanged": ["else"], 86 | "ifequal": ["else"], 87 | "ifnotequal": ["else"], 88 | "for": ["empty"], 89 | }, 90 | }, 91 | "jinja2": { 92 | "self_closing": { 93 | "for": "endfor", 94 | "if": "endif", 95 | "block": "endblock", 96 | "filter": "endfilter", 97 | "with": "endwith", 98 | "call": "endcall", 99 | "macro": "endmacro", 100 | "raw": "endraw", 101 | }, 102 | "may_contain": {"if": ["else", "elif"], "for": ["empty", "else"]}, 103 | }, 104 | } 105 | 106 | def __init__(self, options=None): 107 | self.options = Options(**(options or {})) 108 | 109 | tag_config = self.TAG_CONFIGS[self.options.tag_config] 110 | self.self_closing_tags = tag_config["self_closing"] 111 | self.tags_may_contain = tag_config["may_contain"] 112 | 113 | self.self_closing_tags.update(self.options.custom_self_closing_tags) 114 | 115 | self.inline_variable_regexes = self._create_inline_variable_regexes() 116 | 117 | def process(self, haml): 118 | """ 119 | Converts the given string of Haml to a regular Django HTML 120 | """ 121 | stream = Stream(haml) 122 | 123 | root = Node.create_root(self) 124 | node = None 125 | 126 | while True: 127 | node = read_node(stream, prev=node, compiler=self) 128 | if not node: 129 | break 130 | 131 | root.add_node(node) 132 | 133 | if self.options.debug_tree: # pragma: no cover 134 | return root.debug_tree() 135 | else: 136 | return root.render() 137 | 138 | def _create_inline_variable_regexes(self): 139 | """ 140 | Generates regular expressions for inline variables and escaped inline variables, based on compiler options 141 | """ 142 | prefixes = ["=", "#"] if self.options.django_inline_style else ["#"] 143 | prefixes = "".join(prefixes) 144 | return ( 145 | regex.compile(r"(? timestamp 28 | compiled = dict() 29 | 30 | 31 | class StoreNameValueTagPair(argparse.Action): 32 | def __call__(self, parser, namespace, values, option_string=None): 33 | tags = getattr(namespace, "tags", {}) 34 | for item in values: 35 | n, v = item.split(":") 36 | tags[n] = v 37 | 38 | setattr(namespace, "tags", tags) 39 | 40 | 41 | arg_parser = argparse.ArgumentParser() 42 | arg_parser.add_argument("-v", "--verbose", help="Display verbose output", action="store_true") 43 | arg_parser.add_argument( 44 | "-i", "--input-extension", metavar="EXT", help="The file extensions to look for.", type=str, nargs="+" 45 | ) 46 | arg_parser.add_argument( 47 | "-ext", 48 | "--extension", 49 | metavar="EXT", 50 | default=Options.OUTPUT_EXT, 51 | help="The output file extension. Default is .html", 52 | type=str, 53 | ) 54 | arg_parser.add_argument( 55 | "-r", 56 | "--refresh", 57 | metavar="S", 58 | default=Options.CHECK_INTERVAL, 59 | type=int, 60 | help="Refresh interval for files. Default is %d seconds. Ignored if the --once flag is set." 61 | % Options.CHECK_INTERVAL, 62 | ) 63 | arg_parser.add_argument("input_dir", help="Folder to watch", type=str) 64 | arg_parser.add_argument("output_dir", help="Destination folder", type=str, nargs="?") 65 | arg_parser.add_argument( 66 | "--tag", type=str, nargs=1, action=StoreNameValueTagPair, help="Add self closing tag. eg. --tag macro:endmacro" 67 | ) 68 | arg_parser.add_argument( 69 | "--attr-wrapper", 70 | dest="attr_wrapper", 71 | type=str, 72 | choices=('"', "'"), 73 | default="'", 74 | action="store", 75 | help="The character that should wrap element attributes. This defaults to ' (an apostrophe).", 76 | ) 77 | arg_parser.add_argument( 78 | "--django-inline", 79 | dest="django_inline", 80 | action="store_true", 81 | help="Whether to support ={...} syntax for inline variables in addition to #{...}", 82 | ) 83 | arg_parser.add_argument( 84 | "--jinja", default=False, action="store_true", help="Makes the necessary changes to be used with Jinja2." 85 | ) 86 | arg_parser.add_argument( 87 | "--once", 88 | default=False, 89 | action="store_true", 90 | help="Runs the compiler once and exits on completion. " 91 | "Returns a non-zero exit code if there were any compile errors.", 92 | ) 93 | 94 | 95 | def watch_folder(): 96 | """Main entry point. Expects one or two arguments (the watch folder + optional destination folder).""" 97 | args = arg_parser.parse_args(sys.argv[1:]) 98 | compiler_args = {} 99 | 100 | input_folder = os.path.realpath(args.input_dir) 101 | if not args.output_dir: 102 | output_folder = input_folder 103 | else: 104 | output_folder = os.path.realpath(args.output_dir) 105 | 106 | if args.verbose: 107 | Options.VERBOSE = True 108 | print("Watching {} at refresh interval {} seconds".format(input_folder, args.refresh)) 109 | 110 | if args.extension: 111 | Options.OUTPUT_EXT = args.extension 112 | 113 | if getattr(args, "tags", False): 114 | compiler_args["custom_self_closing_tags"] = args.tags 115 | 116 | if args.input_extension: 117 | input_extensions = [(e[1:] if e.startswith(".") else e) for e in args.input_extension] # strip . chars 118 | else: 119 | input_extensions = HAML_EXTENSIONS 120 | 121 | if args.attr_wrapper: 122 | compiler_args["attr_wrapper"] = args.attr_wrapper 123 | 124 | if args.django_inline: 125 | compiler_args["django_inline_style"] = args.django_inline 126 | 127 | if args.jinja: 128 | compiler_args["tag_config"] = "jinja2" 129 | 130 | # compile once, then exist 131 | if args.once: 132 | (total_files, num_failed) = _watch_folder(input_folder, input_extensions, output_folder, compiler_args) 133 | print("Compiled %d of %d files." % (total_files - num_failed, total_files)) 134 | if num_failed == 0: 135 | print("All files compiled successfully.") 136 | else: 137 | print("Some files have errors.") 138 | sys.exit(num_failed) 139 | 140 | while True: 141 | try: 142 | _watch_folder(input_folder, input_extensions, output_folder, compiler_args) 143 | time.sleep(args.refresh) 144 | except KeyboardInterrupt: 145 | # allow graceful exit (no stacktrace output) 146 | sys.exit(0) 147 | 148 | 149 | def _watch_folder(folder, extensions, destination, compiler_args): 150 | """ 151 | Compares "modified" timestamps against the "compiled" dict, calls compiler if necessary. Returns a tuple of the 152 | number of files hit and the number of failed compiles 153 | """ 154 | total_files = 0 155 | num_failed = 0 156 | 157 | if Options.VERBOSE: 158 | print("_watch_folder(%s, %s, %s)" % (folder, repr(extensions), destination)) 159 | 160 | for dirpath, dirnames, filenames in os.walk(folder): 161 | for filename in filenames: 162 | # ignore filenames starting with ".#" for Emacs compatibility 163 | if filename.startswith(".#"): 164 | continue 165 | 166 | if not _has_extension(filename, extensions): 167 | continue 168 | 169 | fullpath = os.path.join(dirpath, filename) 170 | subfolder = os.path.relpath(dirpath, folder) 171 | mtime = os.stat(fullpath).st_mtime 172 | 173 | # create subfolders in target directory if they don't exist 174 | compiled_folder = os.path.join(destination, subfolder) 175 | if not os.path.exists(compiled_folder): 176 | os.makedirs(compiled_folder) 177 | 178 | compiled_path = _compiled_path(compiled_folder, filename) 179 | if fullpath not in compiled or compiled[fullpath] < mtime or not os.path.isfile(compiled_path): 180 | compiled[fullpath] = mtime 181 | total_files += 1 182 | if not compile_file(fullpath, compiled_path, compiler_args): 183 | num_failed += 1 184 | 185 | return total_files, num_failed 186 | 187 | 188 | def _has_extension(filename, extensions): 189 | """ 190 | Checks if the given filename has one of the given extensions 191 | """ 192 | for ext in extensions: 193 | if filename.endswith("." + ext): 194 | return True 195 | return False 196 | 197 | 198 | def _compiled_path(destination, filename): 199 | return os.path.join(destination, filename[: filename.rfind(".")] + Options.OUTPUT_EXT) 200 | 201 | 202 | def compile_file(fullpath, outfile_name, compiler_args): 203 | """ 204 | Calls HamlPy compiler. Returns True if the file was compiled and written successfully. 205 | """ 206 | if Options.VERBOSE: 207 | print("%s %s -> %s" % (strftime("%H:%M:%S"), fullpath, outfile_name)) 208 | try: 209 | if Options.DEBUG: # pragma: no cover 210 | print("Compiling %s -> %s" % (fullpath, outfile_name)) 211 | haml = codecs.open(fullpath, "r", encoding="utf-8").read() 212 | compiler = Compiler(compiler_args) 213 | output = compiler.process(haml) 214 | outfile = codecs.open(outfile_name, "w", encoding="utf-8") 215 | outfile.write(output) 216 | 217 | return True 218 | except Exception as e: 219 | # import traceback 220 | print("Failed to compile %s -> %s\nReason:\n%s" % (fullpath, outfile_name, e)) 221 | # print traceback.print_exc() 222 | 223 | return False 224 | 225 | 226 | if __name__ == "__main__": # pragma: no cover 227 | watch_folder() 228 | -------------------------------------------------------------------------------- /hamlpy/jinja.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import jinja2.ext 4 | 5 | from hamlpy import HAML_EXTENSIONS 6 | from hamlpy.compiler import Compiler 7 | from hamlpy.parser.core import ParseException 8 | 9 | 10 | class HamlPyExtension(jinja2.ext.Extension): 11 | def preprocess(self, source, name, filename=None): 12 | extension = os.path.splitext(name)[1][1:] 13 | 14 | if extension in HAML_EXTENSIONS: 15 | compiler = Compiler() 16 | try: 17 | return compiler.process(source) 18 | except ParseException as e: 19 | raise jinja2.TemplateSyntaxError(str(e), 1, name=name, filename=filename) 20 | else: 21 | return source 22 | -------------------------------------------------------------------------------- /hamlpy/parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyaruka/django-hamlpy/59e87ee4fceb84adeabef8358a95572544683c7a/hamlpy/parser/__init__.py -------------------------------------------------------------------------------- /hamlpy/parser/attributes.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | import regex 4 | 5 | from .core import ( 6 | STRING_LITERALS, 7 | WHITESPACE_CHARS, 8 | ParseException, 9 | peek_indentation, 10 | read_line, 11 | read_number, 12 | read_quoted_string, 13 | read_symbol, 14 | read_whitespace, 15 | read_word, 16 | ) 17 | from .utils import html_escape 18 | 19 | LEADING_SPACES_REGEX = regex.compile(r"^\s+", regex.V1 | regex.MULTILINE) 20 | 21 | # non-word characters that we allow in attribute keys (in HTML style attribute dicts) 22 | ATTRIBUTE_KEY_EXTRA_CHARS = {":", "-", "$", "?", "[", "]"} 23 | 24 | ATTRIBUTE_VALUE_KEYWORDS = {"none": None, "true": True, "false": False} 25 | 26 | 27 | def read_attribute_value(stream, compiler): 28 | """ 29 | Reads an attribute's value which may be a string, a number or None 30 | """ 31 | ch = stream.text[stream.ptr] 32 | 33 | if ch in STRING_LITERALS: 34 | value = read_quoted_string(stream) 35 | 36 | if compiler.options.escape_attrs: 37 | # TODO handle escape_attrs=once 38 | value = html_escape(value) 39 | 40 | elif ch.isdigit(): 41 | value = read_number(stream) 42 | else: 43 | raw_value = read_word(stream) 44 | 45 | if raw_value.lower() in ATTRIBUTE_VALUE_KEYWORDS: 46 | value = ATTRIBUTE_VALUE_KEYWORDS[raw_value.lower()] 47 | else: 48 | value = "{{ %s }}" % raw_value 49 | 50 | return value 51 | 52 | 53 | def read_attribute_value_list(stream, compiler): 54 | """ 55 | Reads an attribute value which is a list of other values 56 | """ 57 | open_literal = stream.text[stream.ptr] 58 | 59 | assert open_literal in ("(", "[") 60 | 61 | read_tuple = open_literal == "(" 62 | close_literal = ")" if read_tuple else "]" 63 | 64 | data = [] 65 | 66 | stream.ptr += 1 # consume opening symbol 67 | 68 | while True: 69 | read_whitespace(stream) 70 | 71 | if stream.text[stream.ptr] == close_literal: 72 | break 73 | 74 | data.append(read_attribute_value(stream, compiler)) 75 | 76 | read_whitespace(stream) 77 | 78 | if stream.text[stream.ptr] != close_literal: 79 | read_symbol(stream, (",",)) 80 | 81 | stream.ptr += 1 # consume closing symbol 82 | 83 | return data 84 | 85 | 86 | def read_attribute_value_haml(stream, compiler): 87 | """ 88 | Reads an attribute value which is a block of indented Haml 89 | """ 90 | indentation = peek_indentation(stream) 91 | haml_lines = [] 92 | 93 | # read lines below with higher indentation as this filter's content 94 | while stream.ptr < stream.length: 95 | line_indentation = peek_indentation(stream) 96 | 97 | if line_indentation is not None and line_indentation < indentation: 98 | break 99 | 100 | line = read_line(stream) 101 | 102 | # don't preserve whitespace on empty lines 103 | if line.isspace(): 104 | line = "" 105 | 106 | haml_lines.append(line) 107 | 108 | stream.ptr -= 1 # un-consume final newline which will act as separator between this and next entry 109 | 110 | haml = "\n".join(haml_lines) 111 | html = compiler.process(haml) 112 | 113 | # un-format into single line 114 | return LEADING_SPACES_REGEX.sub(" ", html).replace("\n", "").strip() 115 | 116 | 117 | def read_ruby_attribute(stream, compiler): 118 | """ 119 | Reads a Ruby style attribute, e.g. :foo => "bar" or foo: "bar" 120 | """ 121 | old_style = stream.text[stream.ptr] == ":" 122 | 123 | if old_style: 124 | stream.ptr += 1 125 | 126 | key = read_word(stream, include_chars=("-",)) 127 | else: 128 | # new style Ruby / Python style allows attribute to be quoted string 129 | if stream.text[stream.ptr] in STRING_LITERALS: 130 | key = read_quoted_string(stream) 131 | 132 | if not key: 133 | raise ParseException("Attribute name can't be an empty string.", stream) 134 | else: 135 | key = read_word(stream, include_chars=("-",)) 136 | 137 | read_whitespace(stream) 138 | 139 | if stream.text[stream.ptr] in ("=", ":"): 140 | if old_style: 141 | read_symbol(stream, ("=>",)) 142 | else: 143 | read_symbol(stream, (":",)) 144 | 145 | read_whitespace(stream, include_newlines=False) 146 | 147 | stream.expect_input() 148 | 149 | if stream.text[stream.ptr] == "\n": 150 | stream.ptr += 1 151 | 152 | value = read_attribute_value_haml(stream, compiler) 153 | elif stream.text[stream.ptr] in ("(", "["): 154 | value = read_attribute_value_list(stream, compiler) 155 | else: 156 | value = read_attribute_value(stream, compiler) 157 | else: 158 | value = True 159 | 160 | return key, value 161 | 162 | 163 | def read_html_attribute(stream, compiler): 164 | """ 165 | Reads an HTML style attribute, e.g. foo="bar" 166 | """ 167 | key = read_word(stream, include_chars=ATTRIBUTE_KEY_EXTRA_CHARS) 168 | 169 | # can't have attributes without whitespace separating them 170 | ch = stream.text[stream.ptr] 171 | if ch not in WHITESPACE_CHARS and ch not in ("=", ")"): 172 | stream.raise_unexpected() 173 | 174 | read_whitespace(stream) 175 | 176 | if stream.text[stream.ptr] == "=": 177 | read_symbol(stream, "=") 178 | 179 | read_whitespace(stream, include_newlines=False) 180 | 181 | stream.expect_input() 182 | 183 | if stream.text[stream.ptr] == "\n": 184 | stream.ptr += 1 185 | 186 | value = read_attribute_value_haml(stream, compiler) 187 | elif stream.text[stream.ptr] == "[": 188 | value = read_attribute_value_list(stream, compiler) 189 | else: 190 | value = read_attribute_value(stream, compiler) 191 | else: 192 | value = True 193 | 194 | return key, value 195 | 196 | 197 | def read_attribute_dict(stream, compiler): 198 | """ 199 | Reads an attribute dictionary which may use one of 3 syntaxes: 200 | 1. {:foo => "bar", :a => 3} (old Ruby) 201 | 2. {foo: "bar", a: 3} (new Ruby / Python) 202 | 3. (foo="bar" a=3) (HTML) 203 | """ 204 | data = OrderedDict() 205 | 206 | opener = stream.text[stream.ptr] 207 | 208 | assert opener in ("{", "(") 209 | 210 | if opener == "(": 211 | html_style = True 212 | terminator = ")" 213 | else: 214 | html_style = False 215 | terminator = "}" 216 | 217 | stream.ptr += 1 218 | 219 | def record_value(key, value): 220 | if key in data: 221 | raise ParseException('Duplicate attribute: "%s".' % key, stream) 222 | data[key] = value 223 | 224 | while True: 225 | read_whitespace(stream, include_newlines=True) 226 | 227 | if stream.ptr >= stream.length: 228 | raise ParseException("Unterminated attribute dictionary", stream) 229 | 230 | if stream.text[stream.ptr] == terminator: 231 | stream.ptr += 1 232 | break 233 | 234 | # (foo = "bar" a=3) 235 | if html_style: 236 | record_value(*read_html_attribute(stream, compiler)) 237 | 238 | read_whitespace(stream) 239 | 240 | if stream.text[stream.ptr] == ",": 241 | raise ParseException('Unexpected ",".', stream) 242 | 243 | # {:foo => "bar", :a=>3} or {foo: "bar", a: 3} 244 | else: 245 | record_value(*read_ruby_attribute(stream, compiler)) 246 | 247 | read_whitespace(stream) 248 | 249 | if stream.text[stream.ptr] not in (terminator, "\n"): 250 | read_symbol(stream, (",",)) 251 | 252 | return data 253 | -------------------------------------------------------------------------------- /hamlpy/parser/core.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | STRING_LITERALS = ('"', "'") 4 | WHITESPACE_CHARS = (" ", "\t") 5 | WHITESPACE_AND_NEWLINE_CHARS = (" ", "\t", "\r", "\n") 6 | 7 | 8 | class ParseException(Exception): 9 | def __init__(self, message, stream=None): 10 | if stream: 11 | context = stream.text[max(stream.ptr - 31, 0) : stream.ptr + 1] 12 | message = '%s @ "%s" <-' % (message, context) 13 | 14 | super(ParseException, self).__init__(message) 15 | 16 | 17 | class Stream(object): 18 | def __init__(self, text): 19 | self.text = text 20 | self.length = len(self.text) 21 | self.ptr = 0 22 | 23 | def expect_input(self): 24 | """ 25 | We expect more input, raise exception if there isn't any 26 | """ 27 | if self.ptr >= self.length: 28 | raise ParseException("Unexpected end of input.", self) 29 | 30 | def raise_unexpected(self): 31 | """ 32 | Raises exception that current character is unexpected 33 | """ 34 | raise ParseException('Unexpected "%s".' % self.text[self.ptr], self) 35 | 36 | def __repr__(self): # pragma: no cover 37 | return '"%s" >> "%s"' % ( 38 | self.text[: self.ptr].replace("\n", "\\n"), 39 | self.text[self.ptr :].replace("\n", "\\n"), 40 | ) 41 | 42 | 43 | class TreeNode(object): 44 | """ 45 | Generic parent/child tree class 46 | """ 47 | 48 | def __init__(self): 49 | self.parent = None 50 | self.children = [] 51 | 52 | def left_sibling(self): 53 | siblings = self.parent.children 54 | index = siblings.index(self) 55 | return siblings[index - 1] if index > 0 else None 56 | 57 | def right_sibling(self): 58 | siblings = self.parent.children 59 | index = siblings.index(self) 60 | return siblings[index + 1] if index < len(siblings) - 1 else None 61 | 62 | def add_child(self, child): 63 | child.parent = self 64 | self.children.append(child) 65 | 66 | 67 | def read_whitespace(stream, include_newlines=False): 68 | """ 69 | Reads whitespace characters, returning the whitespace characters 70 | """ 71 | whitespace = WHITESPACE_AND_NEWLINE_CHARS if include_newlines else WHITESPACE_CHARS 72 | 73 | start = stream.ptr 74 | 75 | while stream.ptr < stream.length and stream.text[stream.ptr] in whitespace: 76 | stream.ptr += 1 77 | 78 | return stream.text[start : stream.ptr] 79 | 80 | 81 | def peek_indentation(stream): 82 | """ 83 | Counts but doesn't actually read indentation level on new line, returning the count or None if line is blank 84 | """ 85 | indentation = 0 86 | while True: 87 | ch = stream.text[stream.ptr + indentation] 88 | if ch == "\n": 89 | return None 90 | 91 | if not ch.isspace(): 92 | return indentation 93 | 94 | indentation += 1 95 | 96 | 97 | def read_quoted_string(stream): 98 | """ 99 | Reads a single or double quoted string, returning the value without the quotes 100 | """ 101 | terminator = stream.text[stream.ptr] 102 | 103 | assert terminator in STRING_LITERALS 104 | 105 | start = stream.ptr 106 | stream.ptr += 1 # consume opening quote 107 | 108 | while True: 109 | if stream.ptr >= stream.length: 110 | raise ParseException("Unterminated string (expected %s)." % terminator, stream) 111 | 112 | if stream.text[stream.ptr] == terminator and stream.text[stream.ptr - 1] != "\\": 113 | break 114 | 115 | stream.ptr += 1 116 | 117 | stream.ptr += 1 # consume closing quote 118 | 119 | # evaluate as a Python string (evaluates escape sequences) 120 | return ast.literal_eval(stream.text[start : stream.ptr]) 121 | 122 | 123 | def read_line(stream): 124 | """ 125 | Reads a line 126 | """ 127 | start = stream.ptr 128 | 129 | if stream.ptr >= stream.length: 130 | return None 131 | 132 | while stream.ptr < stream.length and stream.text[stream.ptr] != "\n": 133 | stream.ptr += 1 134 | 135 | line = stream.text[start : stream.ptr] 136 | 137 | if stream.ptr < stream.length and stream.text[stream.ptr] == "\n": 138 | stream.ptr += 1 139 | 140 | return line 141 | 142 | 143 | def read_number(stream): 144 | """ 145 | Reads a decimal number, returning value as string 146 | """ 147 | start = stream.ptr 148 | 149 | while True: 150 | if not stream.text[stream.ptr].isdigit() and stream.text[stream.ptr] != ".": 151 | break 152 | 153 | stream.ptr += 1 154 | 155 | return stream.text[start : stream.ptr] 156 | 157 | 158 | def read_symbol(stream, symbols): 159 | """ 160 | Reads one of the given symbols, returning its value 161 | """ 162 | for symbol in symbols: 163 | if stream.text[stream.ptr : stream.ptr + len(symbol)] == symbol: 164 | stream.ptr += len(symbol) 165 | return symbol 166 | 167 | raise ParseException("Expected %s." % " or ".join(['"%s"' % s for s in symbols]), stream) 168 | 169 | 170 | def read_word(stream, include_chars=()): 171 | """ 172 | Reads a sequence of word characters 173 | """ 174 | stream.expect_input() 175 | 176 | start = stream.ptr 177 | 178 | while stream.ptr < stream.length: 179 | ch = stream.text[stream.ptr] 180 | if not (ch.isalnum() or ch == "_" or ch in include_chars): 181 | break 182 | stream.ptr += 1 183 | 184 | # if we immediately hit a non-word character, raise it as unexpected 185 | if start == stream.ptr: 186 | stream.raise_unexpected() 187 | 188 | return stream.text[start : stream.ptr] 189 | -------------------------------------------------------------------------------- /hamlpy/parser/elements.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from .attributes import read_attribute_dict 4 | from .core import read_line, read_word 5 | 6 | # non-word characters that we allow in tag names, ids and classes 7 | TAG_EXTRA_CHARS = ("-",) 8 | 9 | # non-word characters that we allow in ids and classes 10 | ID_OR_CLASS_EXTRA_CHARS = ("-", ":") 11 | 12 | 13 | def read_tag(stream): 14 | """ 15 | Reads an element tag, e.g. span, ng-repeat, cs:dropdown 16 | """ 17 | part1 = read_word(stream, TAG_EXTRA_CHARS) 18 | 19 | if stream.ptr < stream.length and stream.text[stream.ptr] == ":": 20 | stream.ptr += 1 21 | part2 = read_word(stream, TAG_EXTRA_CHARS) 22 | else: 23 | part2 = None 24 | 25 | return (part1 + ":" + part2) if part2 else part1 26 | 27 | 28 | def read_element(stream, compiler): 29 | """ 30 | Reads an element, e.g. %span, #banner{style:"width: 100px"}, .ng-hide(foo=1) 31 | """ 32 | assert stream.text[stream.ptr] in ("%", ".", "#") 33 | 34 | tag = None 35 | empty_class = False 36 | 37 | if stream.text[stream.ptr] == "%": 38 | stream.ptr += 1 39 | tag = read_tag(stream) 40 | 41 | elif stream.text[stream.ptr] == ".": 42 | # Element may start with a period representing an unidentified div rather than a CSS class. In this case it 43 | # can't have other classes or ids, e.g. .{foo:"bar"} 44 | next_ch = stream.text[stream.ptr + 1] if stream.ptr < stream.length - 1 else None 45 | if not (next_ch.isalnum() or next_ch == "_" or next_ch in ID_OR_CLASS_EXTRA_CHARS): 46 | stream.ptr += 1 47 | empty_class = True 48 | 49 | _id = None 50 | classes = [] 51 | 52 | if not empty_class: 53 | while stream.ptr < stream.length and stream.text[stream.ptr] in ("#", "."): 54 | is_id = stream.text[stream.ptr] == "#" 55 | stream.ptr += 1 56 | 57 | id_or_class = read_word(stream, ID_OR_CLASS_EXTRA_CHARS) 58 | if is_id: 59 | _id = id_or_class 60 | else: 61 | classes.append(id_or_class) 62 | 63 | attributes = OrderedDict() 64 | while stream.ptr < stream.length and stream.text[stream.ptr] in ("{", "("): 65 | attributes.update(read_attribute_dict(stream, compiler)) 66 | 67 | if stream.ptr < stream.length and stream.text[stream.ptr] == ">": 68 | stream.ptr += 1 69 | nuke_outer_ws = True 70 | else: 71 | nuke_outer_ws = False 72 | 73 | if stream.ptr < stream.length and stream.text[stream.ptr] == "<": 74 | stream.ptr += 1 75 | nuke_inner_ws = True 76 | else: 77 | nuke_inner_ws = False 78 | 79 | if stream.ptr < stream.length and stream.text[stream.ptr] == "/": 80 | stream.ptr += 1 81 | self_close = True 82 | else: 83 | self_close = tag in Element.SELF_CLOSING 84 | 85 | if stream.ptr < stream.length and stream.text[stream.ptr] == "=": 86 | stream.ptr += 1 87 | django_variable = True 88 | else: 89 | django_variable = False 90 | 91 | if stream.ptr < stream.length: 92 | inline = read_line(stream) 93 | if inline is not None: 94 | inline = inline.strip() 95 | else: 96 | inline = None 97 | 98 | return Element(tag, _id, classes, attributes, nuke_outer_ws, nuke_inner_ws, self_close, django_variable, inline) 99 | 100 | 101 | class Element(object): 102 | """ 103 | An HTML element with an id, classes, attributes etc 104 | """ 105 | 106 | SELF_CLOSING = ( 107 | "meta", 108 | "img", 109 | "link", 110 | "br", 111 | "hr", 112 | "input", 113 | "source", 114 | "track", 115 | "area", 116 | "base", 117 | "col", 118 | "command", 119 | "embed", 120 | "keygen", 121 | "param", 122 | "wbr", 123 | ) 124 | 125 | DEFAULT_TAG = "div" 126 | ESCAPED = {'"': """, "'": "'"} 127 | 128 | def __init__( 129 | self, 130 | tag, 131 | _id, 132 | classes, 133 | attributes, 134 | nuke_outer_whitespace, 135 | nuke_inner_whitespace, 136 | self_close, 137 | django_variable, 138 | inline_content, 139 | ): 140 | self.tag = tag or self.DEFAULT_TAG 141 | self.attributes = attributes 142 | self.nuke_inner_whitespace = nuke_inner_whitespace 143 | self.nuke_outer_whitespace = nuke_outer_whitespace 144 | self.self_close = self_close 145 | self.django_variable = django_variable 146 | self.inline_content = inline_content 147 | 148 | # merge ids from the attribute dictionary 149 | ids = [_id] if _id else [] 150 | id_from_attrs = attributes.get("id") 151 | if isinstance(id_from_attrs, (tuple, list)): 152 | ids += id_from_attrs 153 | elif isinstance(id_from_attrs, str): 154 | ids += [id_from_attrs] 155 | 156 | # merge ids to a single value with _ separators 157 | self.id = "_".join(ids) if ids else None 158 | 159 | # merge classes from the attribute dictionary 160 | class_from_attrs = attributes.get("class", []) 161 | if not isinstance(class_from_attrs, (tuple, list)): 162 | class_from_attrs = [class_from_attrs] 163 | 164 | self.classes = class_from_attrs + classes 165 | 166 | def render_attributes(self, options): 167 | def attr_wrap(val): 168 | return "%s%s%s" % (options.attr_wrapper, val, options.attr_wrapper) 169 | 170 | rendered = [] 171 | 172 | for name, value in self.attributes.items(): 173 | if name in ("id", "class") or value in (None, False): 174 | # this line isn't recorded in coverage because it gets optimized away (http://bugs.python.org/issue2506) 175 | continue # pragma: no cover 176 | 177 | if value is True: # boolean attribute 178 | if options.xhtml: 179 | rendered.append("%s=%s" % (name, attr_wrap(name))) 180 | else: 181 | rendered.append(name) 182 | else: 183 | value = self._escape_attribute_quotes(value, options.attr_wrapper, options.smart_quotes) 184 | rendered.append("%s=%s" % (name, attr_wrap(value))) 185 | 186 | if len(self.classes) > 0: 187 | rendered.append("class=%s" % attr_wrap(" ".join(self.classes))) 188 | 189 | if self.id: 190 | rendered.append("id=%s" % attr_wrap(self.id)) 191 | 192 | return " ".join(rendered) 193 | 194 | @classmethod 195 | def _escape_attribute_quotes(cls, v, attr_wrapper, smart_quotes=False): 196 | """ 197 | Escapes quotes, except those inside a Django tag 198 | """ 199 | escaped = [] 200 | inside_tag = False 201 | for i, _ in enumerate(v): 202 | if v[i : i + 2] == "{%": 203 | inside_tag = True 204 | elif v[i : i + 2] == "%}": 205 | inside_tag = False 206 | 207 | if v[i] == attr_wrapper and not inside_tag: 208 | if smart_quotes and attr_wrapper in ('"', "'"): 209 | repl = '"' if v[i] == "'" else "'" 210 | else: 211 | repl = cls.ESCAPED[attr_wrapper] 212 | 213 | escaped.append(repl) 214 | else: 215 | escaped.append(v[i]) 216 | 217 | return "".join(escaped) 218 | -------------------------------------------------------------------------------- /hamlpy/parser/filters.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from io import StringIO 3 | 4 | from django.conf import settings 5 | 6 | from .core import ParseException 7 | from .utils import html_escape 8 | 9 | """ 10 | Core HamlPy filters. 11 | 12 | The implementation of these should match https://github.com/haml/haml/blob/master/lib/haml/filters.rb as closely as 13 | possible. Where we differ is that we don't compile Stylus, Coffeescript etc into CSS or Javascript - but place the 14 | content into suitable " % (type_attr, before, indent, text, after) 143 | 144 | 145 | def script_filter(text, mime_type, comment, options): 146 | indent = " " if options.cdata else " " 147 | text = text.rstrip().replace("\n", "\n" + indent) 148 | type_attr = " type=%(attr_wrapper)s%(mime_type)s%(attr_wrapper)s" % { 149 | "attr_wrapper": options.attr_wrapper, 150 | "mime_type": mime_type, 151 | } 152 | before, after = (" %s\n" % comment) if options.cdata else ("", "") 153 | 154 | return "\n%s%s%s\n%s" % (type_attr, before, indent, text, after) 155 | 156 | 157 | # ---------------------------------------------------------------------------------- 158 | # Filter registration 159 | # ---------------------------------------------------------------------------------- 160 | 161 | FILTERS = { 162 | "plain": plain, 163 | "preserve": preserve, 164 | "escaped": escaped, 165 | "cdata": cdata, 166 | "css": css, 167 | "stylus": stylus, 168 | "less": less, 169 | "sass": sass, 170 | "javascript": javascript, 171 | "coffee": coffee, 172 | "coffeescript": coffee, 173 | "markdown": markdown, 174 | "highlight": highlight, 175 | "python": python, 176 | } 177 | 178 | 179 | def register_filter(name, callback): 180 | FILTERS[name] = callback 181 | 182 | 183 | def get_filter(name): 184 | if name not in FILTERS: 185 | raise ParseException("No such filter: " + name) 186 | 187 | return FILTERS.get(name) 188 | -------------------------------------------------------------------------------- /hamlpy/parser/nodes.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from .core import ParseException, TreeNode, peek_indentation, read_line, read_whitespace 4 | from .elements import read_element 5 | from .filters import get_filter 6 | 7 | XHTML_DOCTYPES = { 8 | "1.1": '', # noqa 9 | "strict": '', # noqa 10 | "frameset": '', # noqa 11 | "mobile": '', # noqa 12 | "rdfa": '', # noqa 13 | "basic": '', # noqa 14 | "": '', # noqa 15 | } 16 | 17 | HTML4_DOCTYPES = { 18 | "strict": '', 19 | "frameset": '', 20 | "": '', 21 | } 22 | 23 | 24 | DOCTYPE_PREFIX = "!!!" 25 | ELEMENT_PREFIXES = ("%", "#", ".") 26 | HTML_COMMENT_PREFIX = "/" 27 | CONDITIONAL_COMMENT_PREFIX = "/[" 28 | HAML_COMMENT_PREFIX = "-#" 29 | VARIABLE_PREFIX = "=" 30 | TAG_PREFIX = "-" 31 | FILTER_PREFIX = ":" 32 | 33 | HAML_ESCAPE = "\\" 34 | 35 | 36 | def read_node(stream, prev, compiler): 37 | """ 38 | Reads a node, returning either the node or None if we've reached the end of the input 39 | """ 40 | while True: 41 | indent = read_whitespace(stream) 42 | 43 | if stream.ptr >= stream.length: 44 | return None 45 | 46 | # convert indent to be all of the first character 47 | if indent: 48 | indent = indent[0] * len(indent) 49 | 50 | # empty lines are recorded as newlines on previous node 51 | if stream.text[stream.ptr] == "\n": 52 | if prev: 53 | prev.newlines += 1 54 | stream.ptr += 1 55 | continue 56 | 57 | # parse filter node 58 | if stream.text[stream.ptr] == FILTER_PREFIX: 59 | return read_filter_node(stream, indent, compiler) 60 | 61 | # peek ahead to so we don't try to parse an element from a variable node starting #{ or a Django tag ending %} 62 | if stream.text[stream.ptr] in ELEMENT_PREFIXES and stream.text[stream.ptr : stream.ptr + 2] not in ( 63 | "#{", 64 | "%}", 65 | ): 66 | element = read_element(stream, compiler) 67 | return ElementNode(element, indent, compiler) 68 | 69 | # all other nodes are single line 70 | line = read_line(stream) 71 | 72 | inline_var_regex, escaped_var_regex = compiler.inline_variable_regexes 73 | 74 | if inline_var_regex.match(line) or escaped_var_regex.match(line): 75 | return PlaintextNode(line, indent, compiler) 76 | 77 | if line[0] == HAML_ESCAPE: 78 | return PlaintextNode(line, indent, compiler) 79 | 80 | if line.startswith(DOCTYPE_PREFIX): 81 | return DoctypeNode(line, indent, compiler) 82 | 83 | if line.startswith(CONDITIONAL_COMMENT_PREFIX): 84 | return ConditionalCommentNode(line, indent, compiler) 85 | 86 | if line[0] == HTML_COMMENT_PREFIX: 87 | return CommentNode(line, indent, compiler) 88 | 89 | if line.startswith(HAML_COMMENT_PREFIX): 90 | return HamlCommentNode(line, indent, compiler) 91 | 92 | if line[0] == VARIABLE_PREFIX: 93 | return VariableNode(line, indent, compiler) 94 | 95 | if line[0] == TAG_PREFIX: 96 | return TagNode(line, indent, compiler) 97 | 98 | return PlaintextNode(line, indent, compiler) 99 | 100 | 101 | def read_filter_node(stream, indent, compiler): 102 | """ 103 | Reads a filter node including its indented content, e.g. :plain 104 | """ 105 | assert stream.text[stream.ptr] == FILTER_PREFIX 106 | 107 | stream.ptr += 1 # consume the initial colon 108 | name = read_line(stream) 109 | content_lines = [] 110 | 111 | # read lines below with higher indentation as this filter's content 112 | while stream.ptr < stream.length: 113 | line_indentation = peek_indentation(stream) 114 | 115 | if line_indentation is not None and line_indentation <= len(indent): 116 | break 117 | 118 | line = read_line(stream) 119 | 120 | # don't preserve whitespace on empty lines 121 | if line.isspace(): 122 | line = "" 123 | 124 | content_lines.append(line) 125 | 126 | return FilterNode(name.rstrip(), "\n".join(content_lines), indent, compiler) 127 | 128 | 129 | class Node(TreeNode): 130 | """ 131 | Base class of all nodes 132 | """ 133 | 134 | def __init__(self, indent, compiler): 135 | super(Node, self).__init__() 136 | 137 | if indent is not None: 138 | self.indent = indent 139 | self.indentation = len(indent) 140 | else: 141 | self.indent = None 142 | self.indentation = -1 143 | 144 | self.compiler = compiler 145 | 146 | self.newlines = 0 # number of empty lines to render after node 147 | self.before = "" # rendered text at start of node, e.g. "

    \n" 148 | self.after = "" # rendered text at end of node, e.g. "\n

    " 149 | 150 | @classmethod 151 | def create_root(cls, compiler): 152 | return cls(None, compiler) 153 | 154 | def render(self): 155 | # Render (sets self.before and self.after) 156 | self._render_children() 157 | # Post-render (nodes can modify the rendered text of other nodes) 158 | self._post_render() 159 | # Generate HTML 160 | return self._generate_html() 161 | 162 | def render_newlines(self): 163 | return "\n" * (self.newlines + 1) 164 | 165 | def _render_children(self): 166 | for child in self.children: 167 | child._render() 168 | 169 | def _post_render(self): 170 | for child in self.children: 171 | child._post_render() 172 | 173 | def _generate_html(self): 174 | output = [self.before] 175 | for child in self.children: 176 | output.append(child.before) 177 | output += [gc._generate_html() for gc in child.children] 178 | output.append(child.after) 179 | output.append(self.after) 180 | return "".join(output) 181 | 182 | def replace_inline_variables(self, content): 183 | inline_var_regex, escaped_var_regex = self.compiler.inline_variable_regexes 184 | 185 | content = inline_var_regex.sub(r"{{ \2 }}", content) 186 | content = escaped_var_regex.sub(r"\1", content) 187 | return content 188 | 189 | def add_node(self, node): 190 | if self._should_go_inside_last_node(node): 191 | self.children[-1].add_node(node) 192 | else: 193 | self.add_child(node) 194 | 195 | def _should_go_inside_last_node(self, node): 196 | return len(self.children) > 0 and ( 197 | node.indentation > self.children[-1].indentation 198 | or (node.indentation == self.children[-1].indentation and self.children[-1].should_contain(node)) 199 | ) 200 | 201 | def should_contain(self, node): 202 | return False 203 | 204 | def debug_tree(self): # pragma: no cover 205 | return "\n".join(self._debug_tree([self])) 206 | 207 | def _debug_tree(self, nodes): # pragma: no cover 208 | output = [] 209 | for n in nodes: 210 | output.append("%s%s" % (" " * (n.indentation + 2), n)) 211 | if n.children: 212 | output += self._debug_tree(n.children) 213 | return output 214 | 215 | def __repr__(self): # pragma: no cover 216 | return "%s" % type(self).__name__ 217 | 218 | 219 | class LineNode(Node): 220 | """ 221 | Base class of nodes which are a single line of Haml 222 | """ 223 | 224 | def __init__(self, line, indent, compiler): 225 | super(LineNode, self).__init__(indent, compiler) 226 | 227 | self.haml = line.rstrip() 228 | 229 | def __repr__(self): # pragma: no cover 230 | return "%s(indent=%d, newlines=%d): %s" % (type(self).__name__, self.indentation, self.newlines, self.haml) 231 | 232 | 233 | class PlaintextNode(LineNode): 234 | """ 235 | Node that is not modified or processed when rendering 236 | """ 237 | 238 | def _render(self): 239 | text = self.replace_inline_variables(self.haml) 240 | 241 | # remove escape character 242 | if text and text[0] == HAML_ESCAPE: 243 | text = text.replace(HAML_ESCAPE, "", 1) 244 | 245 | self.before = "%s%s" % (self.indent, text) 246 | if self.children: 247 | self.before += self.render_newlines() 248 | else: 249 | self.after = self.render_newlines() 250 | 251 | self._render_children() 252 | 253 | 254 | class ElementNode(Node): 255 | """ 256 | An HTML tag node, e.g. %span 257 | """ 258 | 259 | def __init__(self, element, indent, compiler): 260 | super(ElementNode, self).__init__(indent, compiler) 261 | 262 | self.element = element 263 | 264 | def _render(self): 265 | self.before = self._render_before(self.element) 266 | self.after = self._render_after(self.element) 267 | self._render_children() 268 | 269 | def _render_before(self, element): 270 | """ 271 | Render opening tag and inline content 272 | """ 273 | start = ["%s<%s" % (self.indent, element.tag)] 274 | 275 | attributes = element.render_attributes(self.compiler.options) 276 | if attributes: 277 | start.append(" " + self.replace_inline_variables(attributes)) 278 | 279 | content = self._render_inline_content(self.element.inline_content) 280 | 281 | if element.nuke_inner_whitespace and content: 282 | content = content.strip() 283 | 284 | if element.self_close and not content: 285 | start.append(">" if self.compiler.options.html else " />") 286 | elif content: 287 | start.append(">%s" % content) 288 | elif self.children: 289 | start.append(">%s" % (self.render_newlines())) 290 | else: 291 | start.append(">") 292 | return "".join(start) 293 | 294 | def _render_after(self, element): 295 | """ 296 | Render closing tag 297 | """ 298 | if element.inline_content: 299 | return "%s" % (element.tag, self.render_newlines()) 300 | elif element.self_close: 301 | return self.render_newlines() 302 | elif self.children: 303 | return "%s\n" % (self.indent, element.tag) 304 | else: 305 | return "\n" % element.tag 306 | 307 | def _post_render(self): 308 | # inner whitespace removal 309 | if self.element.nuke_inner_whitespace: 310 | self.before = self.before.rstrip() 311 | self.after = self.after.lstrip() 312 | 313 | if self.children: 314 | node = self 315 | if node.children: 316 | node.children[0].before = node.children[0].before.lstrip() 317 | 318 | if node.children: 319 | node.children[-1].after = node.children[-1].after.rstrip() 320 | 321 | # outer whitespace removal 322 | if self.element.nuke_outer_whitespace: 323 | left_sibling = self.left_sibling() 324 | if left_sibling: 325 | # If node has left sibling, strip whitespace after left sibling 326 | left_sibling.after = left_sibling.after.rstrip() 327 | left_sibling.newlines = 0 328 | else: 329 | # If not, whitespace comes from it's parent node, 330 | # so strip whitespace before the node 331 | self.parent.before = self.parent.before.rstrip() 332 | self.parent.newlines = 0 333 | 334 | self.before = self.before.lstrip() 335 | self.after = self.after.rstrip() 336 | 337 | right_sibling = self.right_sibling() 338 | if right_sibling: 339 | right_sibling.before = right_sibling.before.lstrip() 340 | else: 341 | self.parent.after = self.parent.after.lstrip() 342 | self.parent.newlines = 0 343 | 344 | super(ElementNode, self)._post_render() 345 | 346 | def _render_inline_content(self, inline_content): 347 | if inline_content is None or len(inline_content) == 0: 348 | return None 349 | 350 | if self.element.django_variable: 351 | content = "{{ " + inline_content.strip() + " }}" 352 | return content 353 | else: 354 | return self.replace_inline_variables(inline_content) 355 | 356 | 357 | class CommentNode(LineNode): 358 | """ 359 | An HTML comment node, e.g. / This is a comment 360 | """ 361 | 362 | def _render(self): 363 | self.after = "-->\n" 364 | if self.children: 365 | self.before = "\n" 386 | self._render_children() 387 | 388 | 389 | class DoctypeNode(LineNode): 390 | """ 391 | An XML doctype node, e.g. !!! 5 392 | """ 393 | 394 | def _render(self): 395 | doctype = self.haml.lstrip(DOCTYPE_PREFIX).strip().lower() 396 | 397 | self.before = self.get_header(doctype, self.compiler.options) 398 | self.after = self.render_newlines() 399 | 400 | def get_header(self, doctype, options): 401 | if doctype.startswith("xml"): 402 | if options.html: 403 | return "" 404 | parts = doctype.split() 405 | encoding = parts[1] if len(parts) > 1 else "utf-8" 406 | return "" % ( 407 | options.attr_wrapper, 408 | options.attr_wrapper, 409 | options.attr_wrapper, 410 | encoding, 411 | options.attr_wrapper, 412 | ) 413 | elif options.html5: 414 | return "" 415 | elif options.xhtml: 416 | if doctype == "5": 417 | return "" 418 | else: 419 | return XHTML_DOCTYPES.get(doctype, XHTML_DOCTYPES[""]) 420 | else: 421 | return HTML4_DOCTYPES.get(doctype, HTML4_DOCTYPES[""]) 422 | 423 | 424 | class HamlCommentNode(LineNode): 425 | """ 426 | A Haml comment node, e.g. -# This is a comment 427 | """ 428 | 429 | def _render(self): 430 | self.after = self.render_newlines()[1:] 431 | 432 | def _post_render(self): 433 | pass 434 | 435 | 436 | class VariableNode(LineNode): 437 | """ 438 | A Django variable node, e.g. =person.name 439 | """ 440 | 441 | def __init__(self, haml, indent, compiler): 442 | super(VariableNode, self).__init__(haml, indent, compiler) 443 | 444 | def _render(self): 445 | tag_content = self.haml.lstrip(VARIABLE_PREFIX) 446 | self.before = "%s{{ %s }}" % (self.indent, tag_content.strip()) 447 | self.after = self.render_newlines() 448 | 449 | def _post_render(self): 450 | pass 451 | 452 | 453 | class TagNode(LineNode): 454 | """ 455 | A Django/Jinja server-side tag node, e.g. -block 456 | """ 457 | 458 | def __init__(self, haml, indent, compiler): 459 | super(TagNode, self).__init__(haml, indent, compiler) 460 | 461 | self.tag_statement = self.haml.lstrip(TAG_PREFIX).strip() 462 | self.tag_name = self.tag_statement.split(" ")[0] 463 | 464 | if self.tag_name in self.compiler.self_closing_tags.values(): 465 | raise ParseException("Unexpected closing tag for self-closing tag %s" % self.tag_name) 466 | 467 | def _render(self): 468 | self.before = "%s{%% %s %%}" % (self.indent, self.tag_statement) 469 | 470 | closing_tag = self.compiler.self_closing_tags.get(self.tag_name) 471 | 472 | if closing_tag: 473 | if self.tag_name == "block" and self.compiler.options.endblock_names: 474 | block_name = self.tag_statement.split(" ")[1] 475 | closing_tag += " " + block_name 476 | 477 | self.before += self.render_newlines() 478 | self.after = "%s{%% %s %%}%s" % (self.indent, closing_tag, self.render_newlines()) 479 | else: 480 | if self.children: 481 | self.before += self.render_newlines() 482 | else: 483 | self.after = self.render_newlines() 484 | self._render_children() 485 | 486 | def should_contain(self, node): 487 | return isinstance(node, TagNode) and node.tag_name in self.compiler.tags_may_contain.get(self.tag_name, "") 488 | 489 | 490 | class FilterNode(Node): 491 | """ 492 | A type filter, e.g. :javascript 493 | """ 494 | 495 | def __init__(self, filter_name, content, indent, compiler): 496 | super(FilterNode, self).__init__(indent, compiler) 497 | 498 | self.filter_name = filter_name 499 | self.content = content 500 | 501 | def _render(self): 502 | content = textwrap.dedent(self.content) 503 | 504 | filter_func = get_filter(self.filter_name) 505 | content = filter_func(content, self.compiler.options) 506 | 507 | content = self.indent + content.replace("\n", "\n" + self.indent) 508 | 509 | self.before = content 510 | self.after = self.render_newlines() if self.content else "" 511 | 512 | def _post_render(self): 513 | pass 514 | 515 | def __repr__(self): # pragma: no cover 516 | return "%s(indent=%d, newlines=%d, filter=%s): %s" % ( 517 | type(self).__name__, 518 | self.indentation, 519 | self.newlines, 520 | self.filter_name, 521 | self.content, 522 | ) 523 | -------------------------------------------------------------------------------- /hamlpy/parser/utils.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from .core import Stream 4 | 5 | DJANGO_TAG_OPEN = "{%" 6 | DJANGO_TAG_CLOSE = "%}" 7 | DJANGO_EXP_OPEN = "{{" 8 | DJANGO_EXP_CLOSE = "}}" 9 | HTML_CHARS = {"&": "&", "<": "<", ">": ">", '"': """, "'": "'"} 10 | 11 | 12 | class EscapeState(Enum): 13 | normal = 0 14 | in_tag = 1 15 | in_exp = 2 16 | 17 | 18 | def html_escape(text): 19 | """ 20 | Escapes HTML entities, matching substitutions used by the Ruby Haml library. Entities that occur inside Django tags 21 | or expressions are not escaped. 22 | """ 23 | new_text = [] 24 | state = EscapeState.normal 25 | stream = Stream(text) 26 | 27 | while stream.ptr < stream.length: 28 | ch = stream.text[stream.ptr] 29 | ch2 = stream.text[stream.ptr : stream.ptr + 2] 30 | 31 | if ch2 == DJANGO_TAG_OPEN or ch2 == DJANGO_EXP_OPEN: 32 | state = EscapeState.in_tag if ch2 == DJANGO_TAG_OPEN else EscapeState.in_exp 33 | stream.ptr += 2 34 | new_text.append(ch2) 35 | elif (state == EscapeState.in_tag and ch2 == DJANGO_TAG_CLOSE) or ( 36 | state == EscapeState.in_exp and ch2 == DJANGO_EXP_CLOSE 37 | ): 38 | state = EscapeState.normal 39 | stream.ptr += 2 40 | new_text.append(ch2) 41 | else: 42 | stream.ptr += 1 43 | new_text.append(HTML_CHARS.get(ch, ch) if state == EscapeState.normal else ch) 44 | 45 | return "".join(new_text) 46 | -------------------------------------------------------------------------------- /hamlpy/template/__init__.py: -------------------------------------------------------------------------------- 1 | from .loaders import haml_loaders as _loaders 2 | 3 | locals().update(_loaders) 4 | -------------------------------------------------------------------------------- /hamlpy/template/loaders.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.template import TemplateDoesNotExist 5 | from django.template.loaders import app_directories, filesystem 6 | 7 | from hamlpy import HAML_EXTENSIONS 8 | from hamlpy.compiler import Compiler 9 | from hamlpy.template.utils import get_django_template_loaders 10 | 11 | # Get options from Django settings 12 | options = {} 13 | 14 | if hasattr(settings, "HAMLPY_ATTR_WRAPPER"): 15 | options.update(attr_wrapper=settings.HAMLPY_ATTR_WRAPPER) 16 | 17 | if hasattr(settings, "HAMLPY_DJANGO_INLINE_STYLE"): 18 | options.update(django_inline_style=settings.HAMLPY_DJANGO_INLINE_STYLE) 19 | 20 | 21 | def get_haml_loader(loader): 22 | class Loader(loader.Loader): 23 | def get_contents(self, origin): 24 | # Django>=1.9 25 | contents = super(Loader, self).get_contents(origin) 26 | name, _extension = os.path.splitext(origin.template_name) 27 | # os.path.splitext always returns a period at the start of extension 28 | extension = _extension.lstrip(".") 29 | 30 | if extension in HAML_EXTENSIONS: 31 | compiler = Compiler(options=options) 32 | return compiler.process(contents) 33 | 34 | return contents 35 | 36 | def load_template_source(self, template_name, *args, **kwargs): 37 | # Django<1.9 38 | name, _extension = os.path.splitext(template_name) 39 | # os.path.splitext always returns a period at the start of extension 40 | extension = _extension.lstrip(".") 41 | 42 | if extension in HAML_EXTENSIONS: 43 | try: 44 | haml_source, template_path = super(Loader, self).load_template_source( 45 | self._generate_template_name(name, extension), *args, **kwargs 46 | ) 47 | except TemplateDoesNotExist: # pragma: no cover 48 | pass 49 | else: 50 | compiler = Compiler(options=options) 51 | html = compiler.process(haml_source) 52 | 53 | return html, template_path 54 | 55 | raise TemplateDoesNotExist(template_name) 56 | 57 | load_template_source.is_usable = True 58 | 59 | @staticmethod 60 | def _generate_template_name(name, extension="hamlpy"): 61 | return "%s.%s" % (name, extension) 62 | 63 | return Loader 64 | 65 | 66 | haml_loaders = dict((name, get_haml_loader(loader)) for (name, loader) in get_django_template_loaders()) 67 | 68 | HamlPyFilesystemLoader = get_haml_loader(filesystem) 69 | HamlPyAppDirectoriesLoader = get_haml_loader(app_directories) 70 | -------------------------------------------------------------------------------- /hamlpy/template/templatize.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module decorates the django templatize function to parse haml templates before the translation utility extracts 3 | tags from it. 4 | """ 5 | 6 | import os 7 | 8 | from django.utils import translation 9 | 10 | from hamlpy import HAML_EXTENSIONS 11 | from hamlpy.compiler import Compiler 12 | 13 | 14 | def patch_templatize(func): 15 | def templatize(src, origin=None, charset="utf-8"): 16 | # if the template has no origin then don't attempt to convert it because we don't know if it's Haml 17 | if origin: 18 | extension = os.path.splitext(origin)[1][1:].lower() 19 | 20 | if extension in HAML_EXTENSIONS: 21 | compiler = Compiler() 22 | src = compiler.process(src) 23 | 24 | return func(src, origin=origin) 25 | 26 | return templatize 27 | 28 | 29 | translation.templatize = patch_templatize(translation.templatize) 30 | -------------------------------------------------------------------------------- /hamlpy/template/utils.py: -------------------------------------------------------------------------------- 1 | from importlib import machinery 2 | from os import listdir 3 | from os.path import dirname, splitext 4 | 5 | from django.template import loaders 6 | 7 | MODULE_EXTENSIONS = tuple(machinery.all_suffixes()) 8 | 9 | 10 | def get_django_template_loaders(): 11 | return [ 12 | (loader.__name__.rsplit(".", 1)[1], loader) for loader in get_submodules(loaders) if hasattr(loader, "Loader") 13 | ] 14 | 15 | 16 | def get_submodules(package): 17 | submodules = ("%s.%s" % (package.__name__, module) for module in package_contents(package)) 18 | return [__import__(module, {}, {}, [module.rsplit(".", 1)[-1]]) for module in submodules] 19 | 20 | 21 | def package_contents(package): 22 | package_path = dirname(loaders.__file__) 23 | contents = set([splitext(module)[0] for module in listdir(package_path) if module.endswith(MODULE_EXTENSIONS)]) 24 | return contents 25 | -------------------------------------------------------------------------------- /hamlpy/test/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django 4 | 5 | os.environ["DJANGO_SETTINGS_MODULE"] = "hamlpy.test.settings" 6 | django.setup() 7 | -------------------------------------------------------------------------------- /hamlpy/test/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | 3 | DATABASES = {} 4 | 5 | INSTALLED_APPS = ("hamlpy",) 6 | 7 | TEMPLATES = [ 8 | { 9 | "BACKEND": "django.template.backends.django.DjangoTemplates", 10 | "DIRS": ["hamlpy/test/templates"], 11 | "OPTIONS": { 12 | "loaders": [ 13 | "hamlpy.template.loaders.HamlPyFilesystemLoader", 14 | "hamlpy.template.loaders.HamlPyAppDirectoriesLoader", 15 | "django.template.loaders.filesystem.Loader", 16 | "django.template.loaders.app_directories.Loader", 17 | ], 18 | "debug": True, 19 | }, 20 | } 21 | ] 22 | 23 | SECRET_KEY = "tots top secret" 24 | -------------------------------------------------------------------------------- /hamlpy/test/templates/allIfTypesTest.hamlpy: -------------------------------------------------------------------------------- 1 | - if something 2 | Cool 3 | - ifchanged something 4 | Changed 5 | - ifequal something "booya" 6 | Equal 7 | - ifnotequal something "blamo" 8 | No Equal 9 | - ifchanged something 10 | Changed 11 | - else 12 | No Changed 13 | - ifequal something "booya" 14 | Equal 15 | - else 16 | No Equal 17 | - ifnotequal something "blamo" 18 | No Equal 19 | - else 20 | Equal -------------------------------------------------------------------------------- /hamlpy/test/templates/allIfTypesTest.html: -------------------------------------------------------------------------------- 1 | {% if something %} 2 | Cool 3 | {% endif %} 4 | {% ifchanged something %} 5 | Changed 6 | {% endifchanged %} 7 | {% ifequal something "booya" %} 8 | Equal 9 | {% endifequal %} 10 | {% ifnotequal something "blamo" %} 11 | No Equal 12 | {% endifnotequal %} 13 | {% ifchanged something %} 14 | Changed 15 | {% else %} 16 | No Changed 17 | {% endifchanged %} 18 | {% ifequal something "booya" %} 19 | Equal 20 | {% else %} 21 | No Equal 22 | {% endifequal %} 23 | {% ifnotequal something "blamo" %} 24 | No Equal 25 | {% else %} 26 | Equal 27 | {% endifnotequal %} 28 | -------------------------------------------------------------------------------- /hamlpy/test/templates/classIdMixtures.hamlpy: -------------------------------------------------------------------------------- 1 | %div#Article.article.entry.hover:no-underline{'id':'123', class:'true'} 2 | Now this is interesting 3 | %div.article.entry.hover:no-underline#Article(id='123' class='true') 4 | Now this is really interesting 5 | -------------------------------------------------------------------------------- /hamlpy/test/templates/classIdMixtures.html: -------------------------------------------------------------------------------- 1 |
    2 | Now this is interesting 3 |
    4 |
    5 | Now this is really interesting 6 |
    7 | -------------------------------------------------------------------------------- /hamlpy/test/templates/djangoBase.hamlpy: -------------------------------------------------------------------------------- 1 | %h1 2 | - block title 3 | . 4 | - block content -------------------------------------------------------------------------------- /hamlpy/test/templates/djangoBase.html: -------------------------------------------------------------------------------- 1 |

    2 | {% block title %} 3 | {% endblock %} 4 |

    5 |
    6 | {% block content %} 7 | {% endblock %} 8 |
    9 | -------------------------------------------------------------------------------- /hamlpy/test/templates/djangoCombo.hamlpy: -------------------------------------------------------------------------------- 1 | - extends "djangoBase.hamlpy" 2 | 3 | - block title 4 | = section.title 5 | 6 | - block content 7 | %h2= section.subtitle 8 | 9 | - for story in story_list 10 | %h3 11 | %a{'href':'{{ story.get_absolute_url }}'} 12 | = story.headline|upper 13 | %p= story.tease|truncatewords:"100" 14 | -------------------------------------------------------------------------------- /hamlpy/test/templates/djangoCombo.html: -------------------------------------------------------------------------------- 1 | {% extends "djangoBase.hamlpy" %} 2 | 3 | {% block title %} 4 | {{ section.title }} 5 | 6 | {% endblock %} 7 | {% block content %} 8 |

    {{ section.subtitle }}

    9 | 10 | {% for story in story_list %} 11 |

    12 | 13 | {{ story.headline|upper }} 14 | 15 |

    16 |

    {{ story.tease|truncatewords:"100" }}

    17 | {% endfor %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /hamlpy/test/templates/filterMultilineIgnore.hamlpy: -------------------------------------------------------------------------------- 1 | .multilinetest1{id:'{{myId}}', 2 | alt: "{{nothing}}"} 3 | :plain 4 | These { { braces 5 | 6 | should } not be interpreted as a multiline string 7 | :preserve 8 | These { { braces 9 | 10 | should } not be interpreted as a multiline string 11 | :css 12 | .test { 13 | display: inline; 14 | } 15 | :javascript 16 | These { 17 | Braces should { 18 | also 19 | } be { ignored 20 | 21 | .multilinetest2{id:'{{myId}}', 22 | class:'{{myClass}}', 23 | alt: ""} 24 | / The following is from hjonathan, issue #67 25 | %head 26 | .blah 27 | :javascript 28 | $(document).ready(function(){ 29 | $("#form{{form.initial.id}}").submit(form_submit); 30 | //Double nesting 31 | $(function() { 32 | blahblahblah 33 | }); 34 | 35 | // Javascript comment 36 | }); 37 | :javascript 38 | $(document).ready(function(){ 39 | $("#form{{form.initial.id}}").submit(form_submit); 40 | // Javascript comment 41 | }); 42 | :css 43 | .someClass { 44 | width: 100px; 45 | } 46 | :cdata 47 | if (a < b && a < 0) 48 | { 49 | return 1; 50 | } 51 | -# Regression from Culebron (Github issue #90) 52 | :javascript 53 | ( 54 | { 55 | a 56 | } 57 | ); 58 | :javascript 59 | 60 | { 61 | a 62 | } 63 | ) 64 | -------------------------------------------------------------------------------- /hamlpy/test/templates/filterMultilineIgnore.html: -------------------------------------------------------------------------------- 1 |
    2 | These { { braces 3 | 4 | should } not be interpreted as a multiline string 5 | These { { braces should } not be interpreted as a multiline string 6 | 13 | 21 |
    22 | 23 | 24 |
    25 | 38 |
    39 | 47 | 54 | 60 | 61 | 70 | 79 | -------------------------------------------------------------------------------- /hamlpy/test/templates/filters.hamlpy: -------------------------------------------------------------------------------- 1 | :escaped 2 | <"&'> 3 | :javascript 4 | $(document).ready(function(){ 5 | $("#form{{form.initial.id}}").submit(form_submit); 6 | // Javascript comment 7 | }); 8 | :css 9 | .someClass { 10 | width: 100px; 11 | } 12 | :stylus 13 | .someClass 14 | width: 100px 15 | :less 16 | .someClass { 17 | width: 100px; 18 | } 19 | :sass 20 | .someClass { 21 | width: 100px; 22 | } 23 | :cdata 24 | if (a < b && a < 0) 25 | { 26 | return 1; 27 | } 28 | :python 29 | a=1 30 | for i in range(5): 31 | print(a+i) 32 | -------------------------------------------------------------------------------- /hamlpy/test/templates/filters.html: -------------------------------------------------------------------------------- 1 | <"&'> 2 | 10 | 17 | 23 | 30 | 37 | 43 | 1 44 | 2 45 | 3 46 | 4 47 | 5 48 | 49 | -------------------------------------------------------------------------------- /hamlpy/test/templates/filtersMarkdown.hamlpy: -------------------------------------------------------------------------------- 1 | :markdown 2 | hello 3 | no paragraph 4 | 5 | line break 6 | follow 7 | 8 | New paragraph 9 | 10 | code block 11 | -------------------------------------------------------------------------------- /hamlpy/test/templates/filtersMarkdown.html: -------------------------------------------------------------------------------- 1 |

    hello 2 | no paragraph

    3 |

    line break
    4 | follow

    5 |

    New paragraph

    6 |
    code block
    7 | 
    8 | -------------------------------------------------------------------------------- /hamlpy/test/templates/filtersPygments.hamlpy: -------------------------------------------------------------------------------- 1 | :highlight 2 | print "hi" 3 | 4 | if x: 5 | print "y": 6 | else: 7 | print "z": 8 | -------------------------------------------------------------------------------- /hamlpy/test/templates/filtersPygments.html: -------------------------------------------------------------------------------- 1 |
    print "hi"
    2 | 
    3 | if x:
    4 |     print "y":
    5 | else:
    6 |     print "z":
    7 | 
    8 | 9 | -------------------------------------------------------------------------------- /hamlpy/test/templates/hamlComments.hamlpy: -------------------------------------------------------------------------------- 1 | %div 2 | %div 3 | -# These comments won't show up 4 | This will 5 | -# 6 | None of this 7 | Inside of here will show 8 | up. 9 | Yikes! 10 | %div More 11 | %div Hello 12 | -------------------------------------------------------------------------------- /hamlpy/test/templates/hamlComments.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | This will 4 |
    5 |
    More
    6 |
    7 |
    Hello
    8 | -------------------------------------------------------------------------------- /hamlpy/test/templates/implicitDivs.hamlpy: -------------------------------------------------------------------------------- 1 | .articles 2 | #article_1 3 | .title.bold So happy 4 | #content_1.content 5 | Finally, I can use HAML again! -------------------------------------------------------------------------------- /hamlpy/test/templates/implicitDivs.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    So happy
    4 |
    5 | Finally, I can use HAML again! 6 |
    7 |
    8 |
    9 | -------------------------------------------------------------------------------- /hamlpy/test/templates/multiLineDict.hamlpy: -------------------------------------------------------------------------------- 1 | %div{'class': 'row'} 2 | %img{'src': '/static/imgs/ibl_logo{{id}}.gif', 3 | 'alt': 'IBL Logo'} 4 | %br 5 | %img(src='/static/imgs/ibl_logo.gif' 6 | alt = 'IBL Logo{{id}}') -------------------------------------------------------------------------------- /hamlpy/test/templates/multiLineDict.html: -------------------------------------------------------------------------------- 1 |
    2 | IBL Logo 3 |
    4 | IBL Logo{{id}} 5 |
    6 | -------------------------------------------------------------------------------- /hamlpy/test/templates/nestedComments.hamlpy: -------------------------------------------------------------------------------- 1 | / 2 | %div.someClass 3 | %div 4 | None of this matters -------------------------------------------------------------------------------- /hamlpy/test/templates/nestedComments.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /hamlpy/test/templates/nestedDjangoTags.hamlpy: -------------------------------------------------------------------------------- 1 | %ul 2 | - for story in story_list 3 | %li= story.text 4 | - if story.author_list 5 | - for author in story.author_list 6 | .author= author 7 | - else 8 | .author Anonymous 9 | - empty 10 | .error No stories found -------------------------------------------------------------------------------- /hamlpy/test/templates/nestedDjangoTags.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /hamlpy/test/templates/nestedIfElseBlocks.hamlpy: -------------------------------------------------------------------------------- 1 | - if condition 2 | - if condition 3 | - else 4 | - else -------------------------------------------------------------------------------- /hamlpy/test/templates/nestedIfElseBlocks.html: -------------------------------------------------------------------------------- 1 | {% if condition %} 2 | {% if condition %} 3 | {% else %} 4 | {% endif %} 5 | {% else %} 6 | {% endif %} 7 | -------------------------------------------------------------------------------- /hamlpy/test/templates/nukeInnerWhiteSpace.hamlpy: -------------------------------------------------------------------------------- 1 | %p< Foo 2 | %p< 3 | - if something 4 | %q 5 | Foo 6 | %p< 7 | = Foo 8 | %p 9 | %q< Foo 10 | %p 11 | %q{a: "2"}< Foo 12 | %p 13 | %q<= FooBar 14 | %p 15 | %q< 16 | Foo 17 | Bar 18 | %p 19 | %q{a: "2"}< 20 | Foo 21 | Bar 22 | %p 23 | %q< 24 | %div 25 | Foo 26 | Bar 27 | %p 28 | %q{a: "2"}< 29 | %div 30 | Foo 31 | Bar 32 | 33 | -# Regression test 34 | %p 35 | %q<= foo 36 | %q{a: "2"}< 37 | bar 38 | %q{a: "2"} 39 | bar 40 | -# Filters 41 | %p< 42 | :plain 43 | test1 44 | test2 45 | %p< 46 | :plain 47 | blah 48 | test3 49 | test4 50 | %p< 51 | :plain 52 | test5 53 | test6 54 | :plain 55 | test7 56 | :plain 57 | test8 58 | test9 -------------------------------------------------------------------------------- /hamlpy/test/templates/nukeInnerWhiteSpace.html: -------------------------------------------------------------------------------- 1 |

    Foo

    2 |

    {% if something %} 3 | 4 | Foo 5 | 6 | {% endif %}

    7 |

    {{ Foo }}

    8 |

    9 | Foo 10 |

    11 |

    12 | Foo 13 |

    14 |

    15 | {{ FooBar }} 16 |

    17 |

    18 | Foo 19 | Bar 20 |

    21 |

    22 | Foo 23 | Bar 24 |

    25 |

    26 |

    27 | Foo 28 | Bar 29 |
    30 |

    31 |

    32 |

    33 | Foo 34 | Bar 35 | 36 |
    37 |

    38 |

    39 | {{ foo }} 40 | bar 41 | 42 | bar 43 | 44 |

    45 |

    test1 46 | test2

    47 |

    blah 48 | test3 49 | test4

    50 |

    test5 51 | test6 52 | test7 53 | test8 54 | test9

    55 | -------------------------------------------------------------------------------- /hamlpy/test/templates/nukeOuterWhiteSpace.hamlpy: -------------------------------------------------------------------------------- 1 | %ul#display-inline-block-example 2 | %li Item one 3 | %li> Item two 4 | %li Item three 5 | %p 6 | - if something 7 | %q> 8 | Foo 9 | %p 10 | - sometag 11 | %q> 12 | Foo 13 | %p 14 | /[test] 15 | %q> 16 | Foo 17 | %p 18 | :javascript 19 | test 20 | %q> 21 | blah 22 | -# Tests from Ruby HAML 23 | %p 24 | %p 25 | %q> 26 | Foo 27 | %p 28 | %p 29 | %q{a: '2'}> 30 | Foo 31 | %p 32 | %p 33 | %q> Foo 34 | %p 35 | %p 36 | %q{a: "2"}> Foo 37 | %p 38 | %p 39 | %q> 40 | = Foo 41 | %p 42 | %p 43 | %q{a: "2"}> 44 | = Foo 45 | %p 46 | %p 47 | %q>= Foo 48 | %p 49 | %p 50 | %q{a: "2"}>= Foo 51 | %p 52 | %p 53 | foo 54 | %q> 55 | Foo 56 | bar 57 | %p 58 | %p 59 | foo 60 | %q{a: 2}> 61 | Foo 62 | bar 63 | %p 64 | %p 65 | foo 66 | %q> Foo 67 | bar 68 | %p 69 | %p 70 | foo 71 | %q{a: "2"}> Foo 72 | bar 73 | %p 74 | %p 75 | foo 76 | %q> 77 | = Foo 78 | bar 79 | %p 80 | %p 81 | foo 82 | %q{a: "2"}> 83 | = Foo 84 | bar 85 | %p 86 | %p 87 | foo 88 | %q>= Foo 89 | bar 90 | %p 91 | %p 92 | foo 93 | %q{a: "2"}>= Foo 94 | bar 95 | %p 96 | %p 97 | foo 98 | %q> 99 | = FooBar 100 | bar 101 | %p 102 | %p 103 | foo 104 | %q{a: "2"}> 105 | = FooBar 106 | bar 107 | %p 108 | %p 109 | foo 110 | %q>= FooBar 111 | bar 112 | %p 113 | %p 114 | foo 115 | %q{a: "2"}>= FooBar 116 | bar 117 | %p 118 | %p 119 | %q> 120 | %p 121 | %p 122 | %q>/ 123 | %p 124 | %p 125 | %q{a: "2"}> 126 | %p 127 | %p 128 | %q{a: "2"}>/ -------------------------------------------------------------------------------- /hamlpy/test/templates/nukeOuterWhiteSpace.html: -------------------------------------------------------------------------------- 1 | 4 |

    5 | {% if something %} 6 | Foo 7 | {% endif %} 8 |

    9 |

    10 | {% sometag %} 11 | Foo 12 |

    13 |

    14 | 17 |

    18 |

    19 | 24 | blah 25 |

    26 |

    27 |

    28 | Foo 29 |

    30 |

    31 |

    32 |

    33 | Foo 34 |

    35 |

    36 |

    37 |

    Foo

    38 |

    39 |

    40 |

    Foo

    41 |

    42 |

    43 |

    44 | {{ Foo }} 45 |

    46 |

    47 |

    48 |

    49 | {{ Foo }} 50 |

    51 |

    52 |

    53 |

    {{ Foo }}

    54 |

    55 |

    56 |

    {{ Foo }}

    57 |

    58 |

    59 |

    60 | foo 61 | Foo 62 | bar 63 |

    64 |

    65 |

    66 |

    67 | foo 68 | Foo 69 | bar 70 |

    71 |

    72 |

    73 |

    74 | fooFoobar 75 |

    76 |

    77 |

    78 |

    79 | fooFoobar 80 |

    81 |

    82 |

    83 |

    84 | foo 85 | {{ Foo }} 86 | bar 87 |

    88 |

    89 |

    90 |

    91 | foo 92 | {{ Foo }} 93 | bar 94 |

    95 |

    96 |

    97 |

    98 | foo{{ Foo }}bar 99 |

    100 |

    101 |

    102 |

    103 | foo{{ Foo }}bar 104 |

    105 |

    106 |

    107 |

    108 | foo 109 | {{ FooBar }} 110 | bar 111 |

    112 |

    113 |

    114 |

    115 | foo 116 | {{ FooBar }} 117 | bar 118 |

    119 |

    120 |

    121 |

    122 | foo{{ FooBar }}bar 123 |

    124 |

    125 |

    126 |

    127 | foo{{ FooBar }}bar 128 |

    129 |

    130 |

    131 |

    132 |

    133 |

    134 |

    135 |

    136 |

    137 |

    138 |

    139 |

    140 |

    141 |

    142 | -------------------------------------------------------------------------------- /hamlpy/test/templates/selfClosingDjango.hamlpy: -------------------------------------------------------------------------------- 1 | %ul 2 | - for story in story_list 3 | %li= story.text 4 | - blocktrans with object.duration_as_time as dur 5 | %span.jtimer.bigger {{ dur }} 6 | remain 7 | -------------------------------------------------------------------------------- /hamlpy/test/templates/selfClosingDjango.html: -------------------------------------------------------------------------------- 1 | 6 | {% blocktrans with object.duration_as_time as dur %} 7 | {{ dur }} 8 | remain 9 | {% endblocktrans %} 10 | -------------------------------------------------------------------------------- /hamlpy/test/templates/selfClosingTags.hamlpy: -------------------------------------------------------------------------------- 1 | %div/ 2 | %br/ 3 | %br 4 | %meta{'content':'text/html'} 5 | %img 6 | %link 7 | %br 8 | %hr 9 | -------------------------------------------------------------------------------- /hamlpy/test/templates/selfClosingTags.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 | 6 | 7 |
    8 |
    9 | -------------------------------------------------------------------------------- /hamlpy/test/templates/simple.hamlpy: -------------------------------------------------------------------------------- 1 | %div 2 | %ng-repeat.someClass 3 | Here we go 4 | And more 5 | %div More 6 | -------------------------------------------------------------------------------- /hamlpy/test/templates/simple.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | Here we go 4 | And more 5 | 6 |
    7 |
    More
    8 | -------------------------------------------------------------------------------- /hamlpy/test/templates/whitespacePreservation.hamlpy: -------------------------------------------------------------------------------- 1 | -# Test plaintext/elements 2 | %pre 3 | 4 | 5 | %div{class:'something'} 6 | 7 | 8 | Some text 9 | 10 | Some other text 11 | 12 | 13 | Some more text 14 | 15 | 16 | -# Test self-closing tag 17 | %input{type:'text'} 18 | 19 | 20 | -# Test inline element 21 | %div{class:'something'}= var 22 | 23 | 24 | -# Test Django tag 25 | - if something 26 | 27 | Test 1 28 | 29 | %div 30 | 31 | 32 | Test 2 33 | 34 | 35 | -# Test filters 36 | :plain 37 | 38 | Some text 39 | Some plain text 40 | 41 | Some more text 42 | 43 | 44 | One more text 45 | 46 | :css 47 | 48 | Some text 49 | Some plain text 50 | 51 | Some more text 52 | 53 | 54 | One more text 55 | 56 | :stylus 57 | 58 | Some text 59 | Some plain text 60 | 61 | Some more text 62 | 63 | 64 | One more text 65 | 66 | :cdata 67 | 68 | Some text 69 | Some plain text 70 | 71 | Some more text 72 | 73 | 74 | One more text 75 | 76 | :coffee 77 | 78 | Some text 79 | Some plain text 80 | 81 | Some more text 82 | 83 | 84 | One more text 85 | 86 | :javascript 87 | 88 | Some text 89 | Some plain text 90 | 91 | Some more text 92 | 93 | 94 | One more text 95 | 96 | -# Test inner whitespace removal 97 | 98 | %div< 99 | 100 | Test 101 | 102 | asd 103 | 104 | -# Test outer whitespace removal 105 | 106 | %p 107 | 108 | %li Item one 109 | 110 | %li> Item two 111 | 112 | %li Item three 113 | 114 | %p 115 | 116 | %li> 117 | Item one 118 | 119 | %p 120 | 121 | %li> Item one 122 | 123 | 124 | %li Item two 125 | 126 | %li Item three 127 | 128 | %input 129 | %input> 130 | %input -------------------------------------------------------------------------------- /hamlpy/test/templates/whitespacePreservation.html: -------------------------------------------------------------------------------- 1 |
      2 | 
      3 | 
      4 | 	
    5 | 6 | 7 | Some text 8 | 9 | Some other text 10 | 11 | 12 | Some more text 13 | 14 | 15 |
    16 |
    17 | 18 | 19 | 20 |
    {{ var }}
    21 | 22 | 23 | {% if something %} 24 | 25 | Test 1 26 | 27 |
    28 | 29 | 30 | Test 2 31 | 32 | 33 |
    34 | {% endif %} 35 | 36 | 37 | Some text 38 | Some plain text 39 | 40 | Some more text 41 | 42 | 43 | One more text 44 | 45 | 57 | 69 | 79 | 91 | 103 | 104 |
    Test 105 | 106 | asd
    107 | 108 |

    109 | 110 |

  • Item one
  • Item two
  • Item three
  • 111 | 112 |

    113 |

  • 114 | Item one 115 | 116 |
  • 117 |

  • Item one
  • Item two
  • 118 | 119 |
  • Item three
  • 120 | 121 |

    122 | 123 | -------------------------------------------------------------------------------- /hamlpy/test/test_attributes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from collections import OrderedDict 3 | 4 | from hamlpy.compiler import Compiler 5 | from hamlpy.parser.attributes import read_attribute_dict 6 | from hamlpy.parser.core import ParseException, Stream 7 | 8 | 9 | class AttributeDictParserTest(unittest.TestCase): 10 | @staticmethod 11 | def _parse(text): 12 | return read_attribute_dict(Stream(text), Compiler()) 13 | 14 | def test_read_ruby_style_attribute_dict(self): 15 | # empty dict 16 | stream = Stream("{}><") 17 | assert dict(read_attribute_dict(stream, Compiler())) == {} 18 | assert stream.text[stream.ptr :] == "><" 19 | 20 | # string values 21 | assert dict(self._parse("{'class': 'test'} =Test")) == {"class": "test"} 22 | assert dict(self._parse("{'class': 'test', 'id': 'something'}")) == {"class": "test", "id": "something"} 23 | 24 | # integer values 25 | assert dict(self._parse("{'data-number': 0}")) == {"data-number": "0"} 26 | assert dict(self._parse("{'data-number': 12345}")) == {"data-number": "12345"} 27 | 28 | # float values 29 | assert dict(self._parse("{'data-number': 123.456}")) == {"data-number": "123.456"} 30 | assert dict(self._parse("{'data-number': 0.001}")) == {"data-number": "0.001"} 31 | 32 | # None value 33 | assert dict(self._parse("{'controls': None}")) == {"controls": None} 34 | 35 | # boolean attributes 36 | assert dict(self._parse("{disabled, class:'test', data-number : 123,\n foo:\"bar\"}")) == { 37 | "disabled": True, 38 | "class": "test", 39 | "data-number": "123", 40 | "foo": "bar", 41 | } 42 | 43 | assert dict(self._parse("{class:'test', data-number : 123,\n foo:\"bar\", \t disabled}")) == { 44 | "disabled": True, 45 | "class": "test", 46 | "data-number": "123", 47 | "foo": "bar", 48 | } 49 | 50 | # attribute name has colon 51 | assert dict(self._parse("{'xml:lang': 'en'}")) == {"xml:lang": "en"} 52 | 53 | # attribute value has colon or commas 54 | assert dict(self._parse("{'lang': 'en:g'}")) == {"lang": "en:g"} 55 | assert dict( 56 | self._parse( 57 | '{name:"viewport", content:"width:device-width, initial-scale:1, minimum-scale:1, maximum-scale:1"}' 58 | ) 59 | ) == {"name": "viewport", "content": "width:device-width, initial-scale:1, minimum-scale:1, maximum-scale:1"} 60 | 61 | # double quotes 62 | assert dict(self._parse('{"class": "test", "id": "something"}')) == {"class": "test", "id": "something"} 63 | 64 | # no quotes for key 65 | assert dict(self._parse("{class: 'test', id: 'something'}")) == {"class": "test", "id": "something"} 66 | 67 | # whitespace is ignored 68 | assert dict(self._parse("{ class \t : 'test', data-number: 123 }")) == { 69 | "class": "test", 70 | "data-number": "123", 71 | } 72 | 73 | # trailing commas are fine 74 | assert dict(self._parse("{class: 'test', data-number: 123,}")) == {"class": "test", "data-number": "123"} 75 | 76 | # attributes split onto multiple lines 77 | assert dict(self._parse("{class: 'test',\n data-number: 123}")) == {"class": "test", "data-number": "123"} 78 | 79 | # old style Ruby 80 | assert dict(self._parse("{:class => 'test', :data-number=>123}")) == {"class": "test", "data-number": "123"} 81 | 82 | # list attribute values 83 | assert dict(self._parse("{'class': [ 'a', 'b', 'c' ], data-list:[1, 2, 3]}")) == { 84 | "class": ["a", "b", "c"], 85 | "data-list": ["1", "2", "3"], 86 | } 87 | 88 | # tuple attribute values 89 | assert dict(self._parse("{:class=>( 'a', 'b', 'c' ), :data-list => (1, 2, 3)}")) == { 90 | "class": ["a", "b", "c"], 91 | "data-list": ["1", "2", "3"], 92 | } 93 | 94 | # attribute order is maintained 95 | assert self._parse("{'class': 'test', 'id': 'something', foo: 'bar'}") == OrderedDict( 96 | [("class", "test"), ("id", "something"), ("foo", "bar")] 97 | ) 98 | 99 | # attribute values can be multi-line Haml 100 | haml = """{ 101 | 'class': 102 | - if forloop.first 103 | link-first 104 | \x20 105 | - else 106 | - if forloop.last 107 | link-last 108 | 'href': 109 | - url 'some_view' 110 | }""" 111 | assert dict(self._parse(haml)) == { 112 | "class": "{% if forloop.first %} link-first {% else %} {% if forloop.last %} link-last {% endif %} {% endif %}", # noqa 113 | "href": "{% url 'some_view' %}", 114 | } 115 | 116 | # non-ascii attribute values 117 | assert dict(self._parse("{class: 'test\u1234'}")) == {"class": "test\u1234"} 118 | 119 | def test_read_html_style_attribute_dict(self): 120 | # html style dicts 121 | assert dict(self._parse("()><")) == {} 122 | assert dict(self._parse("( )")) == {} 123 | 124 | # string values 125 | assert dict(self._parse("(class='test') =Test")) == {"class": "test"} 126 | assert dict(self._parse("(class='test' id='something')")) == {"class": "test", "id": "something"} 127 | 128 | # integer values 129 | assert dict(self._parse("(data-number=0)")) == {"data-number": "0"} 130 | assert dict(self._parse("(data-number=12345)")) == {"data-number": "12345"} 131 | 132 | # float values 133 | assert dict(self._parse("(data-number=123.456)")) == {"data-number": "123.456"} 134 | assert dict(self._parse("(data-number=0.001)")) == {"data-number": "0.001"} 135 | 136 | # None value 137 | assert dict(self._parse("(controls=None)")) == {"controls": None} 138 | 139 | # boolean attributes 140 | assert dict(self._parse("(disabled class='test' data-number = 123\n foo=\"bar\")")) == { 141 | "disabled": True, 142 | "class": "test", 143 | "data-number": "123", 144 | "foo": "bar", 145 | } 146 | 147 | assert dict(self._parse("(class='test' data-number = 123\n foo=\"bar\" \t disabled)")) == { 148 | "disabled": True, 149 | "class": "test", 150 | "data-number": "123", 151 | "foo": "bar", 152 | } 153 | 154 | # attribute name has colon 155 | assert dict(self._parse('(xml:lang="en")')) == {"xml:lang": "en"} 156 | 157 | # attribute names with characters found in JS frameworks 158 | assert dict(self._parse('([foo]="a" ?foo$="b")')) == {"[foo]": "a", "?foo$": "b"} 159 | 160 | # double quotes 161 | assert dict(self._parse('(class="test" id="something")')) == {"class": "test", "id": "something"} 162 | 163 | # list attribute values 164 | assert dict(self._parse("(class=[ 'a', 'b', 'c' ] data-list=[1, 2, 3])")) == { 165 | "class": ["a", "b", "c"], 166 | "data-list": ["1", "2", "3"], 167 | } 168 | 169 | # variable attribute values 170 | assert dict(self._parse("(foo=bar)")) == {"foo": "{{ bar }}"} 171 | 172 | # attribute values can be multi-line Haml 173 | haml = """( 174 | class= 175 | - if forloop.first 176 | link-first 177 | \x20 178 | - else 179 | - if forloop.last 180 | link-last 181 | href= 182 | - url 'some_view' 183 | )""" 184 | assert dict(self._parse(haml)) == { 185 | "class": "{% if forloop.first %} link-first {% else %} {% if forloop.last %} link-last {% endif %} {% endif %}", # noqa 186 | "href": "{% url 'some_view' %}", 187 | } 188 | 189 | def test_empty_attribute_name_raises_error(self): 190 | # empty quoted string in Ruby new style 191 | with self.assertRaisesRegex(ParseException, r'Attribute name can\'t be an empty string. @ "{\'\':" <-'): 192 | self._parse("{'': 'test'}") 193 | 194 | # empty old style Ruby attribute 195 | with self.assertRaisesRegex(ParseException, r'Unexpected " ". @ "{: " <-'): 196 | self._parse("{: 'test'}") 197 | 198 | # missing (HTML style) 199 | with self.assertRaisesRegex(ParseException, r'Unexpected "=". @ "\(=" <-'): 200 | self._parse("(='test')") 201 | with self.assertRaisesRegex(ParseException, r'Unexpected "=". @ "\(foo=\'bar\' =" <-'): 202 | self._parse("(foo='bar' ='test')") 203 | 204 | def test_empty_attribute_value_raises_error(self): 205 | with self.assertRaisesRegex(ParseException, r'Unexpected "}". @ "{:class=>}" <-'): 206 | self._parse("{:class=>}") 207 | with self.assertRaisesRegex(ParseException, r'Unexpected "}". @ "{class:}" <-'): 208 | self._parse("{class:}") 209 | with self.assertRaisesRegex(ParseException, r'Unexpected "\)". @ "\(class=\)" <-'): 210 | self._parse("(class=)") 211 | 212 | def test_unterminated_string_raises_error(self): 213 | # on attribute key 214 | with self.assertRaisesRegex(ParseException, r'Unterminated string \(expected \'\). @ "{\'test: 123}" <-'): 215 | self._parse("{'test: 123}") 216 | 217 | # on attribute value 218 | with self.assertRaisesRegex(ParseException, r'Unterminated string \(expected "\). @ "{\'test\': "123}" <-'): 219 | self._parse("{'test': \"123}") 220 | 221 | def test_duplicate_attributes_raise_error(self): 222 | with self.assertRaisesRegex( 223 | ParseException, r'Duplicate attribute: "class". @ "{class: \'test\', class: \'bar\'}" <-' 224 | ): # noqa 225 | self._parse("{class: 'test', class: 'bar'}") 226 | 227 | with self.assertRaisesRegex( 228 | ParseException, r'Duplicate attribute: "class". @ "\(class=\'test\' class=\'bar\'\)" <-' 229 | ): # noqa 230 | self._parse("(class='test' class='bar')") 231 | 232 | def test_mixing_ruby_and_html_syntax_raises_errors(self): 233 | # omit comma in Ruby style dict 234 | with self.assertRaisesRegex(ParseException, r'Expected ",". @ "{class: \'test\' f" <-'): 235 | self._parse("{class: 'test' foo: 'bar'}") 236 | 237 | # use = in Ruby style dict 238 | with self.assertRaisesRegex(ParseException, r'Expected ":". @ "{class=" <-'): 239 | self._parse("{class='test'}") 240 | with self.assertRaisesRegex(ParseException, r'Expected "=>". @ "{:class=" <-'): 241 | self._parse("{:class='test'}") 242 | 243 | # use colon as assignment for old style Ruby attribute 244 | with self.assertRaisesRegex(ParseException, r'Expected "=>". @ "{:class:" <-'): 245 | self._parse("{:class:'test'}") 246 | 247 | # use comma in HTML style dict 248 | with self.assertRaisesRegex(ParseException, r'Unexpected ",". @ "\(class=\'test\'," <-'): 249 | self._parse("(class='test', foo = 'bar')") 250 | 251 | # use : for assignment in HTML style dict (will treat as part of attribute name) 252 | with self.assertRaisesRegex(ParseException, r'Unexpected "\'". @ "\(class:\'" <-'): 253 | self._parse("(class:'test')") 254 | 255 | # use attribute quotes in HTML style dict 256 | with self.assertRaisesRegex(ParseException, r'Unexpected "\'". @ "\(\'" <-'): 257 | self._parse("('class'='test')") 258 | 259 | # use => in HTML style dict 260 | with self.assertRaisesRegex(ParseException, r'Unexpected ">". @ "\(class=>" <-'): 261 | self._parse("(class=>'test')") 262 | 263 | # use tuple syntax in HTML style dict 264 | with self.assertRaisesRegex(ParseException, r'Unexpected "\(". @ "\(class=\(" <-'): 265 | self._parse("(class=(1, 2))") 266 | 267 | def test_unexpected_eof(self): 268 | with self.assertRaisesRegex(ParseException, r'Unexpected end of input. @ "{:class=>" <-'): 269 | self._parse("{:class=>") 270 | with self.assertRaisesRegex(ParseException, r'Unexpected end of input. @ "{class:" <-'): 271 | self._parse("{class:") 272 | with self.assertRaisesRegex(ParseException, r'Unexpected end of input. @ "\(class=" <-'): 273 | self._parse("(class=") 274 | -------------------------------------------------------------------------------- /hamlpy/test/test_compiler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from hamlpy.compiler import Compiler 4 | from hamlpy.parser import filters 5 | from hamlpy.parser.core import ParseException 6 | 7 | 8 | class CompilerTest(unittest.TestCase): 9 | def test_tags(self): 10 | # tags can have xml namespaces 11 | self._test("%fb:tag\n content", "\n content\n") 12 | 13 | # tags can have dashes 14 | self._test("%ng-tag\n content", "\n content\n") 15 | 16 | def test_ids_and_classes(self): 17 | # id on tag 18 | self._test("%div#someId Some text", "
    Some text
    ") 19 | 20 | # id with non-ascii characters 21 | self._test("%div#これはテストです test", "
    test
    ") 22 | 23 | # class on tag 24 | self._test("%div.someClass Some text", "
    Some text
    ") 25 | 26 | # class can contain dash 27 | self._test(".header.span-24.last", "
    ") 28 | 29 | # multiple classes 30 | self._test("%div.someClass.anotherClass Some text", "
    Some text
    ") 31 | 32 | # class can come before id 33 | self._test("%div.someClass#someId", "
    ") 34 | 35 | def test_attribute_dictionaries(self): 36 | # attribute dictionaries 37 | self._test( 38 | "%html{'xmlns':'http://www.w3.org/1999/xhtml', 'xml:lang':'en', 'lang':'en'}", 39 | "", 40 | ) 41 | 42 | # attribute whitespace is ignored 43 | self._test('%form{ id : "myform" }', "
    ") 44 | 45 | # HTML style 46 | self._test('%form(foo=bar id="myform")', "
    ") 47 | 48 | # multiple dicts 49 | self._test('%a(a="b"){:c => "d"} Stuff', "Stuff") 50 | 51 | self._test_error("%a(b=)", 'Unexpected ")". @ "%a(b=)" <-') 52 | 53 | def test_boolean_attributes(self): 54 | self._test("%input{required}", "") 55 | self._test("%input{required, a: 'b'}", "") 56 | self._test("%input{a: 'b', required, b: 'c'}", "") 57 | self._test("%input{a: 'b', required}", "") 58 | self._test("%input{checked, required, visible}", "") 59 | self._test("%input(checked=true)", "") 60 | self._test("%input(checked=true)", "", options={"format": "xhtml"}) 61 | 62 | def test_attribute_values_as_tuples_and_lists(self): 63 | # id attribute as tuple 64 | self._test("%div{'id':('itemType', '5')}", "
    ") 65 | 66 | # attributes as lists 67 | self._test( 68 | "%div{'id':['Article','1'], 'class':['article','entry','visible']} Booyaka", 69 | "
    Booyaka
    ", 70 | ) 71 | self._test( 72 | "%div{'id': ('article', '3'), 'class': ('newest', 'urgent')} Content", 73 | "
    Content
    ", 74 | ) 75 | 76 | def test_comments(self): 77 | self._test("/ some comment", "") 78 | 79 | # comments should hide child nodes 80 | self._test( 81 | """ 82 | -# My comment 83 | #my_div 84 | my text 85 | test""", 86 | "test", 87 | ) 88 | 89 | # conditional comments 90 | self._test("/[if IE] You use a shitty browser", "") 91 | self._test( 92 | "/[if IE]\n %h1 You use a shitty browser", 93 | "", 94 | ) 95 | self._test("/[if lte IE 7]\n\ttest\n#test", "\n
    ") 96 | 97 | def test_django_variables(self): 98 | # whole-line variable with = 99 | self._test("= story.tease", "{{ story.tease }}") 100 | 101 | # element content variable with = 102 | self._test("%div= story.tease", "
    {{ story.tease }}
    ") 103 | 104 | # embedded Django variables using #{...} 105 | self._test("#{greeting} #{name}, how are you #{date}?", "{{ greeting }} {{ name }}, how are you {{ date }}?") 106 | self._test("%h1 Hello, #{person.name}, how are you?", "

    Hello, {{ person.name }}, how are you?

    ") 107 | 108 | # embedded Django variables using ={...} (not enabled by default) 109 | self._test("Hi ={name}, how are you?", "Hi ={name}, how are you?") 110 | self._test("Hi ={name}, how are you?", "Hi {{ name }}, how are you?", options={"django_inline_style": True}) 111 | 112 | # variables can use Django filters 113 | self._test('#{value|center:"15"}', '{{ value|center:"15" }}') 114 | 115 | # variables can be used in attribute values 116 | self._test("%a{'b': '#{greeting} test'} blah", "blah") 117 | 118 | # including in the id or class 119 | self._test("%div{'id':'package_#{object.id}'}", "
    ") 120 | self._test("%div{'class':'package_#{object.id}'}", "
    ") 121 | 122 | # they can be escaped 123 | self._test( 124 | "%a{'b': '\\\\#{greeting} test', title: \"It can't be removed\"} blah", 125 | "blah", 126 | ) 127 | self._test( 128 | "%h1 Hello, \\={name}, how are you ={ date }?", 129 | "

    Hello, ={name}, how are you {{ date }}?

    ", 130 | options={"django_inline_style": True}, 131 | ) 132 | self._test("\\#{name}, how are you?", "#{name}, how are you?") 133 | 134 | def test_django_tags(self): 135 | # if/else 136 | self._test( 137 | "- if something\n %p hello\n- else\n %p goodbye", 138 | "{% if something %}\n

    hello

    \n{% else %}\n

    goodbye

    \n{% endif %}", 139 | ) 140 | 141 | # with 142 | self._test("- with thing1 as another\n stuff", "{% with thing1 as another %}\n stuff\n{% endwith %}") 143 | self._test( 144 | "- with context\n hello\n- with other_context\n goodbye", 145 | "{% with context %}\n hello\n{% endwith %}\n{% with other_context %}\n goodbye\n{% endwith %}", 146 | ) 147 | 148 | # block 149 | self._test( 150 | "-block title\n %p hello\n", 151 | "{% block title %}\n

    hello

    \n{% endblock %}", 152 | ) 153 | self._test( 154 | "-block title\n %p hello\n", 155 | "{% block title %}\n

    hello

    \n{% endblock title %}", 156 | options={"endblock_names": True}, 157 | ) 158 | 159 | # trans 160 | self._test('- trans "Hello"\n', '{% trans "Hello" %}') 161 | 162 | # blocktrans 163 | self._test( 164 | "- blocktrans with amount=num_cookies\n" " There are #{ amount } cookies", 165 | "{% blocktrans with amount=num_cookies %}\n" " There are {{ amount }} cookies\n" "{% endblocktrans %}", 166 | ) 167 | self._test( 168 | "- blocktrans with amount=num_cookies\n" 169 | " There is one cookie\n" 170 | "- plural\n" 171 | " There are #{ amount } cookies", 172 | "{% blocktrans with amount=num_cookies %}\n" 173 | " There is one cookie\n" 174 | "{% plural %}\n" 175 | " There are {{ amount }} cookies\n" 176 | "{% endblocktrans %}", 177 | ) 178 | 179 | # exception using a closing tag of a self-closing tag 180 | parser = Compiler() 181 | self.assertRaises(ParseException, parser.process, "- endfor") 182 | 183 | def test_plain_text(self): 184 | self._test("This should be plain text", "This should be plain text") 185 | self._test( 186 | "This should be plain text\n This should be indented", 187 | "This should be plain text\n This should be indented", 188 | ) 189 | 190 | # native Django tags {% %} should be treated as plain text 191 | self._test("text {%\n trans ''\n%}", "text {%\n trans ''\n%}") 192 | self._test("text\n {%\n trans ''\n%}", "text\n {%\n trans ''\n%}") 193 | 194 | def test_plain_filter(self): 195 | # with indentation 196 | self._test( 197 | ":plain\n -This should be plain text\n .This should be more\n This should be indented", 198 | "-This should be plain text\n.This should be more\n This should be indented", 199 | ) 200 | 201 | # with no children 202 | self._test(":plain\nNothing", "Nothing") 203 | 204 | # with escaped back slash 205 | self._test(":plain\n \\Something", "\\Something") 206 | 207 | # with space after filter name 208 | self._test(":plain \n -This should be plain text\n", "-This should be plain text") 209 | 210 | def test_preserve_filter(self): 211 | # with indentation 212 | self._test( 213 | ":preserve\n -This should be plain text\n .This should be more\n This should be indented", 214 | "-This should be plain text .This should be more This should be indented", 215 | ) 216 | 217 | # with no children 218 | self._test(":preserve\nNothing", "Nothing") 219 | 220 | # with escaped back slash 221 | self._test(":preserve\n \\Something", "\\Something") 222 | 223 | def test_python_filter(self): 224 | self._test(":python\n", "") # empty 225 | self._test( 226 | ':python\n for i in range(0, 5): print("

    item \\%s

    " % i)', 227 | "

    item \\0

    \n

    item \\1

    \n

    item \\2

    \n

    item \\3

    \n

    item \\4

    ", 228 | ) 229 | 230 | self._test_error(":python\n print(10 / 0)", "Error whilst executing python filter node", ZeroDivisionError) 231 | 232 | def test_pygments_filter(self): 233 | self._test(":highlight\n", "") # empty 234 | self._test( 235 | ":highlight\n print(1)\n", '
    print(1)\n
    ' 236 | ) # noqa 237 | 238 | filters._pygments_available = False 239 | 240 | self._test_error(":highlight\n print(1)\n", "Pygments is not available") 241 | 242 | filters._pygments_available = True 243 | 244 | def test_markdown_filter(self): 245 | self._test(":markdown\n", "") # empty 246 | self._test(":markdown\n *Title*\n", "

    Title

    ") 247 | 248 | filters._markdown_available = False 249 | 250 | self._test_error(":markdown\n *Title*\n", "Markdown is not available") 251 | 252 | filters._markdown_available = True 253 | 254 | def test_invalid_filter(self): 255 | self._test_error(":nosuchfilter\n", "No such filter: nosuchfilter") 256 | 257 | def test_doctypes(self): 258 | self._test("!!!", "", options={"format": "html5"}) 259 | self._test("!!! 5", "", options={"format": "xhtml"}) 260 | self._test("!!! 5", "") 261 | self._test( 262 | "!!! strict", 263 | '', # noqa 264 | options={"format": "xhtml"}, 265 | ) 266 | self._test( 267 | "!!! frameset", 268 | '', # noqa 269 | options={"format": "xhtml"}, 270 | ) 271 | self._test( 272 | "!!! mobile", 273 | '', # noqa 274 | options={"format": "xhtml"}, 275 | ) 276 | self._test( 277 | "!!! basic", 278 | '', # noqa 279 | options={"format": "xhtml"}, 280 | ) 281 | self._test( 282 | "!!! transitional", 283 | '', # noqa 284 | options={"format": "xhtml"}, 285 | ) 286 | self._test( 287 | "!!!", 288 | '', # noqa 289 | options={"format": "xhtml"}, 290 | ) 291 | self._test( 292 | "!!! strict", 293 | '', # noqa 294 | options={"format": "html4"}, 295 | ) 296 | self._test( 297 | "!!! frameset", 298 | '', # noqa 299 | options={"format": "html4"}, 300 | ) 301 | self._test( 302 | "!!! transitional", 303 | '', # noqa 304 | options={"format": "html4"}, 305 | ) 306 | self._test( 307 | "!!!", 308 | '', # noqa 309 | options={"format": "html4"}, 310 | ) 311 | 312 | self._test("!!! XML", "", options={"format": "html4"}) 313 | self._test("!!! XML iso-8589", "", options={"format": "xhtml"}) 314 | 315 | def test_attr_wrapper(self): 316 | self._test( 317 | "%p{ :strange => 'attrs'}", "

    ", options={"attr_wrapper": "*", "escape_attrs": True} 318 | ) 319 | self._test( 320 | "%p{ :escaped => 'quo\"te'}", 321 | '

    ', 322 | options={"attr_wrapper": '"', "escape_attrs": True}, 323 | ) 324 | self._test( 325 | "%p{ :escaped => 'quo\\'te'}", 326 | '

    ', 327 | options={"attr_wrapper": '"', "escape_attrs": True}, 328 | ) 329 | self._test( 330 | "%p{ :escaped => 'q\\'uo\"te'}", 331 | '

    ', 332 | options={"attr_wrapper": '"', "escape_attrs": True}, 333 | ) 334 | self._test( 335 | "!!! XML", 336 | '', 337 | options={"attr_wrapper": '"', "format": "xhtml", "escape_attrs": True}, 338 | ) 339 | 340 | # smart quotes... 341 | 342 | self._test( 343 | """%a(onclick="alert('hi')")""", 344 | """""", 345 | options={"attr_wrapper": '"', "smart_quotes": False}, 346 | ) 347 | self._test( 348 | """%a(onclick="alert('hi')")""", 349 | """""", 350 | options={"attr_wrapper": '"', "smart_quotes": True}, 351 | ) 352 | self._test( 353 | """%a(onclick="alert('hi')")""", 354 | """""", 355 | options={"attr_wrapper": "'", "smart_quotes": True}, 356 | ) 357 | self._test( 358 | """%a(onclick='alert("hi {% trans "world" %}")')""", 359 | """""", 360 | options={"attr_wrapper": '"', "smart_quotes": True}, 361 | ) 362 | 363 | def test_attr_escaping(self): 364 | self._test( 365 | """#foo{:class => ''}""", 366 | """
    """, 367 | options={"escape_attrs": False}, 368 | ) 369 | self._test( 370 | """#foo{:class => '"<>&"'}""", 371 | """
    """, 372 | options={"escape_attrs": True}, 373 | ) 374 | self._test( 375 | """#foo{:class => '{% trans "Hello" %}'}""", 376 | """
    """, 377 | options={"escape_attrs": True}, 378 | ) 379 | 380 | def test_node_escaping(self): 381 | self._test("\\= Escaped", "= Escaped") 382 | self._test("\\%}", "%}") 383 | self._test(" \\:python", " :python") 384 | 385 | def test_utf8(self): 386 | self._test( 387 | "%a{'href':'', 'title':'링크(Korean)'} Some Link", "Some Link" 388 | ) 389 | 390 | def test_custom_filter(self): 391 | def upper(text, options): 392 | return text.upper() 393 | 394 | filters.register_filter("upper", upper) 395 | 396 | self._test(":upper\n welcome", "WELCOME") 397 | 398 | def _test(self, haml, expected_html, options=None): 399 | if not options: 400 | options = {"escape_attrs": True} 401 | 402 | compiler = Compiler(options) 403 | result = compiler.process(haml) 404 | 405 | result = result.rstrip("\n") # ignore trailing new lines 406 | 407 | assert result == expected_html 408 | 409 | def _test_error(self, haml, expected_message, expected_cause=None, compiler_options=None): 410 | compiler = Compiler(compiler_options) 411 | 412 | try: 413 | compiler.process(haml) 414 | self.fail("Expected exception to be raised") 415 | except Exception as e: 416 | self.assertIsInstance(e, ParseException) 417 | assert str(e) == expected_message 418 | 419 | if expected_cause: 420 | assert type(e.__cause__) == expected_cause 421 | -------------------------------------------------------------------------------- /hamlpy/test/test_elements.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from collections import OrderedDict 3 | 4 | from hamlpy.compiler import Compiler 5 | from hamlpy.parser.core import Stream 6 | from hamlpy.parser.elements import Element, read_element, read_tag 7 | 8 | 9 | class ElementTest(unittest.TestCase): 10 | def test_read_tag(self): 11 | stream = Stream("angular:ng-repeat(") 12 | self.assertEqual(read_tag(stream), "angular:ng-repeat") 13 | self.assertEqual(stream.text[stream.ptr :], "(") 14 | 15 | def test_read_element(self): 16 | stream = Stream('%angular:ng-repeat.my-class.other:class#my-id(class="test")>Hello\n" 13 | assert extension.preprocess("%span Hello", "test.hamlpy") == "Hello\n" 14 | assert extension.preprocess("%span Hello", "../templates/test.hamlpy") == "Hello\n" 15 | 16 | # non-Haml extension should be ignored 17 | assert extension.preprocess("%span Hello", "test.txt") == "%span Hello" 18 | 19 | # exceptions converted to Jinja2 exceptions 20 | self.assertRaisesRegex( 21 | TemplateSyntaxError, 22 | r'Unterminated string \(expected "\). @ "%span{"}"', 23 | extension.preprocess, 24 | '%span{"}', 25 | "test.haml", 26 | ) 27 | -------------------------------------------------------------------------------- /hamlpy/test/test_loader.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from unittest import mock 3 | 4 | import pytest 5 | 6 | from django.template import TemplateDoesNotExist 7 | from django.template.loader import render_to_string 8 | from django.test import SimpleTestCase 9 | from django.test.utils import override_settings 10 | 11 | from hamlpy.compiler import Compiler 12 | from hamlpy.template import loaders 13 | 14 | 15 | class LoaderTest(SimpleTestCase): 16 | def setUp(self): 17 | super(LoaderTest, self).setUp() 18 | 19 | importlib.reload(loaders) 20 | 21 | @mock.patch("hamlpy.template.loaders.Compiler", wraps=Compiler) 22 | def test_compiler_default_settings(self, mock_compiler_class): 23 | render_to_string("simple.hamlpy") 24 | 25 | mock_compiler_class.assert_called_once_with(options={}) 26 | mock_compiler_class.reset_mock() 27 | 28 | @override_settings(HAMLPY_ATTR_WRAPPER='"', HAMLPY_DJANGO_INLINE_STYLE=False) 29 | def test_compiler_settings(self): 30 | importlib.reload(loaders) 31 | 32 | with mock.patch("hamlpy.template.loaders.Compiler", wraps=Compiler) as mock_compiler_class: 33 | rendered = render_to_string("simple.hamlpy") 34 | 35 | mock_compiler_class.assert_called_once_with(options={"attr_wrapper": '"', "django_inline_style": False}) 36 | 37 | assert '"someClass"' in rendered 38 | 39 | def test_template_rendering(self): 40 | assert render_to_string("simple.hamlpy") == self._load_test_template("simple.html") 41 | 42 | context = { 43 | "section": {"title": "News", "subtitle": "Technology"}, 44 | "story_list": [ 45 | { 46 | "headline": "Haml Helps", 47 | "tease": "Many HAML users...", 48 | "get_absolute_url": lambda: "http://example.com/stories/1/", 49 | } 50 | ], 51 | } 52 | 53 | rendered = render_to_string("djangoCombo.hamlpy", context) 54 | 55 | assert "

    Technology

    " in rendered 56 | assert "HAML HELPS" in rendered 57 | assert "" in rendered 58 | assert "

    Many HAML users...

    " 59 | 60 | def test_should_ignore_non_haml_templates(self): 61 | assert render_to_string("simple.html") == self._load_test_template("simple.html") 62 | 63 | def test_should_raise_exception_when_template_doesnt_exist(self): 64 | with pytest.raises(TemplateDoesNotExist): 65 | render_to_string("simple.xyz") 66 | 67 | def _load_test_template(self, name): 68 | return open("hamlpy/test/templates/" + name, "r").read() 69 | -------------------------------------------------------------------------------- /hamlpy/test/test_nodes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from hamlpy.compiler import Compiler 4 | from hamlpy.parser import nodes 5 | from hamlpy.parser.core import Stream 6 | from hamlpy.parser.nodes import read_filter_node, read_node 7 | 8 | 9 | class NodeTest(unittest.TestCase): 10 | def test_read_node(self): 11 | assert isinstance(self._read_node("%div"), nodes.ElementNode) 12 | assert isinstance(self._read_node(" %html"), nodes.ElementNode) 13 | assert isinstance(self._read_node(".className"), nodes.ElementNode) 14 | assert isinstance(self._read_node(" .className"), nodes.ElementNode) 15 | assert isinstance(self._read_node("#idName"), nodes.ElementNode) 16 | assert isinstance(self._read_node(" #idName"), nodes.ElementNode) 17 | assert isinstance(self._read_node("/ some Comment"), nodes.CommentNode) 18 | assert isinstance(self._read_node(" / some Comment"), nodes.CommentNode) 19 | assert isinstance(self._read_node("just some random text"), nodes.PlaintextNode) 20 | assert isinstance(self._read_node(" more random text"), nodes.PlaintextNode) 21 | assert isinstance(self._read_node("-# This is a haml comment"), nodes.HamlCommentNode) 22 | assert isinstance(self._read_node("= some.variable"), nodes.VariableNode) 23 | assert isinstance(self._read_node("- for something in somethings"), nodes.TagNode) 24 | assert isinstance(self._read_node("\\= some.variable"), nodes.PlaintextNode) 25 | assert isinstance(self._read_node(" \\= some.variable"), nodes.PlaintextNode) 26 | assert isinstance(self._read_node("/[if IE 5]"), nodes.ConditionalCommentNode) 27 | assert isinstance(self._read_node(":plain"), nodes.FilterNode) 28 | assert isinstance(self._read_node(" :css\n"), nodes.FilterNode) 29 | assert isinstance(self._read_node(":stylus"), nodes.FilterNode) 30 | assert isinstance(self._read_node(":javascript"), nodes.FilterNode) 31 | assert isinstance(self._read_node(":coffee"), nodes.FilterNode) 32 | 33 | def test_read_filter_node(self): 34 | stream = Stream(':python\n print("hello")\n') 35 | node = read_filter_node(stream, "", Compiler()) 36 | assert node.filter_name == "python" 37 | assert node.content == ' print("hello")' 38 | assert stream.text[stream.ptr :] == "" 39 | 40 | stream = Stream(":javascript\n var i = 0;\n var j = 1;\n%span") 41 | node = read_filter_node(stream, "", Compiler()) 42 | assert node.filter_name == "javascript" 43 | assert node.content == " var i = 0;\n var j = 1;" 44 | assert stream.text[stream.ptr :] == "%span" 45 | 46 | def test_calculates_indentation_properly(self): 47 | no_indentation = self._read_node("%div") 48 | assert no_indentation.indentation == 0 49 | 50 | three_indentation = self._read_node(" %div") 51 | assert three_indentation.indentation == 3 52 | 53 | six_indentation = self._read_node(" %div") 54 | assert six_indentation.indentation == 6 55 | 56 | def test_indents_tabs_properly(self): 57 | no_indentation = self._read_node("%div") 58 | assert no_indentation.indent == "" 59 | 60 | one_tab = self._read_node(" %div") 61 | assert one_tab.indent == "\t" 62 | 63 | one_space = self._read_node(" %div") 64 | assert one_space.indent == " " 65 | 66 | three_tabs = self._read_node(" %div") 67 | assert three_tabs.indent == "\t\t\t" 68 | 69 | tab_space = self._read_node(" %div") 70 | assert tab_space.indent == "\t\t" 71 | 72 | space_tab = self._read_node(" %div") 73 | assert space_tab.indent == " " 74 | 75 | def test_lines_are_always_stripped_of_whitespace(self): 76 | some_space = self._read_node(" text") 77 | assert some_space.haml == "text" 78 | 79 | lots_of_space = self._read_node(" text ") 80 | assert lots_of_space.haml == "text" 81 | 82 | def test_inserts_nodes_into_proper_tree_depth(self): 83 | no_indentation_node = self._read_node("%div") 84 | one_indentation_node = self._read_node(" %div") 85 | two_indentation_node = self._read_node(" %div") 86 | another_one_indentation_node = self._read_node(" %div") 87 | 88 | no_indentation_node.add_node(one_indentation_node) 89 | no_indentation_node.add_node(two_indentation_node) 90 | no_indentation_node.add_node(another_one_indentation_node) 91 | 92 | assert one_indentation_node == no_indentation_node.children[0] 93 | assert two_indentation_node == no_indentation_node.children[0].children[0] 94 | assert another_one_indentation_node == no_indentation_node.children[1] 95 | 96 | def test_adds_multiple_nodes_to_one(self): 97 | start = self._read_node("%div") 98 | one = self._read_node(" %div") 99 | two = self._read_node(" %div") 100 | three = self._read_node(" %div") 101 | 102 | start.add_node(one) 103 | start.add_node(two) 104 | start.add_node(three) 105 | 106 | assert len(start.children) == 3 107 | 108 | @staticmethod 109 | def _read_node(haml): 110 | return read_node(Stream(haml), None, Compiler()) 111 | -------------------------------------------------------------------------------- /hamlpy/test/test_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from hamlpy.parser.core import ( 4 | ParseException, 5 | Stream, 6 | peek_indentation, 7 | read_line, 8 | read_number, 9 | read_quoted_string, 10 | read_symbol, 11 | read_whitespace, 12 | read_word, 13 | ) 14 | from hamlpy.parser.utils import html_escape 15 | 16 | 17 | class ParserTest(unittest.TestCase): 18 | def test_read_whitespace(self): 19 | stream = Stream(" \t foo \n bar ") 20 | 21 | assert read_whitespace(stream) == " \t " 22 | assert stream.text[stream.ptr :] == "foo \n bar " 23 | 24 | stream.ptr += 3 # skip over foo 25 | 26 | assert read_whitespace(stream) == " " 27 | assert stream.text[stream.ptr :] == "\n bar " 28 | 29 | assert read_whitespace(stream, include_newlines=True) == "\n " 30 | assert stream.text[stream.ptr :] == "bar " 31 | 32 | stream.ptr += 3 # skip over bar 33 | 34 | assert read_whitespace(stream) == " " 35 | assert stream.text[stream.ptr :] == "" 36 | 37 | def test_peek_indentation(self): 38 | assert peek_indentation(Stream("content")) == 0 39 | assert peek_indentation(Stream(" content")) == 2 40 | assert peek_indentation(Stream("\n")) is None 41 | assert peek_indentation(Stream(" \n")) is None 42 | 43 | def test_quoted_string(self): 44 | stream = Stream("'hello'---") 45 | assert read_quoted_string(stream) == "hello" 46 | assert stream.text[stream.ptr :] == "---" 47 | 48 | stream = Stream('"this don\'t \\"x\\" hmm" not in string') 49 | assert read_quoted_string(stream) == 'this don\'t "x" hmm' 50 | assert stream.text[stream.ptr :] == " not in string" 51 | 52 | self.assertRaises(ParseException, read_quoted_string, Stream('"no end quote...')) 53 | 54 | def test_read_line(self): 55 | stream = Stream("line1\n line2\n\nline4\n\n") 56 | assert read_line(stream) == "line1" 57 | assert read_line(stream) == " line2" 58 | assert read_line(stream) == "" 59 | assert read_line(stream) == "line4" 60 | assert read_line(stream) == "" 61 | assert read_line(stream) is None 62 | 63 | assert read_line(Stream("last line ")) == "last line " 64 | 65 | def test_read_number(self): 66 | stream = Stream('123"') 67 | assert read_number(stream) == "123" 68 | assert stream.text[stream.ptr :] == '"' 69 | 70 | stream = Stream("123.4xx") 71 | assert read_number(stream) == "123.4" 72 | assert stream.text[stream.ptr :] == "xx" 73 | 74 | stream = Stream("0.0001 ") 75 | assert read_number(stream) == "0.0001" 76 | assert stream.text[stream.ptr :] == " " 77 | 78 | def test_read_symbol(self): 79 | stream = Stream("=> bar") 80 | assert read_symbol(stream, ["=>", ":"]) == "=>" 81 | assert stream.text[stream.ptr :] == " bar" 82 | 83 | self.assertRaises(ParseException, read_symbol, Stream("foo"), ["=>"]) 84 | 85 | def test_read_word(self): 86 | stream = Stream("foo_bar") 87 | assert read_word(stream) == "foo_bar" 88 | assert stream.text[stream.ptr :] == "" 89 | 90 | stream = Stream("foo_bar ") 91 | assert read_word(stream) == "foo_bar" 92 | assert stream.text[stream.ptr :] == " " 93 | 94 | stream = Stream("ng-repeat(") 95 | assert read_word(stream) == "ng" 96 | assert stream.text[stream.ptr :] == "-repeat(" 97 | 98 | stream = Stream("ng-repeat(") 99 | assert read_word(stream, ("-",)) == "ng-repeat" 100 | assert stream.text[stream.ptr :] == "(" 101 | 102 | stream = Stream("これはテストです...") 103 | assert read_word(stream) == "これはテストです" 104 | assert stream.text[stream.ptr :] == "..." 105 | 106 | 107 | class UtilsTest(unittest.TestCase): 108 | def test_html_escape(self): 109 | assert html_escape("") == "" 110 | assert html_escape("&<>\"'") == "&<>"'" 111 | assert html_escape('{% trans "hello" %}') == '{% trans "hello" %}' 112 | assert html_escape('{{ foo|default:"hello" }}') == '{{ foo|default:"hello" }}' 113 | assert html_escape("{% }} & %}") == "{% }} & %}" 114 | 115 | result = html_escape('<>{% trans "hello" %}<>{{ foo|default:"hello" }}<>') 116 | assert result == '<>{% trans "hello" %}<>{{ foo|default:"hello" }}<>' 117 | -------------------------------------------------------------------------------- /hamlpy/test/test_templates.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import time 4 | import unittest 5 | from os import listdir, path 6 | 7 | import regex 8 | 9 | from hamlpy.compiler import Compiler 10 | 11 | TEMPLATE_DIRECTORY = "/templates/" 12 | TEMPLATE_EXTENSION = ".hamlpy" 13 | 14 | 15 | class TemplateCheck(object): 16 | compiler = Compiler(options={"format": "xhtml", "escape_attrs": True}) 17 | 18 | def __init__(self, name, haml, html): 19 | self.name = name 20 | self.haml = haml 21 | self.expected_html = html.replace("\r", "") # ignore line ending differences 22 | self.actual_html = None 23 | 24 | @classmethod 25 | def load_all(cls): 26 | directory = os.path.dirname(__file__) + TEMPLATE_DIRECTORY 27 | tests = [] 28 | 29 | # load all test files 30 | for f in listdir(directory): 31 | haml_path = path.join(directory, f) 32 | if haml_path.endswith(TEMPLATE_EXTENSION): 33 | html_path = path.splitext(haml_path)[0] + ".html" 34 | 35 | haml = codecs.open(haml_path, encoding="utf-8").read() 36 | html = open(html_path, "r").read() 37 | 38 | tests.append(TemplateCheck(haml_path, haml, html)) 39 | 40 | return tests 41 | 42 | def run(self): 43 | parsed = self.compiler.process(self.haml) 44 | 45 | # ignore line ending differences and blank lines 46 | self.actual_html = parsed.replace("\r", "") 47 | self.actual_html = regex.sub("\n[ \t]+(?=\n)", "\n", self.actual_html) 48 | 49 | def passed(self): 50 | return self.actual_html == self.expected_html 51 | 52 | 53 | class TemplateCompareTest(unittest.TestCase): 54 | def test_templates(self): 55 | tests = TemplateCheck.load_all() 56 | 57 | for test in tests: 58 | print("Template test: " + test.name) 59 | test.run() 60 | 61 | if not test.passed(): 62 | print("\nHTML (actual): ") 63 | print("\n".join(["%d. %s" % (i + 1, line) for i, line in enumerate(test.actual_html.split("\n"))])) 64 | self._print_diff(test.actual_html, test.expected_html) 65 | self.fail() 66 | 67 | @staticmethod 68 | def _print_diff(s1, s2): 69 | if len(s1) > len(s2): 70 | shorter = s2 71 | else: 72 | shorter = s1 73 | 74 | line = 1 75 | col = 1 76 | 77 | for i, _ in enumerate(shorter): 78 | if len(shorter) <= i + 1: 79 | print("Ran out of characters to compare!") 80 | print("Actual len=%d" % len(s1)) 81 | print("Expected len=%d" % len(s2)) 82 | break 83 | if s1[i] != s2[i]: 84 | print("Difference begins at line", line, "column", col) 85 | actual_line = s1.splitlines()[line - 1] 86 | expected_line = s2.splitlines()[line - 1] 87 | print("HTML (actual, len=%2d) : %s" % (len(actual_line), actual_line)) 88 | print("HTML (expected, len=%2d) : %s" % (len(expected_line), expected_line)) 89 | print("Character code (actual) : %d (%s)" % (ord(s1[i]), s1[i])) 90 | print("Character code (expected): %d (%s)" % (ord(s2[i]), s2[i])) 91 | break 92 | 93 | if shorter[i] == "\n": 94 | line += 1 95 | col = 1 96 | else: 97 | col += 1 98 | else: 99 | print("No Difference Found") 100 | 101 | 102 | def performance_test(num_runs): 103 | """ 104 | Performance test which evaluates all the testing templates a given number of times 105 | """ 106 | print("Loading all templates...") 107 | 108 | template_tests = TemplateCheck.load_all() 109 | 110 | print("Running templates tests...") 111 | 112 | times = [] 113 | 114 | for r in range(num_runs): 115 | start = time.time() 116 | 117 | for test in template_tests: 118 | test.run() 119 | 120 | times.append(time.time() - start) 121 | 122 | print( 123 | "Ran template tests %d times in %.2f seconds (average = %.3f secs)" 124 | % (num_runs, sum(times), sum(times) / float(num_runs)) 125 | ) 126 | 127 | 128 | if __name__ == "__main__": 129 | performance_test(500) 130 | -------------------------------------------------------------------------------- /hamlpy/test/test_templatize.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase 2 | 3 | 4 | class TemplatizeTest(SimpleTestCase): 5 | def test_templatize(self): 6 | from django.utils.translation import templatize 7 | 8 | # test regular Django tags 9 | output = templatize('{% trans "Foo" %}{% blocktrans %}\nBar\n{% endblocktrans %}', origin="test.html") 10 | self.assertRegex(output, r"gettext\(u?'Foo'\)") 11 | self.assertRegex(output, r"gettext\(u?'\\nBar\\n'\)") 12 | 13 | # test Haml tags with HTML origin 14 | output = templatize('- trans "Foo"\n- blocktrans\n Bar\n', origin="test.haml") 15 | self.assertRegex(output, r"gettext\(u?'Foo'\)") 16 | self.assertRegex(output, r"gettext\(u?'\\n Bar\\n'\)") 17 | 18 | # test Haml tags and HTML origin 19 | self.assertNotIn("gettext", templatize('- trans "Foo"\n- blocktrans\n Bar\n', origin="test.html")) 20 | 21 | # test Haml tags and no origin 22 | self.assertNotIn("gettext", templatize('- trans "Foo"\n- blocktrans\n Bar\n')) 23 | -------------------------------------------------------------------------------- /hamlpy/test/test_views.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from hamlpy.views.generic import CreateView, DetailView, UpdateView 4 | 5 | 6 | class DummyCreateView(CreateView): 7 | template_name = "create.html" 8 | 9 | 10 | class DummyDetailView(DetailView): 11 | template_name = "detail.htm" 12 | 13 | 14 | class DummyUpdateView(UpdateView): 15 | template_name = "update.xml" 16 | 17 | 18 | class DjangoViewsTest(unittest.TestCase): 19 | def test_get_template_names(self): 20 | assert DummyCreateView().get_template_names() == ["create.haml", "create.hamlpy", "create.html"] 21 | assert DummyDetailView().get_template_names() == ["detail.haml", "detail.hamlpy", "detail.htm"] 22 | assert DummyUpdateView().get_template_names() == ["update.haml", "update.hamlpy", "update.xml"] 23 | -------------------------------------------------------------------------------- /hamlpy/test/test_watcher.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | import time 5 | import unittest 6 | from unittest.mock import patch 7 | 8 | from hamlpy.hamlpy_watcher import watch_folder 9 | 10 | WORKING_DIR = ".watcher_test" 11 | INPUT_DIR = WORKING_DIR + os.sep + "input" 12 | OUTPUT_DIR = WORKING_DIR + os.sep + "output" 13 | 14 | 15 | class ScriptExit(Exception): 16 | def __init__(self, exit_code): 17 | self.exit_code = exit_code 18 | 19 | 20 | class WatcherTest(unittest.TestCase): 21 | def test_watch_folder(self): 22 | # remove working directory if it exists and re-create it 23 | if os.path.exists(WORKING_DIR): 24 | shutil.rmtree(WORKING_DIR) 25 | 26 | os.makedirs(INPUT_DIR) 27 | 28 | # create some haml files for testing 29 | self._write_file(INPUT_DIR + os.sep + "test.haml", "%span{'class': 'test'}\n- shoutout\n") 30 | self._write_file(INPUT_DIR + os.sep + "error.haml", "%div{") 31 | self._write_file(INPUT_DIR + os.sep + ".#test.haml", "%hr") # will be ignored 32 | 33 | # run as once off pass - should return 1 for number of failed conversions 34 | self._run_script( 35 | [ 36 | "hamlpy_watcher.py", 37 | INPUT_DIR, 38 | OUTPUT_DIR, 39 | "--once", 40 | "--input-extension=haml", 41 | "--verbose", 42 | "--tag=shoutout:endshoutout", 43 | "--django-inline", 44 | "--jinja", 45 | ], 46 | 1, 47 | ) 48 | 49 | # check file without errors was converted 50 | self.assertFileContents( 51 | OUTPUT_DIR + os.sep + "test.html", "\n{% shoutout %}\n{% endshoutout %}\n" 52 | ) 53 | 54 | # check .#test.haml was ignored 55 | assert not os.path.exists(OUTPUT_DIR + os.sep + ".#test.html") 56 | 57 | # run without output directory which should make it default to re-using the input directory 58 | self._run_script(["hamlpy_watcher.py", INPUT_DIR, "--once", "--tag=shoutout:endshoutout"], 1) 59 | 60 | self.assertFileContents( 61 | INPUT_DIR + os.sep + "test.html", "\n{% shoutout %}\n{% endshoutout %}\n" 62 | ) 63 | 64 | # fix file with error 65 | self._write_file(INPUT_DIR + os.sep + "error.haml", "%div{}") 66 | 67 | # check exit code is zero 68 | self._run_script(["hamlpy_watcher.py", INPUT_DIR, OUTPUT_DIR, "--once"], 0) 69 | 70 | # run in watch mode with 1 second refresh 71 | self._run_script( 72 | ["hamlpy_watcher.py", INPUT_DIR, "--refresh=1", "--input-extension=haml", "--tag=shoutout:endshoutout"], 1 73 | ) 74 | 75 | def assertFileContents(self, path, contents): 76 | with open(path, "r") as f: 77 | self.assertEqual(f.read(), contents) 78 | 79 | def _write_file(self, path, text): 80 | with open(path, "w") as f: 81 | f.write(text) 82 | 83 | def _run_script(self, script_args, expected_exit_code): 84 | def raise_exception_with_code(code): 85 | raise ScriptExit(code) 86 | 87 | # patch sys.exit so it throws an exception so we can return execution to this test 88 | # patch sys.argv to pass our arguments to the script 89 | # patch time.sleep to be interrupted 90 | with patch.object(sys, "exit", side_effect=raise_exception_with_code), patch.object( 91 | sys, "argv", script_args 92 | ), patch.object(time, "sleep", side_effect=KeyboardInterrupt), self.assertRaises( 93 | ScriptExit 94 | ) as raises: # noqa 95 | watch_folder() 96 | 97 | assert raises.exception.exit_code == expected_exit_code 98 | -------------------------------------------------------------------------------- /hamlpy/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyaruka/django-hamlpy/59e87ee4fceb84adeabef8358a95572544683c7a/hamlpy/views/__init__.py -------------------------------------------------------------------------------- /hamlpy/views/generic/__init__.py: -------------------------------------------------------------------------------- 1 | import django.views.generic 2 | 3 | from hamlpy import HAML_EXTENSIONS 4 | 5 | pouet = [ 6 | "ArchiveIndexView", 7 | "YearArchiveView", 8 | "MonthArchiveView", 9 | "WeekArchiveView", 10 | "DayArchiveView", 11 | "TodayArchiveView", 12 | "DateDetailView", 13 | "DetailView", 14 | "CreateView", 15 | "UpdateView", 16 | "DeleteView", 17 | "ListView", 18 | ] 19 | 20 | NON_HAML_EXTENSIONS = ("html", "htm", "xml") 21 | 22 | 23 | class HamlExtensionTemplateView(object): 24 | def get_template_names(self): 25 | names = super(HamlExtensionTemplateView, self).get_template_names() 26 | 27 | haml_names = [] 28 | 29 | for name in names: 30 | for ext in NON_HAML_EXTENSIONS: 31 | if name.endswith("." + ext): 32 | base_name = name[: -len(ext)] 33 | 34 | for haml_ext in HAML_EXTENSIONS: 35 | haml_names.append(base_name + haml_ext) 36 | 37 | return haml_names + names 38 | 39 | 40 | for view in pouet: 41 | locals()[view] = type(str(view), (HamlExtensionTemplateView, getattr(django.views.generic, view)), {}) 42 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-hamlpy" 3 | version = "1.7.0" 4 | description = "HAML like syntax for Django templates" 5 | authors = ["Nyaruka "] 6 | readme = "README.md" 7 | license = "MIT" 8 | classifiers=[ 9 | "Environment :: Web Environment", 10 | "Intended Audience :: Developers", 11 | "License :: OSI Approved :: MIT License", 12 | "Operating System :: OS Independent", 13 | "Programming Language :: Python", 14 | "Framework :: Django", 15 | ] 16 | packages = [ 17 | { include = "hamlpy" }, 18 | ] 19 | 20 | [tool.poetry.urls] 21 | repository = "http://github.com/nyaruka/django-hamlpy" 22 | 23 | [tool.poetry.dependencies] 24 | python = "^3.9" 25 | django = ">=3.2,<5.0" 26 | regex = ">=2020.1.1" 27 | 28 | [tool.poetry.dev-dependencies] 29 | Markdown = "^3.4.1" 30 | pytest = "^6.1.2" 31 | pytest-cov = "^4.0.0" 32 | Pygments = "^2.14.0" 33 | Jinja2 = "^3.1.2" 34 | black = "^23.1.0" 35 | isort = "^5.11.0" 36 | 37 | [tool.poetry.scripts] 38 | hamlpy-watcher = "hamlpy.hamlpy_watcher:watch_folder" 39 | 40 | [tool.poetry.group.dev.dependencies] 41 | ruff = "^0.0.253" 42 | 43 | [tool.black] 44 | line-length = 119 45 | 46 | [tool.ruff] 47 | line-length = 120 48 | select = ["E", "F", "W"] 49 | ignore = ["E501", "F405"] 50 | fix = true 51 | exclude = ["./.tox/*", "./.venv/*", "./env/*", "*/migrations/*", "./build/*"] 52 | 53 | [tool.isort] 54 | multi_line_output = 3 55 | force_grid_wrap = 0 56 | line_length = 119 57 | include_trailing_comma = true 58 | combine_as_imports = true 59 | sections = ["FUTURE", "STDLIB", "THIRDPARTY", "DJANGO", "FIRSTPARTY", "LOCALFOLDER"] 60 | known_django = ["django"] 61 | 62 | [tool.coverage.run] 63 | source = ["hamlpy"] 64 | 65 | [tool.coverage.report] 66 | omit = ["*/migrations/*", "*/tests*", "*__init__*", "*settings*"] 67 | 68 | 69 | [build-system] 70 | requires = ["poetry-core>=1.0.0"] 71 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [coverage:run] 2 | source = hamlpy 3 | omit = hamlpy/test/* 4 | 5 | [flake8] 6 | max-line-length = 120 7 | exclude = ./env/*,./build/*,./dist/*,./docs/* 8 | ignore = E501,E203,W503 9 | 10 | [isort] 11 | multi_line_output = 3 12 | include_trailing_comma = True 13 | force_grid_wrap = 0 14 | line_length = 119 15 | combine_as_imports = True 16 | sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 17 | 18 | [wheel] 19 | universal = 1 20 | 21 | --------------------------------------------------------------------------------