Eliminado venv y www del repositorio, agrege un requirements igual

This commit is contained in:
2020-11-22 21:14:46 -03:00
parent 18cf2d335a
commit 199a1e2a61
820 changed files with 15495 additions and 22017 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
venv/
www/

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
jinja2
markdown
pygments

View File

@@ -19,7 +19,7 @@
}); });
} }
window.addEventListener('DOMContentLoaded', (event) => { window.addEventListener('load', (event) => {
makeImagesClickeable(); makeImagesClickeable();
}); });
</script> </script>

View File

@@ -1,6 +1,6 @@
<# <#
.Synopsis .Synopsis
Activate a Python virtual environment for the current Powershell session. Activate a Python virtual environment for the current PowerShell session.
.Description .Description
Pushes the python executable for a virtual environment to the front of the Pushes the python executable for a virtual environment to the front of the
@@ -37,6 +37,15 @@ Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active. parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#> #>
Param( Param(
@@ -137,7 +146,7 @@ function Get-PyVenvConfig(
$val = $keyval[1] $val = $keyval[1]
# Remove extraneous quotations around a string value. # Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0,1))) { if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2) $val = $val.Substring(1, $val.Length - 2)
} }
@@ -165,7 +174,8 @@ Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# VenvExecDir if specified on the command line. # VenvExecDir if specified on the command line.
if ($VenvDir) { if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
} else { }
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir" Write-Verbose "VenvDir=$VenvDir"
@@ -179,7 +189,8 @@ $pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# just use the name of the virtual environment folder. # just use the name of the virtual environment folder.
if ($Prompt) { if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'" Write-Verbose "Prompt specified as argument, using '$Prompt'"
} else { }
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) { if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"

View File

@@ -37,7 +37,7 @@ deactivate () {
# unset irrelevant variables # unset irrelevant variables
deactivate nondestructive deactivate nondestructive
VIRTUAL_ENV="/home/ryuuji/src/generator/venv" VIRTUAL_ENV="/home/ryuuji/src/danielcortes.xyz/venv"
export VIRTUAL_ENV export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH" _OLD_VIRTUAL_PATH="$PATH"
@@ -59,7 +59,7 @@ if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
else else
if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then
# special case for Aspen magic directories # special case for Aspen magic directories
# see http://www.zetadev.com/software/aspen/ # see https://aspen.io/
PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1" PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1"
else else
PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1" PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1"

View File

@@ -8,7 +8,7 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA
# Unset irrelevant variables. # Unset irrelevant variables.
deactivate nondestructive deactivate nondestructive
setenv VIRTUAL_ENV "/home/ryuuji/src/generator/venv" setenv VIRTUAL_ENV "/home/ryuuji/src/danielcortes.xyz/venv"
set _OLD_VIRTUAL_PATH="$PATH" set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/bin:$PATH" setenv PATH "$VIRTUAL_ENV/bin:$PATH"
@@ -22,7 +22,7 @@ if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
else else
if (`basename "VIRTUAL_ENV"` == "__") then if (`basename "VIRTUAL_ENV"` == "__") then
# special case for Aspen magic directories # special case for Aspen magic directories
# see http://www.zetadev.com/software/aspen/ # see https://aspen.io/
set env_name = `basename \`dirname "$VIRTUAL_ENV"\`` set env_name = `basename \`dirname "$VIRTUAL_ENV"\``
else else
set env_name = `basename "$VIRTUAL_ENV"` set env_name = `basename "$VIRTUAL_ENV"`

View File

@@ -29,7 +29,7 @@ end
# unset irrelevant variables # unset irrelevant variables
deactivate nondestructive deactivate nondestructive
set -gx VIRTUAL_ENV "/home/ryuuji/src/generator/venv" set -gx VIRTUAL_ENV "/home/ryuuji/src/danielcortes.xyz/venv"
set -gx _OLD_VIRTUAL_PATH $PATH set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/bin" $PATH set -gx PATH "$VIRTUAL_ENV/bin" $PATH
@@ -59,7 +59,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
set -l _checkbase (basename "$VIRTUAL_ENV") set -l _checkbase (basename "$VIRTUAL_ENV")
if test $_checkbase = "__" if test $_checkbase = "__"
# special case for Aspen magic directories # special case for Aspen magic directories
# see http://www.zetadev.com/software/aspen/ # see https://aspen.io/
printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal) printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal)
else else
printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal) printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal)

View File

@@ -1,10 +1,8 @@
#!/home/ryuuji/src/generator/venv/bin/python #!/home/ryuuji/src/danielcortes.xyz/venv/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import sys import sys
from setuptools.command.easy_install import main from setuptools.command.easy_install import main
if __name__ == '__main__': if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main()) sys.exit(main())

View File

@@ -1,10 +1,8 @@
#!/home/ryuuji/src/generator/venv/bin/python #!/home/ryuuji/src/danielcortes.xyz/venv/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import sys import sys
from setuptools.command.easy_install import main from setuptools.command.easy_install import main
if __name__ == '__main__': if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main()) sys.exit(main())

View File

@@ -1,10 +1,8 @@
#!/home/ryuuji/src/generator/venv/bin/python #!/home/ryuuji/src/danielcortes.xyz/venv/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import sys import sys
from markdown.__main__ import run from markdown.__main__ import run
if __name__ == '__main__': if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(run()) sys.exit(run())

View File

@@ -1,10 +1,8 @@
#!/home/ryuuji/src/generator/venv/bin/python #!/home/ryuuji/src/danielcortes.xyz/venv/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import sys import sys
from pip._internal.cli.main import main
from pip._internal import main
if __name__ == '__main__': if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main()) sys.exit(main())

View File

@@ -1,10 +1,8 @@
#!/home/ryuuji/src/generator/venv/bin/python #!/home/ryuuji/src/danielcortes.xyz/venv/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import sys import sys
from pip._internal.cli.main import main
from pip._internal import main
if __name__ == '__main__': if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main()) sys.exit(main())

View File

@@ -1,10 +1,8 @@
#!/home/ryuuji/src/generator/venv/bin/python #!/home/ryuuji/src/danielcortes.xyz/venv/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import sys import sys
from pip._internal.cli.main import main
from pip._internal import main
if __name__ == '__main__': if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main()) sys.exit(main())

View File

@@ -2,6 +2,7 @@ Jinja2-2.11.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuw
Jinja2-2.11.2.dist-info/LICENSE.rst,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475 Jinja2-2.11.2.dist-info/LICENSE.rst,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475
Jinja2-2.11.2.dist-info/METADATA,sha256=5ZHRZoIRAMHsJPnqhlJ622_dRPsYePYJ-9EH4-Ry7yI,3535 Jinja2-2.11.2.dist-info/METADATA,sha256=5ZHRZoIRAMHsJPnqhlJ622_dRPsYePYJ-9EH4-Ry7yI,3535
Jinja2-2.11.2.dist-info/RECORD,, Jinja2-2.11.2.dist-info/RECORD,,
Jinja2-2.11.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
Jinja2-2.11.2.dist-info/WHEEL,sha256=kGT74LWyRUZrL4VgLh6_g12IeVl_9u9ZVhadrgXZUEY,110 Jinja2-2.11.2.dist-info/WHEEL,sha256=kGT74LWyRUZrL4VgLh6_g12IeVl_9u9ZVhadrgXZUEY,110
Jinja2-2.11.2.dist-info/entry_points.txt,sha256=Qy_DkVo6Xj_zzOtmErrATe8lHZhOqdjpt3e4JJAGyi8,61 Jinja2-2.11.2.dist-info/entry_points.txt,sha256=Qy_DkVo6Xj_zzOtmErrATe8lHZhOqdjpt3e4JJAGyi8,61
Jinja2-2.11.2.dist-info/top_level.txt,sha256=PkeVWtLb3-CqjWi1fO29OCbj55EhX_chhKrCdrVe_zs,7 Jinja2-2.11.2.dist-info/top_level.txt,sha256=PkeVWtLb3-CqjWi1fO29OCbj55EhX_chhKrCdrVe_zs,7

View File

@@ -1,29 +0,0 @@
Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later)
Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
Copyright 2004 Manfred Stienstra (the original version)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Python Markdown Project nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE PYTHON MARKDOWN PROJECT ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTORS TO THE PYTHON MARKDOWN PROJECT
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,57 +0,0 @@
Metadata-Version: 2.1
Name: Markdown
Version: 3.2.1
Summary: Python implementation of Markdown.
Home-page: https://Python-Markdown.github.io/
Author: Manfred Stienstra, Yuri takhteyev and Waylan limberg
Author-email: waylan.limberg@icloud.com
Maintainer: Waylan Limberg
Maintainer-email: waylan.limberg@icloud.com
License: BSD License
Download-URL: http://pypi.python.org/packages/source/M/Markdown/Markdown-3.2.1-py2.py3-none-any.whl
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Communications :: Email :: Filters
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
Classifier: Topic :: Software Development :: Documentation
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Filters
Classifier: Topic :: Text Processing :: Markup :: HTML
Requires-Python: >=3.5
Requires-Dist: setuptools (>=36)
Provides-Extra: testing
Requires-Dist: coverage ; extra == 'testing'
Requires-Dist: pyyaml ; extra == 'testing'
This is a Python implementation of John Gruber's Markdown_.
It is almost completely compliant with the reference implementation,
though there are a few known issues. See Features_ for information
on what exactly is supported and what is not. Additional features are
supported by the `Available Extensions`_.
.. _Markdown: https://daringfireball.net/projects/markdown/
.. _Features: https://Python-Markdown.github.io#features
.. _`Available Extensions`: https://Python-Markdown.github.io/extensions/
Support
=======
You may report bugs, ask for help, and discuss various other issues on
the `bug tracker`_.
.. _`bug tracker`: https://github.com/Python-Markdown/markdown/issues

View File

