├── .coveragerc ├── .flake8 ├── .github └── workflows │ └── lint_and_test.yml ├── .gitignore ├── CHANGELOG.textile ├── CONTRIBUTORS.txt ├── LICENSE.txt ├── Makefile ├── README.textile ├── TODO.textile ├── pyproject.toml ├── pytest.ini ├── tests ├── __init__.py ├── fixtures │ └── README.txt ├── test_attributes.py ├── test_block.py ├── test_cli.py ├── test_footnoteRef.py ├── test_getRefs.py ├── test_getimagesize.py ├── test_github_issues.py ├── test_glyphs.py ├── test_image.py ├── test_imagesize.py ├── test_lists.py ├── test_retrieve.py ├── test_span.py ├── test_subclassing.py ├── test_table.py ├── test_textile.py ├── test_textilefactory.py ├── test_urls.py ├── test_utils.py └── test_values.py └── textile ├── __init__.py ├── __main__.py ├── core.py ├── objects ├── __init__.py ├── block.py └── table.py ├── regex_strings.py ├── textilefactory.py ├── utils.py └── version.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = textile 4 | parallel = True 5 | 6 | [report] 7 | show_missing = True 8 | omit = 9 | textile/tests/* -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # line too long 4 | E501 5 | exclude = 6 | build/ 7 | -------------------------------------------------------------------------------- /.github/workflows/lint_and_test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: python-textile 3 | 4 | on: [push] 5 | 6 | jobs: 7 | lint_and_test: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"] 12 | image_size: ['true', 'false'] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Python flake8 Lint 20 | uses: py-actions/flake8@v2.3.0 21 | - name: Install dependencies 22 | run: | 23 | imagesize='' 24 | pip install -U pytest pytest-cov coverage codecov 25 | if [[ ${{ matrix.image_size }} == true ]] ; then imagesize='[imagesize]' ; fi 26 | pip install -e ".${imagesize}" 27 | - name: run tests 28 | run: | 29 | pytest 30 | - name: Codecov 31 | uses: codecov/codecov-action@v4 32 | env: 33 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.orig 3 | *.rej 4 | *~ 5 | *.pyo 6 | *.egg-info 7 | .cache/ 8 | .coverage 9 | .eggs/ 10 | .noseids* 11 | .pytest_cache 12 | docs/build 13 | docs/coverage 14 | build 15 | bin 16 | dist 17 | eggs 18 | htmlcov 19 | parts 20 | develop-eggs 21 | .DS_Store 22 | *.swp 23 | .tox 24 | README.txt 25 | -------------------------------------------------------------------------------- /CHANGELOG.textile: -------------------------------------------------------------------------------- 1 | h1. Textile Changelog 2 | 3 | h2. Version 4.0.3 4 | * Update supported Python versions to 3.8 - 3.12 ("#83":https://github.com/textile/python-textile/issues/83) 5 | * Replace html5lib with nh3 for html sanitization 6 | * General code cleanup 7 | * Bugfixes: 8 | ** Wrong HTML output when "bc.." is the very last in the document ("#81":https://github.com/textile/python-textile/issues/81) 9 | * Other: 10 | ** Use github actions instead of travis for automated testing 11 | 12 | h2. Version 4.0.2 13 | * Bugfixes: 14 | ** Support non-http schemas in url refs ("#75":https://github.com/textile/python-textile/pull/75) 15 | ** pytest-runner is deprecated ("#77":https://github.com/textile/python-textile/issues/77) 16 | *** other changes related to CI infrastructure 17 | 18 | h2. Version 4.0.1 19 | * Bugfixes: 20 | ** SyntaxWarnings with Python 3.8 i("#71":https://github.com/textile/python-textile/issues/71) 21 | ** testsuite: internal error with coverage 5.0.X ("#72":https://github.com/textile/python-textile/issues/72) 22 | ** DeprecationWarnings about invalid escape sequences ("#73":https://github.com/textile/python-textile/issues/73) 23 | 24 | h2. Version 4.0.0 25 | * Drop support for Python 2, hence the version bump. Update list of PY3K versions to currently-supported versions. If you need to use textile on Python 2.7 or Python 3.3 or 3.4, please use textile Version 3.0.4. 26 | * For use in PyPy environments, textile used to work well with the regex package. Lately, it's running into trouble. Please uninstall regex if this is the case for you. 27 | 28 | h2. Version 3.0.4 29 | * BUGFIX: Restricted mode strips out CSS attributes again. 30 | * Update travis to more current versions and test against current Pillow version. 31 | 32 | h2. Version 3.0.3 33 | * BUGFIX: Improve handling code block following extended p block ("#63":https://github.com/textile/python-textile/pull/63) 34 | 35 | h2. Version 3.0.2 36 | * BUGFIX: Fix for multiple multi-line paragraphs. ("#62":https://github.com/textile/python-textile/pull/62) 37 | 38 | h2. Version 3.0.1 39 | * BUGFIX: Fix improper handling of extended code blocks. ("#61":https://github.com/textile/python-textile/pull/61) 40 | 41 | h2. Version 3.0.0 42 | * Drop support for Python 2.6 and 3.2. 43 | * Update to the current version of html5lib 44 | * Bugfixes: 45 | ** Fix handling of HTML entities in extended pre blocks. ("#55":https://github.com/textile/python-textile/issues/55) 46 | ** Empty definitions in definition lists raised an exception ("#56":https://github.com/textile/python-textile/issues/56) 47 | ** Fix handling of unicode in img attributes ("#58":https://github.com/textile/python-textile/issues/58) 48 | 49 | h2. Version 2.3.16 50 | * Bugfixes: 51 | ** Fix processing of extended code blocks ("#50":https://github.com/textile/python-textile/issues/50) 52 | ** Don't break when links fail to include "http:" ("#51":https://github.com/textile/python-textile/issues/51) 53 | ** Better handling of poorly-formatted tables ("#52":https://github.com/textile/python-textile/issues/52) 54 | 55 | h2. Version 2.3.15 56 | * Bugfix: Don't break on unicode characters in the fragment of a url. 57 | 58 | h2. Version 2.3.14 59 | * Bugfix: Fix textile on Python 2.6 ("#48":https://github.com/textile/python-textile/issues/48) 60 | 61 | h2. Version 2.3.13 62 | * Remove extraneous arguments from textile method. These were originally added long ago to work with django, but markup languages are long gone from django. 63 | * Bugfix: Don't mangle percent-encoded URLs so much. ("#45":https://github.com/textile/python-textile/issues/45) 64 | * Bugfix: More fixes for poorly-formatted lists. ("#46":https://github.com/textile/python-textile/issues/46) 65 | * Bugfix: Improve handling of whitespace in pre-formatted blocks. This now matches php-textile's handling of pre blocks much more closely. ("#47":https://github.com/textile/python-textile/issues/47) 66 | 67 | h2. Version 2.3.12 68 | * Bugfix: Don't die on pre blocks with unicode characters. ("#43":https://github.com/textile/python-textile/issues/43) 69 | * Bugfix: Fix regressions introduced into the code between 2.2.2 and 2.3.11. (Special thanks to "@adam-iris":https://github.com/adam-iris for providing pull request "#44":https://github.com/textile/python-textile/pull/44) 70 | * Bugfix: Don't just die when processing poorly-formatted textile lists. ("#37":https://github.com/textile/python-textile/issues/37) 71 | * Add Python 3.6 to testing. 72 | * Add a "print the version string and exit" argument to the cli tool: @pytextile -v@ 73 | 74 | h2. Version 2.3.11 75 | * Bugfix: Don't strip leading dot from image URIs ("#42":https://github.com/textile/python-textile/issues/42) 76 | 77 | h2. Version 2.3.10 78 | * Packaging: cleanup in MANIFEST.IN leads to better linux packaging, and smaller wheel size. 79 | 80 | h2. Version 2.3.9 81 | * Packaging: remove extraneous files from the source distribution upload. 82 | * Remove a lingering file from a feature branch for overhauling list handling. This brings coverage back up to 100% 83 | 84 | h2. Version 2.3.8 85 | * Bugfix: Fix process of string containing only whitespaces ("#40":https://github.com/textile/python-textile/issues/40) 86 | * Bugfix: Fix process of formatted text after lists ("#37":https://github.com/textile/python-textile/issues/37) 87 | * Test: Use sys.executable instead of 'python' to test the CLI ("#38":https://github.com/textile/python-textile/issues/38) 88 | 89 | h2. Version 2.3.7 90 | * Bugfix: Don't assume pytest is available to be imported in setup.py ("#39":https://github.com/textile/python-textile/issues/39) 91 | 92 | h2. Version 2.3.6 93 | * Packaging: @tests@ directory is correctly included in source-tarball. ("#33":https://github.com/textile/python-textile/issues/33) 94 | 95 | h2. Version 2.3.5 96 | * Bugfix: Correctly handle unicode text in url query-strings. ("#36":https://github.com/textile/python-textile/issues/36) 97 | 98 | h2. Version 2.3.4 99 | * Bugfix: fix an issue with extended block code 100 | * Remove misplaced shebang on non-callable files. 101 | * Packaging: Add test-command to setup.py directly. 102 | * Packaging: Included the tests/ directory for source-tarballs, useful for packaging checks. ("#33":https://github.com/textile/python-textile/issues/33) 103 | * Add a cli tool @pytextile@ which takes textile input and prints html output. See @pytextile -h@ for details. 104 | 105 | h2. Version 2.3.3 106 | * Bugfix: Unicode in URL titles no longer break everything ("#30":https://github.com/textile/python-textile/issues/30) 107 | * Display DeprecationWarning when using textile on Python 2.6. 108 | 109 | h2. Version 2.3.2 110 | * Bugfix: properly handle @":"@ as text, not a link. 111 | 112 | h2. Version 2.3.1 113 | * Regression bugfix: empty string input returns empty string again. 114 | 115 | h2. Version 2.3.0 116 | 117 | * Bugfixes: 118 | ** Support data URIs in img tags 119 | ** Fix autolink urls with image references ("#17":https://github.com/textile/python-textile/issues/17) 120 | ** Fix textile links containing parentheses ("#20":https://github.com/textile/python-textile/issues/20) 121 | ** Fix double-encoding of code blocks ("#21":https://github.com/textile/python-textile/issues/21) 122 | ** Fix handling of scheme in self-linked URLs ("#16":https://github.com/textile/python-textile/issues/16) 123 | ** Fix Markup not parsed if followed by certain characters ("#22":Markup not parsed if followed by certain characters) 124 | * Convert testing over to "py.test":http://pytest.org/, improving unicode testing 125 | * Update functionality for tables, notelists, and footnotes. This involved a major reworking of parts of the code, but it should now match php-textile and txstyle.org precisely. Please file an issue for any bugs you come across. 126 | * Remove @head_offset@ option from parse. I'm not sure it ever existed in php-textile. 127 | 128 | h2. Version 2.2.2 129 | 130 | * bugfix: "regex":https://pypi.python.org/pypi/regex is now an optional dependency 131 | 132 | h2. Version 2.2.1 133 | 134 | * drop textilefactory support for html. 135 | * Various development-related bugfixes. 136 | * Added this changelog. 137 | 138 | h2. Version 2.2.0 139 | 140 | * Started refactoring the code to be less repetitive. @textile.Textile().parse()@ is a little more friendly than @textile.Textile().textile()@ There may be more work to be done on this front to make the flow a little smoother. 141 | * We now support versions 2.6 - 3.4 (including 3.2) using the same codebase. Many thanks to Radek Czajka for this. 142 | * Drop support for html4. We now only output xhtml or html5. 143 | * Various development-related bugfixes. 144 | 145 | h2. Version 2.1.8 146 | 147 | * Add support for html5 output. 148 | * Lots of new functionality added bringing us in line with the official Textile 2.4 149 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Dennis Burke 2 | Radek Czajka 3 | Roberto A. F. De Almeida 4 | Matt Layman 5 | Mark Pilgrim 6 | Alex Shiels 7 | Jason Samsa 8 | Kurt Raschke 9 | Dave Brondsema 10 | Dmitry Shachnev 11 | Kirill Mavreshko 12 | Brad Schoening -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | L I C E N S E 2 | ============= 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | * Neither the name Textile nor the names of its contributors may be used to 14 | endorse or promote products derived from this software without specific 15 | prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | $(RM) README.txt 3 | $(RM) -r ./dist ./build 4 | 5 | generate_pypi_README: 6 | ${VIRTUAL_ENV}/bin/pytextile README.textile | sed -e 's/^\t//' > README.txt 7 | 8 | build: generate_pypi_README 9 | python -m build 10 | 11 | upload_to_test: build 12 | twine check ./dist/* 13 | twine upload --repository test_textile ./dist/* 14 | 15 | upload_to_prod: build 16 | twine check ./dist/* 17 | # for now, don't actually upload to prod PyPI, just output the command to do so. 18 | @echo "twine upload --repository textile ./dist/*" 19 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | !https://github.com/textile/python-textile/actions/workflows/lint_and_test.yml/badge.svg(python-textile)!:https://github.com/textile/python-textile/actions/workflows/lint_and_test.yml !https://codecov.io/github/textile/python-textile/coverage.svg!:https://codecov.io/github/textile/python-textile !https://img.shields.io/pypi/pyversions/textile! !https://img.shields.io/pypi/wheel/textile! 2 | 3 | h1. python-textile 4 | 5 | python-textile is a Python port of "Textile":https://textile-lang.com/, Dean Allen's humane web text generator. 6 | 7 | h2. Installation 8 | 9 | @pip install textile@ 10 | 11 | Dependencies: 12 | * "nh3":https://pypi.org/project/nh3/ 13 | * "regex":https://pypi.org/project/regex/ (The regex package causes problems with PyPy, and is not installed as a dependency in such environments. If you are upgrading a textile install on PyPy which had regex previously included, you may need to uninstall it.) 14 | 15 | Optional dependencies include: 16 | * "PIL/Pillow":http://python-pillow.github.io/ (for checking image sizes). If needed, install via @pip install 'textile[imagesize]'@ 17 | 18 | h2. Usage 19 | 20 | bc.. import textile 21 | >>> s = """ 22 | ... _This_ is a *test.* 23 | ... 24 | ... * One 25 | ... * Two 26 | ... * Three 27 | ... 28 | ... Link to "Slashdot":http://slashdot.org/ 29 | ... """ 30 | >>> html = textile.textile(s) 31 | >>> print html 32 |
This is a test.
33 | 34 |Link to Slashdot
41 | >>> 42 | 43 | h3. Notes: 44 | 45 | * Active development supports Python 3.8 or later. 46 | 47 | h3. Running Tests 48 | 49 | To run the test suite, use pytest. `pytest-cov` is required as well. 50 | 51 | When textile is installed locally: 52 | 53 | bc. pytest 54 | 55 | When textile is not installed locally: 56 | 57 | bc. PYTHONPATH=. pytest 58 | -------------------------------------------------------------------------------- /TODO.textile: -------------------------------------------------------------------------------- 1 | TODO 2 | 3 | * Improve documentation, both of the code and Textile syntax. 4 | ** Not all functions have docstrings or adequate docstrings. 5 | ** Because the Textile syntax implemented by PyTextile has deviated from the syntax implemented by other implementations of Textile, PyTextile-specific documentation needs to be produced for end-users. 6 | * Update to comply with Textile 2.5 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm", "nh3"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "textile" 7 | authors = [ 8 | { name = "Dennis Burke", email = "ikirudennis@gmail.com"} 9 | ] 10 | description = 'Textile processing for python.' 11 | classifiers = [ 12 | 'Development Status :: 5 - Production/Stable', 13 | 'Environment :: Web Environment', 14 | 'Intended Audience :: Developers', 15 | 'License :: OSI Approved :: BSD License', 16 | 'Operating System :: OS Independent', 17 | 'Programming Language :: Python', 18 | 'Programming Language :: Python :: 3', 19 | 'Programming Language :: Python :: 3 :: Only', 20 | 'Programming Language :: Python :: 3.8', 21 | 'Programming Language :: Python :: 3.9', 22 | 'Programming Language :: Python :: 3.10', 23 | 'Programming Language :: Python :: 3.11', 24 | 'Programming Language :: Python :: 3.12', 25 | 'Topic :: Software Development :: Libraries :: Python Modules', 26 | ] 27 | dynamic = ["version",] 28 | dependencies = [ 29 | 'nh3', 30 | 'regex>1.0; implementation_name != "pypy"', 31 | ] 32 | requires-python = '>=3.8' 33 | keywords = ['textile', 'text', 'html markup'] 34 | # Use the following command to generate a README.txt which is compatible with 35 | # pypi's readme rendering: 36 | # pytextile README.textile | sed -e 's/^\t//' > README.txt 37 | readme = {file = 'README.txt', content-type = 'text/markdown'} 38 | 39 | [project.optional-dependencies] 40 | develop = ['pytest', 'pytest-cov'] 41 | imagesize = ['Pillow>=3.0.0',] 42 | 43 | [project.urls] 44 | Homepage = "https://github.com/textile/python-textile" 45 | Repository = "https://github.com/textile/python-textile.git" 46 | Issues = "https://github.com/textile/python-textile/issues" 47 | 48 | [project.scripts] 49 | pytextile = "textile.__main__:main" 50 | 51 | [tool.setuptools.dynamic] 52 | version = {attr = "textile.__version__"} 53 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | addopts = --cov=textile --cov-report=html --cov-append --cov-report=term-missing 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textile/python-textile/a1c31e525dfdb1745ae8d09d5a08323ef579f414/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/README.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 |python-textile is a Python port of Textile, Dean Allen’s humane web text generator.
6 | 7 |pip install textile
Dependencies: 12 |
Optional dependencies include: 18 |
pip install 'textile[imagesize]'
import textile
25 | >>> s = """
26 | ... _This_ is a *test.*
27 | ...
28 | ... * One
29 | ... * Two
30 | ... * Three
31 | ...
32 | ... Link to "Slashdot":http://slashdot.org/
33 | ... """
34 | >>> html = textile.textile(s)
35 | >>> print html
36 | <p><em>This</em> is a <strong>test.</strong></p>
37 |
38 | <ul>
39 | <li>One</li>
40 | <li>Two</li>
41 | <li>Three</li>
42 | </ul>
43 |
44 | <p>Link to <a href="http://slashdot.org/">Slashdot</a></p>
45 | >>>
46 |
47 | To run the test suite, use pytest. `pytest-cov` is required as well.
56 | 57 |When textile is installed locally:
58 | 59 |pytest
60 |
61 | When textile is not installed locally:
62 | 63 |PYTHONPATH=. pytest
--------------------------------------------------------------------------------
/tests/test_attributes.py:
--------------------------------------------------------------------------------
1 | from typing import OrderedDict
2 | from textile.utils import parse_attributes
3 |
4 |
5 | def test_parse_attributes():
6 | assert parse_attributes('\\1', element='td') == {'colspan': '1'}
7 | assert parse_attributes('/1', element='td') == {'rowspan': '1'}
8 | assert parse_attributes('^', element='td') == {'style': 'vertical-align:top;'}
9 | assert parse_attributes('{color: blue}') == {'style': 'color: blue;'}
10 | assert parse_attributes('[en]') == {'lang': 'en'}
11 | assert parse_attributes('(cssclass)') == {'class': 'cssclass'}
12 | assert parse_attributes('(') == {'style': 'padding-left:1em;'}
13 | assert parse_attributes(')') == {'style': 'padding-right:1em;'}
14 | assert parse_attributes('<') == {'style': 'text-align:left;'}
15 | assert parse_attributes('(c#i)') == {'class': 'c', 'id': 'i'}
16 | assert parse_attributes('\\2 100', element='col') == {'span': '2', 'width': '100'}
17 |
18 |
19 | def test_parse_attributes_edge_cases():
20 | result = parse_attributes('(:c#i)')
21 | expect = OrderedDict({'id': 'i'})
22 | assert result == expect
23 |
24 | assert parse_attributes('(<)') == OrderedDict()
25 |
--------------------------------------------------------------------------------
/tests/test_block.py:
--------------------------------------------------------------------------------
1 | import textile
2 | from textile.objects import Block
3 |
4 | try:
5 | from collections import OrderedDict
6 | except ImportError:
7 | from ordereddict import OrderedDict
8 |
9 |
10 | def test_block():
11 | t = textile.Textile()
12 | result = t.block('h1. foobar baby')
13 | expect = '\ttext\nmoretext\n\nevenmoretext\n\nmoremoretext
\n\n\ttest
' 54 | t = textile.Textile() 55 | result = t.parse(input) 56 | assert result == expect 57 | 58 | 59 | def test_blockcode_in_README(): 60 | with open('README.textile') as f: 61 | readme = ''.join(f.readlines()) 62 | result = textile.textile(readme) 63 | with open('tests/fixtures/README.txt') as f: 64 | expect = ''.join(f.readlines()) 65 | assert result == expect 66 | 67 | 68 | def test_blockcode_comment(): 69 | input = '###.. block comment\nanother line\n\np. New line' 70 | expect = '\tNew line
' 71 | t = textile.Textile() 72 | result = t.parse(input) 73 | assert result == expect 74 | 75 | 76 | def test_extended_pre_block_with_many_newlines(): 77 | """Extra newlines in an extended pre block should not get cut down to only 78 | two.""" 79 | text = '''pre.. word 80 | 81 | another 82 | 83 | word 84 | 85 | 86 | yet anothe word''' 87 | expect = '''word 88 | 89 | another 90 | 91 | word 92 | 93 | 94 | yet anothe word''' 95 | result = textile.textile(text) 96 | assert result == expect 97 | 98 | text = 'p. text text\n\n\nh1. Hello\n' 99 | expect = '\t
text text
\n\n\n\tgoogle.com google.com blackhole@sun.comet
' 8 | assert result == expect 9 | 10 | 11 | def test_github_issue_17(): 12 | result = textile.textile('!http://www.ox.ac.uk/favicon.ico!') 13 | expect = '\tThis is a link to a Wikipedia article about Textile.
' 21 | assert result == expect 22 | 23 | 24 | def test_github_issue_21(): 25 | text = ('''h1. xml example 26 | 27 | bc. ''' 28 | ''' 29 |\n<foo>\n bar\n</foo>
'
34 | assert result == expect
35 |
36 |
37 | def test_github_issue_22():
38 | text = '''_(artist-name)Ty Segall_’s'''
39 | result = textile.textile(text)
40 | expect = '\tTy Segall’s
' 41 | assert result == expect 42 | 43 | 44 | def test_github_issue_26(): 45 | text = '' 46 | result = textile.textile(text) 47 | expect = '' 48 | assert result == expect 49 | 50 | 51 | def test_github_issue_27(): 52 | test = """* Folders with ":" in their names are displayed with a forward slash "/" instead. (Filed as "#4581709":/test/link, which was considered "normal behaviour" - quote: "Please note that Finder presents the 'Carbon filesystem' view, regardless of the underlying filesystem.")""" 53 | result = textile.textile(test) 54 | expect = """\t62 | def parseWapProfile(self, url): 63 | result = fetch.fetchURL(url) 64 | soup = BeautifulStoneSoup(result['data'], convertEntities=BeautifulStoneSoup.HTML_ENTITIES) 65 | try: 66 | width, height = soup('prf:screensize')[0].contents[0].split('x') 67 | except: 68 | width = height = None 69 | return {"width": width, "height": height} 70 |71 | 72 | Of course there's a lot more error handling to do (and useful data to glean off the "XML":XML), but being able to cut through all the usual parsing crap is immensely gratifying.""" 73 | result = textile.textile(test) 74 | expect = ("""\t
So here I am porting my ancient newspipe front-end to Snakelets and Python, and I’ve just trimmed down over 20 lines of PHP down to essentially one line of BeautifulSoup retrieval:
75 | 76 |77 | def parseWapProfile(self, url): 78 | result = fetch.fetchURL(url) 79 | soup = BeautifulStoneSoup(result['data'], convertEntities=BeautifulStoneSoup.HTML_ENTITIES) 80 | try: 81 | width, height = soup('prf:screensize')[0].contents[0].split('x') 82 | except: 83 | width = height = None 84 | return {"width": width, "height": height} 85 |86 | 87 | \t
Of course there’s a lot more error handling to do (and useful data to glean off the XML), but being able to cut through all the usual parsing crap is immensely gratifying.
""") 88 | assert result == expect 89 | 90 | 91 | def test_github_issue_30(): 92 | text = '"Tëxtíle (Tëxtíle)":http://lala.com' 93 | result = textile.textile(text) 94 | expect = '\t' 95 | assert result == expect 96 | 97 | text = '!http://lala.com/lol.gif(♡ imáges)!' 98 | result = textile.textile(text) 99 | expect = '\t\t
Highlights
119 | 120 | \tsmart ‘quotes’ are not smart!' 150 | assert result == expect 151 | 152 | 153 | def test_github_issue_45(): 154 | """Incorrect transform unicode url""" 155 | text = '"test":https://myabstractwiki.ru/index.php/%D0%97%D0%B0%D0%B3%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0' 156 | result = textile.textile(text) 157 | expect = '\t' 158 | assert result == expect 159 | 160 | 161 | def test_github_issue_46(): 162 | """Key error on mal-formed numbered lists. CAUTION: both the input and the 163 | ouput are ugly.""" 164 | text = '# test\n### test\n## test' 165 | expect = ('\t
word 183 | 184 | another 185 | 186 | word 187 | 188 | yet anothe word''' 189 | assert result == expect 190 | 191 | 192 | def test_github_issue_49(): 193 | """Key error on russian hash-route link""" 194 | s = '"link":https://ru.vuejs.org/v2/guide/components.html#Входные-параметры' 195 | result = textile.textile(s) 196 | expect = '\t' 197 | assert result == expect 198 | 199 | 200 | def test_github_issue_50(): 201 | """Incorrect wrap code with Java generics in pre""" 202 | test = ('pre.. public class Tynopet
public class Tynopet<T extends Framework> {}\n\n' 206 | 'final List<List<String>> multipleList = new ' 207 | 'ArrayList<>();') 208 | assert result == expect 209 | 210 | 211 | def test_github_issue_51(): 212 | """Link build with $ sign without "http" prefix broken.""" 213 | test = '"$":www.google.com.br' 214 | result = textile.textile(test) 215 | expect = '\t' 216 | assert result == expect 217 | 218 | 219 | def test_github_issue_52(): 220 | """Table build without space after aligment raise a AttributeError.""" 221 | test = '|=.First Header |=. Second Header |' 222 | result = textile.textile(test) 223 | expect = ('\t
=.First Header ' 224 | ' | \n\t\t\tSecond Header | ' 225 | '\n\t\t
this is the first line\n\nbut "quotes" in an ' 235 | 'extended pre block need to be handled properly.') 236 | assert result == expect 237 | 238 | # supplied input 239 | test = ('pre.. import org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;' 240 | '\nimport ru.onyma.job.Context;\nimport ru.onyma.job.' 241 | 'RescheduleTask;\n\nimport java.util.concurrent.' 242 | 'ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;' 243 | '\n\n/**\n* @author ustits\n*/\npublic abstract class ' 244 | 'MainService
import org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;' 260 | '\nimport ru.onyma.job.Context;\nimport ru.onyma.job.' 261 | 'RescheduleTask;\n\nimport java.util.concurrent.' 262 | 'ScheduledExecutorService;\nimport java.util.concurrent.' 263 | 'TimeUnit;\n\n/**\n* @author ustits\n*/\npublic abstract class ' 264 | 'MainService<T> extends RescheduleTask implements ' 265 | 'Context<T> {\n\nprivate static final Logger log = ' 266 | 'LoggerFactory.getLogger(MainService.class);\nprivate final ' 267 | 'ScheduledExecutorService scheduler;\n\nprivate boolean ' 268 | 'isFirstRun = true;\nprivate T configs;\n\npublic MainService(' 269 | 'final ScheduledExecutorService scheduler) {\nsuper(scheduler);' 270 | '\nthis.scheduler = scheduler;\n}\n\n@Override\npublic void ' 271 | 'setConfig(final T configs) {\nthis.configs = configs;\nif (' 272 | 'isFirstRun) {\nscheduler.schedule(this, 0, TimeUnit.SECONDS);' 273 | '\nisFirstRun = false;\n}\n}\n\n@Override\npublic void stop() {' 274 | '\nsuper.stop();\nscheduler.shutdown();\ntry {\nscheduler.' 275 | 'awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);\n} catch ' 276 | '(InterruptedException ie) {\nlog.warn("Unable to wait ' 277 | 'for syncs termination", ie);\nThread.currentThread().' 278 | 'interrupt();\n}\n}\n\nprotected final T getConfigs() {\n' 279 | 'return configs;\n}\n}') 280 | assert result == expect 281 | 282 | 283 | def test_github_issue_56(): 284 | """Empty description lists throw error""" 285 | result = textile.textile("- :=\n-") 286 | expect = '
This is some TEXT inside a "Code BLOCK"
309 |
310 | {
311 | if (JSON) {
312 |
313 | return {"JSON":"value"}
314 | }
315 | }
316 |
317 | Back to 10-4 CAPS
318 |
319 | Some multiline Paragragh
320 | 321 |Here is some output!!! “Some” CAPS
''' 322 | t = textile.Textile() 323 | result = t.parse(test) 324 | assert result == expect 325 | 326 | 327 | def test_github_pull_62(): 328 | """Fix for paragraph multiline, only last paragraph is rendered 329 | correctly""" 330 | test = '''p.. First one 'is' 331 | 332 | ESCAPED "bad" 333 | 334 | p.. Second one 'is' 335 | 336 | 337 | 338 | ESCAPED "bad" 339 | 340 | p.. Third one 'is' 341 | 342 | ESCAPED "bad" 343 | 344 | p.. Last one 'is' 345 | 346 | ESCAPED "good" test''' 347 | 348 | expect = '''First one ‘is’
349 | 350 |ESCAPED “bad”
351 | 352 |Second one ‘is’
353 | 354 | 355 | 356 |ESCAPED “bad”
357 | 358 |Third one ‘is’
359 | 360 |ESCAPED “bad”
361 | 362 |Last one ‘is’
363 | 364 |ESCAPED “good” test
''' 365 | t = textile.Textile() 366 | result = t.parse(test) 367 | assert result == expect 368 | 369 | 370 | def test_github_pull_63(): 371 | """Forgot to set multiline_para to False""" 372 | test = '''p.. First one 'is' 373 | 374 | ESCAPED "bad" 375 | 376 | bc.. { 377 | First code BLOCK 378 | 379 | {"JSON":'value'} 380 | } 381 | 382 | p.. Second one 'is' 383 | 384 | 385 | 386 | ESCAPED "bad" 387 | 388 | p.. Third one 'is' 389 | 390 | ESCAPED "bad" 391 | 392 | bc.. { 393 | Last code BLOCK 394 | 395 | {"JSON":'value'} 396 | } 397 | 398 | p.. Last one 'is' 399 | 400 | ESCAPED "good" test''' 401 | 402 | expect = '''First one ‘is’
403 | 404 |ESCAPED “bad”
405 | 406 |{
407 | First code BLOCK
408 |
409 | {"JSON":'value'}
410 | }
411 |
412 | Second one ‘is’
413 | 414 | 415 | 416 |ESCAPED “bad”
417 | 418 |Third one ‘is’
419 | 420 |ESCAPED “bad”
421 | 422 |{
423 | Last code BLOCK
424 |
425 | {"JSON":'value'}
426 | }
427 |
428 | Last one ‘is’
429 | 430 |ESCAPED “good” test
''' 431 | t = textile.Textile() 432 | result = t.parse(test) 433 | assert result == expect 434 | -------------------------------------------------------------------------------- /tests/test_glyphs.py: -------------------------------------------------------------------------------- 1 | from textile import Textile 2 | 3 | 4 | def test_glyphs(): 5 | t = Textile() 6 | 7 | result = t.glyphs("apostrophe's") 8 | expect = 'apostrophe’s' 9 | assert result == expect 10 | 11 | result = t.glyphs("back in '88") 12 | expect = 'back in ’88' 13 | assert result == expect 14 | 15 | result = t.glyphs('foo ...') 16 | expect = 'foo …' 17 | assert result == expect 18 | 19 | result = t.glyphs('--') 20 | expect = '—' 21 | assert result == expect 22 | 23 | result = t.glyphs('FooBar[tm]') 24 | expect = 'FooBar™' 25 | assert result == expect 26 | 27 | result = t.glyphs("Cat's Cradle by Vonnegut
") 28 | expect = 'Cat’s Cradle by Vonnegut
' 29 | assert result == expect 30 | -------------------------------------------------------------------------------- /tests/test_image.py: -------------------------------------------------------------------------------- 1 | from textile import Textile 2 | 3 | 4 | def test_image(): 5 | t = Textile() 6 | result = t.image('!/imgs/myphoto.jpg!:http://jsamsa.com') 7 | expect = ('Test „quotes”.
' 11 | result = TextilePL().parse(test) 12 | assert expect == result 13 | 14 | # Base Textile is unchanged. 15 | expect = '\tTest “quotes”.
' 16 | result = textile.textile(test) 17 | assert expect == result 18 | -------------------------------------------------------------------------------- /tests/test_table.py: -------------------------------------------------------------------------------- 1 | from textile import Textile 2 | 3 | 4 | def test_table(): 5 | t = Textile() 6 | result = t.table('(rowclass). |one|two|three|\n|a|b|c|') 7 | expect = '\tone | \n\t\t\ttwo | \n\t\t\tthree | \n\t\t
a | \n\t\t\tb | \n\t\t\tc | \n\t\t
one | \n\t\t\ttwo | \n\t\t\tthree | \n\t\t
a | \n\t\t\tb | \n\t\t\tc | \n\t\t
* test\n* test | \n\t\t\t1 | \n\t\t\t2 | \n\t\t
YACC1
', html) is not None 10 | 11 | 12 | def test_Footnote(): 13 | html = textile.textile('This is covered elsewhere[1].\n\nfn1. Down here, in fact.\n\nfn2. Here is another footnote.') 14 | assert re.search(r'^\tThis is covered elsewhere1.
\n\n\t1 Down here, in fact.
\n\n\t2 Here is another footnote.
$', html) is not None 15 | 16 | html = textile.textile('''See[1] for details -- or perhaps[100] at a push.\n\nfn1. Here are the details.\n\nfn100(footy#otherid). A totally unrelated footnote.''') 17 | assert re.search(r'^\tSee1 for details — or perhaps100 at a push.
\n\n\t1 Here are the details.
\n\n\t100 A totally unrelated footnote.
$', html) is not None 18 | 19 | html = textile.textile('''See[2] for details, and later, reference it again[2].\n\nfn2^(footy#otherid)[en]. Here are the details.''') 20 | assert re.search(r'^\tSee2 for details, and later, reference it again2.
\n\n\t2 Here are the details.
$', html) is not None 21 | 22 | html = textile.textile('''See[3!] for details.\n\nfn3. Here are the details.''') 23 | assert re.search(r'^\tSee3 for details.
\n\n\t3 Here are the details.
$', html) is not None 24 | 25 | html = textile.textile('''See[4!] for details.\n\nfn4^. Here are the details.''') 26 | assert re.search(r'^\tSee4 for details.
\n\n\t4 Here are the details.
$', html) is not None 27 | 28 | 29 | def test_issue_35(): 30 | result = textile.textile('"z"') 31 | expect = '\t“z”
' 32 | assert result == expect 33 | 34 | result = textile.textile('" z"') 35 | expect = '\t“ z”
' 36 | assert result == expect 37 | 38 | 39 | def test_restricted(): 40 | # Note that the HTML is escaped, thus rendering the " 42 | result = textile.textile_restricted(test) 43 | expect = "\tHere is some text.
\n<script>alert(‘hello world’)</script>
Here’s some <!— commented out —> text.
" 50 | 51 | assert result == expect 52 | 53 | test = "p[fr]. Partir, c'est toujours mourir un peu." 54 | result = textile.textile_restricted(test) 55 | expect = '\tPartir, c’est toujours mourir un peu.
' 56 | 57 | assert result == expect 58 | 59 | test = "p{color:blue}. is this blue?" 60 | result = textile.textile_restricted(test) 61 | expect = '\tis this blue?
' 62 | 63 | assert result == expect 64 | 65 | test = """\ 66 | table{border:1px solid black}. 67 | |={color:gray}. Your caption goes here 68 | |~. 69 | |{position:absolute}. A footer | foo | 70 | |-. 71 | |_{font-size:xxlarge}. header|_=. centered header| 72 | |~. bottom aligned|{background:red;width:200px}. asfd|""" 73 | result = textile.textile_restricted(test, lite=False) 74 | # styles from alignment hints like =. and ~. are ok 75 | expect = '''\ 76 | \tA footer | 81 | \t\t\tfoo | 82 | \t\t
header | 87 | \t\t\tcentered header | 88 | \t\t
---|---|
bottom aligned | 91 | \t\t\tasfd | 92 | \t\t
текст1
$', re.U).search(html) is not None 102 | 103 | 104 | def test_autolinking(): 105 | test = """some text "test":http://www.google.com http://www.google.com "$":http://www.google.com""" 106 | result = """\tsome text test http://www.google.com www.google.com
""" 107 | expect = textile.textile(test) 108 | 109 | assert result == expect 110 | 111 | 112 | def test_sanitize(): 113 | test = "a paragraph of benign text" 114 | result = "\ta paragraph of benign text
" 115 | expect = textile.Textile().parse(test, sanitize=True) 116 | assert result == expect 117 | 118 | test = """a paragraph of evil text
""" 119 | result = 'a paragraph of evil text
' 120 | expect = textile.Textile().parse(test, sanitize=True) 121 | assert result == expect 122 | 123 | test = """a paragraph of benign text
and more text
a paragraph of benign text
\nand more text
Scientists say the moon is slowly shrinking1.
\n\n\tTim Berners-Lee is one of the pioneer voices in favour of Net Neutrality1 and has expressed the view that ISPs should supply “connectivity with no strings attached”1 2
\n\n\tBerners-Lee admitted that the forward slashes \(“//”\) in a web address were actually unnecessary. He told the newspaper that he could easily have designed URLs not to have the forward slashes. “… it seemed like a good idea at the time,”3
\n\n\tScientists say1 the moon is quite small. But I, for one, don’t believe them. Others claim it to be made of cheese2. If this proves true I suspect we are in for troubled times3 as people argue over their “share” of the moon’s cheese. In the end, its limited size1 may prove problematic.
\n\n\tScientists say1 the moon is quite small. But I, for one, don’t believe them. Others claim it to be made of cheese2. If this proves true I suspect we are in for troubled times3 as people argue over their “share” of the moon’s cheese. In the end, its limited size1 may prove problematic.
\n\n\tScientists say the moon is slowly shrinking1.
\n\n\tSee2 for details, and later, reference it again2.
\n\n\t2 Here are the details.
$' 217 | assert re.compile(searchstring).search(html) is not None 218 | 219 | 220 | def test_footnote_without_reflink(): 221 | html = textile.textile('''See[3!] for details.\n\nfn3. Here are the details.''') 222 | searchstring = r'^\tSee3 for details.
\n\n\t3 Here are the details.
$' 223 | assert re.compile(searchstring).search(html) is not None 224 | 225 | 226 | def testSquareBrackets(): 227 | html = textile.textile("""1[^st^], 2[^nd^], 3[^rd^]. 2 log[~n~]\n\nA close[!http://textpattern.com/favicon.ico!]image.\nA tight["text":http://textpattern.com/]link.\nA ["footnoted link":http://textpattern.com/][182].""") 228 | searchstring = r'^\t1st, 2nd, 3rd. 2 logn
\n\n\tA closeimage.
\nA tighttextlink.
\nA footnoted link182.
We use CSS.
' 237 | expect = textile.textile(test, html_type="html5") 238 | assert result == expect 239 | 240 | 241 | def test_relURL(): 242 | t = textile.Textile() 243 | t.restricted = True 244 | assert t.relURL("gopher://gopher.com/") == '#' 245 | -------------------------------------------------------------------------------- /tests/test_textilefactory.py: -------------------------------------------------------------------------------- 1 | from textile import textilefactory 2 | import pytest 3 | 4 | 5 | def test_TextileFactory(): 6 | f = textilefactory.TextileFactory() 7 | result = f.process("some text here") 8 | expect = '\tsome text here
' 9 | assert result == expect 10 | 11 | f = textilefactory.TextileFactory(restricted=True) 12 | result = f.process("more text here") 13 | expect = '\tmore text here
' 14 | assert result == expect 15 | 16 | f = textilefactory.TextileFactory(noimage=True) 17 | result = f.process("this covers a partial branch.") 18 | expect = '\tthis covers a partial branch.
' 19 | assert result == expect 20 | 21 | # Certain parameter values are not permitted because they are illogical: 22 | 23 | with pytest.raises(ValueError) as ve: 24 | f = textilefactory.TextileFactory(lite=True) 25 | assert 'lite can only be enabled in restricted mode' in str(ve.value) 26 | 27 | with pytest.raises(ValueError) as ve: 28 | f = textilefactory.TextileFactory(html_type='invalid') 29 | assert "html_type must be 'xhtml' or 'html5'" in str(ve.value) 30 | -------------------------------------------------------------------------------- /tests/test_urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from textile import Textile 3 | 4 | 5 | def test_urls(): 6 | t = Textile() 7 | assert t.relURL("http://www.google.com/") == 'http://www.google.com/' 8 | 9 | result = t.links('fooobar "Google":http://google.com/foobar/ and hello world "flickr":http://flickr.com/photos/jsamsa/ ') 10 | expect = 'fooobar {0}2:shelve and hello world {0}4:shelve '.format(t.uid) 11 | assert result == expect 12 | 13 | result = t.links('""Open the door, HAL!"":https://xkcd.com/375/') 14 | expect = '{0}6:shelve'.format(t.uid) 15 | assert result == expect 16 | 17 | result = t.links('"$":http://domain.tld/test_[brackets]') 18 | expect = '{0}8:shelve'.format(t.uid) 19 | assert result == expect 20 | 21 | result = t.links('"$":http://domain.tld/test_') 22 | expect = '{0}10:shelve'.format(t.uid) 23 | assert result == expect 24 | 25 | expect = '"":test' 26 | result = t.links(expect) 27 | assert result == expect 28 | 29 | expect = '"$":htt://domain.tld' 30 | result = t.links(expect) 31 | assert result == expect 32 | 33 | result = t.shelveURL('') 34 | expect = '' 35 | assert result == expect 36 | 37 | result = t.retrieveURLs('{0}2:url'.format(t.uid)) 38 | expect = '' 39 | assert result == expect 40 | 41 | result = t.encode_url('http://domain.tld/übermensch') 42 | expect = 'http://domain.tld/%C3%BCbermensch' 43 | assert result == expect 44 | 45 | result = t.parse('A link that starts with an h is "handled":/test/ incorrectly.') 46 | expect = '\tA link that starts with an h is handled incorrectly.
' 47 | assert result == expect 48 | 49 | result = t.parse('A link that starts with a space" raises":/test/ an exception.') 50 | expect = '\tA link that starts with a space” raises an exception.
' 51 | assert result == expect 52 | 53 | result = t.parse('A link that "contains a\nnewline":/test/ raises an exception.') 54 | expect = '\tA link that contains a\nnewline raises an exception.
' 55 | assert result == expect 56 | 57 | 58 | def test_rel_attribute(): 59 | t = Textile(rel='nofollow') 60 | result = t.parse('"$":http://domain.tld') 61 | expect = '\t' 62 | assert result == expect 63 | 64 | 65 | def test_quotes_in_link_text(): 66 | """quotes in link text are tricky.""" 67 | test = '""this is a quote in link text"":url' 68 | t = Textile() 69 | result = t.parse(test) 70 | expect = '\t“this is a quote in link text”
' 71 | assert result == expect 72 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from textile import utils 3 | 4 | 5 | def test_encode_html(): 6 | result = utils.encode_html('''this is a "test" of text that's safe to ''' 7 | 'put in an attribute.') 8 | expect = ('this is a "test" of text that's safe to put in ' 9 | 'an <html> attribute.') 10 | assert result == expect 11 | 12 | 13 | def test_has_raw_text(): 14 | assert utils.has_raw_text('foo bar biz baz
') is False 15 | assert utils.has_raw_text(' why yes, yes it does') is True 16 | 17 | 18 | def test_is_rel_url(): 19 | assert utils.is_rel_url("http://www.google.com/") is False 20 | assert utils.is_rel_url("/foo") is True 21 | 22 | 23 | def test_generate_tag(): 24 | result = utils.generate_tag('span', 'inner text', {'class': 'test'}) 25 | expect = 'inner text' 26 | assert result == expect 27 | 28 | text = 'Übermensch' 29 | attributes = {'href': 'http://de.wikipedia.org/wiki/%C3%C9bermensch'} 30 | expect = 'Übermensch' 31 | result = utils.generate_tag('a', text, attributes) 32 | assert result == expect 33 | 34 | 35 | def test_human_readable_url_edge_case(): 36 | assert utils.human_readable_url('google.com') == 'google.com' 37 | assert utils.human_readable_url('tel:1-800-555-1212') == '1-800-555-1212' 38 | -------------------------------------------------------------------------------- /tests/test_values.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import textile 3 | import pytest 4 | 5 | xhtml_known_values = ( 6 | ('hello, world', '\thello, world
'), 7 | 8 | ('A single paragraph.\n\nFollowed by another.', 9 | '\tA single paragraph.
\n\n\tFollowed by another.
'), 10 | 11 | ('I am very serious.\n\n\nI am very serious.\n', 12 | '\t
I am very serious.
\n\n\nI am <b>very</b> serious.\n'), 13 | 14 | ('I spoke.\nAnd none replied.', '\t
I spoke.
\nAnd none replied.
“Observe!”
'), 17 | 18 | ('Observe -- very nice!', '\tObserve — very nice!
'), 19 | 20 | ('Observe - tiny and brief.', '\tObserve – tiny and brief.
'), 21 | 22 | ('Observe...', '\tObserve…
'), 23 | 24 | ('Observe ...', '\tObserve …
'), 25 | 26 | ('Observe: 2 x 2.', '\tObserve: 2 × 2.
'), 27 | 28 | ('one(TM), two(R), three(C).', '\tone™, two®, three©.
'), 29 | 30 | ('h1. Header 1', '\tAn old text
\n\n\t\n\t\t\n\n\tA block quotation.
\n\t
Any old text
'), 38 | 39 | ('I _believe_ every word.', '\tI believe every word.
'), 40 | 41 | ('And then? She *fell*!', '\tAnd then? She fell!
'), 42 | 43 | ('I __know__.\nI **really** __know__.', '\tI know.
\nI really know.
Cat’s Cradle by Vonnegut
'), 46 | 47 | ('Convert with @str(foo)@', '\tConvert with str(foo)
I’m sure not sure.
You are a pleasant child.
'), 52 | 53 | ('a ^2^ + b ^2^ = c ^2^', '\ta 2 + b 2 = c 2
'), 54 | 55 | ('log ~2~ x', '\tlog 2 x
'), 56 | 57 | ('I\'m %unaware% of most soft drinks.', '\tI’m unaware of most soft drinks.
'), 58 | 59 | ("I'm %{color:red}unaware%\nof most soft drinks.", '\tI’m unaware
\nof most soft drinks.
An example
'), 62 | 63 | ('p(#big-red). Red here', '\tRed here
'), 64 | 65 | ('p(example1#big-red2). Red here', '\tRed here
'), 66 | 67 | ('p{color:blue;margin:30px}. Spacey blue', '\tSpacey blue
'), 68 | 69 | ('p[fr]. rouge', '\trouge
'), 70 | 71 | ('I seriously *{color:red}blushed*\nwhen I _(big)sprouted_ that\ncorn stalk from my\n%[es]cabeza%.', 72 | '\tI seriously blushed
\nwhen I sprouted'
73 | ' that
\ncorn stalk from my
\ncabeza.
align left
'), 76 | 77 | ('p>. align right', '\talign right
'), 78 | 79 | ('p=. centered', '\tcentered
'), 80 | 81 | ('p<>. justified', '\tjustified
'), 82 | 83 | ('p(. left ident 1em', '\tleft ident 1em
'), 84 | 85 | ('p((. left ident 2em', '\tleft ident 2em
'), 86 | 87 | ('p))). right ident 3em', '\tright ident 3em
'), 88 | 89 | ('h2()>. Bingo.', '\t\n\na.gsub!( /, "" )\n
\n
',
94 | '\n\na.gsub!( /</, "" )\n
\n
'),
95 |
96 | ('The main text of the
\n'
100 | 'page goes here and will
\nstay to the left of the
\nsidebar.
I searched Google.
'), 115 | 116 | ('I searched "a search engine (Google)":http://google.com.', '\tI searched a search engine.
'), 117 | 118 | ('I am crazy about "Hobix":hobix\nand "it\'s":hobix "all":hobix I ever\n"link to":hobix!\n\n[hobix]http://hobix.com', 119 | '\tI am crazy about Hobix
\nand it’s '
120 | 'all I ever
\nlink to!
And others sat all round the small
\nmachine and paid it to sing to them.
We use CSS.
'), 133 | 134 | ('|one|two|three|\n|a|b|c|', 135 | '\tone | \n\t\t\ttwo | \n\t\t\tthree | \n\t\t
a | \n\t\t\tb | \n\t\t\tc | \n\t\t
name | \n\t\t\tage | \n\t\t\tsex | \n\t\t
joan | \n\t\t\t24 | \n\t\t\tf | \n\t\t
archie | \n\t\t\t29 | \n\t\t\tm | \n\t\t
bella | \n\t\t\t45 | \n\t\t\tf | \n\t\t
name | \n\t\t\tage | \n\t\t\tsex | \n\t\t
---|---|---|
joan | \n\t\t\t24 | \n\t\t\tf | \n\t\t
archie | \n\t\t\t29 | \n\t\t\tm | \n\t\t
bella | \n\t\t\t45 | \n\t\t\tf | \n\t\t
Hello\n\nHello Again\n\n\t
normal text
'), 153 | 154 | ('this is in a pre tag', '
this is in a pre tag'), 155 | 156 | ('"test1":http://foo.com/bar--baz\n\n"test2":http://foo.com/bar---baz\n\n"test3":http://foo.com/bar-17-18-baz', 157 | '\t\n\n\t' 158 | '\n\n\t' 159 | ''), 160 | 161 | ('"foo ==(bar)==":#foobar', '\t'), 162 | 163 | ('!http://render.mathim.com/A%5EtAx%20%3D%20A%5Et%28Ax%29.!', 164 | '\t
array[4] = 8
Links (like this), are now mangled in 2.1.0, whereas 2.0 parsed them correctly.
'), 176 | 177 | ('@monospaced text@, followed by text', 178 | '\tmonospaced text
, followed by text
some text
'), 181 | 182 | ('pre.. foo bar baz\nquux', 'foo bar baz\nquux'), 183 | 184 | ('line of text\n\n leading spaces', 185 | '\t
line of text
\n\n leading spaces'), 186 | 187 | ('"some text":http://www.example.com/?q=foo%20bar and more text', 188 | '\tsome text and more text
'), 189 | 190 | ('(??some text??)', '\t(some text)
'), 191 | 192 | ('(*bold text*)', '\t(bold text)
'), 193 | 194 | ('H[~2~]O', '\tH2O
'), 195 | 196 | ("p=. Où est l'école, l'église s'il vous plaît?", 197 | """\tOù est l’école, l’église s’il vous plaît?
"""), 198 | 199 | ("p=. *_The_* _*Prisoner*_", 200 | """\tThe Prisoner
"""), 201 | 202 | ("""p=. "An emphasised _word._" & "*A spanned phrase.*" """, 203 | """\t“An emphasised word.” & “A spanned phrase.”
"""), 204 | 205 | ("""p=. "*Here*'s a word!" """, 206 | """\t“Here’s a word!”
"""), 207 | 208 | ("""p=. "Please visit our "Textile Test Page":http://textile.sitemonks.com" """, 209 | """\t“Please visit our Textile Test Page”
"""), 210 | ("""| Foreign EXPÓŅÉNTIAL |""", 211 | """\tForeign EXPÓŅÉNTIAL | \n\t\t
Piękne ŹDŹBŁO
"""), 214 | 215 | ("""p=. Tell me, what is AJAX(Asynchronous Javascript and XML), please?""", 216 | """\tTell me, what is AJAX, please?
"""), 217 | ('p{font-size:0.8em}. *TxStyle* is a documentation project of Textile 2.4 for "Textpattern CMS":http://texpattern.com.', 218 | '\tTxStyle is a documentation project of Textile 2.4 for Textpattern CMS.
'), 219 | (""""Übermensch":http://de.wikipedia.org/wiki/Übermensch""", """\t"""), 220 | ("""Here is some text with a block.\n\n\n\n\n\nbc. """, 221 | """\tHere is some text with a block.
\n\n\n\n\n\n<!-- Here is a comment block in a code block. -->
"""),
222 | (""""Textile(c)" is a registered(r) 'trademark' of Textpattern(tm) -- or TXP(That's textpattern!) -- at least it was - back in '88 when 2x4 was (+/-)5(o)C ... QED!\n\np{font-size: 200%;}. 2(1/4) 3(1/2) 4(3/4)""",
223 | """\t“Textile©” is a registered® ‘trademark’ of Textpattern™ — or TXP — at least it was – back in ’88 when 2×4 was ±5°C … QED!
\n\n\t2¼ 3½ 4¾
"""), 224 | ("""|=. Testing colgroup and col syntax\n|:\\5. 80\n|a|b|c|d|e|\n\n|=. Testing colgroup and col syntax|\n|:\\5. 80|\n|a|b|c|d|e|""", """\ta | \n\t\t\tb | \n\t\t\tc | \n\t\t\td | \n\t\t\te | \n\t\t
a | \n\t\t\tb | \n\t\t\tc | \n\t\t\td | \n\t\t\te | \n\t\t
Title | \n\t\t\tStarring | \n\t\t\tDirector | \n\t\t\tWriter | \n\t\t\tNotes | \n\t\t
---|---|---|---|---|
This is the tfoot, centred | \n\t\t||||
The Usual Suspects | \n\t\t\tBenicio Del Toro, Gabriel Byrne, Stephen Baldwin, Kevin Spacey | \n\t\t\tBryan Singer | \n\t\t\tChris McQaurrie | \n\t\t\tOne of the finest films ever made | \n\t\t
Se7en | \n\t\t\tMorgan Freeman, Brad Pitt, Kevin Spacey | \n\t\t\tDavid Fincher | \n\t\t\tAndrew Kevin Walker | \n\t\t\tGreat psychological thriller | \n\t\t
Primer | \n\t\t\tDavid Sullivan, Shane Carruth | \n\t\t\tShane Carruth | \n\t\t\tShane Carruth | \n\t\t\t Amazing insight into trust and human psychology \nrather than science fiction. Terrific! | \n\t\t
District 9 | \n\t\t\tSharlto Copley, Jason Cope | \n\t\t\tNeill Blomkamp | \n\t\t\tNeill Blomkamp, Terri Tatchell | \n\t\t\t Social commentary layered on thick, \nbut boy is it done well | \n\t\t
Arlington Road | \n\t\t\tTim Robbins, Jeff Bridges | \n\t\t\tMark Pellington | \n\t\t\tEhren Kruger | \n\t\t\tAwesome study in neighbourly relations | \n\t\t
Phone Booth | \n\t\t\tColin Farrell, Kiefer Sutherland, Forest Whitaker | \n\t\t\tJoel Schumacher | \n\t\t\tLarry Cohen | \n\t\t\t Edge-of-the-seat stuff in this \nshort but brilliantly executed thriller | \n\t\t
Nourishing beverage for baby cows.
\nCold drink that goes great with cookies.
Here is a comment
\n\n\tHere is a comment
\n\n\tHere is a class that is a little extended and is
\nfollowed by a strong word!
; Content-type: text/javascript\n; Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\n; Expires: Sat, 24 Jul 2003 05:00:00 GMT\n; Last-Modified: Wed, 1 Jan 2025 05:00:00 GMT\n; Pragma: no-cache
\n\n\t123 test
\n\n\ttest 123
\n\n\t123 test
\n\n\ttest 123
"""), 233 | ("""#_(first#list) one\n# two\n# three\n\ntest\n\n#(ordered#list2).\n# one\n# two\n# three\n\ntest\n\n#_(class_4).\n# four\n# five\n# six\n\ntest\n\n#_ seven\n# eight\n# nine\n\ntest\n\n# one\n# two\n# three\n\ntest\n\n#22 22\n# 23\n# 24""", 234 | """\ttest
\n\n\ttest
\n\n\ttest
\n\n\ttest
\n\n\ttest
\n\n\ttest
\n\n\ttest
\n\n\t\t
| \n\t\t
\t
| \n\t\t
| \n\t\t
table | \n\t\t\tmore | \n\t\t\tbadass | \n\t\t
---|---|---|
Horizontal span of 3 | \n\t\t||
first | \n\t\t\tHAL | \n\t\t\t1 | \n\t\t
some | \n\t\t\tstyled | \n\t\t\tcontent | \n\t\t
spans 2 rows | \n\t\t\tthis is | \n\t\t\tquite a | \n\t\t
deep test | \n\t\t\tdon’t you think? | \n\t\t|
fifth | \n\t\t\tI’m a lumberjack | \n\t\t\t5 | \n\t\t
sixth | \n\t\t\tbold italics | \n\t\t\t6 | \n\t\t
strong | \n\t\t
em | \n\t\t
Inter-word | \n\t\t\tZIP-codes are 5- or 9-digit codes | \n\t\t
attribute list | \n\t\t
---|
align left | \n\t\t
align right | \n\t\t
center | \n\t\t
justify me | \n\t\t
valign top | \n\t\t
bottom | \n\t\t
Goodbye.
"""), 248 | ("""h2. A Definition list which covers the instance where a new definition list is created with a term without a definition\n\n- term :=\n- term2 := def""", """\t& test
'), 252 | ) 253 | 254 | # A few extra cases for HTML4 255 | html_known_values = ( 256 | ("pre.. The beginning\n\nbc.. This code\n\nis the last\n\nblock in the document\n", 257 | "The beginning\n\n
This code\n\nis the last\n\nblock in the document
"),
258 | ("bc.. This code\n\nis not\n\nsurrounded by anything\n",
259 | "This code\n\nis not\n\nsurrounded by anything
"),
260 | ("bc.. Paragraph 1\n\nParagraph 2\n\nParagraph 3\n\np.. post-code paragraph",
261 | "Paragraph 1\n\nParagraph 2\n\nParagraph 3
\n\npost-code paragraph
"), 262 | ("bc.. Paragraph 1\n\nParagraph 2\n\nParagraph 3\n\npre.. post-code non-p block", 263 | "Paragraph 1\n\nParagraph 2\n\nParagraph 3
\n\npost-code non-p block"), 264 | ('I spoke.\nAnd none replied.', '\t
I spoke.
\nAnd none replied.
I know.
\nI really know.
I’m unaware
\nof most soft drinks.
I seriously blushed
\nwhen I sprouted'
269 | ' that
\ncorn stalk from my
\ncabeza.
\n\na.gsub!( /, "" )\n
\n
',
271 | '\n\na.gsub!( /</, "" )\n
\n
'),
272 | ('The main text of the
\n'
276 | 'page goes here and will
\nstay to the left of the
\nsidebar.
I am crazy about Hobix
\nand it’s '
279 | 'all I ever
\nlink to!
And others sat all round the small
\nmachine and paid it to sing to them.
quux
'), 290 | ('"foo":http://google.com/one--two', '\t'), 291 | # issue 24 colspan 292 | ('|\\2. spans two cols |\n| col 1 | col 2 |', '\tspans two cols | \n\t\t|
col 1 | \n\t\t\tcol 2 | \n\t\t
Hello\n\nAgain\n\n\t
normal text
'), 297 | # url with parentheses 298 | ('"python":http://en.wikipedia.org/wiki/Python_(programming_language)', '\t'), 299 | # table with hyphen styles 300 | ('table(linkblog-thumbnail).\n|(linkblog-thumbnail-cell). apple|bear|', '\tapple | \n\t\t\tbear | \n\t\t
thing | \n\t\t\t\n\t\t\t | \n\t\t\t | otherthing | \n\t\t
test text
"), 308 | ("_*test text*_", "\ttest text
"), 309 | # quotes in code block 310 | ("'quoted string'
", "\t'quoted string'
some preformatted textother text", "\t
some preformatted textother text"), 312 | # at sign and notextile in table 313 | ("|@
<A1> | \n\t\t\t<A2> <A3> | \n\t\t
*B1* | \n\t\t\t*B2* *B3* | \n\t\t
\n\t\t'), 316 | ('Hello ["(Mum) & dad"]', '\tText…
\n\t
Hello [“(Mum) & dad”]
'), 317 | # Dimensions 318 | ( 319 | ('[1/2] x [1/4] and (1/2)" x [1/4]" and (1/2)\' x (1/4)\'\n\n' 320 | '(2 x 10) X (3 / 4) x (200 + 64)\n\n' 321 | '1 x 1 = 1\n\n' 322 | '1 x1 = 1\n\n' 323 | '1x 1 = 1\n\n' 324 | '1x1 = 1\n\n' 325 | '1 X 1 = 1\n\n' 326 | '1 X1 = 1\n\n' 327 | '1X 1 = 1\n\n' 328 | '1X1 = 1\n\n' 329 | 'What is 1 x 1?\n\n' 330 | 'What is 1x1?\n\n' 331 | 'What is 1 X 1?\n\n' 332 | 'What is 1X1?\n\n' 333 | '1 x 2 x 3 = 6\n\n' 334 | '1x2x3=6\n\n' 335 | '1x2 x 1x3 = 6\n\n' 336 | '2\' x 2\' = 4 sqft.\n\n' 337 | '2\'x 2\' = 4 sqft.\n\n' 338 | '2\' x2\' = 4 sqft.\n\n' 339 | '2\'x2\' = 4 sqft.\n\n' 340 | '2\' X 2\' = 4 sqft.\n\n' 341 | '2\'X 2\' = 4 sqft.\n\n' 342 | '2\' X2\' = 4 sqft.\n\n' 343 | '2\'X2\' = 4 sqft.\n\n' 344 | '2" x 2" = 4 sqin.\n\n' 345 | '2"x 2" = 4 sqin.\n\n' 346 | '2" x2" = 4 sqin.\n\n' 347 | '2"x2" = 4 sqin.\n\n' 348 | '2" X 2" = 4 sqin.\n\n' 349 | '2"X 2" = 4 sqin.\n\n' 350 | '2" X2" = 4 sqin.\n\n' 351 | '2"X2" = 4in[^2^].\n\n' 352 | 'What is 1.2 x 3.5?\n\n' 353 | 'What is .2 x .5?\n\n' 354 | 'What is 1.2x3.5?\n\n' 355 | 'What is .2x.5?\n\n' 356 | 'What is 1.2\' x3.5\'?\n\n' 357 | 'What is .2"x .5"?\n\n' 358 | '1 x $10.00 x -£ 1.23 x ¥20,000 x -¤120.00 x ฿1,000,000 x -€110,00\n\n'), 359 | 360 | ('\t½ × ¼ and ½” × ¼” and ½’ × ¼’
\n\n' 361 | '\t(2 × 10) × (3 / 4) × (200 + 64)
\n\n' 362 | '\t1 × 1 = 1
\n\n' 363 | '\t1 ×1 = 1
\n\n' 364 | '\t1× 1 = 1
\n\n' 365 | '\t1×1 = 1
\n\n' 366 | '\t1 × 1 = 1
\n\n' 367 | '\t1 ×1 = 1
\n\n' 368 | '\t1× 1 = 1
\n\n' 369 | '\t1×1 = 1
\n\n' 370 | '\tWhat is 1 × 1?
\n\n' 371 | '\tWhat is 1×1?
\n\n' 372 | '\tWhat is 1 × 1?
\n\n' 373 | '\tWhat is 1×1?
\n\n' 374 | '\t1 × 2 × 3 = 6
\n\n' 375 | '\t1×2×3=6
\n\n' 376 | '\t1×2 × 1×3 = 6
\n\n' 377 | '\t2’ × 2’ = 4 sqft.
\n\n' 378 | '\t2’× 2’ = 4 sqft.
\n\n' 379 | '\t2’ ×2’ = 4 sqft.
\n\n' 380 | '\t2’×2’ = 4 sqft.
\n\n' 381 | '\t2’ × 2’ = 4 sqft.
\n\n' 382 | '\t2’× 2’ = 4 sqft.
\n\n' 383 | '\t2’ ×2’ = 4 sqft.
\n\n' 384 | '\t2’×2’ = 4 sqft.
\n\n' 385 | '\t2” × 2” = 4 sqin.
\n\n' 386 | '\t2”× 2” = 4 sqin.
\n\n' 387 | '\t2” ×2” = 4 sqin.
\n\n' 388 | '\t2”×2” = 4 sqin.
\n\n' 389 | '\t2” × 2” = 4 sqin.
\n\n' 390 | '\t2”× 2” = 4 sqin.
\n\n' 391 | '\t2” ×2” = 4 sqin.
\n\n' 392 | '\t2”×2” = 4in2.
\n\n' 393 | '\tWhat is 1.2 × 3.5?
\n\n' 394 | '\tWhat is .2 × .5?
\n\n' 395 | '\tWhat is 1.2×3.5?
\n\n' 396 | '\tWhat is .2×.5?
\n\n' 397 | '\tWhat is 1.2’ ×3.5’?
\n\n' 398 | '\tWhat is .2”× .5”?
\n\n' 399 | '\t1 × $10.00 × -£ 1.23 × ¥20,000 × -¤120.00 × ฿1,000,000 × -€110,00
') 400 | ), 401 | # Empty note lists 402 | ('There should be nothing below.\n\nnotelist.', '\tThere should be nothing below.
\n\n\t'), 403 | # Empty things 404 | (('\'\'\n\n""\n\n%%\n\n^^\n\n&&\n\n**\n\n__\n\n--\n\n++\n\n~~\n\n{}\n\n' 405 | '[]\n\n()\n\n<>\n\n\\\\\n\n//\n\n??\n\n==\n\n@@\n\n##\n\n$$\n\n!!\n\n' 406 | '::\n\n;;\n\n..\n\n,,\n\n||\n\n` `\n\n\' \'\n\n" "\n\n% %\n\n^ ^\n\n' 407 | '& &\n\n* *\n\n_ _\n\n- -\n\n+ +\n\n~ ~\n\n{ }\n\n[ ]\n\n( )\n\n< >\n\n' 408 | '\\ \\\n\n/ /\n\n? ?\n\n= =\n\n@ @\n\n# #\n\n$ $\n\n! !\n\n: :\n\n; ;\n\n' 409 | '. .\n\n, ,'), 410 | ("\t‘’
\n\n\t“”
\n\n\t%%
\n\n\t^^
\n\n\t" 411 | "&&
\n\n\t**
\n\n\t__
\n\n\t—
\n\n\t++
\n\n\t" 412 | "~~
\n\n\t{}
\n\n\t[]
\n\n\t()
\n\n\t<>
\n\n\t\\\\
\n\n\t" 413 | "//
\n\n\t??
\n\n\t==
\n\n\t##
\n\n\t$$
\n\n\t" 414 | "!!
\n\n\t::
\n\n\t;;
\n\n\t..
\n\n\t,,
\n\n\t" 415 | "\n\t\t |
` `
\n\n\t‘ ‘
\n\n\t" 416 | "“ “
\n\n\t% %
\n\n\t^ ^
\n\n\t& &
\n\n\t" 417 | "_ _
\n\n\t- -
\n\n\t+ +
\n\n\t~ ~
\n\n\t" 418 | "{ }
\n\n\t[ ]
\n\n\t( )
\n\n\t< >
\n\n\t\\ \\
\n\n\t" 419 | "/ /
\n\n\t? ?
\n\n\t= =
\n\n\t
$ $
\n\n\t! !
\n\n\t. .
\n\n\t, ,
")), 422 | # A lone standing comment must be preserved as is: 423 | # withouth wrapping it into a paragraph 424 | (('An ordinary block.\n\n' 425 | '\n'), 426 | '\tAn ordinary block.
\n\n'), 427 | # Headers must be "breakable", just like paragraphs. 428 | ('h1. Two line with *strong*\nheading\n', 429 | '\t“test”
\n\n" 435 | "\t“test”
\n\n" 436 | "\ttest
")), 437 | # Nested and mixed multi-level ordered and unordered lists 438 | (("* bullet\n" 439 | "*# number\n" 440 | "*# number\n" 441 | "*#* bullet\n" 442 | "*# number\n" 443 | "*# number with\n" 444 | "a break\n" 445 | "* bullet\n" 446 | "** okay"), 447 | ("\t
| \n"
474 | "\t\t
Here is a ‘spanned’ word.
'), 480 | # Using $-links with link aliases 481 | ("\"$\":test\n[test]https://textpattern.com/start\n", 482 | "\t"), 483 | ('Please check on "$":test for any updates.\n[test]https://de.wikipedia.org/wiki/Übermensch', 484 | '\tPlease check on de.wikipedia.org/wiki/Übermensch for any updates.
'), 485 | # Make sure smileys don't get recognised as a definition list. 486 | (":(\n\n:)\n\n:( \n:( \n:( \n:) \n\nPinocchio!\n:^)\n\nBaboon!\n:=)\n\nWink!\n;)\n\n:[ \n:]\n\n;(\nsomething\ndark side\n:) \n\n;(c)[de] Item", 487 | '\t:(
\n\n\t:)
\n\n\t:(
\n:(
\n:(
\n:)
Pinocchio!
\n:^)
Baboon!
\n:=)
Wink!
\n;)
:[
\n:]
;(
\nsomething
\ndark side
\n:)
text1 text2
![]() | \n\t\t\t ![]() | \n\t\t
a | \n\t\t\tb | \n\t\t\tc | \n\t\t\td | \n\t\t\te | \n\t\t
touch this! | \n\t\t\ttouch this! | \n\t\t
a | \n\t\t\tb | \n\t\t\tc | \n\t\t\td | \n\t\t\te | \n\t\t
..
are added to line 441 | multiline_para = False 442 | 443 | tag = 'p' 444 | atts = cite = ext = '' 445 | 446 | out = [] 447 | 448 | for line in text: 449 | # the line is just whitespace, add it to the output, and move on 450 | if not line.strip(): 451 | if not eat_whitespace: 452 | out.append(line) 453 | continue 454 | 455 | eat_whitespace = False 456 | 457 | pattern = (r'^(?P\[)? # Optionally open with a square bracket eg. Look ["here":url] 810 | {0}linkStartMarker:" # marks start of the link 811 | (?P(?:.|\n)*?) # grab the content of the inner "..." part of the link, can be anything but 812 | # do not worry about matching class, id, lang or title yet 813 | ": # literal ": marks end of atts + text + title block 814 | (?P [^{1}]*) # url upto a stopchar 815 | """.format(self.uid, stopchars) 816 | text = re.compile(pattern, flags=re.X | re.U).sub(self.fLink, text) 817 | return text 818 | 819 | def fLink(self, m): 820 | in_ = m.group() 821 | pre, inner, url = m.groups() 822 | pre = pre or '' 823 | 824 | if inner == '': 825 | return '{0}"{1}":{2}'.format(pre, inner, url) 826 | 827 | m = re.search(r'''^ 828 | (?P {0}) # $atts (if any) 829 | {1}* # any optional spaces 830 | (?P # $text is... 831 | (!.+!) # an image 832 | | # else... 833 | .+? # link text 834 | ) # end of $text 835 | (?:\((?P [^)]+?)\))? # $title (if any) 836 | $'''.format(cls_re_s, regex_snippets['space']), inner, 837 | flags=re.X | re.U) 838 | 839 | atts = (m and m.group('atts')) or '' 840 | text = (m and m.group('text')) or inner 841 | title = (m and m.group('title')) or '' 842 | 843 | pop, tight = '', '' 844 | counts = {'[': None, ']': url.count(']'), '(': None, ')': None} 845 | 846 | # Look for footnotes or other square-bracket delimited stuff at the end 847 | # of the url... 848 | # 849 | # eg. "text":url][otherstuff... will have "[otherstuff" popped back 850 | # out. 851 | # 852 | # "text":url?q[]=x][123] will have "[123]" popped off the back, the 853 | # remaining closing square brackets will later be tested for balance 854 | if (counts[']']): 855 | m = re.search(r'(?P ^.*\])(?P \[.*?)$', url, flags=re.U) 856 | if m: 857 | url, tight = m.groups() 858 | 859 | # Split off any trailing text that isn't part of an array assignment. 860 | # eg. "text":...?q[]=value1&q[]=value2 ... is ok 861 | # "text":...?q[]=value1]following ... would have "following" popped 862 | # back out and the remaining square bracket will later be tested for 863 | # balance 864 | if (counts[']']): 865 | m = re.search(r'(?P ^.*\])(?!=)(?P .*?)$', url, flags=re.U) 866 | url = m.group('url') 867 | tight = '{0}{1}'.format(m.group('end'), tight) 868 | 869 | # Now we have the array of all the multi-byte chars in the url we will 870 | # parse the uri backwards and pop off any chars that don't belong 871 | # there (like . or , or unmatched brackets of various kinds). 872 | first = True 873 | popped = True 874 | 875 | counts[']'] = url.count(']') 876 | url_chars = list(url) 877 | 878 | def _endchar(c, pop, popped, url_chars, counts, pre): 879 | """Textile URL shouldn't end in these characters, we pop them off 880 | the end and push them out the back of the url again.""" 881 | pop = '{0}{1}'.format(c, pop) 882 | url_chars.pop() 883 | popped = True 884 | return pop, popped, url_chars, counts, pre 885 | 886 | def _rightanglebracket(c, pop, popped, url_chars, counts, pre): 887 | url_chars.pop() 888 | urlLeft = ''.join(url_chars) 889 | 890 | m = re.search(r'(?P .*)(?P <\/[a-z]+)$', urlLeft) 891 | url_chars = m.group('url_chars') 892 | pop = '{0}{1}{2}'.format(m.group('tag'), c, pop) 893 | popped = True 894 | return pop, popped, url_chars, counts, pre 895 | 896 | def _closingsquarebracket(c, pop, popped, url_chars, counts, pre): 897 | """If we find a closing square bracket we are going to see if it is 898 | balanced. If it is balanced with matching opening bracket then it 899 | is part of the URL else we spit it back out of the URL.""" 900 | # If counts['['] is None, count the occurrences of '[' 901 | counts['['] = counts['['] or url.count('[') 902 | 903 | if counts['['] == counts[']']: 904 | # It is balanced, so keep it 905 | url_chars.append(c) 906 | else: 907 | # In the case of un-matched closing square brackets we just eat 908 | # it 909 | popped = True 910 | url_chars.pop() 911 | counts[']'] = counts[']'] - 1 912 | if first: # pragma: no branch 913 | pre = '' 914 | return pop, popped, url_chars, counts, pre 915 | 916 | def _closingparenthesis(c, pop, popped, url_chars, counts, pre): 917 | if counts[')'] is None: # pragma: no branch 918 | counts['('] = url.count('(') 919 | counts[')'] = url.count(')') 920 | 921 | if counts['('] != counts[')']: 922 | # Unbalanced so spit it out the back end 923 | popped = True 924 | pop = '{0}{1}'.format(url_chars.pop(), pop) 925 | counts[')'] = counts[')'] - 1 926 | return pop, popped, url_chars, counts, pre 927 | 928 | def _casesdefault(c, pop, popped, url_chars, counts, pre): 929 | return pop, popped, url_chars, counts, pre 930 | 931 | cases = { 932 | '!': _endchar, 933 | '?': _endchar, 934 | ':': _endchar, 935 | ';': _endchar, 936 | '.': _endchar, 937 | ',': _endchar, 938 | '>': _rightanglebracket, 939 | ']': _closingsquarebracket, 940 | ')': _closingparenthesis, 941 | } 942 | for c in url_chars[-1::-1]: # pragma: no branch 943 | popped = False 944 | pop, popped, url_chars, counts, pre = cases.get( 945 | c, _casesdefault)(c, pop, popped, url_chars, counts, pre) 946 | first = False 947 | if popped is False: 948 | break 949 | 950 | url = ''.join(url_chars) 951 | uri_parts = urlsplit(url) 952 | 953 | scheme_in_list = uri_parts.scheme in self.url_schemes 954 | valid_scheme = (uri_parts.scheme and scheme_in_list) 955 | if not is_valid_url(url) and not valid_scheme: 956 | return in_.replace('{0}linkStartMarker:'.format(self.uid), '') 957 | 958 | if text == '$': 959 | if valid_scheme: 960 | text = human_readable_url(url) 961 | else: 962 | ref_url = self.urlrefs.get(url) 963 | if ref_url is not None: 964 | text = human_readable_url(ref_url) 965 | else: 966 | text = url 967 | 968 | text = text.strip() 969 | title = encode_html(title) 970 | 971 | if not self.noimage: # pragma: no branch 972 | text = self.image(text) 973 | text = self.span(text) 974 | text = self.glyphs(text) 975 | url = self.shelveURL(self.encode_url(urlunsplit(uri_parts))) 976 | attributes = parse_attributes(atts, restricted=self.restricted) 977 | attributes['href'] = url 978 | if title: 979 | # if the title contains unicode data, it is annoying to get Python 980 | # 2.6 and all the latter versions working properly. But shelving 981 | # the title is a quick and dirty solution. 982 | attributes['title'] = self.shelve(title) 983 | if self.rel: 984 | attributes['rel'] = self.rel 985 | a_text = generate_tag('a', text, attributes) 986 | a_shelf_id = self.shelve(a_text) 987 | 988 | out = '{0}{1}{2}{3}'.format(pre, a_shelf_id, pop, tight) 989 | 990 | return out 991 | 992 | def encode_url(self, url): 993 | """ 994 | Converts a (unicode) URL to an ASCII URL, with the domain part 995 | IDNA-encoded and the path part %-encoded (as per RFC 3986). 996 | 997 | Fixed version of the following code fragment from Stack Overflow: 998 | http://stackoverflow.com/a/804380/72656 999 | """ 1000 | # parse it 1001 | parsed = urlsplit(url) 1002 | 1003 | if parsed.netloc: 1004 | # divide the netloc further 1005 | netloc_pattern = re.compile(r""" 1006 | (?:(?P [^:@]+)(?::(?P [^:@]+))?@)? 1007 | (?P [^:]+) 1008 | (?::(?P [0-9]+))? 1009 | """, re.X | re.U) 1010 | netloc_parsed = netloc_pattern.match(parsed.netloc).groupdict() 1011 | else: 1012 | netloc_parsed = {'user': '', 'password': '', 'host': '', 'port': ''} 1013 | 1014 | # encode each component 1015 | scheme = parsed.scheme 1016 | user = netloc_parsed['user'] and quote(netloc_parsed['user']) 1017 | password = ( 1018 | netloc_parsed['password'] and quote(netloc_parsed['password']) 1019 | ) 1020 | host = netloc_parsed['host'] 1021 | port = netloc_parsed['port'] and netloc_parsed['port'] 1022 | # the below splits the path portion of the url by slashes, translates 1023 | # percent-encoded characters back into strings, then re-percent-encodes 1024 | # what's necessary. Sounds screwy, but the url could include encoded 1025 | # slashes, and this is a way to clean that up. It branches for PY2/3 1026 | # because the quote and unquote functions expects different input 1027 | # types: unicode strings for PY2 and str for PY3. 1028 | path_parts = (quote(unquote(pce), b'') for pce in 1029 | parsed.path.split('/')) 1030 | path = '/'.join(path_parts) 1031 | 1032 | # put it back together 1033 | netloc = '' 1034 | if user: 1035 | netloc = '{0}{1}'.format(netloc, user) 1036 | if password: 1037 | netloc = '{0}:{1}'.format(netloc, password) 1038 | netloc = '{0}@'.format(netloc) 1039 | netloc = '{0}{1}'.format(netloc, host) 1040 | if port: 1041 | netloc = '{0}:{1}'.format(netloc, port) 1042 | return urlunsplit((scheme, netloc, path, parsed.query, parsed.fragment)) 1043 | 1044 | def span(self, text): 1045 | qtags = (r'\*\*', r'\*', r'\?\?', r'\-', r'__', 1046 | r'_', r'%', r'\+', r'~', r'\^') 1047 | pnct = r""".,"'?!;:‹›«»„“”‚‘’""" 1048 | self.span_depth = self.span_depth + 1 1049 | 1050 | if self.span_depth <= self.max_span_depth: 1051 | for tag in qtags: 1052 | pattern = re.compile(r""" 1053 | (?P ^|(?<=[\s>{pnct}\(])|[{{[]) 1054 | (?P{tag})(?!{tag}) 1055 | (?P {cls}) 1056 | (?!{tag}) 1057 | (?::(?P\S+[^{tag}]{space}))? 1058 | (?P [^{space}{tag}]+|\S.*?[^\s{tag}\n]) 1059 | (?P [{pnct}]*) 1060 | {tag} 1061 | (?P $|[\[\]}}<]|(?=[{pnct}]{{1,2}}[^0-9]|\s|\))) 1062 | """.format( 1063 | **{'tag': tag, 'cls': cls_re_s, 'pnct': pnct, 'space': 1064 | regex_snippets['space']} 1065 | ), flags=re.X | re.U) 1066 | text = pattern.sub(self.fSpan, text) 1067 | self.span_depth = self.span_depth - 1 1068 | return text 1069 | 1070 | def getSpecialOptions(self, pre, tail): 1071 | for before, after in self.spanWrappers: 1072 | if pre == before and tail == after: 1073 | pre = tail = '' 1074 | break 1075 | return (pre, tail) 1076 | 1077 | def fSpan(self, match): 1078 | pre, tag, atts, cite, content, end, tail = match.groups() 1079 | pre, tail = self.getSpecialOptions(pre, tail) 1080 | 1081 | qtags = { 1082 | '*': 'strong', # noqa: E241 1083 | '**': 'b', # noqa: E241 1084 | '??': 'cite', # noqa: E241 1085 | '_': 'em', # noqa: E241 1086 | '__': 'i', # noqa: E241 1087 | '-': 'del', # noqa: E241 1088 | '%': 'span', # noqa: E241 1089 | '+': 'ins', # noqa: E241 1090 | '~': 'sub', # noqa: E241 1091 | '^': 'sup' # noqa: E241 1092 | } 1093 | 1094 | tag = qtags[tag] 1095 | atts = pba(atts, restricted=self.restricted) 1096 | if cite: 1097 | atts = '{0} cite="{1}"'.format(atts, cite.rstrip()) 1098 | 1099 | content = self.span(content) 1100 | opentag = '<{0}{1}>'.format(tag, atts) 1101 | closetag = '{0}>'.format(tag) 1102 | tags = self.storeTags(opentag, closetag) 1103 | return pre + tags['open'] + content + end + tags['close'] + tail 1104 | 1105 | def storeTags(self, opentag, closetag=''): 1106 | tags = {} 1107 | self.refIndex += 1 1108 | self.refCache[self.refIndex] = opentag 1109 | tags['open'] = self.uid + str(self.refIndex) + ':ospan ' 1110 | 1111 | self.refIndex += 1 1112 | self.refCache[self.refIndex] = closetag 1113 | tags['close'] = ' ' + self.uid + str(self.refIndex) + ':cspan' 1114 | return tags 1115 | 1116 | def retrieveTags(self, text): 1117 | text = (re.compile('{0}(?P [0-9]+):ospan '.format(self.uid), re.U) 1118 | .sub(self.fRetrieveTags, text)) 1119 | text = (re.compile(' {0}(?P [0-9]+):cspan'.format(self.uid), re.U) 1120 | .sub(self.fRetrieveTags, text)) 1121 | return text 1122 | 1123 | def fRetrieveTags(self, match): 1124 | return self.refCache[int(match.group('token'))] 1125 | 1126 | def image(self, text): 1127 | pattern = re.compile(r""" 1128 | (?:[\[{{])? # pre 1129 | \! # opening ! 1130 | (\<|\=|\>)? # optional alignment atts 1131 | ({0}) # optional style,class atts 1132 | (?:\.\s)? # optional dot-space 1133 | ([^\s(!]+) # presume this is the src 1134 | \s? # optional space 1135 | (?:\(([^\)]+)\))? # optional title 1136 | \! # closing 1137 | (?::(\S+)(?': 'right'} 1149 | 1150 | if not title: 1151 | title = '' 1152 | 1153 | if not is_rel_url(url) and self.get_sizes: 1154 | size = getimagesize(url) 1155 | 1156 | if href: 1157 | href = self.shelveURL(href) 1158 | 1159 | url = self.shelveURL(url) 1160 | 1161 | if align: 1162 | atts.update(align=alignments[align]) 1163 | atts.update(alt=title) 1164 | if size: 1165 | atts.update(height="{0}".format(size[1])) 1166 | atts.update(src=url) 1167 | if attributes: 1168 | atts.update(parse_attributes(attributes, restricted=self.restricted)) 1169 | if title: 1170 | atts.update(title=title) 1171 | if size: 1172 | atts.update(width="{0}".format(size[0])) 1173 | img = generate_tag('img', ' /', atts) 1174 | if href: 1175 | a_atts = OrderedDict(href=href) 1176 | if self.rel: 1177 | a_atts.update(rel=self.rel) 1178 | img = generate_tag('a', img, a_atts) 1179 | return img 1180 | 1181 | def code(self, text): 1182 | text = self.doSpecial(text, ' ', '
', self.fCode) 1183 | text = self.doSpecial(text, '@', '@', self.fCode) 1184 | text = self.doSpecial(text, '', '', self.fPre) 1185 | return text 1186 | 1187 | def fCode(self, match): 1188 | before, text, after = match.groups() 1189 | after = after or '' 1190 | before, after = self.getSpecialOptions(before, after) 1191 | # text needs to be escaped 1192 | text = encode_html(text, quotes=False) 1193 | return ''.join([before, self.shelve('{0}
'.format(text)), after]) 1194 | 1195 | def fPre(self, match): 1196 | before, text, after = match.groups() 1197 | if after is None: 1198 | after = '' 1199 | before, after = self.getSpecialOptions(before, after) 1200 | # text needs to be escaped 1201 | text = encode_html(text) 1202 | return ''.join([before, '', self.shelve(text), '', after]) 1203 | 1204 | def doSpecial(self, text, start, end, method): 1205 | pattern = re.compile(r'(^|\s|[\[({{>|]){0}(.*?){1}($|[\])}}])?'.format( 1206 | re.escape(start), re.escape(end)), re.M | re.S) 1207 | return pattern.sub(method, text) 1208 | 1209 | def noTextile(self, text): 1210 | text = self.doSpecial(text, '', ' ', 1211 | self.fTextile) 1212 | return self.doSpecial(text, '==', '==', self.fTextile) 1213 | 1214 | def fTextile(self, match): 1215 | before, notextile, after = match.groups() 1216 | if after is None: # pragma: no branch 1217 | after = '' 1218 | before, after = self.getSpecialOptions(before, after) 1219 | return ''.join([before, self.shelve(notextile), after]) 1220 | 1221 | def getHTMLComments(self, text): 1222 | """Search the string for HTML comments, e.g. . We 1223 | send the text that matches this to fParseHTMLComments.""" 1224 | return self.doSpecial(text, '', self.fParseHTMLComments) 1225 | 1226 | def fParseHTMLComments(self, match): 1227 | """If self.restricted is True, clean the matched contents of the HTML 1228 | comment. Otherwise, return the comments unchanged. 1229 | The original php had an if statement in here regarding restricted mode. 1230 | nose reported that this line wasn't covered. It's correct. In 1231 | restricted mode, the html comment tags have already been converted to 1232 | <!*#8212; and —> so they don't match in getHTMLComments, 1233 | and never arrive here. 1234 | """ 1235 | before, commenttext, after = match.groups() 1236 | commenttext = self.shelve(commenttext) 1237 | return '{0}'.format(before, commenttext) 1238 | 1239 | def redcloth_list(self, text): 1240 | """Parse the text for definition lists and send them to be 1241 | formatted.""" 1242 | pattern = re.compile(r"^([-]+{0}[ .].*:=.*)$(?![^-])".format(cls_re_s), 1243 | re.M | re.U | re.S) 1244 | return pattern.sub(self.fRCList, text) 1245 | 1246 | def fRCList(self, match): 1247 | """Format a definition list.""" 1248 | out = [] 1249 | text = re.split(r'\n(?=[-])', match.group(), flags=re.M) 1250 | for line in text: 1251 | # parse the attributes and content 1252 | m = re.match(r'^[-]+({0})[ .](.*)$'.format(cls_re_s), line, 1253 | flags=re.M | re.S) 1254 | if not m: 1255 | continue 1256 | 1257 | atts, content = m.groups() 1258 | # cleanup 1259 | content = content.strip() 1260 | atts = pba(atts, restricted=self.restricted) 1261 | 1262 | # split the content into the term and definition 1263 | xm = re.match( 1264 | r'^(.*?){0}*:=(.*?){0}*(=:|:=)?{0}*$' 1265 | .format(regex_snippets['space']), 1266 | content, 1267 | re.S) 1268 | term, definition, _ = xm.groups() 1269 | # cleanup 1270 | term = term.strip() 1271 | definition = definition.strip(' ') 1272 | 1273 | # if this is the first time through, out as a bool is False 1274 | if not out: 1275 | if definition == '': 1276 | dltag = "".format(atts) 1277 | else: 1278 | dltag = "
" 1279 | out.append(dltag) 1280 | 1281 | if term != '': 1282 | is_newline_started_def = definition.startswith('\n') 1283 | definition = ( 1284 | definition 1285 | .strip() 1286 | .replace('\n', '
') 1300 | out = '\n'.join(out) 1301 | return out 1302 | 1303 | def placeNoteLists(self, text): 1304 | """Parse the text for endnotes.""" 1305 | if self.notes: 1306 | o = OrderedDict() 1307 | for label, info in self.notes.items(): 1308 | if 'seq' in info: 1309 | i = info['seq'] 1310 | info['seq'] = label 1311 | o[i] = info 1312 | else: 1313 | self.unreferencedNotes[label] = info 1314 | 1315 | if o: # pragma: no branch 1316 | # sort o by key 1317 | o = OrderedDict(sorted(o.items(), key=lambda t: t[0])) 1318 | self.notes = o 1319 | text_re = re.compile(r'
')) 1287 | 1288 | if is_newline_started_def: 1289 | definition = '{0}
'.format(definition) 1290 | term = term.replace('\n', '
') 1291 | 1292 | term = self.graf(term) 1293 | definition = self.graf(definition) 1294 | 1295 | out.append('\t- {1}
'.format(atts, term)) 1296 | if definition: 1297 | out.append('\t- {0}
'.format(definition)) 1298 | 1299 | out.append('notelist({0})(?:\:([\w|{1}]))?([\^!]?)(\+?)' 1320 | r'\.?[\s]*
'.format(cls_re_s, syms_re_s), re.U) 1321 | text = text_re.sub(self.fNoteLists, text) 1322 | return text 1323 | 1324 | def fNoteLists(self, match): 1325 | """Given the text that matches as a note, format it into HTML.""" 1326 | att, start_char, g_links, extras = match.groups() 1327 | start_char = start_char or 'a' 1328 | index = '{0}{1}{2}'.format(g_links, extras, start_char) 1329 | result = '' 1330 | 1331 | if index not in self.notelist_cache: # pragma: no branch 1332 | o = [] 1333 | if self.notes: # pragma: no branch 1334 | for seq, info in self.notes.items(): 1335 | links = self.makeBackrefLink(info, g_links, start_char) 1336 | atts = '' 1337 | if 'def' in info: 1338 | infoid = info['id'] 1339 | atts = info['def']['atts'] 1340 | content = info['def']['content'] 1341 | li = ('\t\t{1} ' 1342 | '{3} ').format(atts, links, infoid, 1343 | content) 1344 | else: 1345 | li = ('\t\t{1} Undefined Note [#{2}]. ' 1346 | ).format(atts, links, info['seq']) 1347 | o.append(li) 1348 | if '+' == extras and self.unreferencedNotes: 1349 | for seq, info in self.unreferencedNotes.items(): 1350 | atts = info['def']['atts'] 1351 | content = info['def']['content'] 1352 | li = '\t\t{1} '.format(atts, content) 1353 | o.append(li) 1354 | self.notelist_cache[index] = "\n".join(o) 1355 | result = self.notelist_cache[index] 1356 | if result: 1357 | list_atts = pba(att, restricted=self.restricted) 1358 | result = '\n{1}\n\t
'.format(list_atts, result) 1359 | return result 1360 | 1361 | def makeBackrefLink(self, info, g_links, i): 1362 | """Given the pieces of a back reference link, create an tag.""" 1363 | link = '' 1364 | if 'def' in info: 1365 | link = info['def']['link'] 1366 | backlink_type = link or g_links 1367 | i_ = encode_high(i) 1368 | allow_inc = i not in syms_re_s 1369 | i_ = int(i_) 1370 | 1371 | if backlink_type == "!": 1372 | return '' 1373 | elif backlink_type == '^': 1374 | return """{1}""".format( 1375 | info['refids'][0], i) 1376 | else: 1377 | result = [] 1378 | for refid in info['refids']: 1379 | i_entity = decode_high(i_) 1380 | sup = """{1}""".format( 1381 | refid, i_entity) 1382 | if allow_inc: 1383 | i_ = i_ + 1 1384 | result.append(sup) 1385 | result = ' '.join(result) 1386 | return result 1387 | 1388 | def fParseNoteDefs(self, m): 1389 | """Parse the note definitions and format them as HTML""" 1390 | label = m.group('label') 1391 | link = m.group('link') 1392 | att = m.group('att') 1393 | content = m.group('content') 1394 | 1395 | # Assign an id if the note reference parse hasn't found the label yet. 1396 | if label not in self.notes: 1397 | self.notes[label] = {'id': '{0}{1}'.format( 1398 | self.linkPrefix, self._increment_link_index())} 1399 | 1400 | # Ignores subsequent defs using the same label 1401 | if 'def' not in self.notes[label]: # pragma: no branch 1402 | self.notes[label]['def'] = { 1403 | 'atts': pba(att, restricted=self.restricted), 'content': 1404 | self.graf(content), 'link': link} 1405 | return '' 1406 | 1407 | def noteRef(self, text): 1408 | """Search the text looking for note references.""" 1409 | text_re = re.compile(r""" 1410 | \[ # start 1411 | ({0}) # !atts 1412 | \# 1413 | ([^\]!]+) # !label 1414 | ([!]?) # !nolink 1415 | \]""".format(cls_re_s), re.X) 1416 | text = text_re.sub(self.fParseNoteRefs, text) 1417 | return text 1418 | 1419 | def fParseNoteRefs(self, match): 1420 | """Parse and format the matched text into note references. 1421 | By the time this function is called, all the defs will have been 1422 | processed into the notes array. So now we can resolve the link numbers 1423 | in the order we process the refs...""" 1424 | atts, label, nolink = match.groups() 1425 | atts = pba(atts, restricted=self.restricted) 1426 | nolink = nolink == '!' 1427 | 1428 | # Assign a sequence number to this reference if there isn't one already 1429 | if label in self.notes: 1430 | num = self.notes[label]['seq'] 1431 | else: 1432 | self.notes[label] = { 1433 | 'seq': self.note_index, 'refids': [], 'id': '' 1434 | } 1435 | num = self.note_index 1436 | self.note_index = self.note_index + 1 1437 | 1438 | # Make our anchor point and stash it for possible use in backlinks when 1439 | # the note list is generated later... 1440 | refid = '{0}{1}'.format(self.linkPrefix, self._increment_link_index()) 1441 | self.notes[label]['refids'].append(refid) 1442 | 1443 | # If we are referencing a note that hasn't had the definition parsed 1444 | # yet, then assign it an ID... 1445 | if not self.notes[label]['id']: 1446 | self.notes[label]['id'] = '{0}{1}'.format( 1447 | self.linkPrefix, self._increment_link_index()) 1448 | labelid = self.notes[label]['id'] 1449 | 1450 | # Build the link (if any)... 1451 | result = '{1}'.format(refid, num) 1452 | if not nolink: 1453 | result = '{1}'.format(labelid, result) 1454 | 1455 | # Build the reference... 1456 | result = '{1}'.format(atts, result) 1457 | return result 1458 | 1459 | def shelveURL(self, text): 1460 | if text == '': 1461 | return '' 1462 | self.refIndex = self.refIndex + 1 1463 | self.refCache[self.refIndex] = text 1464 | output = '{0}{1}{2}'.format(self.uid, self.refIndex, ':url') 1465 | return output 1466 | 1467 | def retrieveURLs(self, text): 1468 | return re.sub(r'{0}(?P[0-9]+):url'.format(self.uid), self.retrieveURL, text) 1469 | 1470 | def retrieveURL(self, match): 1471 | url = self.refCache.get(int(match.group('token')), '') 1472 | if url == '': 1473 | return url 1474 | 1475 | if url in self.urlrefs: 1476 | url = self.urlrefs[url] 1477 | 1478 | return url 1479 | 1480 | def _increment_link_index(self): 1481 | """The self.linkIndex property needs to be incremented in various 1482 | places. Don't Repeat Yourself.""" 1483 | self.linkIndex = self.linkIndex + 1 1484 | return self.linkIndex 1485 | 1486 | 1487 | def textile(text, html_type='xhtml'): 1488 | """ 1489 | Apply Textile to a block of text. 1490 | 1491 | This function takes the following additional parameters: 1492 | 1493 | html_type - 'xhtml' or 'html5' style tags (default: 'xhtml') 1494 | 1495 | """ 1496 | return Textile(html_type=html_type).parse(text) 1497 | 1498 | 1499 | def textile_restricted(text, lite=True, noimage=True, html_type='xhtml'): 1500 | """ 1501 | Apply Textile to a block of text, with restrictions designed for weblog 1502 | comments and other untrusted input. Raw HTML is escaped, style attributes 1503 | are disabled, and rel='nofollow' is added to external links. 1504 | 1505 | This function takes the following additional parameters: 1506 | 1507 | html_type - 'xhtml' or 'html5' style tags (default: 'xhtml') 1508 | lite - restrict block tags to p, bq, and bc, disable tables (default: True) 1509 | noimage - disable image tags (default: True) 1510 | 1511 | """ 1512 | return Textile(restricted=True, lite=lite, noimage=noimage, 1513 | html_type=html_type, rel='nofollow').parse(text) 1514 | -------------------------------------------------------------------------------- /textile/objects/__init__.py: -------------------------------------------------------------------------------- 1 | from .block import Block 2 | from .table import Table 3 | 4 | __all__ = ['Block', 'Table'] 5 | -------------------------------------------------------------------------------- /textile/objects/block.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from collections import OrderedDict 3 | try: 4 | import regex as re 5 | except ImportError: 6 | import re 7 | 8 | from textile.regex_strings import cls_re_s, regex_snippets 9 | from textile.utils import encode_html, generate_tag, parse_attributes 10 | 11 | 12 | class Block(object): 13 | def __init__(self, textile, tag, atts, ext, cite, content): 14 | self.textile = textile 15 | self.tag = tag 16 | self.atts = atts 17 | self.ext = ext 18 | self.cite = cite 19 | self.content = content 20 | 21 | self.attributes = parse_attributes(atts, restricted=self.textile.restricted) 22 | self.outer_tag = '' 23 | self.inner_tag = '' 24 | self.outer_atts = OrderedDict() 25 | self.inner_atts = OrderedDict() 26 | self.eat = False 27 | self.process() 28 | 29 | def process(self): 30 | if self.tag == 'p': 31 | # is this an anonymous block with a note definition? 32 | notedef_re = re.compile(r""" 33 | ^note\# # start of note def marker 34 | (?P