diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..9f8ccc3 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = + # line too long + E501 +exclude = + build/ diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml new file mode 100644 index 0000000..14ced37 --- /dev/null +++ b/.github/workflows/lint_and_test.yml @@ -0,0 +1,33 @@ +--- +name: python-textile + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"] + image_size: ['true', 'false'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Python flake8 Lint + uses: py-actions/flake8@v2.3.0 + - name: Install dependencies + run: | + imagesize='' + pip install -U pytest pytest-cov coverage codecov + if [[ ${{ matrix.image_size }} == true ]] ; then imagesize='[imagesize]' ; fi + pip install -e ".${imagesize}" + - name: run tests + run: | + pytest + - name: Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3f7d77c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -dist: xenial # required for Python >= 3.7 -language: python -env: - - IMAGESIZE=true - - IMAGESIZE=false -python: - - "3.5" - - "3.6" - - "3.7" - - "3.8" - - "3.9" - # PyPy versions - - "pypy3" -# command to install dependencies -install: - - imagesize='' - - pip install -U pytest pytest-cov coverage codecov - - if [[ $IMAGESIZE == true ]] ; then imagesize='[imagesize]' ; fi - - pip install -e ".${imagesize}" -# command to run tests -script: py.test -after_success: - - codecov diff --git a/README.textile b/README.textile index 98f4fbd..797516c 100644 --- a/README.textile +++ b/README.textile @@ -1,4 +1,4 @@ -!https://travis-ci.org/textile/python-textile.svg!:https://travis-ci.org/textile/python-textile !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! +!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! h1. python-textile diff --git a/setup.py b/setup.py index 118c2fb..b14b9a6 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ -from setuptools import setup, find_packages -from setuptools.command.test import test as TestCommand import os -import sys +from setuptools import setup, find_packages def get_version(): @@ -12,6 +10,7 @@ def get_version(): return variables.get('VERSION') raise RuntimeError('No version info found.') + setup( name='textile', version=get_version(), @@ -29,18 +28,18 @@ def get_version(): 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Software Development :: Libraries :: Python Modules', ], keywords='textile,text,html markup', install_requires=[ 'html5lib>=1.0.1', 'regex>1.0; implementation_name != "pypy"', - ], + ], extras_require={ 'develop': ['pytest', 'pytest-cov'], 'imagesize': ['Pillow>=3.0.0'], @@ -49,5 +48,5 @@ def get_version(): tests_require=['pytest', 'pytest-cov'], include_package_data=True, zip_safe=False, - python_requires='>=3.5', + python_requires='>=3.8', ) diff --git a/tests/fixtures/README.txt b/tests/fixtures/README.txt index 949db70..a485943 100644 --- a/tests/fixtures/README.txt +++ b/tests/fixtures/README.txt @@ -1,4 +1,4 @@ -

+

python-textile

python-textile

diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 70da842..b07712f 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -1,5 +1,5 @@ from textile.utils import parse_attributes -import re + def test_parse_attributes(): assert parse_attributes('\\1', element='td') == {'colspan': '1'} diff --git a/tests/test_block.py b/tests/test_block.py index 44f3ea2..691d21a 100644 --- a/tests/test_block.py +++ b/tests/test_block.py @@ -8,6 +8,7 @@ except ImportError: from ordereddict import OrderedDict + def test_block(): t = textile.Textile() result = t.block('h1. foobar baby') @@ -16,15 +17,14 @@ def test_block(): b = Block(t, "bq", "", None, "", "Hello BlockQuote") expect = ('blockquote', OrderedDict(), 'p', OrderedDict(), - 'Hello BlockQuote') + 'Hello BlockQuote') result = (b.outer_tag, b.outer_atts, b.inner_tag, b.inner_atts, b.content) assert result == expect b = Block(t, "bq", "", None, "http://google.com", "Hello BlockQuote") - citation = '{0}1:url'.format(t.uid) expect = ('blockquote', OrderedDict([('cite', - '{0.uid}{0.refIndex}:url'.format(t))]), 'p', OrderedDict(), - 'Hello BlockQuote') + '{0.uid}{0.refIndex}:url'.format(t))]), 'p', OrderedDict(), + 'Hello BlockQuote') result = (b.outer_tag, b.outer_atts, b.inner_tag, b.inner_atts, b.content) assert result == expect @@ -40,6 +40,7 @@ def test_block(): result = (b.outer_tag, b.outer_atts, b.inner_tag, b.inner_atts, b.content) assert result == expect + def test_block_tags_false(): t = textile.Textile(block_tags=False) assert t.block_tags is False @@ -48,6 +49,7 @@ def test_block_tags_false(): expect = 'test' assert result == expect + def test_blockcode_extended(): input = 'bc.. text\nmoretext\n\nevenmoretext\n\nmoremoretext\n\np. test' expect = '
text\nmoretext\n\nevenmoretext\n\nmoremoretext
\n\n\t

test

' @@ -55,6 +57,7 @@ def test_blockcode_extended(): result = t.parse(input) assert result == expect + def test_blockcode_in_README(): with open('README.textile') as f: readme = ''.join(f.readlines()) @@ -63,6 +66,7 @@ def test_blockcode_in_README(): expect = ''.join(f.readlines()) assert result == expect + def test_blockcode_comment(): input = '###.. block comment\nanother line\n\np. New line' expect = '\t

New line

' @@ -70,6 +74,7 @@ def test_blockcode_comment(): result = t.parse(input) assert result == expect + def test_extended_pre_block_with_many_newlines(): """Extra newlines in an extended pre block should not get cut down to only two.""" diff --git a/tests/test_cli.py b/tests/test_cli.py index 5f6e501..5e6ab79 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,28 +3,30 @@ import textile + def test_console_script(): command = [sys.executable, '-m', 'textile', 'README.textile'] try: result = subprocess.check_output(command) except AttributeError: command[2] = 'textile.__main__' - result = subprocess.Popen(command, - stdout=subprocess.PIPE).communicate()[0] + result = subprocess.Popen( + command, stdout=subprocess.PIPE).communicate()[0] with open('tests/fixtures/README.txt') as f: expect = ''.join(f.readlines()) - if type(result) == bytes: + if isinstance(result, bytes): result = result.decode('utf-8') assert result == expect + def test_version_string(): command = [sys.executable, '-m', 'textile', '-v'] try: result = subprocess.check_output(command) except AttributeError: command[2] = 'textile.__main__' - result = subprocess.Popen(command, - stdout=subprocess.PIPE).communicate()[0] - if type(result) == bytes: + result = subprocess.Popen( + command, stdout=subprocess.PIPE).communicate()[0] + if isinstance(result, bytes): result = result.decode('utf-8') assert result.strip() == textile.__version__ diff --git a/tests/test_footnoteRef.py b/tests/test_footnoteRef.py index b773ad2..5ac2ea4 100644 --- a/tests/test_footnoteRef.py +++ b/tests/test_footnoteRef.py @@ -1,5 +1,5 @@ from textile import Textile -import re + def test_footnoteRef(): t = Textile() diff --git a/tests/test_getRefs.py b/tests/test_getRefs.py index d3cfcd7..8a22d4f 100644 --- a/tests/test_getRefs.py +++ b/tests/test_getRefs.py @@ -1,5 +1,6 @@ from textile import Textile + def test_getRefs(): t = Textile() result = t.getRefs("some text [Google]http://www.google.com") diff --git a/tests/test_getimagesize.py b/tests/test_getimagesize.py index 43f85e3..3a3c0a9 100644 --- a/tests/test_getimagesize.py +++ b/tests/test_getimagesize.py @@ -3,6 +3,7 @@ PIL = pytest.importorskip('PIL') + def test_imagesize(): assert getimagesize("http://www.google.com/intl/en_ALL/images/logo.gif") == (276, 110) assert getimagesize("http://bad.domain/") == '' diff --git a/tests/test_github_issues.py b/tests/test_github_issues.py index 2507e5f..c08d067 100644 --- a/tests/test_github_issues.py +++ b/tests/test_github_issues.py @@ -3,51 +3,60 @@ import textile + def test_github_issue_16(): result = textile.textile('"$":http://google.com "$":https://google.com "$":mailto:blackhole@sun.comet') expect = '\t

google.com google.com blackhole@sun.comet

' assert result == expect + def test_github_issue_17(): result = textile.textile('!http://www.ox.ac.uk/favicon.ico!') expect = '\t