@@ -1,74 +0,0 @@
../../../bin/markdown_py,sha256=CN-Ji-5IbiekxHQIaCGuLnffDcAJe1KbGNP2M75sqGM,243
Markdown-3.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
Markdown-3.2.1.dist-info/LICENSE.md,sha256=bxGTy2NHGOZcOlN9biXr1hSCDsDvaTz8EiSBEmONZNo,1645
Markdown-3.2.1.dist-info/METADATA,sha256=PK6UzXb9yL09qZJH7SCqZd6-mj8keovCmxQLt89NlEQ,2383
Markdown-3.2.1.dist-info/RECORD,,
Markdown-3.2.1.dist-info/WHEEL,sha256=h_aVn5OB2IERUjMbi2pucmR_zzWJtk303YXvhh60NJ8,110
Markdown-3.2.1.dist-info/entry_points.txt,sha256=j4jiKg-iwZGImvi8OzotZePWoFbJJ4GrfzDqH03u3SQ,1103
Markdown-3.2.1.dist-info/top_level.txt,sha256=IAxs8x618RXoH1uCqeLLxXsDefJvE_mIibr_M4sOlyk,9
markdown/__init__.py,sha256=002-LuHviYzROW2rg_gBGai81nMouUNO9UFj5nSsTSk,2065
markdown/__main__.py,sha256=MpVK3zlwQ-4AzDzZmIScPB90PpunMGVgS5KBmJuHYTw,5802
markdown/__meta__.py,sha256=xhmwLb0Eb6kfiapdM21pCb80lyVEl8hxv8Re_X6wsI0,1837
markdown/__pycache__/__init__.cpython-38.pyc,,
markdown/__pycache__/__main__.cpython-38.pyc,,
markdown/__pycache__/__meta__.cpython-38.pyc,,
markdown/__pycache__/blockparser.cpython-38.pyc,,
markdown/__pycache__/blockprocessors.cpython-38.pyc,,
markdown/__pycache__/core.cpython-38.pyc,,
markdown/__pycache__/inlinepatterns.cpython-38.pyc,,
markdown/__pycache__/pep562.cpython-38.pyc,,
markdown/__pycache__/postprocessors.cpython-38.pyc,,
markdown/__pycache__/preprocessors.cpython-38.pyc,,
markdown/__pycache__/serializers.cpython-38.pyc,,
markdown/__pycache__/test_tools.cpython-38.pyc,,
markdown/__pycache__/treeprocessors.cpython-38.pyc,,
markdown/__pycache__/util.cpython-38.pyc,,
markdown/blockparser.py,sha256=JpBhOokOoBUGCXolftOc5m1hPcR2y9s9hVd9WSuhHzo,4285
markdown/blockprocessors.py,sha256=l4gmkAN9b2L340EX0gm24EyWS7UzBviPqX6wYrcgEco,23736
markdown/core.py,sha256=JLR5hIMwWSeIHRQhTzAymB3QUD3gHCdITFvmuuCpIcA,15360
markdown/extensions/__init__.py,sha256=6kUSgoqDT4gGUVsqf7F9oQD_jA0RJCbX5EK3JVo8iQE,3517
markdown/extensions/__pycache__/__init__.cpython-38.pyc,,
markdown/extensions/__pycache__/abbr.cpython-38.pyc,,
markdown/extensions/__pycache__/admonition.cpython-38.pyc,,
markdown/extensions/__pycache__/attr_list.cpython-38.pyc,,
markdown/extensions/__pycache__/codehilite.cpython-38.pyc,,
markdown/extensions/__pycache__/def_list.cpython-38.pyc,,
markdown/extensions/__pycache__/extra.cpython-38.pyc,,
markdown/extensions/__pycache__/fenced_code.cpython-38.pyc,,
markdown/extensions/__pycache__/footnotes.cpython-38.pyc,,
markdown/extensions/__pycache__/legacy_attrs.cpython-38.pyc,,
markdown/extensions/__pycache__/legacy_em.cpython-38.pyc,,
markdown/extensions/__pycache__/md_in_html.cpython-38.pyc,,
markdown/extensions/__pycache__/meta.cpython-38.pyc,,
markdown/extensions/__pycache__/nl2br.cpython-38.pyc,,
markdown/extensions/__pycache__/sane_lists.cpython-38.pyc,,
markdown/extensions/__pycache__/smarty.cpython-38.pyc,,
markdown/extensions/__pycache__/tables.cpython-38.pyc,,
markdown/extensions/__pycache__/toc.cpython-38.pyc,,
markdown/extensions/__pycache__/wikilinks.cpython-38.pyc,,
markdown/extensions/abbr.py,sha256=pqp2HnOR2giT-iYKyqtsp2_eUOWBR0j_hUfjvUV5c88,2916
markdown/extensions/admonition.py,sha256=HWHHjuYZPAPOg5X8hbpDuSbw8gB6k0odw8GuTT1v_N4,3124
markdown/extensions/attr_list.py,sha256=m9a1H-S33rV2twtlFYuoxSiCAf22ndU5tziSzNF2dNg,6003
markdown/extensions/codehilite.py,sha256=rVZVOIjp2KEIZsnz90mX6E2_xnwVPQZpVVQVJMuMVU0,9834
markdown/extensions/def_list.py,sha256=iqRXAEl2XnyF415afCxihAgOmEUOK1hIuBPIK1k7Tzo,3521
markdown/extensions/extra.py,sha256=udRN8OvSWcq3UwkPygvsFl1RlCVtCJ-ARVg2IwVH6VY,1831
markdown/extensions/fenced_code.py,sha256=dww9rDu2kQtkoTpjn9BBgeGCTNdE1bMPJ2wgR6695iM,3897
markdown/extensions/footnotes.py,sha256=a9sb8RoKqFU8p8ZhpTObrn_Uek0hbyPFVGYpRaEDXaw,15339
markdown/extensions/legacy_attrs.py,sha256=2EaVQkxQoNnP8_lMPvGRBdNda8L4weUQroiyEuVdS-w,2547
markdown/extensions/legacy_em.py,sha256=9ZMGCTrFh01eiOpnFjS0jVkqgYXiTzCGn-eNvYcvObg,1579
markdown/extensions/md_in_html.py,sha256=ohSiGcgR5yBqusuTs0opbTO_5fq442fqPK-klFd_qaM,4040
markdown/extensions/meta.py,sha256=EUfkzM7l7UpH__Or9K3pl8ldVddwndlCZWA3d712RAE,2331
markdown/extensions/nl2br.py,sha256=wAqTNOuf2L1NzlEvEqoID70n9y-aiYaGLkuyQk3CD0w,783
markdown/extensions/sane_lists.py,sha256=ZQmCf-247KBexVG0fc62nDvokGkV6W1uavYbieNKSG4,1505
markdown/extensions/smarty.py,sha256=0padzkVCNACainKw-Xj1S5UfT0125VCTfNejmrCZItA,10238
markdown/extensions/tables.py,sha256=bicFx_wqhnEx6Y_8MJqA56rh71pt5fOe94oiWbvcobY,7685
markdown/extensions/toc.py,sha256=E-d3R4etcM_R2sQyTpKkejRv2NHrHPCvaXK9hUqfK58,13224
markdown/extensions/wikilinks.py,sha256=GkgT9BY7b1-qW--dIwFAhC9V20RoeF13b7CFdw_V21Q,2812
markdown/inlinepatterns.py,sha256=EnYq9aU_Hi1gu5e8dcbUxUu0mRz-pHFV79uGQCYbD5I,29378
markdown/pep562.py,sha256=5UkqT7sb-cQufgbOl_jF-RYUVVHS7VThzlMzR9vrd3I,8917
markdown/postprocessors.py,sha256=25g6qqpJ4kuiq4RBrGz8RA6GMb7ArUi1AN2VDVnR35U,3738
markdown/preprocessors.py,sha256=dsmMVPP2afKAZ0s59_mFidM_mCiNfgdBJ9aVDWu_viE,15323
markdown/serializers.py,sha256=_wQl-iJrPSUEQ4Q1owWYqN9qceVh6TOlAOH_i44BKAQ,6540
markdown/test_tools.py,sha256=zFHFzmtzjfMRroyyli3LY4SP8yLfLf4S7SsU3z7Z1SQ,6823
markdown/treeprocessors.py,sha256=NBaYc9TEGP7TBaN6YRROIqE5Lj-AMoAqp0jN-coGW3Q,15401
markdown/util.py,sha256=0ySktJgYplEV7g6TOOs8fatAS4Fi-6F7iv4D9Vw3g0c,15201

View File

@@ -1,6 +0,0 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.33.4)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any

View File

@@ -1,23 +0,0 @@
[console_scripts]
markdown_py = markdown.__main__:run
[markdown.extensions]
abbr = markdown.extensions.abbr:AbbrExtension
admonition = markdown.extensions.admonition:AdmonitionExtension
attr_list = markdown.extensions.attr_list:AttrListExtension
codehilite = markdown.extensions.codehilite:CodeHiliteExtension
def_list = markdown.extensions.def_list:DefListExtension
extra = markdown.extensions.extra:ExtraExtension
fenced_code = markdown.extensions.fenced_code:FencedCodeExtension
footnotes = markdown.extensions.footnotes:FootnoteExtension
legacy_attrs = markdown.extensions.legacy_attrs:LegacyAttrExtension
legacy_em = markdown.extensions.legacy_em:LegacyEmExtension
md_in_html = markdown.extensions.md_in_html:MarkdownInHtmlExtension
meta = markdown.extensions.meta:MetaExtension
nl2br = markdown.extensions.nl2br:Nl2BrExtension
sane_lists = markdown.extensions.sane_lists:SaneListExtension
smarty = markdown.extensions.smarty:SmartyExtension
tables = markdown.extensions.tables:TableExtension
toc = markdown.extensions.toc:TocExtension
wikilinks = markdown.extensions.wikilinks:WikiLinkExtension

View File

@@ -19,11 +19,6 @@ Copyright 2004 Manfred Stienstra (the original version)
License: BSD (see LICENSE.md for details). License: BSD (see LICENSE.md for details).
""" """
try:
import packaging.version
except ImportError:
from pkg_resources.extern import packaging
# __version_info__ format: # __version_info__ format:
# (major, minor, patch, dev/alpha/beta/rc/final, #) # (major, minor, patch, dev/alpha/beta/rc/final, #)
# (1, 1, 2, 'dev', 0) => "1.1.2.dev0" # (1, 1, 2, 'dev', 0) => "1.1.2.dev0"
@@ -31,25 +26,24 @@ except ImportError:
# (1, 2, 0, 'beta', 2) => "1.2b2" # (1, 2, 0, 'beta', 2) => "1.2b2"
# (1, 2, 0, 'rc', 4) => "1.2rc4" # (1, 2, 0, 'rc', 4) => "1.2rc4"
# (1, 2, 0, 'final', 0) => "1.2" # (1, 2, 0, 'final', 0) => "1.2"
__version_info__ = (3, 2, 1, 'final', 0) __version_info__ = (3, 3, 3, 'final', 0)
def _get_version(): # pragma: no cover def _get_version(version_info):
" Returns a PEP 440-compliant version number from version_info. " " Returns a PEP 440-compliant version number from version_info. "
assert len(__version_info__) == 5 assert len(version_info) == 5
assert __version_info__[3] in ('dev', 'alpha', 'beta', 'rc', 'final') assert version_info[3] in ('dev', 'alpha', 'beta', 'rc', 'final')
parts = 2 if __version_info__[2] == 0 else 3 parts = 2 if version_info[2] == 0 else 3
v = '.'.join(map(str, __version_info__[:parts])) v = '.'.join(map(str, version_info[:parts]))
if __version_info__[3] == 'dev': if version_info[3] == 'dev':
v += '.dev' + str(__version_info__[4]) v += '.dev' + str(version_info[4])
elif __version_info__[3] != 'final': elif version_info[3] != 'final':
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'} mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'}
v += mapping[__version_info__[3]] + str(__version_info__[4]) v += mapping[version_info[3]] + str(version_info[4])
# Ensure version is valid and normalized return v
return str(packaging.version.Version(v))
__version__ = _get_version() __version__ = _get_version(__version_info__)

View File

@@ -51,6 +51,7 @@ def build_block_parser(md, **kwargs):
parser.blockprocessors.register(OListProcessor(parser), 'olist', 40) parser.blockprocessors.register(OListProcessor(parser), 'olist', 40)
parser.blockprocessors.register(UListProcessor(parser), 'ulist', 30) parser.blockprocessors.register(UListProcessor(parser), 'ulist', 30)
parser.blockprocessors.register(BlockQuoteProcessor(parser), 'quote', 20) parser.blockprocessors.register(BlockQuoteProcessor(parser), 'quote', 20)
parser.blockprocessors.register(ReferenceProcessor(parser), 'reference', 15)
parser.blockprocessors.register(ParagraphProcessor(parser), 'paragraph', 10) parser.blockprocessors.register(ParagraphProcessor(parser), 'paragraph', 10)
return parser return parser
@@ -276,7 +277,7 @@ class BlockQuoteProcessor(BlockProcessor):
RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)') RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)')
def test(self, parent, block): def test(self, parent, block):
return bool(self.RE.search(block)) return bool(self.RE.search(block)) and not util.nearing_recursion_limit()
def run(self, parent, blocks): def run(self, parent, blocks):
block = blocks.pop(0) block = blocks.pop(0)
@@ -495,16 +496,15 @@ class SetextHeaderProcessor(BlockProcessor):
class HRProcessor(BlockProcessor): class HRProcessor(BlockProcessor):
""" Process Horizontal Rules. """ """ Process Horizontal Rules. """
RE = r'^[ ]{0,3}((-+[ ]{0,2}){3,}|(_+[ ]{0,2}){3,}|(\*+[ ]{0,2}){3,})[ ]*' # Python's re module doesn't officially support atomic grouping. However you can fake it.
# See https://stackoverflow.com/a/13577411/866026
RE = r'^[ ]{0,3}(?=(?P<atomicgroup>(-+[ ]{0,2}){3,}|(_+[ ]{0,2}){3,}|(\*+[ ]{0,2}){3,}))(?P=atomicgroup)[ ]*$'
# Detect hr on any line of a block. # Detect hr on any line of a block.
SEARCH_RE = re.compile(RE, re.MULTILINE) SEARCH_RE = re.compile(RE, re.MULTILINE)
def test(self, parent, block): def test(self, parent, block):
m = self.SEARCH_RE.search(block) m = self.SEARCH_RE.search(block)
# No atomic grouping in python so we simulate it here for performance. if m:
# The regex only matches what would be in the atomic group - the HR.
# Then check if we are at end of block or if next char is a newline.
if m and (m.end() == len(block) or block[m.end()] == '\n'):
# Save match object on class instance so we can use it later. # Save match object on class instance so we can use it later.
self.match = m self.match = m
return True return True
@@ -554,6 +554,35 @@ class EmptyBlockProcessor(BlockProcessor):
) )
class ReferenceProcessor(BlockProcessor):
""" Process link references. """
RE = re.compile(
r'^[ ]{0,3}\[([^\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*\n?[ ]*((["\'])(.*)\4|\((.*)\))?[ ]*$', re.MULTILINE
)
def test(self, parent, block):
return True
def run(self, parent, blocks):
block = blocks.pop(0)
m = self.RE.search(block)
if m:
id = m.group(1).strip().lower()
link = m.group(2).lstrip('<').rstrip('>')
title = m.group(5) or m.group(6)
self.parser.md.references[id] = (link, title)
if block[m.end():].strip():
# Add any content after match back to blocks as separate block
blocks.insert(0, block[m.end():].lstrip('\n'))
if block[:m.start()].strip():
# Add any content before match back to blocks as separate block
blocks.insert(0, block[:m.start()].rstrip('\n'))
return True
# No match. Restore block.
blocks.insert(0, block)
return False
class ParagraphProcessor(BlockProcessor): class ParagraphProcessor(BlockProcessor):
""" Process Paragraph blocks. """ """ Process Paragraph blocks. """

View File

@@ -23,7 +23,6 @@ import codecs
import sys import sys
import logging import logging
import importlib import importlib
import pkg_resources
from . import util from . import util
from .preprocessors import build_preprocessors from .preprocessors import build_preprocessors
from .blockprocessors import build_block_parser from .blockprocessors import build_block_parser
@@ -78,11 +77,12 @@ class Markdown:
# See https://w3c.github.io/html/grouping-content.html#the-p-element # See https://w3c.github.io/html/grouping-content.html#the-p-element
'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl', 'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl',
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
'h4', 'h5', 'h6', 'header', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol',
'section', 'table', 'ul', 'p', 'pre', 'section', 'table', 'ul',
# Other elements which Markdown should not be mucking up the contents of. # Other elements which Markdown should not be mucking up the contents of.
'canvas', 'dd', 'dt', 'group', 'iframe', 'li', 'math', 'noscript', 'output', 'canvas', 'colgroup', 'dd', 'body', 'dt', 'group', 'iframe', 'li', 'legend',
'progress', 'script', 'style', 'tbody', 'td', 'th', 'thead', 'tr', 'video' 'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script',
'style', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video'
] ]
self.registeredExtensions = [] self.registeredExtensions = []
@@ -141,9 +141,8 @@ class Markdown:
Build extension from a string name, then return an instance. Build extension from a string name, then return an instance.
First attempt to load an entry point. The string name must be registered as an entry point in the First attempt to load an entry point. The string name must be registered as an entry point in the
`markdown.extensions` group which points to a subclass of the `markdown.extensions.Extension` class. If `markdown.extensions` group which points to a subclass of the `markdown.extensions.Extension` class.
multiple distributions have registered the same name, the first one found by `pkg_resources.iter_entry_points` If multiple distributions have registered the same name, the first one found is returned.
is returned.
If no entry point is found, assume dot notation (`path.to.module:ClassName`). Load the specified class and If no entry point is found, assume dot notation (`path.to.module:ClassName`). Load the specified class and
return an instance. If no class is specified, import the module and call a `makeExtension` function and return return an instance. If no class is specified, import the module and call a `makeExtension` function and return
@@ -151,7 +150,7 @@ class Markdown:
""" """
configs = dict(configs) configs = dict(configs)
entry_points = [ep for ep in pkg_resources.iter_entry_points('markdown.extensions', ext_name)] entry_points = [ep for ep in util.INSTALLED_EXTENSIONS if ep.name == ext_name]
if entry_points: if entry_points:
ext = entry_points[0].load() ext = entry_points[0].load()
return ext(**configs) return ext(**configs)
@@ -278,14 +277,14 @@ class Markdown:
'<%s>' % self.doc_tag) + len(self.doc_tag) + 2 '<%s>' % self.doc_tag) + len(self.doc_tag) + 2
end = output.rindex('</%s>' % self.doc_tag) end = output.rindex('</%s>' % self.doc_tag)
output = output[start:end].strip() output = output[start:end].strip()
except ValueError: # pragma: no cover except ValueError as e: # pragma: no cover
if output.strip().endswith('<%s />' % self.doc_tag): if output.strip().endswith('<%s />' % self.doc_tag):
# We have an empty document # We have an empty document
output = '' output = ''
else: else:
# We have a serious problem # We have a serious problem
raise ValueError('Markdown failed to strip top-level ' raise ValueError('Markdown failed to strip top-level '
'tags. Document=%r' % output.strip()) 'tags. Document=%r' % output.strip()) from e
# Run the text post-processors # Run the text post-processors
for pp in self.postprocessors: for pp in self.postprocessors:

View File

@@ -75,15 +75,18 @@ class Extension:
md = args[0] md = args[0]
try: try:
self.extendMarkdown(md) self.extendMarkdown(md)
except TypeError: except TypeError as e:
# Must be a 2.x extension. Pass in a dumby md_globals. if "missing 1 required positional argument" in str(e):
self.extendMarkdown(md, {}) # Must be a 2.x extension. Pass in a dumby md_globals.
warnings.warn( self.extendMarkdown(md, {})
"The 'md_globals' parameter of '{}.{}.extendMarkdown' is " warnings.warn(
"deprecated.".format(self.__class__.__module__, self.__class__.__name__), "The 'md_globals' parameter of '{}.{}.extendMarkdown' is "
category=DeprecationWarning, "deprecated.".format(self.__class__.__module__, self.__class__.__name__),
stacklevel=2 category=DeprecationWarning,
) stacklevel=2
)
else:
raise
def extendMarkdown(self, md): def extendMarkdown(self, md):
""" """

View File

@@ -17,48 +17,53 @@ License: [BSD](https://opensource.org/licenses/bsd-license.php)
''' '''
from . import Extension from . import Extension
from ..preprocessors import Preprocessor from ..blockprocessors import BlockProcessor
from ..inlinepatterns import InlineProcessor from ..inlinepatterns import InlineProcessor
from ..util import AtomicString from ..util import AtomicString
import re import re
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
# Global Vars
ABBR_REF_RE = re.compile(r'[*]\[(?P<abbr>[^\]]*)\][ ]?:\s*(?P<title>.*)')
class AbbrExtension(Extension): class AbbrExtension(Extension):
""" Abbreviation Extension for Python-Markdown. """ """ Abbreviation Extension for Python-Markdown. """
def extendMarkdown(self, md): def extendMarkdown(self, md):
""" Insert AbbrPreprocessor before ReferencePreprocessor. """ """ Insert AbbrPreprocessor before ReferencePreprocessor. """
md.preprocessors.register(AbbrPreprocessor(md), 'abbr', 12) md.parser.blockprocessors.register(AbbrPreprocessor(md.parser), 'abbr', 16)
class AbbrPreprocessor(Preprocessor): class AbbrPreprocessor(BlockProcessor):
""" Abbreviation Preprocessor - parse text for abbr references. """ """ Abbreviation Preprocessor - parse text for abbr references. """
def run(self, lines): RE = re.compile(r'^[*]\[(?P<abbr>[^\]]*)\][ ]?:[ ]*\n?[ ]*(?P<title>.*)$', re.MULTILINE)
def test(self, parent, block):
return True
def run(self, parent, blocks):
''' '''
Find and remove all Abbreviation references from the text. Find and remove all Abbreviation references from the text.
Each reference is set as a new AbbrPattern in the markdown instance. Each reference is set as a new AbbrPattern in the markdown instance.
''' '''
new_text = [] block = blocks.pop(0)
for line in lines: m = self.RE.search(block)
m = ABBR_REF_RE.match(line) if m:
if m: abbr = m.group('abbr').strip()
abbr = m.group('abbr').strip() title = m.group('title').strip()
title = m.group('title').strip() self.parser.md.inlinePatterns.register(
self.md.inlinePatterns.register( AbbrInlineProcessor(self._generate_pattern(abbr), title), 'abbr-%s' % abbr, 2
AbbrInlineProcessor(self._generate_pattern(abbr), title), 'abbr-%s' % abbr, 2 )
) if block[m.end():].strip():
# Preserve the line to prevent raw HTML indexing issue. # Add any content after match back to blocks as separate block
# https://github.com/Python-Markdown/markdown/issues/584 blocks.insert(0, block[m.end():].lstrip('\n'))
new_text.append('') if block[:m.start()].strip():
else: # Add any content before match back to blocks as separate block
new_text.append(line) blocks.insert(0, block[:m.start()].rstrip('\n'))
return new_text return True
# No match. Restore block.
blocks.insert(0, block)
return False
def _generate_pattern(self, text): def _generate_pattern(self, text):
''' '''

View File

@@ -40,19 +40,82 @@ class AdmonitionProcessor(BlockProcessor):
RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)') RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)')
RE_SPACES = re.compile(' +') RE_SPACES = re.compile(' +')
def test(self, parent, block): def __init__(self, parser):
"""Initialization."""
super().__init__(parser)
self.current_sibling = None
self.content_indention = 0
def get_sibling(self, parent, block):
"""Get sibling admontion.
Retrieve the appropriate siblimg element. This can get trickly when
dealing with lists.
"""
# We already acquired the block via test
if self.current_sibling is not None:
sibling = self.current_sibling
block = block[self.content_indent:]
self.current_sibling = None
self.content_indent = 0
return sibling, block
sibling = self.lastChild(parent) sibling = self.lastChild(parent)
return self.RE.search(block) or \
(block.startswith(' ' * self.tab_length) and sibling is not None and if sibling is None or sibling.get('class', '').find(self.CLASSNAME) == -1:
sibling.get('class', '').find(self.CLASSNAME) != -1) sibling = None
else:
# If the last child is a list and the content is idented sufficient
# to be under it, then the content's is sibling is in the list.
last_child = self.lastChild(sibling)
indent = 0
while last_child:
if (
sibling and block.startswith(' ' * self.tab_length * 2) and
last_child and last_child.tag in ('ul', 'ol', 'dl')
):
# The expectation is that we'll find an <li> or <dt>.
# We should get it's last child as well.
sibling = self.lastChild(last_child)
last_child = self.lastChild(sibling) if sibling else None
# Context has been lost at this point, so we must adjust the
# text's identation level so it will be evaluated correctly
# under the list.
block = block[self.tab_length:]
indent += self.tab_length
else:
last_child = None
if not block.startswith(' ' * self.tab_length):
sibling = None
if sibling is not None:
self.current_sibling = sibling
self.content_indent = indent
return sibling, block
def test(self, parent, block):
if self.RE.search(block):
return True
else:
return self.get_sibling(parent, block)[0] is not None
def run(self, parent, blocks): def run(self, parent, blocks):
sibling = self.lastChild(parent)
block = blocks.pop(0) block = blocks.pop(0)
m = self.RE.search(block) m = self.RE.search(block)
if m: if m:
block = block[m.end():] # removes the first line block = block[m.end():] # removes the first line
else:
sibling, block = self.get_sibling(parent, block)
block, theRest = self.detab(block) block, theRest = self.detab(block)
@@ -65,6 +128,13 @@ class AdmonitionProcessor(BlockProcessor):
p.text = title p.text = title
p.set('class', self.CLASSNAME_TITLE) p.set('class', self.CLASSNAME_TITLE)
else: else:
# Sibling is a list item, but we need to wrap it's content should be wrapped in <p>
if sibling.tag in ('li', 'dd') and sibling.text:
text = sibling.text
sibling.text = ''
p = etree.SubElement(sibling, 'p')
p.text = text
div = sibling div = sibling
self.parser.parseChunk(div, block) self.parser.parseChunk(div, block)

View File

@@ -64,10 +64,10 @@ def isheader(elem):
class AttrListTreeprocessor(Treeprocessor): class AttrListTreeprocessor(Treeprocessor):
BASE_RE = r'\{\:?([^\}\n]*)\}' BASE_RE = r'\{\:?[ ]*([^\}\n ][^\}\n]*)[ ]*\}'
HEADER_RE = re.compile(r'[ ]+%s[ ]*$' % BASE_RE) HEADER_RE = re.compile(r'[ ]+{}[ ]*$'.format(BASE_RE))
BLOCK_RE = re.compile(r'\n[ ]*%s[ ]*$' % BASE_RE) BLOCK_RE = re.compile(r'\n[ ]*{}[ ]*$'.format(BASE_RE))
INLINE_RE = re.compile(r'^%s' % BASE_RE) INLINE_RE = re.compile(r'^{}'.format(BASE_RE))
NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff' NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff'
r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d' r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d'
r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff' r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff'
@@ -79,8 +79,8 @@ class AttrListTreeprocessor(Treeprocessor):
if self.md.is_block_level(elem.tag): if self.md.is_block_level(elem.tag):
# Block level: check for attrs on last line of text # Block level: check for attrs on last line of text
RE = self.BLOCK_RE RE = self.BLOCK_RE
if isheader(elem) or elem.tag == 'dt': if isheader(elem) or elem.tag in ['dt', 'td', 'th']:
# header or def-term: check for attrs at end of line # header, def-term, or table cell: check for attrs at end of element
RE = self.HEADER_RE RE = self.HEADER_RE
if len(elem) and elem.tag == 'li': if len(elem) and elem.tag == 'li':
# special case list items. children may include a ul or ol. # special case list items. children may include a ul or ol.
@@ -120,8 +120,6 @@ class AttrListTreeprocessor(Treeprocessor):
elif elem.text: elif elem.text:
# no children. Get from text. # no children. Get from text.
m = RE.search(elem.text) m = RE.search(elem.text)
if not m and elem.tag == 'td':
m = re.search(self.BASE_RE, elem.text)
if m: if m:
self.assign_attrs(elem, m.group(1)) self.assign_attrs(elem, m.group(1))
elem.text = elem.text[:m.start()] elem.text = elem.text[:m.start()]
@@ -161,6 +159,7 @@ class AttrListTreeprocessor(Treeprocessor):
class AttrListExtension(Extension): class AttrListExtension(Extension):
def extendMarkdown(self, md): def extendMarkdown(self, md):
md.treeprocessors.register(AttrListTreeprocessor(md), 'attr_list', 8) md.treeprocessors.register(AttrListTreeprocessor(md), 'attr_list', 8)
md.registerExtension(self)
def makeExtension(**kwargs): # pragma: no cover def makeExtension(**kwargs): # pragma: no cover