' assert result == expect + def test_github_issue_20(): text = 'This is a link to a ["Wikipedia article about Textile":http://en.wikipedia.org/wiki/Textile_(markup_language)].' result = textile.textile(text) expect = '\t

This is a link to a Wikipedia article about Textile.

' assert result == expect + def test_github_issue_21(): - text = '''h1. xml example + text = ('''h1. xml example -bc. +bc. ''' + ''' bar -''' +''') result = textile.textile(text) expect = '\t

xml example

\n\n
\n<foo>\n  bar\n</foo>
' assert result == expect + def test_github_issue_22(): text = '''_(artist-name)Ty Segall_’s''' result = textile.textile(text) expect = '\t

Ty Segall’s

' assert result == expect + def test_github_issue_26(): text = '' result = textile.textile(text) expect = '' assert result == expect + def test_github_issue_27(): 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.")""" result = textile.textile(test) expect = """\t""" assert result == expect + def test_github_issue_28(): test = """So here I am porting my ancient "newspipe":newspipe "front-end":blog/2006/09/30/0950 to "Snakelets":Snakelets and "Python":Python, and I've just trimmed down over 20 lines of "PHP":PHP down to essentially one line of "BeautifulSoup":BeautifulSoup retrieval: @@ -80,23 +89,26 @@ def parseWapProfile(self, url): \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.

""") assert result == expect + def test_github_issue_30(): - text ='"Tëxtíle (Tëxtíle)":http://lala.com' + text = '"Tëxtíle (Tëxtíle)":http://lala.com' result = textile.textile(text) expect = '\t

Tëxtíle

' assert result == expect - text ='!http://lala.com/lol.gif(♡ imáges)!' + text = '!http://lala.com/lol.gif(♡ imáges)!' result = textile.textile(text) expect = '\t

♡ imáges

' assert result == expect + def test_github_issue_36(): text = '"Chögyam Trungpa":https://www.google.com/search?q=Chögyam+Trungpa' result = textile.textile(text) expect = '\t

Chögyam Trungpa

' assert result == expect + def test_github_issue_37(): text = '# xxx\n# yyy\n*blah*' result = textile.textile(text) @@ -118,24 +130,28 @@ def test_github_issue_37(): \t''' assert result == expect + def test_github_issue_40(): text = '\r\n' result = textile.textile(text) expect = '\r\n' assert result == expect + def test_github_issue_42(): text = '!./image.png!' result = textile.textile(text) expect = '\t

' assert result == expect + def test_github_issue_43(): text = 'pre. smart ‘quotes’ are not smart!' result = textile.textile(text) expect = '
smart ‘quotes’ are not smart!
' assert result == expect + def test_github_issue_45(): """Incorrect transform unicode url""" 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' @@ -143,6 +159,7 @@ def test_github_issue_45(): expect = '\t

test

' assert result == expect + def test_github_issue_46(): """Key error on mal-formed numbered lists. CAUTION: both the input and the ouput are ugly.""" @@ -153,6 +170,7 @@ def test_github_issue_46(): result = textile.textile(text) assert result == expect + def test_github_issue_47(): """Incorrect wrap pre-formatted value""" text = '''pre.. word @@ -172,6 +190,7 @@ def test_github_issue_47(): yet anothe word''' assert result == expect + def test_github_issue_49(): """Key error on russian hash-route link""" s = '"link":https://ru.vuejs.org/v2/guide/components.html#Входные-параметры' @@ -179,6 +198,7 @@ def test_github_issue_49(): expect = '\t

link

' assert result == expect + def test_github_issue_50(): """Incorrect wrap code with Java generics in pre""" test = ('pre.. public class Tynopet {}\n\nfinal ' @@ -189,6 +209,7 @@ def test_github_issue_50(): 'ArrayList<>();') assert result == expect + def test_github_issue_51(): """Link build with $ sign without "http" prefix broken.""" test = '"$":www.google.com.br' @@ -196,6 +217,7 @@ def test_github_issue_51(): expect = '\t

www.google.com.br

' assert result == expect + def test_github_issue_52(): """Table build without space after aligment raise a AttributeError.""" test = '|=.First Header |=. Second Header |' @@ -205,6 +227,7 @@ def test_github_issue_52(): '\n\t\t\n\t') assert result == expect + def test_github_issue_55(): """Incorrect handling of quote entities in extended pre block""" test = ('pre.. this is the first line\n\nbut "quotes" in an extended pre ' @@ -258,15 +281,17 @@ def test_github_issue_55(): 'return configs;\n}\n}') assert result == expect + def test_github_issue_56(): """Empty description lists throw error""" result = textile.textile("- :=\n-") expect = '
\n
' assert result == expect + def test_github_pull_61(): """Fixed code block multiline encoding on quotes/span""" - test = '''bc.. This is some TEXT inside a "Code BLOCK" + test = ('''bc.. This is some TEXT inside a "Code BLOCK" { if (JSON) { @@ -275,11 +300,12 @@ def test_github_pull_61(): } } -Back to 10-4 CAPS +Back to 10-4 CAPS ''' + ''' p.. Some multiline Paragragh -Here is some output!!! "Some" CAPS''' +Here is some output!!! "Some" CAPS''') expect = '''
This is some TEXT inside a "Code BLOCK"
 
@@ -299,6 +325,7 @@ def test_github_pull_61():
     result = t.parse(test)
     assert result == expect
 
+
 def test_github_pull_62():
     """Fix for paragraph multiline, only last paragraph is rendered
     correctly"""
@@ -341,6 +368,7 @@ def test_github_pull_62():
     result = t.parse(test)
     assert result == expect
 
+
 def test_github_pull_63():
     """Forgot to set multiline_para to False"""
     test = '''p.. First one 'is'
diff --git a/tests/test_glyphs.py b/tests/test_glyphs.py
index 56b0d27..ed50ad5 100644
--- a/tests/test_glyphs.py
+++ b/tests/test_glyphs.py
@@ -1,5 +1,6 @@
 from textile import Textile
 
+
 def test_glyphs():
     t = Textile()
 
diff --git a/tests/test_image.py b/tests/test_image.py
index aad39e2..b746292 100644
--- a/tests/test_image.py
+++ b/tests/test_image.py
@@ -1,5 +1,6 @@
 from textile import Textile
 
+
 def test_image():
     t = Textile()
     result = t.image('!/imgs/myphoto.jpg!:http://jsamsa.com')
@@ -17,5 +18,5 @@ def test_image():
     t = Textile(rel='nofollow')
     result = t.image('!/imgs/myphoto.jpg!:http://jsamsa.com')
     expect = (''.format(t.uid))
+              '/>'.format(t.uid))
     assert result == expect
diff --git a/tests/test_imagesize.py b/tests/test_imagesize.py
index 112989e..e7d9d88 100644
--- a/tests/test_imagesize.py
+++ b/tests/test_imagesize.py
@@ -1,10 +1,11 @@
 import textile
 
+
 def test_imagesize():
     imgurl = 'http://www.google.com/intl/en_ALL/images/srpr/logo1w.png'
     result = textile.tools.imagesize.getimagesize(imgurl)
     try:
-        import PIL
+        import PIL  # noqa: F401
 
         expect = (275, 95)
         assert result == expect
diff --git a/tests/test_lists.py b/tests/test_lists.py
index 4e85f4c..06d13c3 100644
--- a/tests/test_lists.py
+++ b/tests/test_lists.py
@@ -1,5 +1,6 @@
 from textile import Textile
 
+
 def test_lists():
     t = Textile()
     result = t.textileLists("* one\n* two\n* three")
diff --git a/tests/test_retrieve.py b/tests/test_retrieve.py
index 10bd173..a416524 100644
--- a/tests/test_retrieve.py
+++ b/tests/test_retrieve.py
@@ -1,5 +1,6 @@
 from textile import Textile
 
+
 def test_retrieve():
     t = Textile()
     id = t.shelve("foobar")
diff --git a/tests/test_span.py b/tests/test_span.py
index bafe01c..7ae5b4b 100644
--- a/tests/test_span.py
+++ b/tests/test_span.py
@@ -1,10 +1,11 @@
 from textile import Textile
 
+
 def test_span():
     t = Textile()
     result = t.retrieveTags(t.span("hello %(bob)span *strong* and **bold**% goodbye"))
     expect = ('hello span strong and '
-            'bold goodbye')
+              'bold goodbye')
     assert result == expect
 
     result = t.retrieveTags(t.span('%:http://domain.tld test%'))
diff --git a/tests/test_subclassing.py b/tests/test_subclassing.py
index 9235e03..a7db99a 100644
--- a/tests/test_subclassing.py
+++ b/tests/test_subclassing.py
@@ -1,10 +1,10 @@
 import textile
 
+
 def test_change_glyphs():
     class TextilePL(textile.Textile):
         glyph_definitions = dict(textile.Textile.glyph_definitions,
-            quote_double_open = '„'
-        )
+                                 quote_double_open='„')
 
     test = 'Test "quotes".'
     expect = '\t

Test „quotes”.

' diff --git a/tests/test_table.py b/tests/test_table.py index 0a3cb0d..1ea34e9 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -1,5 +1,6 @@ from textile import Textile + def test_table(): t = Textile() result = t.table('(rowclass). |one|two|three|\n|a|b|c|') diff --git a/tests/test_textile.py b/tests/test_textile.py index e66e2dc..0b2abca 100644 --- a/tests/test_textile.py +++ b/tests/test_textile.py @@ -4,10 +4,12 @@ import re import textile + def test_FootnoteReference(): html = textile.textile('YACC[1]') assert re.search(r'^\t

YACC1

', html) is not None + def test_Footnote(): html = textile.textile('This is covered elsewhere[1].\n\nfn1. Down here, in fact.\n\nfn2. Here is another footnote.') assert re.search(r'^\t

This is covered elsewhere1.

\n\n\t

1 Down here, in fact.

\n\n\t

2 Here is another footnote.

$', html) is not None @@ -24,6 +26,7 @@ def test_Footnote(): html = textile.textile('''See[4!] for details.\n\nfn4^. Here are the details.''') assert re.search(r'^\t

See4 for details.

\n\n\t

4 Here are the details.

$', html) is not None + def test_issue_35(): result = textile.textile('"z"') expect = '\t

“z”

' @@ -33,8 +36,9 @@ def test_issue_35(): expect = '\t

“ z”

' assert result == expect + def test_restricted(): - #Note that the HTML is escaped, thus rendering the " result = textile.textile_restricted(test) expect = "\t

Here is some text.
\n<script>alert(‘hello world’)</script>

" @@ -93,10 +97,12 @@ def test_restricted(): assert result == expect + def test_unicode_footnote(): html = textile.textile('текст[1]') assert re.compile(r'^\t

текст1

$', re.U).search(html) is not None + def test_autolinking(): test = """some text "test":http://www.google.com http://www.google.com "$":http://www.google.com""" result = """\t

some text test http://www.google.com www.google.com

""" @@ -104,6 +110,7 @@ def test_autolinking(): assert result == expect + def test_sanitize(): test = "a paragraph of benign text" result = "\t

a paragraph of benign text

" @@ -120,14 +127,16 @@ def test_sanitize(): expect = textile.Textile(html_type='html5').parse(test, sanitize=True) assert result == expect + def test_imagesize(): - PIL = pytest.importorskip('PIL') + PIL = pytest.importorskip('PIL') # noqa: F841 test = "!http://www.google.com/intl/en_ALL/images/srpr/logo1w.png!" result = '\t

' expect = textile.Textile(get_sizes=True).parse(test) assert result == expect + def test_endnotes_simple(): test = """Scientists say the moon is slowly shrinking[#my_first_label].\n\nnotelist!.\n\nnote#my_first_label Over the past billion years, about a quarter of the moon's 4.5 billion-year lifespan, it has shrunk about 200 meters (700 feet) in diameter.""" html = textile.textile(test) @@ -135,6 +144,7 @@ def test_endnotes_simple(): result_re = re.compile(result_pattern) assert result_re.search(html) is not None + def test_endnotes_complex(): test = """Tim Berners-Lee is one of the pioneer voices in favour of Net Neutrality[#netneutral] and has expressed the view that ISPs should supply "connectivity with no strings attached"[#netneutral!] [#tbl_quote]\n\nBerners-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,"[#slashes]\n\nnote#netneutral. "Web creator rejects net tracking":http://news.bbc.co.uk/2/hi/technology/7613201.stm. BBC. 15 September 2008\n\nnote#tbl_quote. "Web inventor's warning on spy software":http://www.telegraph.co.uk/news/uknews/1581938/Web-inventor%27s-warning-on-spy-software.html. The Daily Telegraph (London). 25 May 2008\n\nnote#slashes. "Berners-Lee 'sorry' for slashes":http://news.bbc.co.uk/1/hi/technology/8306631.stm. BBC. 14 October 2009\n\nnotelist.""" html = textile.textile(test) @@ -142,6 +152,7 @@ def test_endnotes_complex(): result_re = re.compile(result_pattern) assert result_re.search(html) is not None + def test_endnotes_unreferenced_note(): test = """Scientists say[#lavader] the moon is quite small. But I, for one, don't believe them. Others claim it to be made of cheese[#aardman]. If this proves true I suspect we are in for troubled times[#apollo13] as people argue over their "share" of the moon's cheese. In the end, its limited size[#lavader] may prove problematic.\n\nnote#lavader(noteclass). "Proof of the small moon hypothesis":http://antwrp.gsfc.nasa.gov/apod/ap080801.html. Copyright(c) Laurent Laveder\n\nnote#aardman(#noteid). "Proof of a cheese moon":http://www.imdb.com/title/tt0104361\n\nnote#apollo13. After all, things do go "wrong":http://en.wikipedia.org/wiki/Apollo_13#The_oxygen_tank_incident.\n\nnotelist{padding:1em; margin:1em; border-bottom:1px solid gray}.\n\nnotelist{padding:1em; margin:1em; border-bottom:1px solid gray}:§^.\n\nnotelist{padding:1em; margin:1em; border-bottom:1px solid gray}:‡""" html = textile.textile(test) @@ -149,6 +160,7 @@ def test_endnotes_unreferenced_note(): result_re = re.compile(result_pattern, re.U) assert result_re.search(html) is not None + def test_endnotes_malformed(): test = """Scientists say[#lavader] the moon is quite small. But I, for one, don't believe them. Others claim it to be made of cheese[#aardman]. If this proves true I suspect we are in for troubled times[#apollo13!] as people argue over their "share" of the moon's cheese. In the end, its limited size[#lavader] may prove problematic.\n\nnote#unused An unreferenced note.\n\nnote#lavader^ "Proof of the small moon hypothesis":http://antwrp.gsfc.nasa.gov/apod/ap080801.html. Copyright(c) Laurent Laveder\n\nnote#aardman^ "Proof of a cheese moon":http://www.imdb.com/title/tt0104361\n\nnote#apollo13^ After all, things do go "wrong":http://en.wikipedia.org/wiki/Apollo_13#The_oxygen_tank_incident.\n\nnotelist{padding:1em; margin:1em; border-bottom:1px solid gray}:α!+""" html = textile.textile(test) @@ -156,6 +168,7 @@ def test_endnotes_malformed(): result_re = re.compile(result_pattern, re.U) assert result_re.search(html) is not None + def test_endnotes_undefined_note(): test = """Scientists say the moon is slowly shrinking[#my_first_label].\n\nnotelist!.""" html = textile.textile(test) @@ -163,6 +176,7 @@ def test_endnotes_undefined_note(): result_re = re.compile(result_pattern) assert result_re.search(html) is not None + def test_encode_url(): # I tried adding these as doctests, but the unicode tests weren't # returning the correct results. @@ -198,21 +212,25 @@ def test_encode_url(): eurl = t.encode_url(url) assert eurl == result + def test_footnote_crosslink(): html = textile.textile('''See[2] for details, and later, reference it again[2].\n\nfn2^(footy#otherid)[en]. Here are the details.''') searchstring = r'\t

See2 for details, and later, reference it again2.

\n\n\t

2 Here are the details.

$' assert re.compile(searchstring).search(html) is not None + def test_footnote_without_reflink(): html = textile.textile('''See[3!] for details.\n\nfn3. Here are the details.''') searchstring = r'^\t

See3 for details.

\n\n\t

3 Here are the details.

$' assert re.compile(searchstring).search(html) is not None + def testSquareBrackets(): 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].""") searchstring = r'^\t

1st, 2nd, 3rd. 2 logn

\n\n\t

A closeimage.
\nA tighttextlink.
\nA footnoted link182.

' assert re.compile(searchstring).search(html) is not None + def test_html5(): """docstring for testHTML5""" @@ -221,6 +239,7 @@ def test_html5(): expect = textile.textile(test, html_type="html5") assert result == expect + def test_relURL(): t = textile.Textile() t.restricted = True diff --git a/tests/test_textilefactory.py b/tests/test_textilefactory.py index 846b927..e9fc027 100644 --- a/tests/test_textilefactory.py +++ b/tests/test_textilefactory.py @@ -1,6 +1,7 @@ from textile import textilefactory import pytest + def test_TextileFactory(): f = textilefactory.TextileFactory() result = f.process("some text here") diff --git a/tests/test_urls.py b/tests/test_urls.py index 7a9798e..1cd09f9 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from textile import Textile -import re + def test_urls(): t = Textile() @@ -54,12 +54,14 @@ def test_urls(): expect = '\t

A link that contains a\nnewline raises an exception.

' assert result == expect + def test_rel_attribute(): t = Textile(rel='nofollow') result = t.parse('"$":http://domain.tld') expect = '\t

domain.tld

' assert result == expect + def test_quotes_in_link_text(): """quotes in link text are tricky.""" test = '""this is a quote in link text"":url' diff --git a/tests/test_utils.py b/tests/test_utils.py index 7f386a9..a6e88f8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,21 +3,25 @@ from textile import utils + def test_encode_html(): result = utils.encode_html('''this is a "test" of text that's safe to ''' - 'put in an attribute.') + 'put in an attribute.') expect = ('this is a "test" of text that's safe to put in ' - 'an <html> attribute.') + 'an <html> attribute.') assert result == expect + def test_has_raw_text(): assert utils.has_raw_text('

foo bar biz baz

') is False assert utils.has_raw_text(' why yes, yes it does') is True + def test_is_rel_url(): assert utils.is_rel_url("http://www.google.com/") is False assert utils.is_rel_url("/foo") is True + def test_generate_tag(): result = utils.generate_tag('span', 'inner text', {'class': 'test'}) expect = 'inner text' diff --git a/tests/test_values.py b/tests/test_values.py index 5d15950..f08ac6f 100644 --- a/tests/test_values.py +++ b/tests/test_values.py @@ -35,7 +35,7 @@ ('h3. Header 3', '\t

Header 3

'), ('An old text\n\nbq. A block quotation.\n\nAny old text''', - '\t

An old text

\n\n\t
\n\t\t

A block quotation.

\n\t
\n\n\t

Any old text

'), + '\t

An old text

\n\n\t
\n\t\t

A block quotation.

\n\t
\n\n\t

Any old text

'), ('I _believe_ every word.', '\t

I believe every word.

'), @@ -70,8 +70,8 @@ ('p[fr]. rouge', '\t

rouge

'), ('I seriously *{color:red}blushed*\nwhen I _(big)sprouted_ that\ncorn stalk from my\n%[es]cabeza%.', - '\t

I seriously blushed
\nwhen I sprouted' - ' that
\ncorn stalk from my
\ncabeza.

'), + '\t

I seriously blushed
\nwhen I sprouted' + ' that
\ncorn stalk from my
\ncabeza.

'), ('p<. align left', '\t

align left

'), @@ -226,7 +226,7 @@ ("""table(#dvds){border-collapse:collapse}. Great films on DVD employing Textile summary, caption, thead, tfoot, two tbody elements and colgroups\n|={font-size:140%;margin-bottom:15px}. DVDs with two Textiled tbody elements\n|:\\3. 100 |{background:#ddd}|250||50|300|\n|^(header).\n|_. Title |_. Starring |_. Director |_. Writer |_. Notes |\n|~(footer).\n|\\5=. This is the tfoot, centred |\n|-(toplist){background:#c5f7f6}.\n| _The Usual Suspects_ | Benicio Del Toro, Gabriel Byrne, Stephen Baldwin, Kevin Spacey | Bryan Singer | Chris McQaurrie | One of the finest films ever made |\n| _Se7en_ | Morgan Freeman, Brad Pitt, Kevin Spacey | David Fincher | Andrew Kevin Walker | Great psychological thriller |\n| _Primer_ | David Sullivan, Shane Carruth | Shane Carruth | Shane Carruth | Amazing insight into trust and human psychology
rather than science fiction. Terrific! |\n| _District 9_ | Sharlto Copley, Jason Cope | Neill Blomkamp | Neill Blomkamp, Terri Tatchell | Social commentary layered on thick,\nbut boy is it done well |\n|-(medlist){background:#e7e895;}.\n| _Arlington Road_ | Tim Robbins, Jeff Bridges | Mark Pellington | Ehren Kruger | Awesome study in neighbourly relations |\n| _Phone Booth_ | Colin Farrell, Kiefer Sutherland, Forest Whitaker | Joel Schumacher | Larry Cohen | Edge-of-the-seat stuff in this\nshort but brilliantly executed thriller |""", """\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t
DVDs with two Textiled tbody elements
Title Starring Director Writer Notes
This is the tfoot, centred
The Usual Suspects Benicio Del Toro, Gabriel Byrne, Stephen Baldwin, Kevin Spacey Bryan Singer Chris McQaurrie One of the finest films ever made
Se7en Morgan Freeman, Brad Pitt, Kevin Spacey David Fincher Andrew Kevin Walker Great psychological thriller
Primer David Sullivan, Shane Carruth Shane Carruth Shane Carruth Amazing insight into trust and human psychology
\nrather than science fiction. Terrific!
District 9 Sharlto Copley, Jason Cope Neill Blomkamp Neill Blomkamp, Terri Tatchell Social commentary layered on thick,
\nbut boy is it done well
Arlington Road Tim Robbins, Jeff Bridges Mark Pellington Ehren Kruger Awesome study in neighbourly relations
Phone Booth Colin Farrell, Kiefer Sutherland, Forest Whitaker Joel Schumacher Larry Cohen Edge-of-the-seat stuff in this
\nshort but brilliantly executed thriller
"""), ("""-(hot) *coffee* := Hot _and_ black\n-(hot#tea) tea := Also hot, but a little less black\n-(cold) milk := Nourishing beverage for baby cows.\nCold drink that goes great with cookies. =:\n\n-(hot) coffee := Hot and black\n-(hot#tea) tea := Also hot, but a little less black\n-(cold) milk :=\nNourishing beverage for baby cows.\nCold drink that goes great with cookies. =:""", - """
\n\t
coffee
\n\t
Hot and black
\n\t
tea
\n\t
Also hot, but a little less black
\n\t
milk
\n\t
Nourishing beverage for baby cows.
\nCold drink that goes great with cookies.
\n
\n\n
\n\t
coffee
\n\t
Hot and black
\n\t
tea
\n\t
Also hot, but a little less black
\n\t
milk
\n\t

Nourishing beverage for baby cows.
\nCold drink that goes great with cookies.

\n
"""), + """
\n\t
coffee
\n\t
Hot and black
\n\t
tea
\n\t
Also hot, but a little less black
\n\t
milk
\n\t
Nourishing beverage for baby cows.
\nCold drink that goes great with cookies.
\n
\n\n
\n\t
coffee
\n\t
Hot and black
\n\t
tea
\n\t
Also hot, but a little less black
\n\t
milk
\n\t

Nourishing beverage for baby cows.
\nCold drink that goes great with cookies.

\n
"""), (""";(class#id) Term 1\n: Def 1\n: Def 2\n: Def 3""", """\t
\n\t\t
Term 1
\n\t\t
Def 1
\n\t\t
Def 2
\n\t\t
Def 3
\n\t
"""), ("""*Here is a comment*\n\nHere is *(class)a comment*\n\n*(class)Here is a class* that is a little extended and is\n*followed* by a strong word!\n\nbc. ; 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*123 test*\n\n*test 123*\n\n**123 test**\n\n**test 123**""", @@ -266,8 +266,8 @@ ('I __know__.\nI **really** __know__.', '\t

I know.
\nI really know.

'), ("I'm %{color:red}unaware%\nof most soft drinks.", '\t

I’m unaware
\nof most soft drinks.

'), ('I seriously *{color:red}blushed*\nwhen I _(big)sprouted_ that\ncorn stalk from my\n%[es]cabeza%.', - '\t

I seriously blushed
\nwhen I sprouted' - ' that
\ncorn stalk from my
\ncabeza.

'), + '\t

I seriously blushed
\nwhen I sprouted' + ' that
\ncorn stalk from my
\ncabeza.

'), ('
\n\na.gsub!( /\n
', '
\n\na.gsub!( /</, "" )\n\n
'), ('
\n\nh3. Sidebar\n\n"Hobix":http://hobix.com/\n"Ruby":http://ruby-lang.org/\n\n
\n\n' @@ -506,12 +506,14 @@ ) + @pytest.mark.parametrize("input, expected_output", xhtml_known_values) def test_KnownValuesXHTML(input, expected_output): # XHTML output = textile.textile(input, html_type='xhtml') assert output == expected_output + @pytest.mark.parametrize("input, expected_output", html_known_values) def test_KnownValuesHTML(input, expected_output): # HTML5 diff --git a/textile/__init__.py b/textile/__init__.py index bb7829f..08a242f 100644 --- a/textile/__init__.py +++ b/textile/__init__.py @@ -1,9 +1,6 @@ from __future__ import unicode_literals -import sys -import warnings - -from .core import textile, textile_restricted, Textile +from .core import textile, textile_restricted, Textile # noqa: F401 from .version import VERSION __all__ = ['textile', 'textile_restricted'] diff --git a/textile/__main__.py b/textile/__main__.py index 1845961..210c147 100644 --- a/textile/__main__.py +++ b/textile/__main__.py @@ -33,5 +33,5 @@ def main(): outfile.write(output) -if __name__ == '__main__': #pragma: no cover +if __name__ == '__main__': # pragma: no cover main() diff --git a/textile/core.py b/textile/core.py index 89ea7c3..18dc8bd 100644 --- a/textile/core.py +++ b/textile/core.py @@ -23,10 +23,10 @@ from textile.tools import sanitizer, imagesize from textile.regex_strings import (align_re_s, cls_re_s, pnct_re_s, - regex_snippets, syms_re_s, table_span_re_s) + regex_snippets, syms_re_s, table_span_re_s) from textile.utils import (decode_high, encode_high, encode_html, generate_tag, - has_raw_text, is_rel_url, is_valid_url, list_type, normalize_newlines, - parse_attributes, pba) + has_raw_text, is_rel_url, is_valid_url, list_type, + normalize_newlines, parse_attributes, pba) from textile.objects import Block, Table try: @@ -149,8 +149,8 @@ def human_readable_url(url): class Textile(object): restricted_url_schemes = ('http', 'https', 'ftp', 'mailto') - unrestricted_url_schemes = restricted_url_schemes + ('file', 'tel', - 'callto', 'sftp', 'data') + unrestricted_url_schemes = restricted_url_schemes + ( + 'file', 'tel', 'callto', 'sftp', 'data') btag = ('bq', 'bc', 'notextile', 'pre', 'h[1-6]', r'fn\d+', 'p', '###') btag_lite = ('bq', 'bc', 'p') @@ -160,26 +160,26 @@ class Textile(object): doctype_whitelist = ['xhtml', 'html5'] glyph_definitions = { - 'quote_single_open': '‘', - 'quote_single_close': '’', - 'quote_double_open': '“', - 'quote_double_close': '”', - 'apostrophe': '’', - 'prime': '′', - 'prime_double': '″', - 'ellipsis': '…', - 'ampersand': '&', - 'emdash': '—', - 'endash': '–', - 'dimension': '×', - 'trademark': '™', - 'registered': '®', - 'copyright': '©', - 'half': '½', - 'quarter': '¼', - 'threequarters': '¾', - 'degrees': '°', - 'plusminus': '±', + 'quote_single_open': '‘', # noqa: E241 + 'quote_single_close': '’', # noqa: E241 + 'quote_double_open': '“', # noqa: E241 + 'quote_double_close': '”', # noqa: E241 + 'apostrophe': '’', # noqa: E241 + 'prime': '′', # noqa: E241 + 'prime_double': '″', # noqa: E241 + 'ellipsis': '…', # noqa: E241 + 'ampersand': '&', # noqa: E241 + 'emdash': '—', # noqa: E241 + 'endash': '–', # noqa: E241 + 'dimension': '×', # noqa: E241 + 'trademark': '™', # noqa: E241 + 'registered': '®', # noqa: E241 + 'copyright': '©', # noqa: E241 + 'half': '½', # noqa: E241 + 'quarter': '¼', # noqa: E241 + 'threequarters': '¾', # noqa: E241 + 'degrees': '°', # noqa: E241 + 'plusminus': '±', # noqa: E241 } spanWrappers = ( @@ -187,7 +187,7 @@ class Textile(object): ) def __init__(self, restricted=False, lite=False, noimage=False, - get_sizes=False, html_type='xhtml', rel='', block_tags=True): + get_sizes=False, html_type='xhtml', rel='', block_tags=True): """Textile properties that are common to regular textile and textile_restricted""" self.restricted = restricted @@ -210,9 +210,9 @@ def __init__(self, restricted=False, lite=False, noimage=False, self.block_tags = block_tags cur = r'' - if regex_snippets['cur']: # pragma: no branch + if regex_snippets['cur']: # pragma: no branch cur = r'(?:[{0}]{1}*)?'.format(regex_snippets['cur'], - regex_snippets['space']) + regex_snippets['space']) self.glyph_replacers = make_glyph_replacers( html_type, self.uid, self.glyph_definitions) @@ -251,9 +251,9 @@ def parse(self, text, rel=None, sanitize=False): self.blocktag_whitelist = ['bq', 'p'] text = self.block(text) else: - self.blocktag_whitelist = [ 'bq', 'p', 'bc', 'notextile', - 'pre', 'h[1-6]', - 'fn{0}+'.format(regex_snippets['digit']), '###'] + self.blocktag_whitelist = ['bq', 'p', 'bc', 'notextile', + 'pre', 'h[1-6]', 'fn{0}+'.format( + regex_snippets['digit']), '###'] text = self.block(text) text = self.placeNoteLists(text) else: @@ -290,13 +290,11 @@ def parse(self, text, rel=None, sanitize=False): def table(self, text): text = "{0}\n\n".format(text) - pattern = re.compile( - (r'^(?:table(?P_?{s}{a}{c})\.' - r'(?P.*?)\n)?^(?P{a}{c}\.? ?\|.*\|)' - r'{sp}*\n\n') - .format(**{'s': table_span_re_s, 'a': align_re_s, - 'c': cls_re_s, 'sp': regex_snippets['space']}), - flags=re.S | re.M | re.U) + pattern = re.compile(r'^(?:table(?P_?{s}{a}{c})\.' + r'(?P.*?)\n)?^(?P{a}{c}\.? ?\|.*\|)' + r'[\s]*\n\n'.format( + **{'s': table_span_re_s, 'a': align_re_s, + 'c': cls_re_s}), flags=re.S | re.M | re.U) match = pattern.search(text) if match: table = Table(self, **match.groupdict()) @@ -305,7 +303,7 @@ def table(self, text): def textileLists(self, text): pattern = re.compile(r'^((?:[*;:]+|[*;:#]*#(?:_|\d+)?){0}[ .].*)$' - r'(?![^#*;:])'.format(cls_re_s), re.U | re.M | re.S) + r'(?![^#*;:])'.format(cls_re_s), re.U | re.M | re.S) return pattern.sub(self.fTextileList, text) def fTextileList(self, match): @@ -320,7 +318,7 @@ def fTextileList(self, match): nextline = '' m = re.search(r"^(?P[#*;:]+)(?P_|\d+)?(?P{0})[ .]" - "(?P.*)$".format(cls_re_s), line, re.S) + "(?P.*)$".format(cls_re_s), line, re.S) if m: tl, start, atts, content = m.groups() content = content.strip() @@ -368,7 +366,7 @@ def fTextileList(self, match): self.olstarts[tl] = 1 nm = re.match(r"^(?P[#\*;:]+)(_|[\d]+)?{0}" - r"[ .].*".format(cls_re_s), nextline) + r"[ .].*".format(cls_re_s), nextline) if nm: nl = nm.group('nextlistitem') @@ -388,7 +386,7 @@ def fTextileList(self, match): if tl not in ls: ls[tl] = 1 itemtag = ("\n{0}\t<{1}>{2}".format(tabs, litem, content) if - showitem else '') + showitem else '') line = "<{0}l{1}{2}>{3}".format(ltype, atts, start, itemtag) else: line = ("\t<{0}{1}>{2}".format(litem, atts, content) if @@ -401,18 +399,13 @@ def fTextileList(self, match): for k, v in reversed(list(ls.items())): if len(k) > len(nl): if v != 2: - line = "{0}\n{1}".format(line, tabs, - list_type(k)) + line = "{0}\n{1}".format( + line, tabs, list_type(k)) if len(k) > 1 and v != 2: line = "{0}".format(line, litem) del ls[k] # Remember the current Textile tag: pt = tl - # This else exists in the original php version. I'm not sure how - # to come up with a case where the line would not match. I think - # it may have been necessary due to the way php returns matches. - # else: - #line = "{0}\n".format(line) result.append(line) return self.doTagBr(litem, "\n".join(result)) @@ -442,7 +435,7 @@ def doBr(self, match): re.I) .sub(r'\1
', match.group(3))) return '<{0}{1}>{2}{3}'.format(match.group(1), match.group(2), content, - match.group(4)) + match.group(4)) def block(self, text): if not self.lite: @@ -478,8 +471,8 @@ def block(self, text): eat_whitespace = False pattern = (r'^(?P{0})(?P{1}{2})\.(?P\.?)' - r'(?::(?P\S+))? (?P.*)$'.format(tre, - align_re_s, cls_re_s)) + r'(?::(?P\S+))? (?P.*)$'.format( + tre, align_re_s, cls_re_s)) match = re.search(pattern, line, flags=re.S | re.U) # tag specified on this line. if match: @@ -495,15 +488,17 @@ def block(self, text): content = out[-2] if not multiline_para: - content = generate_tag(block.inner_tag, content, - block.inner_atts) - content = generate_tag(block.outer_tag, content, - block.outer_atts) + # block will have been defined in a previous run of the + # loop + content = generate_tag(block.inner_tag, content, # noqa: F821 + block.inner_atts) # noqa: F821 + content = generate_tag(block.outer_tag, content, # noqa: F821 + block.outer_atts) # noqa: F821 out[-2] = content tag, atts, ext, cite, content = match.groups() block = Block(self, **match.groupdict()) inner_block = generate_tag(block.inner_tag, block.content, - block.inner_atts) + block.inner_atts) # code tags and raw text won't be indented inside outer_tag. if block.inner_tag != 'code' and not has_raw_text(inner_block): inner_block = "\n\t\t{0}\n\t".format(inner_block) @@ -511,7 +506,7 @@ def block(self, text): line = block.content else: line = generate_tag(block.outer_tag, inner_block, - block.outer_atts) + block.outer_atts) # pre tags and raw text won't be indented. if block.outer_tag != 'pre' and not has_raw_text(line): line = "\t{0}".format(line) @@ -543,7 +538,7 @@ def block(self, text): line = block.content else: line = generate_tag(block.outer_tag, block.content, - block.outer_atts) + block.outer_atts) line = "\t{0}".format(line) else: if block.tag in ('pre', 'notextile') or block.inner_tag == 'code': @@ -596,14 +591,15 @@ def block(self, text): def footnoteRef(self, text): # somehow php-textile gets away with not capturing the space. return re.compile(r'(?<=\S)\[(?P{0}+)(?P!?)\]' - r'(?P{1}?)'.format(regex_snippets['digit'], - regex_snippets['space']), re.U).sub(self.footnoteID, text) + r'(?P{1}?)'.format( + regex_snippets['digit'], regex_snippets['space']), + re.U).sub(self.footnoteID, text) def footnoteID(self, m): fn_att = OrderedDict({'class': 'footnote'}) if m.group('id') not in self.fn: - self.fn[m.group('id')] = '{0}{1}'.format(self.linkPrefix, - self._increment_link_index()) + self.fn[m.group('id')] = '{0}{1}'.format( + self.linkPrefix, self._increment_link_index()) fnid = self.fn[m.group('id')] fn_att['id'] = 'fnrev{0}'.format(fnid) fnid = self.fn[m.group('id')] @@ -756,7 +752,7 @@ def markStartOfLinks(self, text): linkparts = [] i = 0 - while balanced != 0 or i == 0: # pragma: no branch + while balanced != 0 or i == 0: # pragma: no branch # Starting at the end, pop off the previous part of the # slice's fragments. @@ -765,9 +761,9 @@ def markStartOfLinks(self, text): if len(possibility) > 0: # did this part inc or dec the balanced count? - if re.search(r'^\S|=$', possibility, flags=re.U): # pragma: no branch + if re.search(r'^\S|=$', possibility, flags=re.U): # pragma: no branch balanced = balanced - 1 - if re.search(r'\S$', possibility, flags=re.U): # pragma: no branch + if re.search(r'\S$', possibility, flags=re.U): # pragma: no branch balanced = balanced + 1 try: possibility = possible_start_quotes.pop() @@ -787,7 +783,7 @@ def markStartOfLinks(self, text): try: possibility = possible_start_quotes.pop() - except IndexError: # pragma: no cover + except IndexError: # pragma: no cover # If out of possible starting segments we back the # last one from the linkparts array linkparts.pop() @@ -796,7 +792,7 @@ def markStartOfLinks(self, text): # we have a closing ". if (possibility == '' or possibility.endswith(' ')): # force search exit - balanced = 0; + balanced = 0 if balanced <= 0: possible_start_quotes.append(possibility) @@ -812,7 +808,7 @@ def markStartOfLinks(self, text): # Re-assemble the link starts with a specific marker for the # next regex. o = '{0}{1}linkStartMarker:"{2}'.format(pre_link, self.uid, - link_content) + link_content) output.append(o) # Add the last part back @@ -854,14 +850,14 @@ def fLink(self, m): ) # end of $text (?:\((?P[^)]+?)\))? # $title (if any) $'''.format(cls_re_s, regex_snippets['space']), inner, - flags=re.X | re.U) + flags=re.X | re.U) atts = (m and m.group('atts')) or '' text = (m and m.group('text')) or inner title = (m and m.group('title')) or '' pop, tight = '', '' - counts = { '[': None, ']': url.count(']'), '(': None, ')': None } + counts = {'[': None, ']': url.count(']'), '(': None, ')': None} # Look for footnotes or other square-bracket delimited stuff at the end # of the url... @@ -928,13 +924,13 @@ def _closingsquarebracket(c, pop, popped, url_chars, counts, pre): # it popped = True url_chars.pop() - counts[']'] = counts[']'] - 1; - if first: # pragma: no branch + counts[']'] = counts[']'] - 1 + if first: # pragma: no branch pre = '' return pop, popped, url_chars, counts, pre def _closingparenthesis(c, pop, popped, url_chars, counts, pre): - if counts[')'] is None: # pragma: no branch + if counts[')'] is None: # pragma: no branch counts['('] = url.count('(') counts[')'] = url.count(')') @@ -949,20 +945,20 @@ def _casesdefault(c, pop, popped, url_chars, counts, pre): return pop, popped, url_chars, counts, pre cases = { - '!': _endchar, - '?': _endchar, - ':': _endchar, - ';': _endchar, - '.': _endchar, - ',': _endchar, - '>': _rightanglebracket, - ']': _closingsquarebracket, - ')': _closingparenthesis, - } - for c in url_chars[-1::-1]: # pragma: no branch + '!': _endchar, + '?': _endchar, + ':': _endchar, + ';': _endchar, + '.': _endchar, + ',': _endchar, + '>': _rightanglebracket, + ']': _closingsquarebracket, + ')': _closingparenthesis, + } + for c in url_chars[-1::-1]: # pragma: no branch popped = False - pop, popped, url_chars, counts, pre = cases.get(c, - _casesdefault)(c, pop, popped, url_chars, counts, pre) + pop, popped, url_chars, counts, pre = cases.get( + c, _casesdefault)(c, pop, popped, url_chars, counts, pre) first = False if popped is False: break @@ -988,7 +984,7 @@ def _casesdefault(c, pop, popped, url_chars, counts, pre): text = text.strip() title = encode_html(title) - if not self.noimage: # pragma: no branch + if not self.noimage: # pragma: no branch text = self.image(text) text = self.span(text) text = self.glyphs(text) @@ -1029,14 +1025,14 @@ def encode_url(self, url): """, re.X | re.U) netloc_parsed = netloc_pattern.match(parsed.netloc).groupdict() else: - netloc_parsed = {'user': '', 'password': '', 'host': '', 'port': - ''} + netloc_parsed = {'user': '', 'password': '', 'host': '', 'port': ''} # encode each component scheme = parsed.scheme user = netloc_parsed['user'] and quote(netloc_parsed['user']) - password = (netloc_parsed['password'] and - quote(netloc_parsed['password'])) + password = ( + netloc_parsed['password'] and quote(netloc_parsed['password']) + ) host = netloc_parsed['host'] port = netloc_parsed['port'] and netloc_parsed['port'] # the below splits the path portion of the url by slashes, translates @@ -1046,7 +1042,7 @@ def encode_url(self, url): # because the quote and unquote functions expects different input # types: unicode strings for PY2 and str for PY3. path_parts = (quote(unquote(pce), b'') for pce in - parsed.path.split('/')) + parsed.path.split('/')) path = '/'.join(path_parts) # put it back together @@ -1079,8 +1075,10 @@ def span(self, text): (?P<end>[{pnct}]*) {tag} (?P<tail>$|[\[\]}}<]|(?=[{pnct}]{{1,2}}[^0-9]|\s|\))) - """.format(**{'tag': tag, 'cls': cls_re_s, 'pnct': pnct, - 'space': regex_snippets['space']}), flags=re.X | re.U) + """.format( + **{'tag': tag, 'cls': cls_re_s, 'pnct': pnct, 'space': + regex_snippets['space']} + ), flags=re.X | re.U) text = pattern.sub(self.fSpan, text) self.span_depth = self.span_depth - 1 return text @@ -1097,16 +1095,16 @@ def fSpan(self, match): pre, tail = self.getSpecialOptions(pre, tail) qtags = { - '*': 'strong', - '**': 'b', - '??': 'cite', - '_': 'em', - '__': 'i', - '-': 'del', - '%': 'span', - '+': 'ins', - '~': 'sub', - '^': 'sup' + '*': 'strong', # noqa: E241 + '**': 'b', # noqa: E241 + '??': 'cite', # noqa: E241 + '_': 'em', # noqa: E241 + '__': 'i', # noqa: E241 + '-': 'del', # noqa: E241 + '%': 'span', # noqa: E241 + '+': 'ins', # noqa: E241 + '~': 'sub', # noqa: E241 + '^': 'sup' # noqa: E241 } tag = qtags[tag] @@ -1232,7 +1230,7 @@ def noTextile(self, text): def fTextile(self, match): before, notextile, after = match.groups() - if after is None: # pragma: no branch + if after is None: # pragma: no branch after = '' before, after = self.getSpecialOptions(before, after) return ''.join([before, self.shelve(notextile), after]) @@ -1259,7 +1257,7 @@ def redcloth_list(self, text): """Parse the text for definition lists and send them to be formatted.""" pattern = re.compile(r"^([-]+{0}[ .].*:=.*)$(?![^-])".format(cls_re_s), - re.M | re.U | re.S) + re.M | re.U | re.S) return pattern.sub(self.fRCList, text) def fRCList(self, match): @@ -1268,8 +1266,8 @@ def fRCList(self, match): text = re.split(r'\n(?=[-])', match.group(), flags=re.M) for line in text: # parse the attributes and content - m = re.match(r'^[-]+({0})\.? (.*)$'.format(cls_re_s), line, - flags=re.M | re.S) + m = re.match(r'^[-]+({0})[ .](.*)$'.format(cls_re_s), line, + flags=re.M | re.S) if not m: continue @@ -1334,12 +1332,12 @@ def placeNoteLists(self, text): else: self.unreferencedNotes[label] = info - if o: # pragma: no branch + if o: # pragma: no branch # sort o by key o = OrderedDict(sorted(o.items(), key=lambda t: t[0])) self.notes = o text_re = re.compile(r'<p>notelist({0})(?:\:([\w|{1}]))?([\^!]?)(\+?)' - r'\.?[\s]*</p>'.format(cls_re_s, syms_re_s), re.U) + r'\.?[\s]*</p>'.format(cls_re_s, syms_re_s), re.U) text = text_re.sub(self.fNoteLists, text) return text @@ -1350,9 +1348,9 @@ def fNoteLists(self, match): index = '{0}{1}{2}'.format(g_links, extras, start_char) result = '' - if index not in self.notelist_cache: # pragma: no branch + if index not in self.notelist_cache: # pragma: no branch o = [] - if self.notes: # pragma: no branch + if self.notes: # pragma: no branch for seq, info in self.notes.items(): links = self.makeBackrefLink(info, g_links, start_char) atts = '' @@ -1361,11 +1359,12 @@ def fNoteLists(self, match): atts = info['def']['atts'] content = info['def']['content'] li = ('\t\t<li{0}>{1}<span id="note{2}"> ' - '</span>{3}</li>').format(atts, links, infoid, - content) + '</span>{3}</li>').format(atts, links, infoid, + content) else: - li = ('\t\t<li{0}>{1} Undefined Note [#{2}].</li>' - ).format(atts, links, info['seq']) + li = ( + '\t\t<li{0}>{1} Undefined Note [#{2}].<li>' + ).format(atts, links, info['seq']) o.append(li) if '+' == extras and self.unreferencedNotes: for seq, info in self.unreferencedNotes.items(): @@ -1382,7 +1381,7 @@ def fNoteLists(self, match): def makeBackrefLink(self, info, g_links, i): """Given the pieces of a back reference link, create an <a> tag.""" - atts, content, infoid, link = '', '', '', '' + link = '' if 'def' in info: link = info['def']['link'] backlink_type = link or g_links @@ -1400,7 +1399,7 @@ def makeBackrefLink(self, info, g_links, i): for refid in info['refids']: i_entity = decode_high(i_) sup = """<sup><a href="#noteref{0}">{1}</a></sup>""".format( - refid, i_entity) + refid, i_entity) if allow_inc: i_ = i_ + 1 result.append(sup) @@ -1416,13 +1415,14 @@ def fParseNoteDefs(self, m): # Assign an id if the note reference parse hasn't found the label yet. if label not in self.notes: - self.notes[label] = {'id': '{0}{1}'.format(self.linkPrefix, - self._increment_link_index())} + self.notes[label] = {'id': '{0}{1}'.format( + self.linkPrefix, self._increment_link_index())} # Ignores subsequent defs using the same label - if 'def' not in self.notes[label]: # pragma: no branch - self.notes[label]['def'] = {'atts': pba(att, restricted=self.restricted), 'content': - self.graf(content), 'link': link} + if 'def' not in self.notes[label]: # pragma: no branch + self.notes[label]['def'] = { + 'atts': pba(att, restricted=self.restricted), 'content': + self.graf(content), 'link': link} return '' def noteRef(self, text): @@ -1464,8 +1464,8 @@ def fParseNoteRefs(self, match): # If we are referencing a note that hasn't had the definition parsed # yet, then assign it an ID... if not self.notes[label]['id']: - self.notes[label]['id'] = '{0}{1}'.format(self.linkPrefix, - self._increment_link_index()) + self.notes[label]['id'] = '{0}{1}'.format( + self.linkPrefix, self._increment_link_index()) labelid = self.notes[label]['id'] # Build the link (if any)... @@ -1531,5 +1531,4 @@ def textile_restricted(text, lite=True, noimage=True, html_type='xhtml'): """ return Textile(restricted=True, lite=lite, noimage=noimage, - html_type=html_type, rel='nofollow').parse( - text) + html_type=html_type, rel='nofollow').parse(text) diff --git a/textile/objects/block.py b/textile/objects/block.py index de993e8..8f44ad1 100644 --- a/textile/objects/block.py +++ b/textile/objects/block.py @@ -40,7 +40,7 @@ def process(self): [{space}]+ # whitespace ends def marker (?P<content>.*)$ # content""".format( space=regex_snippets['space'], cls=cls_re_s), - flags=re.X | re.U) + flags=re.X | re.U) notedef = notedef_re.sub(self.textile.fParseNoteDefs, self.content) # It will be empty if the regex matched and ate it. @@ -49,13 +49,13 @@ def process(self): self.eat = True fns = re.search(r'fn(?P<fnid>{0}+)'.format(regex_snippets['digit']), - self.tag, flags=re.U) + self.tag, flags=re.U) if fns: self.tag = 'p' fnid = self.textile.fn.get(fns.group('fnid'), None) if fnid is None: fnid = '{0}{1}'.format(self.textile.linkPrefix, - self.textile._increment_link_index()) + self.textile._increment_link_index()) # If there is an author-specified ID goes on the wrapper & the # auto-id gets pushed to the <sup> @@ -71,12 +71,11 @@ def process(self): else: supp_id = parse_attributes('(#fn{0})'.format(fnid), restricted=self.textile.restricted) - if '^' not in self.atts: sup = generate_tag('sup', fns.group('fnid'), supp_id) else: fnrev = generate_tag('a', fns.group('fnid'), {'href': - '#fnrev{0}'.format(fnid)}) + '#fnrev{0}'.format(fnid)}) sup = generate_tag('sup', fnrev, supp_id) self.content = '{0} {1}'.format(sup, self.content) diff --git a/textile/objects/table.py b/textile/objects/table.py index ea31500..2d0d7ac 100644 --- a/textile/objects/table.py +++ b/textile/objects/table.py @@ -75,8 +75,9 @@ def process(self): # search the row for a table group - thead, tfoot, or tbody grpmatchpattern = (r"(:?^\|(?P<part>{v})(?P<rgrpatts>{s}{a}{c})" - r"\.\s*$\n)?^(?P<row>.*)").format(**{'v': valign_re_s, 's': - table_span_re_s, 'a': align_re_s, 'c': cls_re_s}) + r"\.\s*$\n)?^(?P<row>.*)").format( + **{'v': valign_re_s, 's': table_span_re_s, + 'a': align_re_s, 'c': cls_re_s}) grpmatch_re = re.compile(grpmatchpattern, re.S | re.M) grpmatch = grpmatch_re.match(row.lstrip()) @@ -106,8 +107,9 @@ def process(self): ctag = 'th' cmtch = re.search(r'^(?P<catts>_?{0}{1}{2}\. )' - '(?P<cell>.*)'.format(table_span_re_s, align_re_s, - cls_re_s), cell, flags=re.S) + '(?P<cell>.*)'.format( + table_span_re_s, align_re_s, cls_re_s), + cell, flags=re.S) if cmtch: catts = cmtch.group('catts') cell_atts = parse_attributes(catts, 'td', restricted=self.textile.restricted) @@ -117,7 +119,7 @@ def process(self): if not self.textile.lite: a_pattern = r'(?P<space>{0}*)(?P<cell>.*)'.format( - regex_snippets['space']) + regex_snippets['space']) a = re.search(a_pattern, cell, flags=re.S) cell = self.textile.redcloth_list(a.group('cell')) cell = self.textile.textileLists(cell) @@ -140,8 +142,8 @@ def process(self): if rgrp: groups.append('\n\t{0}'.format(rgrp.process())) - content = '{0}{1}{2}{3}\n\t'.format(self.caption, self.colgroup, - ''.join(groups), ''.join(self.content)) + content = '{0}{1}{2}{3}\n\t'.format( + self.caption, self.colgroup, ''.join(groups), ''.join(self.content)) tbl = generate_tag('table', content, self.attributes) return '\t{0}\n\n'.format(tbl) @@ -156,6 +158,35 @@ def process(self, cap): return '\t{0}\n'.format(tag) +class Colgroup(object): + def __init__(self, cols, atts, restricted): + self.row = '' + self.attributes = atts + self.cols = cols + self.restricted = restricted + + def process(self): + enc = 'unicode' + + group_atts = parse_attributes(self.attributes, 'col', restricted=self.restricted) + colgroup = ElementTree.Element('colgroup', attrib=group_atts) + colgroup.text = '\n\t' + if self.cols is not None: + match_cols = self.cols.replace('.', '').split('|') + # colgroup is the first item in match_cols, the remaining items are + # cols. + for idx, col in enumerate(match_cols): + col_atts = parse_attributes(col.strip(), 'col', restricted=self.restricted) + ElementTree.SubElement(colgroup, 'col', col_atts) + colgrp = ElementTree.tostring(colgroup, encoding=enc) + # cleanup the extra xml declaration if it exists, (python versions + # differ) and then format the resulting string accordingly: newline and + # tab between cols and a newline at the end + xml_declaration = "<?xml version='1.0' encoding='UTF-8'?>\n" + colgrp = colgrp.replace(xml_declaration, '') + return colgrp.replace('><', '>\n\t<') + + class Row(object): def __init__(self, attributes, row): self.tag = 'tr' diff --git a/textile/regex_strings.py b/textile/regex_strings.py index deb4a4a..2e096fb 100644 --- a/textile/regex_strings.py +++ b/textile/regex_strings.py @@ -4,7 +4,7 @@ try: # Use regex module for matching uppercase characters if installed, # otherwise fall back to finding all the uppercase chars in a loop. - import regex as re + import regex as re # noqa: F401 upper_re_s = r'\p{Lu}' regex_snippets = { 'acr': r'\p{Lu}\p{Nd}', @@ -15,12 +15,12 @@ 'digit': r'\p{N}', 'space': r'(?:\p{Zs}|\v)', 'char': r'(?:[^\p{Zs}\v])', - } + } except ImportError: from sys import maxunicode upper_re_s = "".join( - [chr(c) for c in range(maxunicode) if chr(c).isupper()] - ) + [chr(c) for c in range(maxunicode) if chr(c).isupper()] + ) regex_snippets = { 'acr': r'{0}0-9'.format(upper_re_s), 'abr': r'{0}'.format(upper_re_s), @@ -33,7 +33,7 @@ 'digit': r'\d', 'space': r'(?:\s|\v)', 'char': r'\S', - } + } halign_re_s = r'(?:\<(?!>)|(?<!<)\>|\<\>|\=|[()]+(?! ))' valign_re_s = r'[\-^~]' @@ -46,10 +46,10 @@ table_span_re_s = r'(?:{0}|{1})*'.format(colspan_re_s, rowspan_re_s) # regex string to match class, style and language attributes cls_re_s = (r'(?:' - r'{c}(?:{l}(?:{s})?|{s}(?:{l})?)?|' - r'{l}(?:{c}(?:{s})?|{s}(?:{c})?)?|' - r'{s}(?:{c}(?:{l})?|{l}(?:{c})?)?' + r'{c}(?:{l}(?:{s})?|{s}(?:{l})?)?|' + r'{l}(?:{c}(?:{s})?|{s}(?:{c})?)?|' + r'{s}(?:{c}(?:{l})?|{l}(?:{c})?)?' r')?' - ).format(c=class_re_s, s=style_re_s, l=language_re_s) + ).format(c=class_re_s, s=style_re_s, l=language_re_s) pnct_re_s = r'[-!"#$%&()*+,/:;<=>?@\'\[\\\]\.^_`{|}~]' syms_re_s = '¤§µ¶†‡•∗∴◊♠♣♥♦' diff --git a/textile/utils.py b/textile/utils.py index 80e57cc..84dadcf 100644 --- a/textile/utils.py +++ b/textile/utils.py @@ -32,10 +32,12 @@ def decode_high(text): text = '&#{0};'.format(text) return html.unescape(text) + def encode_high(text): """Encode the text so that it is an appropriate HTML entity.""" return ord(text) + def encode_html(text, quotes=True): """Return text that's safe for an HTML attribute.""" a = ( @@ -51,6 +53,7 @@ def encode_html(text, quotes=True): text = text.replace(k, v) return text + def generate_tag(tag, content, attributes=None): """Generate a complete html tag using the ElementTree module. tag and content are strings, the attributes argument is a dictionary. As @@ -71,11 +74,12 @@ def generate_tag(tag, content, attributes=None): # non-ascii text being html-entity encoded. Not bad, but not entirely # matching php-textile either. element_tag = ElementTree.tostringlist(element, encoding=enc, - method='html') + method='html') element_tag.insert(len(element_tag) - 1, content) element_text = ''.join(element_tag) return element_text + def has_raw_text(text): """checks whether the text has text not already enclosed by a block tag""" r = text.strip() @@ -83,17 +87,20 @@ def has_raw_text(text): r = pattern.sub('', r).strip() return r != '' + def is_rel_url(url): """Identify relative urls.""" (scheme, netloc) = urlparse(url)[0:2] return not scheme and not netloc + def is_valid_url(url): parsed = urlparse(url) if parsed.scheme == '': return True return False + def list_type(list_string): listtypes = { list_string.endswith('*'): 'u', @@ -103,12 +110,14 @@ def list_type(list_string): } return listtypes.get(True, False) + def normalize_newlines(string): out = re.sub(r'\r\n?', '\n', string) out = re.compile(r'^[ \t]*\n', flags=re.M).sub('\n', out) out = out.strip('\n') return out + def parse_attributes(block_attributes, element=None, include_id=True, restricted=False): vAlign = {'^': 'top', '-': 'middle', '~': 'bottom'} hAlign = {'<': 'left', '=': 'center', '>': 'right', '<>': 'justify'} @@ -216,6 +225,7 @@ def parse_attributes(block_attributes, element=None, include_id=True, restricted result['width'] = width return result + def pba(block_attributes, element=None, include_id=True, restricted=False): """Parse block attributes.""" attrs = parse_attributes(block_attributes, element, include_id, restricted)