View File

@@ -17,13 +17,14 @@ License: [BSD](https://opensource.org/licenses/bsd-license.php)
from . import Extension from . import Extension
from ..treeprocessors import Treeprocessor from ..treeprocessors import Treeprocessor
from ..util import parseBoolValue
try: try: # pragma: no cover
from pygments import highlight from pygments import highlight
from pygments.lexers import get_lexer_by_name, guess_lexer from pygments.lexers import get_lexer_by_name, guess_lexer
from pygments.formatters import get_formatter_by_name from pygments.formatters import get_formatter_by_name
pygments = True pygments = True
except ImportError: except ImportError: # pragma: no cover
pygments = False pygments = False
@@ -38,52 +39,78 @@ def parse_hl_lines(expr):
try: try:
return list(map(int, expr.split())) return list(map(int, expr.split()))
except ValueError: except ValueError: # pragma: no cover
return [] return []
# ------------------ The Main CodeHilite Class ---------------------- # ------------------ The Main CodeHilite Class ----------------------
class CodeHilite: class CodeHilite:
""" """
Determine language of source code, and pass it into pygments hilighter. Determine language of source code, and pass it on to the Pygments highlighter.
Basic Usage: Usage:
>>> code = CodeHilite(src = 'some text') code = CodeHilite(src=some_code, lang='python')
>>> html = code.hilite() html = code.hilite()
Arguments:
* src: Source string or any object with a .readline attribute. * src: Source string or any object with a .readline attribute.
* linenums: (Boolean) Set line numbering to 'on' (True), * lang: String name of Pygments lexer to use for highlighting. Default: `None`.
'off' (False) or 'auto'(None). Set to 'auto' by default.
* guess_lang: (Boolean) Turn language auto-detection * guess_lang: Auto-detect which lexer to use. Ignored if `lang` is set to a valid
'on' or 'off' (on by default). value. Default: `True`.
* css_class: Set class name of wrapper div ('codehilite' by default). * use_pygments: Pass code to pygments for code highlighting. If `False`, the code is
instead wrapped for highlighting by a JavaScript library. Default: `True`.
* hl_lines: (List of integers) Lines to emphasize, 1-indexed. * linenums: An alias to Pygments `linenos` formatter option. Default: `None`.
Low Level Usage: * css_class: An alias to Pygments `cssclass` formatter option. Default: 'codehilite'.
>>> code = CodeHilite()
>>> code.src = 'some text' # String or anything with a .readline attr. * lang_prefix: Prefix prepended to the language when `use_pygments` is `False`.
>>> code.linenos = True # Turns line numbering on or of. Default: "language-".
>>> html = code.hilite()
Other Options:
Any other options are accepted and passed on to the lexer and formatter. Therefore,
valid options include any options which are accepted by the `html` formatter or
whichever lexer the code's language uses. Note that most lexers do not have any
options. However, a few have very useful options, such as PHP's `startinline` option.
Any invalid options are ignored without error.
Formatter options: https://pygments.org/docs/formatters/#HtmlFormatter
Lexer Options: https://pygments.org/docs/lexers/
Advanced Usage:
code = CodeHilite(
src = some_code,
lang = 'php',
startinline = True, # Lexer option. Snippet does not start with `<?php`.
linenostart = 42, # Formatter option. Snippet starts on line 42.
hl_lines = [45, 49, 50], # Formatter option. Highlight lines 45, 49, and 50.
linenos = 'inline' # Formatter option. Avoid alignment problems.
)
html = code.hilite()
""" """
def __init__(self, src=None, linenums=None, guess_lang=True, def __init__(self, src, **options):
css_class="codehilite", lang=None, style='default',
noclasses=False, tab_length=4, hl_lines=None, use_pygments=True):
self.src = src self.src = src
self.lang = lang self.lang = options.pop('lang', None)
self.linenums = linenums self.guess_lang = options.pop('guess_lang', True)
self.guess_lang = guess_lang self.use_pygments = options.pop('use_pygments', True)
self.css_class = css_class self.lang_prefix = options.pop('lang_prefix', 'language-')
self.style = style
self.noclasses = noclasses if 'linenos' not in options:
self.tab_length = tab_length options['linenos'] = options.pop('linenums', None)
self.hl_lines = hl_lines or [] if 'cssclass' not in options:
self.use_pygments = use_pygments options['cssclass'] = options.pop('css_class', 'codehilite')
if 'wrapcode' not in options:
# Override pygments default
options['wrapcode'] = True
# Disallow use of `full` option
options['full'] = False
self.options = options
def hilite(self): def hilite(self):
""" """
@@ -103,22 +130,16 @@ class CodeHilite:
if pygments and self.use_pygments: if pygments and self.use_pygments:
try: try:
lexer = get_lexer_by_name(self.lang) lexer = get_lexer_by_name(self.lang, **self.options)
except ValueError: except ValueError:
try: try:
if self.guess_lang: if self.guess_lang:
lexer = guess_lexer(self.src) lexer = guess_lexer(self.src, **self.options)
else: else:
lexer = get_lexer_by_name('text') lexer = get_lexer_by_name('text', **self.options)
except ValueError: except ValueError: # pragma: no cover
lexer = get_lexer_by_name('text') lexer = get_lexer_by_name('text', **self.options)
formatter = get_formatter_by_name('html', formatter = get_formatter_by_name('html', **self.options)
linenos=self.linenums,
cssclass=self.css_class,
style=self.style,
noclasses=self.noclasses,
hl_lines=self.hl_lines,
wrapcode=True)
return highlight(self.src, lexer, formatter) return highlight(self.src, lexer, formatter)
else: else:
# just escape and build markup usable by JS highlighting libs # just escape and build markup usable by JS highlighting libs
@@ -128,27 +149,30 @@ class CodeHilite:
txt = txt.replace('"', '&quot;') txt = txt.replace('"', '&quot;')
classes = [] classes = []
if self.lang: if self.lang:
classes.append('language-%s' % self.lang) classes.append('{}{}'.format(self.lang_prefix, self.lang))
if self.linenums: if self.options['linenos']:
classes.append('linenums') classes.append('linenums')
class_str = '' class_str = ''
if classes: if classes:
class_str = ' class="%s"' % ' '.join(classes) class_str = ' class="{}"'.format(' '.join(classes))
return '<pre class="%s"><code%s>%s</code></pre>\n' % \ return '<pre class="{}"><code{}>{}\n</code></pre>\n'.format(
(self.css_class, class_str, txt) self.options['cssclass'],
class_str,
txt
)
def _parseHeader(self): def _parseHeader(self):
""" """
Determines language of a code block from shebang line and whether said Determines language of a code block from shebang line and whether the
line should be removed or left in place. If the sheband line contains a said line should be removed or left in place. If the sheband line
path (even a single /) then it is assumed to be a real shebang line and contains a path (even a single /) then it is assumed to be a real
left alone. However, if no path is given (e.i.: #!python or :::python) shebang line and left alone. However, if no path is given
then it is assumed to be a mock shebang for language identifitation of (e.i.: #!python or :::python) then it is assumed to be a mock shebang
a code fragment and removed from the code block prior to processing for for language identification of a code fragment and removed from the
code highlighting. When a mock shebang (e.i: #!python) is found, line code block prior to processing for code highlighting. When a mock
numbering is turned on. When colons are found in place of a shebang shebang (e.i: #!python) is found, line numbering is turned on. When
(e.i.: :::python), line numbering is left in the current state - off colons are found in place of a shebang (e.i.: :::python), line
by default. numbering is left in the current state - off by default.
Also parses optional list of highlight lines, like: Also parses optional list of highlight lines, like:
@@ -176,16 +200,16 @@ class CodeHilite:
# we have a match # we have a match
try: try:
self.lang = m.group('lang').lower() self.lang = m.group('lang').lower()
except IndexError: except IndexError: # pragma: no cover
self.lang = None self.lang = None
if m.group('path'): if m.group('path'):
# path exists - restore first line # path exists - restore first line
lines.insert(0, fl) lines.insert(0, fl)
if self.linenums is None and m.group('shebang'): if self.options['linenos'] is None and m.group('shebang'):
# Overridable and Shebang exists - use line numbers # Overridable and Shebang exists - use line numbers
self.linenums = True self.options['linenos'] = True
self.hl_lines = parse_hl_lines(m.group('hl_lines')) self.options['hl_lines'] = parse_hl_lines(m.group('hl_lines'))
else: else:
# No match # No match
lines.insert(0, fl) lines.insert(0, fl)
@@ -201,9 +225,11 @@ class HiliteTreeprocessor(Treeprocessor):
def code_unescape(self, text): def code_unescape(self, text):
"""Unescape code.""" """Unescape code."""
text = text.replace("&amp;", "&")
text = text.replace("&lt;", "<") text = text.replace("&lt;", "<")
text = text.replace("&gt;", ">") text = text.replace("&gt;", ">")
# Escaped '&' should be replaced at the end to avoid
# conflicting with < and >.
text = text.replace("&amp;", "&")
return text return text
def run(self, root): def run(self, root):
@@ -213,13 +239,9 @@ class HiliteTreeprocessor(Treeprocessor):
if len(block) == 1 and block[0].tag == 'code': if len(block) == 1 and block[0].tag == 'code':
code = CodeHilite( code = CodeHilite(
self.code_unescape(block[0].text), self.code_unescape(block[0].text),
linenums=self.config['linenums'],
guess_lang=self.config['guess_lang'],
css_class=self.config['css_class'],
style=self.config['pygments_style'],
noclasses=self.config['noclasses'],
tab_length=self.md.tab_length, tab_length=self.md.tab_length,
use_pygments=self.config['use_pygments'] style=self.config.pop('pygments_style', 'default'),
**self.config
) )
placeholder = self.md.htmlStash.store(code.hilite()) placeholder = self.md.htmlStash.store(code.hilite())
# Clear codeblock in etree instance # Clear codeblock in etree instance
@@ -237,7 +259,7 @@ class CodeHiliteExtension(Extension):
# define default configs # define default configs
self.config = { self.config = {
'linenums': [None, 'linenums': [None,
"Use lines numbers. True=yes, False=no, None=auto"], "Use lines numbers. True|table|inline=yes, False=no, None=auto"],
'guess_lang': [True, 'guess_lang': [True,
"Automatic language detection - Default: True"], "Automatic language detection - Default: True"],
'css_class': ["codehilite", 'css_class': ["codehilite",
@@ -252,10 +274,25 @@ class CodeHiliteExtension(Extension):
'use_pygments': [True, 'use_pygments': [True,
'Use Pygments to Highlight code blocks. ' 'Use Pygments to Highlight code blocks. '
'Disable if using a JavaScript library. ' 'Disable if using a JavaScript library. '
'Default: True'] 'Default: True'],
'lang_prefix': [
'language-',
'Prefix prepended to the language when use_pygments is false. Default: "language-"'
]
} }
super().__init__(**kwargs) for key, value in kwargs.items():
if key in self.config:
self.setConfig(key, value)
else:
# manually set unknown keywords.
if isinstance(value, str):
try:
# Attempt to parse str as a bool value
value = parseBoolValue(value, preserve_none=True)
except ValueError:
pass # Assume it's not a bool value. Use as-is.
self.config[key] = [value, '']
def extendMarkdown(self, md): def extendMarkdown(self, md):
""" Add HilitePostprocessor to Markdown instance. """ """ Add HilitePostprocessor to Markdown instance. """

View File

@@ -34,8 +34,8 @@ class DefListProcessor(BlockProcessor):
raw_block = blocks.pop(0) raw_block = blocks.pop(0)
m = self.RE.search(raw_block) m = self.RE.search(raw_block)
terms = [l.strip() for l in terms = [term.strip() for term in
raw_block[:m.start()].split('\n') if l.strip()] raw_block[:m.start()].split('\n') if term.strip()]
block = raw_block[m.end():] block = raw_block[m.end():]
no_indent = self.NO_INDENT_RE.match(block) no_indent = self.NO_INDENT_RE.match(block)
if no_indent: if no_indent:
@@ -87,11 +87,13 @@ class DefListProcessor(BlockProcessor):
class DefListIndentProcessor(ListIndentProcessor): class DefListIndentProcessor(ListIndentProcessor):
""" Process indented children of definition list items. """ """ Process indented children of definition list items. """
ITEM_TYPES = ['dd'] # Defintion lists need to be aware of all list types
LIST_TYPES = ['dl'] ITEM_TYPES = ['dd', 'li']
LIST_TYPES = ['dl', 'ol', 'ul']
def create_item(self, parent, block): def create_item(self, parent, block):
""" Create a new dd and parse the block with it as the parent. """ """ Create a new dd or li (depending on parent) and parse the block with it as the parent. """
dd = etree.SubElement(parent, 'dd') dd = etree.SubElement(parent, 'dd')
self.parser.parseBlocks(dd, [block]) self.parser.parseBlocks(dd, [block])

View File

@@ -15,78 +15,130 @@ All changes Copyright 2008-2014 The Python Markdown Project
License: [BSD](https://opensource.org/licenses/bsd-license.php) License: [BSD](https://opensource.org/licenses/bsd-license.php)
""" """
from textwrap import dedent
from . import Extension from . import Extension
from ..preprocessors import Preprocessor from ..preprocessors import Preprocessor
from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines
from .attr_list import get_attrs, AttrListExtension
from ..util import parseBoolValue
import re import re
class FencedCodeExtension(Extension): class FencedCodeExtension(Extension):
def __init__(self, **kwargs):
self.config = {
'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"']
}
super().__init__(**kwargs)
def extendMarkdown(self, md): def extendMarkdown(self, md):
""" Add FencedBlockPreprocessor to the Markdown instance. """ """ Add FencedBlockPreprocessor to the Markdown instance. """
md.registerExtension(self) md.registerExtension(self)
md.preprocessors.register(FencedBlockPreprocessor(md), 'fenced_code_block', 25) md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25)
class FencedBlockPreprocessor(Preprocessor): class FencedBlockPreprocessor(Preprocessor):
FENCED_BLOCK_RE = re.compile(r''' FENCED_BLOCK_RE = re.compile(
(?P<fence>^(?:~{3,}|`{3,}))[ ]* # Opening ``` or ~~~ dedent(r'''
(\{?\.?(?P<lang>[\w#.+-]*))?[ ]* # Optional {, and lang (?P<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence
# Optional highlight lines, single- or double-quote-delimited ((\{(?P<attrs>[^\}\n]*)\})?| # (optional {attrs} or
(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?[ ]* (\.?(?P<lang>[\w#.+-]*))?[ ]* # optional (.)lang
}?[ ]*\n # Optional closing } (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?) # optional hl_lines)
(?P<code>.*?)(?<=\n) [ ]*\n # newline (end of opening fence)
(?P=fence)[ ]*$''', re.MULTILINE | re.DOTALL | re.VERBOSE) (?P<code>.*?)(?<=\n) # the code block
CODE_WRAP = '<pre><code%s>%s</code></pre>' (?P=fence)[ ]*$ # closing fence
LANG_TAG = ' class="%s"' '''),
re.MULTILINE | re.DOTALL | re.VERBOSE
)
def __init__(self, md): def __init__(self, md, config):
super().__init__(md) super().__init__(md)
self.config = config
self.checked_for_codehilite = False self.checked_for_deps = False
self.codehilite_conf = {} self.codehilite_conf = {}
self.use_attr_list = False
# List of options to convert to bool values
self.bool_options = [
'linenums',
'guess_lang',
'noclasses',
'use_pygments'
]
def run(self, lines): def run(self, lines):
""" Match and store Fenced Code Blocks in the HtmlStash. """ """ Match and store Fenced Code Blocks in the HtmlStash. """
# Check for code hilite extension # Check for dependent extensions
if not self.checked_for_codehilite: if not self.checked_for_deps:
for ext in self.md.registeredExtensions: for ext in self.md.registeredExtensions:
if isinstance(ext, CodeHiliteExtension): if isinstance(ext, CodeHiliteExtension):
self.codehilite_conf = ext.config self.codehilite_conf = ext.getConfigs()
break if isinstance(ext, AttrListExtension):
self.use_attr_list = True
self.checked_for_codehilite = True self.checked_for_deps = True
text = "\n".join(lines) text = "\n".join(lines)
while 1: while 1:
m = self.FENCED_BLOCK_RE.search(text) m = self.FENCED_BLOCK_RE.search(text)
if m: if m:
lang = '' lang, id, classes, config = None, '', [], {}
if m.group('lang'): if m.group('attrs'):
lang = self.LANG_TAG % m.group('lang') id, classes, config = self.handle_attrs(get_attrs(m.group('attrs')))
if len(classes):
lang = classes.pop(0)
else:
if m.group('lang'):
lang = m.group('lang')
if m.group('hl_lines'):
# Support hl_lines outside of attrs for backward-compatibility
config['hl_lines'] = parse_hl_lines(m.group('hl_lines'))
# If config is not empty, then the codehighlite extension # If config is not empty, then the codehighlite extension
# is enabled, so we call it to highlight the code # is enabled, so we call it to highlight the code
if self.codehilite_conf: if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True):
local_config = self.codehilite_conf.copy()
local_config.update(config)
# Combine classes with cssclass. Ensure cssclass is at end
# as pygments appends a suffix under certain circumstances.
# Ignore ID as Pygments does not offer an option to set it.
if classes:
local_config['css_class'] = '{} {}'.format(
' '.join(classes),
local_config['css_class']
)
highliter = CodeHilite( highliter = CodeHilite(
m.group('code'), m.group('code'),
linenums=self.codehilite_conf['linenums'][0], lang=lang,
guess_lang=self.codehilite_conf['guess_lang'][0], style=local_config.pop('pygments_style', 'default'),
css_class=self.codehilite_conf['css_class'][0], **local_config
style=self.codehilite_conf['pygments_style'][0],
use_pygments=self.codehilite_conf['use_pygments'][0],
lang=(m.group('lang') or None),
noclasses=self.codehilite_conf['noclasses'][0],
hl_lines=parse_hl_lines(m.group('hl_lines'))
) )
code = highliter.hilite() code = highliter.hilite()
else: else:
code = self.CODE_WRAP % (lang, id_attr = lang_attr = class_attr = kv_pairs = ''
self._escape(m.group('code'))) if lang:
lang_attr = ' class="{}{}"'.format(self.config.get('lang_prefix', 'language-'), lang)
if classes:
class_attr = ' class="{}"'.format(' '.join(classes))
if id:
id_attr = ' id="{}"'.format(id)
if self.use_attr_list and config and not config.get('use_pygments', False):
# Only assign key/value pairs to code element if attr_list ext is enabled, key/value pairs
# were defined on the code block, and the `use_pygments` key was not set to True. The
# `use_pygments` key could be either set to False or not defined. It is omitted from output.
kv_pairs = ' ' + ' '.join(
'{k}="{v}"'.format(k=k, v=v) for k, v in config.items() if k != 'use_pygments'
)
code = '<pre{id}{cls}><code{lang}{kv}>{code}</code></pre>'.format(
id=id_attr,
cls=class_attr,
lang=lang_attr,
kv=kv_pairs,
code=self._escape(m.group('code'))
)
placeholder = self.md.htmlStash.store(code) placeholder = self.md.htmlStash.store(code)
text = '{}\n{}\n{}'.format(text[:m.start()], text = '{}\n{}\n{}'.format(text[:m.start()],
@@ -96,6 +148,24 @@ class FencedBlockPreprocessor(Preprocessor):
break break
return text.split("\n") return text.split("\n")
def handle_attrs(self, attrs):
""" Return tuple: (id, [list, of, classes], {configs}) """
id = ''
classes = []
configs = {}
for k, v in attrs:
if k == 'id':
id = v
elif k == '.':
classes.append(v)
elif k == 'hl_lines':
configs[k] = parse_hl_lines(v)
elif k in self.bool_options:
configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True)
else:
configs[k] = v
return id, classes, configs
def _escape(self, txt): def _escape(self, txt):
""" basic html escaping """ """ basic html escaping """
txt = txt.replace('&', '&amp;') txt = txt.replace('&', '&amp;')

View File

@@ -14,7 +14,7 @@ License: [BSD](https://opensource.org/licenses/bsd-license.php)
""" """
from . import Extension from . import Extension
from ..preprocessors import Preprocessor from ..blockprocessors import BlockProcessor
from ..inlinepatterns import InlineProcessor from ..inlinepatterns import InlineProcessor
from ..treeprocessors import Treeprocessor from ..treeprocessors import Treeprocessor
from ..postprocessors import Postprocessor from ..postprocessors import Postprocessor
@@ -26,8 +26,6 @@ import xml.etree.ElementTree as etree
FN_BACKLINK_TEXT = util.STX + "zz1337820767766393qq" + util.ETX FN_BACKLINK_TEXT = util.STX + "zz1337820767766393qq" + util.ETX
NBSP_PLACEHOLDER = util.STX + "qq3936677670287331zz" + util.ETX NBSP_PLACEHOLDER = util.STX + "qq3936677670287331zz" + util.ETX
DEF_RE = re.compile(r'[ ]{0,3}\[\^([^\]]*)\]:\s*(.*)')
TABBED_RE = re.compile(r'((\t)|( ))(.*)')
RE_REF_ID = re.compile(r'(fnref)(\d+)') RE_REF_ID = re.compile(r'(fnref)(\d+)')
@@ -72,8 +70,8 @@ class FootnoteExtension(Extension):
md.registerExtension(self) md.registerExtension(self)
self.parser = md.parser self.parser = md.parser
self.md = md self.md = md
# Insert a preprocessor before ReferencePreprocessor # Insert a blockprocessor before ReferencePreprocessor
md.preprocessors.register(FootnotePreprocessor(self), 'footnote', 15) md.parser.blockprocessors.register(FootnoteBlockProcessor(self), 'footnote', 17)
# Insert an inline pattern before ImageReferencePattern # Insert an inline pattern before ImageReferencePattern
FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah
@@ -202,106 +200,92 @@ class FootnoteExtension(Extension):
return div return div
class FootnotePreprocessor(Preprocessor): class FootnoteBlockProcessor(BlockProcessor):
""" Find all footnote references and store for later use. """ """ Find all footnote references and store for later use. """
RE = re.compile(r'^[ ]{0,3}\[\^([^\]]*)\]:[ ]*(.*)$', re.MULTILINE)
def __init__(self, footnotes): def __init__(self, footnotes):
super().__init__(footnotes.parser)
self.footnotes = footnotes self.footnotes = footnotes
def run(self, lines): def test(self, parent, block):
""" return True
Loop through lines and find, set, and remove footnote definitions.
Keywords: def run(self, parent, blocks):
""" Find, set, and remove footnote definitions. """
block = blocks.pop(0)
m = self.RE.search(block)
if m:
id = m.group(1)
fn_blocks = [m.group(2)]
* lines: A list of lines of text # Handle rest of block
therest = block[m.end():].lstrip('\n')
Return: A list of lines of text with footnote definitions removed. m2 = self.RE.search(therest)
if m2:
""" # Another footnote exists in the rest of this block.
newlines = [] # Any content before match is continuation of this footnote, which may be lazily indented.
i = 0 before = therest[:m2.start()].rstrip('\n')
while True: fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(before)]).lstrip('\n')
m = DEF_RE.match(lines[i]) # Add back to blocks everything from begining of match forward for next iteration.
if m: blocks.insert(0, therest[m2.start():])
fn, _i = self.detectTabbed(lines[i+1:])
fn.insert(0, m.group(2))
i += _i-1 # skip past footnote
footnote = "\n".join(fn)
self.footnotes.setFootnote(m.group(1), footnote.rstrip())
# Preserve a line for each block to prevent raw HTML indexing issue.
# https://github.com/Python-Markdown/markdown/issues/584
num_blocks = (len(footnote.split('\n\n')) * 2)
newlines.extend([''] * (num_blocks))
else: else:
newlines.append(lines[i]) # All remaining lines of block are continuation of this footnote, which may be lazily indented.
if len(lines) > i+1: fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(therest)]).strip('\n')
i += 1
else:
break
return newlines
def detectTabbed(self, lines): # Check for child elements in remaining blocks.
fn_blocks.extend(self.detectTabbed(blocks))
footnote = "\n\n".join(fn_blocks)
self.footnotes.setFootnote(id, footnote.rstrip())
if block[:m.start()].strip():
# Add any content before match back to blocks as separate block
blocks.insert(0, block[:m.start()].rstrip('\n'))
return True
# No match. Restore block.
blocks.insert(0, block)
return False
def detectTabbed(self, blocks):
""" Find indented text and remove indent before further proccesing. """ Find indented text and remove indent before further proccesing.
Keyword arguments: Returns: a list of blocks with indentation removed.
* lines: an array of strings
Returns: a list of post processed items and the index of last line.
""" """
items = [] fn_blocks = []
blank_line = False # have we encountered a blank line yet? while blocks:
i = 0 # to keep track of where we are if blocks[0].startswith(' '*4):
block = blocks.pop(0)
def detab(line): # Check for new footnotes within this block and split at new footnote.
match = TABBED_RE.match(line) m = self.RE.search(block)
if match: if m:
return match.group(4) # Another footnote exists in this block.
# Any content before match is continuation of this footnote, which may be lazily indented.
for line in lines: before = block[:m.start()].rstrip('\n')
if line.strip(): # Non-blank line fn_blocks.append(self.detab(before))
detabbed_line = detab(line) # Add back to blocks everything from begining of match forward for next iteration.
if detabbed_line: blocks.insert(0, block[m.start():])
items.append(detabbed_line) # End of this footnote.
i += 1 break
continue
elif not blank_line and not DEF_RE.match(line):
# not tabbed but still part of first par.
items.append(line)
i += 1
continue
else: else:
return items, i+1 # Entire block is part of this footnote.
fn_blocks.append(self.detab(block))
else:
# End of this footnote.
break
return fn_blocks
else: # Blank line: _maybe_ we are done. def detab(self, block):
blank_line = True """ Remove one level of indent from a block.
i += 1 # advance
# Find the next non-blank line Preserve lazily indented blocks by only removing indent from indented lines.
for j in range(i, len(lines)): """
if lines[j].strip(): lines = block.split('\n')
next_line = lines[j] for i, line in enumerate(lines):
break if line.startswith(' '*4):
else: lines[i] = line[4:]
# Include extreaneous padding to prevent raw HTML return '\n'.join(lines)
# parsing issue: https://github.com/Python-Markdown/markdown/issues/584
items.append("")
i += 1
else:
break # There is no more text; we are done.
# Check if the next non-blank line is tabbed
if detab(next_line): # Yes, more work to do.
items.append("")
continue
else:
break # No, we are done.
else:
i += 1
return items, i
class FootnoteInlineProcessor(InlineProcessor): class FootnoteInlineProcessor(InlineProcessor):
@@ -347,8 +331,8 @@ class FootnotePostTreeprocessor(Treeprocessor):
self.offset += 1 self.offset += 1
# Add all the new duplicate links. # Add all the new duplicate links.
el = list(li)[-1] el = list(li)[-1]
for l in links: for link in links:
el.append(l) el.append(link)
break break
def get_num_duplicates(self, li): def get_num_duplicates(self, li):

View File

@@ -21,7 +21,7 @@ EMPHASIS_RE = r'(_)([^_]+)\1'
STRONG_RE = r'(_{2})(.+?)\1' STRONG_RE = r'(_{2})(.+?)\1'
# __strong_em___ # __strong_em___
STRONG_EM_RE = r'(_)\1(?!\1)(.+?)\1(?!\1)(.+?)\1{3}' STRONG_EM_RE = r'(_)\1(?!\1)([^_]+?)\1(?!\1)(.+?)\1{3}'
class LegacyUnderscoreProcessor(UnderscoreProcessor): class LegacyUnderscoreProcessor(UnderscoreProcessor):

View File

@@ -16,68 +16,313 @@ License: [BSD](https://opensource.org/licenses/bsd-license.php)
from . import Extension from . import Extension
from ..blockprocessors import BlockProcessor from ..blockprocessors import BlockProcessor
from ..preprocessors import Preprocessor
from ..postprocessors import RawHtmlPostprocessor
from .. import util from .. import util
import re from ..htmlparser import HTMLExtractor
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
class MarkdownInHtmlProcessor(BlockProcessor): class HTMLExtractorExtra(HTMLExtractor):
"""Process Markdown Inside HTML Blocks.""" """
def test(self, parent, block): Override HTMLExtractor and create etree Elements for any elements which should have content parsed as Markdown.
return block == util.TAG_PLACEHOLDER % \ """
str(self.parser.blockprocessors.tag_counter + 1)
def _process_nests(self, element, block): def __init__(self, md, *args, **kwargs):
"""Process the element's child elements in self.run.""" # All block-level tags.
# Build list of indexes of each nest within the parent element. self.block_level_tags = set(md.block_level_elements.copy())
nest_index = [] # a list of tuples: (left index, right index) # Block-level tags in which the content only gets span level parsing
i = self.parser.blockprocessors.tag_counter + 1 self.span_tags = set(
while len(self._tag_data) > i and self._tag_data[i]['left_index']: ['address', 'dd', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'legend', 'li', 'p', 'td', 'th']
left_child_index = self._tag_data[i]['left_index'] )
right_child_index = self._tag_data[i]['right_index'] # Block-level tags which never get their content parsed.
nest_index.append((left_child_index - 1, right_child_index)) self.raw_tags = set(['canvas', 'math', 'option', 'pre', 'script', 'style', 'textarea'])
i += 1 # Block-level tags in which the content gets parsed as blocks
super().__init__(md, *args, **kwargs)
# Create each nest subelement. self.block_tags = set(self.block_level_tags) - (self.span_tags | self.raw_tags | self.empty_tags)
for i, (left_index, right_index) in enumerate(nest_index[:-1]): self.span_and_blocks_tags = self.block_tags | self.span_tags
self.run(element, block[left_index:right_index],
block[right_index:nest_index[i + 1][0]], True)
self.run(element, block[nest_index[-1][0]:nest_index[-1][1]], # last
block[nest_index[-1][1]:], True) # nest
def run(self, parent, blocks, tail=None, nest=False): def reset(self):
self._tag_data = self.parser.md.htmlStash.tag_data """Reset this instance. Loses all unprocessed data."""
self.mdstack = [] # When markdown=1, stack contains a list of tags
self.treebuilder = etree.TreeBuilder()
self.mdstate = [] # one of 'block', 'span', 'off', or None
super().reset()
self.parser.blockprocessors.tag_counter += 1 def close(self):
tag = self._tag_data[self.parser.blockprocessors.tag_counter] """Handle any buffered data."""
super().close()
# Handle any unclosed tags.
if self.mdstack:
# Close the outermost parent. handle_endtag will close all unclosed children.
self.handle_endtag(self.mdstack[0])
# Create Element def get_element(self):
markdown_value = tag['attrs'].pop('markdown') """ Return element from treebuilder and reset treebuilder for later use. """
element = etree.SubElement(parent, tag['tag'], tag['attrs']) element = self.treebuilder.close()
self.treebuilder = etree.TreeBuilder()
return element
# Slice Off Block def get_state(self, tag, attrs):
if nest: """ Return state from tag and `markdown` attr. One of 'block', 'span', or 'off'. """
self.parser.parseBlocks(parent, tail) # Process Tail md_attr = attrs.get('markdown', '0')
block = blocks[1:] if md_attr == 'markdown':
else: # includes nests since a third level of nesting isn't supported # `<tag markdown>` is the same as `<tag markdown='1'>`.
block = blocks[tag['left_index'] + 1: tag['right_index']] md_attr = '1'
del blocks[:tag['right_index']] parent_state = self.mdstate[-1] if self.mdstate else None
if parent_state == 'off' or (parent_state == 'span' and md_attr != '0'):
# Only use the parent state if it is more restrictive than the markdown attribute.
md_attr = parent_state
if ((md_attr == '1' and tag in self.block_tags) or
(md_attr == 'block' and tag in self.span_and_blocks_tags)):
return 'block'
elif ((md_attr == '1' and tag in self.span_tags) or
(md_attr == 'span' and tag in self.span_and_blocks_tags)):
return 'span'
elif tag in self.block_level_tags:
return 'off'
else: # pragma: no cover
return None
# Process Text def at_line_start(self):
if (self.parser.blockprocessors.contain_span_tags.match( # Span Mode """At line start."""
tag['tag']) and markdown_value != 'block') or \
markdown_value == 'span': value = super().at_line_start()
element.text = '\n'.join(block) if not value and self.cleandoc and self.cleandoc[-1].endswith('\n'):
else: # Block Mode value = True
i = self.parser.blockprocessors.tag_counter + 1 return value
if len(self._tag_data) > i and self._tag_data[i]['left_index']:
first_subelement_index = self._tag_data[i]['left_index'] - 1 def handle_starttag(self, tag, attrs):
self.parser.parseBlocks( # Handle tags that should always be empty and do not specify a closing tag
element, block[:first_subelement_index]) if tag in self.empty_tags:
if not nest: attrs = {key: value if value is not None else key for key, value in attrs}
block = self._process_nests(element, block) if "markdown" in attrs:
attrs.pop('markdown')
element = etree.Element(tag, attrs)
data = etree.tostring(element, encoding='unicode', method='html')
else: else:
self.parser.parseBlocks(element, block) data = self.get_starttag_text()
self.handle_empty_tag(data, True)
return
if tag in self.block_level_tags:
# Valueless attr (ex: `<tag checked>`) results in `[('checked', None)]`.
# Convert to `{'checked': 'checked'}`.
attrs = {key: value if value is not None else key for key, value in attrs}
state = self.get_state(tag, attrs)
if self.inraw or (state in [None, 'off'] and not self.mdstack) or not self.at_line_start():
# fall back to default behavior
attrs.pop('markdown', None)
super().handle_starttag(tag, attrs)
else:
if 'p' in self.mdstack and tag in self.block_level_tags:
# Close unclosed 'p' tag
self.handle_endtag('p')
self.mdstate.append(state)
self.mdstack.append(tag)
attrs['markdown'] = state
self.treebuilder.start(tag, attrs)
else:
# Span level tag
if self.inraw:
super().handle_starttag(tag, attrs)
else:
text = self.get_starttag_text()
if self.mdstate and self.mdstate[-1] == "off":
self.handle_data(self.md.htmlStash.store(text))
else:
self.handle_data(text)
def handle_endtag(self, tag):
if tag in self.block_level_tags:
if self.inraw:
super().handle_endtag(tag)
elif tag in self.mdstack:
# Close element and any unclosed children
while self.mdstack:
item = self.mdstack.pop()
self.mdstate.pop()
self.treebuilder.end(item)
if item == tag:
break
if not self.mdstack:
# Last item in stack is closed. Stash it
element = self.get_element()
# Get last entry to see if it ends in newlines
# If it is an element, assume there is no newlines
item = self.cleandoc[-1] if self.cleandoc else ''
# If we only have one newline before block element, add another
if not item.endswith('\n\n') and item.endswith('\n'):
self.cleandoc.append('\n')
self.cleandoc.append(self.md.htmlStash.store(element))
self.cleandoc.append('\n\n')
self.state = []
else:
# Treat orphan closing tag as a span level tag.
text = self.get_endtag_text(tag)
if self.mdstate and self.mdstate[-1] == "off":
self.handle_data(self.md.htmlStash.store(text))
else:
self.handle_data(text)
else:
# Span level tag
if self.inraw:
super().handle_endtag(tag)
else:
text = self.get_endtag_text(tag)
if self.mdstate and self.mdstate[-1] == "off":
self.handle_data(self.md.htmlStash.store(text))
else:
self.handle_data(text)
def handle_startendtag(self, tag, attrs):
if tag in self.empty_tags:
attrs = {key: value if value is not None else key for key, value in attrs}
if "markdown" in attrs:
attrs.pop('markdown')
element = etree.Element(tag, attrs)
data = etree.tostring(element, encoding='unicode', method='html')
else:
data = self.get_starttag_text()
else:
data = self.get_starttag_text()
self.handle_empty_tag(data, is_block=self.md.is_block_level(tag))
def handle_data(self, data):
if self.inraw or not self.mdstack:
super().handle_data(data)
else:
self.treebuilder.data(data)
def handle_empty_tag(self, data, is_block):
if self.inraw or not self.mdstack:
super().handle_empty_tag(data, is_block)
else:
if self.at_line_start() and is_block:
self.handle_data('\n' + self.md.htmlStash.store(data) + '\n\n')
else:
if self.mdstate and self.mdstate[-1] == "off":
self.handle_data(self.md.htmlStash.store(data))
else:
self.handle_data(data)
class HtmlBlockPreprocessor(Preprocessor):
"""Remove html blocks from the text and store them for later retrieval."""
def run(self, lines):
source = '\n'.join(lines)
parser = HTMLExtractorExtra(self.md)
parser.feed(source)
parser.close()
return ''.join(parser.cleandoc).split('\n')
class MarkdownInHtmlProcessor(BlockProcessor):
"""Process Markdown Inside HTML Blocks which have been stored in the HtmlStash."""
def test(self, parent, block):
# ALways return True. `run` will return `False` it not a valid match.
return True
def parse_element_content(self, element):
"""
Resursively parse the text content of an etree Element as Markdown.
Any block level elements generated from the Markdown will be inserted as children of the element in place
of the text content. All `markdown` attributes are removed. For any elements in which Markdown parsing has
been dissabled, the text content of it and its chidlren are wrapped in an `AtomicString`.
"""
md_attr = element.attrib.pop('markdown', 'off')
if md_attr == 'block':
# Parse content as block level
# The order in which the different parts are parsed (text, children, tails) is important here as the
# order of elements needs to be preserved. We can't be inserting items at a later point in the current
# iteration as we don't want to do raw processing on elements created from parsing Markdown text (for
# example). Therefore, the order of operations is children, tails, text.
# Recursively parse existing children from raw HTML
for child in list(element):
self.parse_element_content(child)
# Parse Markdown text in tail of children. Do this seperate to avoid raw HTML parsing.
# Save the position of each item to be inserted later in reverse.
tails = []
for pos, child in enumerate(element):
if child.tail:
block = child.tail.rstrip('\n')
child.tail = ''
# Use a dummy placeholder element.
dummy = etree.Element('div')
self.parser.parseBlocks(dummy, block.split('\n\n'))
children = list(dummy)
children.reverse()
tails.append((pos + 1, children))
# Insert the elements created from the tails in reverse.
tails.reverse()
for pos, tail in tails:
for item in tail:
element.insert(pos, item)
# Parse Markdown text content. Do this last to avoid raw HTML parsing.
if element.text:
block = element.text.rstrip('\n')
element.text = ''
# Use a dummy placeholder element as the content needs to get inserted before existing children.
dummy = etree.Element('div')
self.parser.parseBlocks(dummy, block.split('\n\n'))
children = list(dummy)
children.reverse()
for child in children:
element.insert(0, child)
elif md_attr == 'span':
# Span level parsing will be handled by inlineprocessors.
# Walk children here to remove any `markdown` attributes.
for child in list(element):
self.parse_element_content(child)
else:
# Disable inline parsing for everything else
if element.text is None:
element.text = ''
element.text = util.AtomicString(element.text)
for child in list(element):
self.parse_element_content(child)
if child.tail:
child.tail = util.AtomicString(child.tail)
def run(self, parent, blocks):
m = util.HTML_PLACEHOLDER_RE.match(blocks[0])
if m:
index = int(m.group(1))
element = self.parser.md.htmlStash.rawHtmlBlocks[index]
if isinstance(element, etree.Element):
# We have a matched element. Process it.
blocks.pop(0)
self.parse_element_content(element)
parent.append(element)
# Cleanup stash. Replace element with empty string to avoid confusing postprocessor.
self.parser.md.htmlStash.rawHtmlBlocks.pop(index)
self.parser.md.htmlStash.rawHtmlBlocks.insert(index, '')
# Comfirm the match to the blockparser.
return True
# No match found.
return False
class MarkdownInHTMLPostprocessor(RawHtmlPostprocessor):
def stash_to_string(self, text):
""" Override default to handle any etree elements still in the stash. """
if isinstance(text, etree.Element):
return self.md.serializer(text)
else:
return str(text)
class MarkdownInHtmlExtension(Extension): class MarkdownInHtmlExtension(Extension):
@@ -86,14 +331,14 @@ class MarkdownInHtmlExtension(Extension):
def extendMarkdown(self, md): def extendMarkdown(self, md):
""" Register extension instances. """ """ Register extension instances. """
# Turn on processing of markdown text within raw html # Replace raw HTML preprocessor
md.preprocessors['html_block'].markdown_in_raw = True md.preprocessors.register(HtmlBlockPreprocessor(md), 'html_block', 20)
# Add blockprocessor which handles the placeholders for etree elements
md.parser.blockprocessors.register( md.parser.blockprocessors.register(
MarkdownInHtmlProcessor(md.parser), 'markdown_block', 105 MarkdownInHtmlProcessor(md.parser), 'markdown_block', 105
) )
md.parser.blockprocessors.tag_counter = -1 # Replace raw HTML postprocessor
md.parser.blockprocessors.contain_span_tags = re.compile( md.postprocessors.register(MarkdownInHTMLPostprocessor(md), 'raw_html', 30)
r'^(p|h[1-6]|li|dd|dt|td|th|legend|address)$', re.IGNORECASE)
def makeExtension(**kwargs): # pragma: no cover def makeExtension(**kwargs): # pragma: no cover

View File

@@ -15,18 +15,24 @@ License: [BSD](https://opensource.org/licenses/bsd-license.php)
from . import Extension from . import Extension
from ..treeprocessors import Treeprocessor from ..treeprocessors import Treeprocessor
from ..util import code_escape, parseBoolValue, AMP_SUBSTITUTE, HTML_PLACEHOLDER_RE from ..util import code_escape, parseBoolValue, AMP_SUBSTITUTE, HTML_PLACEHOLDER_RE, AtomicString
from ..postprocessors import UnescapePostprocessor from ..postprocessors import UnescapePostprocessor
import re import re
import html
import unicodedata import unicodedata
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
def slugify(value, separator): def slugify(value, separator, encoding='ascii'):
""" Slugify a string, to make it URL friendly. """ """ Slugify a string, to make it URL friendly. """
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') value = unicodedata.normalize('NFKD', value).encode(encoding, 'ignore')
value = re.sub(r'[^\w\s-]', '', value.decode('ascii')).strip().lower() value = re.sub(r'[^\w\s-]', '', value.decode(encoding)).strip().lower()
return re.sub(r'[%s\s]+' % separator, separator, value) return re.sub(r'[{}\s]+'.format(separator), separator, value)
def slugify_unicode(value, separator):
""" Slugify a string, to make it URL friendly while preserving Unicode characters. """
return slugify(value, separator, 'utf-8')
IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$') IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$')
@@ -44,6 +50,18 @@ def unique(id, ids):
return id return id
def get_name(el):
"""Get title name."""
text = []
for c in el.itertext():
if isinstance(c, AtomicString):
text.append(html.unescape(c))
else:
text.append(c)
return ''.join(text).strip()
def stashedHTML2text(text, md, strip_entities=True): def stashedHTML2text(text, md, strip_entities=True):
""" Extract raw HTML from stash, reduce to plain text and swap with placeholder. """ """ Extract raw HTML from stash, reduce to plain text and swap with placeholder. """
def _html_sub(m): def _html_sub(m):
@@ -253,7 +271,7 @@ class TocTreeprocessor(Treeprocessor):
self.set_level(el) self.set_level(el)
if int(el.tag[-1]) < self.toc_top or int(el.tag[-1]) > self.toc_bottom: if int(el.tag[-1]) < self.toc_top or int(el.tag[-1]) > self.toc_bottom:
continue continue
text = ''.join(el.itertext()).strip() text = get_name(el)
# Do not override pre-existing ids # Do not override pre-existing ids
if "id" not in el.attrib: if "id" not in el.attrib:

View File

@@ -84,6 +84,9 @@ def build_inlinepatterns(md, **kwargs):
inlinePatterns.register( inlinePatterns.register(
ShortReferenceInlineProcessor(REFERENCE_RE, md), 'short_reference', 130 ShortReferenceInlineProcessor(REFERENCE_RE, md), 'short_reference', 130
) )
inlinePatterns.register(
ShortImageReferenceInlineProcessor(IMAGE_REFERENCE_RE, md), 'short_image_ref', 125
)
inlinePatterns.register(AutolinkInlineProcessor(AUTOLINK_RE, md), 'autolink', 120) inlinePatterns.register(AutolinkInlineProcessor(AUTOLINK_RE, md), 'autolink', 120)
inlinePatterns.register(AutomailInlineProcessor(AUTOMAIL_RE, md), 'automail', 110) inlinePatterns.register(AutomailInlineProcessor(AUTOMAIL_RE, md), 'automail', 110)
inlinePatterns.register(SubstituteTagInlineProcessor(LINE_BREAK_RE, 'br'), 'linebreak', 100) inlinePatterns.register(SubstituteTagInlineProcessor(LINE_BREAK_RE, 'br'), 'linebreak', 100)
@@ -135,8 +138,8 @@ STRONG_EM_RE = r'(\*)\1{2}(.+?)\1{2}(.*?)\1'
# ___strong__em_ # ___strong__em_
STRONG_EM2_RE = r'(_)\1{2}(.+?)\1{2}(.*?)\1' STRONG_EM2_RE = r'(_)\1{2}(.+?)\1{2}(.*?)\1'
# __strong_em___ # **strong*em***
STRONG_EM3_RE = r'(\*)\1(?!\1)(.+?)\1(?!\1)(.+?)\1{3}' STRONG_EM3_RE = r'(\*)\1(?!\1)([^*]+?)\1(?!\1)(.+?)\1{3}'
# [text](url) or [text](<url>) or [text](url "title") # [text](url) or [text](<url>) or [text](url "title")
LINK_RE = NOIMG + r'\[' LINK_RE = NOIMG + r'\['
@@ -844,6 +847,14 @@ class ImageReferenceInlineProcessor(ReferenceInlineProcessor):
return el return el
class ShortImageReferenceInlineProcessor(ImageReferenceInlineProcessor):
""" Short form of inage reference: ![ref]. """
def evalId(self, data, index, text):
"""Evaluate the id from of [ref] """
return text.lower(), index, True
class AutolinkInlineProcessor(InlineProcessor): class AutolinkInlineProcessor(InlineProcessor):
""" Return a link Element given an autolink (`<http://example/com>`). """ """ Return a link Element given an autolink (`<http://example/com>`). """
def handleMatch(self, m, data): def handleMatch(self, m, data):

View File

@@ -69,11 +69,10 @@ class RawHtmlPostprocessor(Postprocessor):
""" Iterate over html stash and restore html. """ """ Iterate over html stash and restore html. """
replacements = OrderedDict() replacements = OrderedDict()
for i in range(self.md.htmlStash.html_counter): for i in range(self.md.htmlStash.html_counter):
html = self.md.htmlStash.rawHtmlBlocks[i] html = self.stash_to_string(self.md.htmlStash.rawHtmlBlocks[i])
if self.isblocklevel(html): if self.isblocklevel(html):
replacements["<p>%s</p>" % replacements["<p>{}</p>".format(
(self.md.htmlStash.get_placeholder(i))] = \ self.md.htmlStash.get_placeholder(i))] = html
html + "\n"
replacements[self.md.htmlStash.get_placeholder(i)] = html replacements[self.md.htmlStash.get_placeholder(i)] = html
if replacements: if replacements:
@@ -96,6 +95,10 @@ class RawHtmlPostprocessor(Postprocessor):
return self.md.is_block_level(m.group(1)) return self.md.is_block_level(m.group(1))
return False return False
def stash_to_string(self, text):
""" Convert a stashed object to a string. """
return str(text)
class AndSubstitutePostprocessor(Postprocessor): class AndSubstitutePostprocessor(Postprocessor):
""" Restore valid entities """ """ Restore valid entities """

View File

@@ -26,6 +26,7 @@ complicated.
""" """
from . import util from . import util
from .htmlparser import HTMLExtractor
import re import re
@@ -34,7 +35,6 @@ def build_preprocessors(md, **kwargs):
preprocessors = util.Registry() preprocessors = util.Registry()
preprocessors.register(NormalizeWhitespace(md), 'normalize_whitespace', 30) preprocessors.register(NormalizeWhitespace(md), 'normalize_whitespace', 30)
preprocessors.register(HtmlBlockPreprocessor(md), 'html_block', 20) preprocessors.register(HtmlBlockPreprocessor(md), 'html_block', 20)
preprocessors.register(ReferencePreprocessor(md), 'reference', 10)
return preprocessors return preprocessors
@@ -74,297 +74,9 @@ class NormalizeWhitespace(Preprocessor):
class HtmlBlockPreprocessor(Preprocessor): class HtmlBlockPreprocessor(Preprocessor):
"""Remove html blocks from the text and store them for later retrieval.""" """Remove html blocks from the text and store them for later retrieval."""
right_tag_patterns = ["</%s>", "%s>"]
attrs_pattern = r"""
\s+(?P<attr>[^>"'/= ]+)=(?P<q>['"])(?P<value>.*?)(?P=q) # attr="value"
| # OR
\s+(?P<attr1>[^>"'/= ]+)=(?P<value1>[^> ]+) # attr=value
| # OR
\s+(?P<attr2>[^>"'/= ]+) # attr
"""
left_tag_pattern = r'^\<(?P<tag>[^> ]+)(?P<attrs>(%s)*)\s*\/?\>?' % \
attrs_pattern
attrs_re = re.compile(attrs_pattern, re.VERBOSE)
left_tag_re = re.compile(left_tag_pattern, re.VERBOSE)
markdown_in_raw = False
def _get_left_tag(self, block):
m = self.left_tag_re.match(block)
if m:
tag = m.group('tag')
raw_attrs = m.group('attrs')
attrs = {}
if raw_attrs:
for ma in self.attrs_re.finditer(raw_attrs):
if ma.group('attr'):
if ma.group('value'):
attrs[ma.group('attr').strip()] = ma.group('value')
else:
attrs[ma.group('attr').strip()] = ""
elif ma.group('attr1'):
if ma.group('value1'):
attrs[ma.group('attr1').strip()] = ma.group(
'value1'
)
else:
attrs[ma.group('attr1').strip()] = ""
elif ma.group('attr2'):
attrs[ma.group('attr2').strip()] = ""
return tag, len(m.group(0)), attrs
else:
tag = block[1:].split(">", 1)[0].lower()
return tag, len(tag)+2, {}
def _recursive_tagfind(self, ltag, rtag, start_index, block):
while 1:
i = block.find(rtag, start_index)
if i == -1:
return -1
j = block.find(ltag, start_index)
# if no ltag, or rtag found before another ltag, return index
if (j > i or j == -1):
return i + len(rtag)
# another ltag found before rtag, use end of ltag as starting
# point and search again
j = block.find('>', j)
start_index = self._recursive_tagfind(ltag, rtag, j + 1, block)
if start_index == -1:
# HTML potentially malformed- ltag has no corresponding
# rtag
return -1
def _get_right_tag(self, left_tag, left_index, block):
for p in self.right_tag_patterns:
tag = p % left_tag
i = self._recursive_tagfind(
"<%s" % left_tag, tag, left_index, block
)
if i > 2:
return tag.lstrip("<").rstrip(">"), i
return block.rstrip()[-left_index:-1].lower(), len(block)
def _equal_tags(self, left_tag, right_tag):
if left_tag[0] in ['?', '@', '%']: # handle PHP, etc.
return True
if ("/" + left_tag) == right_tag:
return True
if (right_tag == "--" and left_tag == "--"):
return True
elif left_tag == right_tag[1:] and right_tag[0] == "/":
return True
else:
return False
def _is_oneliner(self, tag):
return (tag in ['hr', 'hr/'])
def _stringindex_to_listindex(self, stringindex, items):
"""
Same effect as concatenating the strings in items,
finding the character to which stringindex refers in that string,
and returning the index of the item in which that character resides.
"""
items.append('dummy')
i, count = 0, 0
while count <= stringindex:
count += len(items[i])
i += 1
return i - 1
def _nested_markdown_in_html(self, items):
"""Find and process html child elements of the given element block."""
for i, item in enumerate(items):
if self.left_tag_re.match(item):
left_tag, left_index, attrs = \
self._get_left_tag(''.join(items[i:]))
right_tag, data_index = self._get_right_tag(
left_tag, left_index, ''.join(items[i:]))
right_listindex = \
self._stringindex_to_listindex(data_index, items[i:]) + i
if 'markdown' in attrs.keys():
items[i] = items[i][left_index:] # remove opening tag
placeholder = self.md.htmlStash.store_tag(
left_tag, attrs, i + 1, right_listindex + 1)
items.insert(i, placeholder)
if len(items) - right_listindex <= 1: # last nest, no tail
right_listindex -= 1
items[right_listindex] = items[right_listindex][
:-len(right_tag) - 2] # remove closing tag
else: # raw html
if len(items) - right_listindex <= 1: # last element
right_listindex -= 1
if right_listindex <= i:
right_listindex = i + 1
placeholder = self.md.htmlStash.store('\n\n'.join(
items[i:right_listindex]))
del items[i:right_listindex]
items.insert(i, placeholder)
return items
def run(self, lines): def run(self, lines):
text = "\n".join(lines) source = '\n'.join(lines)
new_blocks = [] parser = HTMLExtractor(self.md)
text = text.rsplit("\n\n") parser.feed(source)
items = [] parser.close()
left_tag = '' return ''.join(parser.cleandoc).split('\n')
right_tag = ''
in_tag = False # flag
while text:
block = text[0]
if block.startswith("\n"):
block = block[1:]
text = text[1:]
if block.startswith("\n"):
block = block[1:]
if not in_tag:
if block.startswith("<") and len(block.strip()) > 1:
if block[1:4] == "!--":
# is a comment block
left_tag, left_index, attrs = "--", 2, {}
else:
left_tag, left_index, attrs = self._get_left_tag(block)
right_tag, data_index = self._get_right_tag(left_tag,
left_index,
block)
# keep checking conditions below and maybe just append
if data_index < len(block) and (self.md.is_block_level(left_tag) or left_tag == '--'):
text.insert(0, block[data_index:])
block = block[:data_index]
if not (self.md.is_block_level(left_tag) or block[1] in ["!", "?", "@", "%"]):
new_blocks.append(block)
continue
if self._is_oneliner(left_tag):
new_blocks.append(block.strip())
continue
if block.rstrip().endswith(">") \
and self._equal_tags(left_tag, right_tag):
if self.markdown_in_raw and 'markdown' in attrs.keys():
block = block[left_index:-len(right_tag) - 2]
new_blocks.append(self.md.htmlStash.
store_tag(left_tag, attrs, 0, 2))
new_blocks.extend([block])
else:
new_blocks.append(
self.md.htmlStash.store(block.strip()))
continue
else:
# if is block level tag and is not complete
if (not self._equal_tags(left_tag, right_tag)) and \
(self.md.is_block_level(left_tag) or left_tag == "--"):
items.append(block.strip())
in_tag = True
else:
new_blocks.append(
self.md.htmlStash.store(block.strip())
)
continue
else:
new_blocks.append(block)
else:
items.append(block)
# Need to evaluate all items so we can calculate relative to the left index.
right_tag, data_index = self._get_right_tag(left_tag, left_index, ''.join(items))
# Adjust data_index: relative to items -> relative to last block
prev_block_length = 0
for item in items[:-1]:
prev_block_length += len(item)
data_index -= prev_block_length
if self._equal_tags(left_tag, right_tag):
# if find closing tag
if data_index < len(block):
# we have more text after right_tag
items[-1] = block[:data_index]
text.insert(0, block[data_index:])
in_tag = False
if self.markdown_in_raw and 'markdown' in attrs.keys():
items[0] = items[0][left_index:]
items[-1] = items[-1][:-len(right_tag) - 2]
if items[len(items) - 1]: # not a newline/empty string
right_index = len(items) + 3
else:
right_index = len(items) + 2
new_blocks.append(self.md.htmlStash.store_tag(
left_tag, attrs, 0, right_index))
placeholderslen = len(self.md.htmlStash.tag_data)
new_blocks.extend(
self._nested_markdown_in_html(items))
nests = len(self.md.htmlStash.tag_data) - \
placeholderslen
self.md.htmlStash.tag_data[-1 - nests][
'right_index'] += nests - 2
else:
new_blocks.append(
self.md.htmlStash.store('\n\n'.join(items)))
items = []
if items:
if self.markdown_in_raw and 'markdown' in attrs.keys():
items[0] = items[0][left_index:]
items[-1] = items[-1][:-len(right_tag) - 2]
if items[len(items) - 1]: # not a newline/empty string
right_index = len(items) + 3
else:
right_index = len(items) + 2
new_blocks.append(
self.md.htmlStash.store_tag(
left_tag, attrs, 0, right_index))
placeholderslen = len(self.md.htmlStash.tag_data)
new_blocks.extend(self._nested_markdown_in_html(items))
nests = len(self.md.htmlStash.tag_data) - placeholderslen
self.md.htmlStash.tag_data[-1 - nests][
'right_index'] += nests - 2
else:
new_blocks.append(
self.md.htmlStash.store('\n\n'.join(items)))
new_blocks.append('\n')
new_text = "\n\n".join(new_blocks)
return new_text.split("\n")
class ReferencePreprocessor(Preprocessor):
""" Remove reference definitions from text and store for later use. """
TITLE = r'[ ]*(\"(.*)\"|\'(.*)\'|\((.*)\))[ ]*'
RE = re.compile(
r'^[ ]{0,3}\[([^\]]*)\]:\s*([^ ]*)[ ]*(%s)?$' % TITLE, re.DOTALL
)
TITLE_RE = re.compile(r'^%s$' % TITLE)
def run(self, lines):
new_text = []
while lines:
line = lines.pop(0)
m = self.RE.match(line)
if m:
id = m.group(1).strip().lower()
link = m.group(2).lstrip('<').rstrip('>')
t = m.group(5) or m.group(6) or m.group(7)
if not t:
# Check next line for title
tm = self.TITLE_RE.match(lines[0])
if tm:
lines.pop(0)
t = tm.group(2) or tm.group(3) or tm.group(4)
self.md.references[id] = (link, t)
# Preserve the line to prevent raw HTML indexing issue.
# https://github.com/Python-Markdown/markdown/issues/584
new_text.append('')
else:
new_text.append(line)
return new_text # + "\n"

View File

@@ -20,9 +20,10 @@ License: BSD (see LICENSE.md for details).
""" """
import os import os
import sys
import unittest import unittest
import textwrap import textwrap
from . import markdown from . import markdown, util
try: try:
import tidylib import tidylib
@@ -73,6 +74,32 @@ class TestCase(unittest.TestCase):
return textwrap.dedent(text).strip() return textwrap.dedent(text).strip()
class recursionlimit:
"""
A context manager which temporarily modifies the Python recursion limit.
The testing framework, coverage, etc. may add an arbitrary number of levels to the depth. To maintain consistency
in the tests, the current stack depth is determined when called, then added to the provided limit.
Example usage:
with recursionlimit(20):
# test code here
See https://stackoverflow.com/a/50120316/866026
"""
def __init__(self, limit):
self.limit = util._get_stack_depth() + limit
self.old_limit = sys.getrecursionlimit()
def __enter__(self):
sys.setrecursionlimit(self.limit)
def __exit__(self, type, value, tb):
sys.setrecursionlimit(self.old_limit)
######################### #########################
# Legacy Test Framework # # Legacy Test Framework #
######################### #########################
@@ -113,8 +140,11 @@ class LegacyTestMeta(type):
expected = f.read().replace("\r\n", "\n") expected = f.read().replace("\r\n", "\n")
output = markdown(input, **kwargs) output = markdown(input, **kwargs)
if tidylib and normalize: if tidylib and normalize:
expected = _normalize_whitespace(expected) try:
output = _normalize_whitespace(output) expected = _normalize_whitespace(expected)
output = _normalize_whitespace(output)
except OSError:
self.skipTest("Tidylib's c library not available.")
elif normalize: elif normalize:
self.skipTest('Tidylib not available.') self.skipTest('Tidylib not available.')
self.assertMultiLineEqual(output, expected) self.assertMultiLineEqual(output, expected)
@@ -167,7 +197,7 @@ class LegacyTestCase(unittest.TestCase, metaclass=LegacyTestMeta):
arguments for all test files in the directory. arguments for all test files in the directory.
In addition, properties can be defined for each individual set of test files within In addition, properties can be defined for each individual set of test files within
the directory. The property should be given the name of the file wihtout the file the directory. The property should be given the name of the file without the file
extension. Any spaces and dashes in the filename should be replaced with extension. Any spaces and dashes in the filename should be replaced with
underscores. The value of the property should be a `Kwargs` instance which underscores. The value of the property should be a `Kwargs` instance which
contains the keyword arguments that should be passed to `Markdown` for that contains the keyword arguments that should be passed to `Markdown` for that

Some files were not shown because too many files have changed in this diff Show More