Added venv to gitignore
This commit is contained in:
parent
a0fa0071fd
commit
eaf2b26ee2
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
venv/
|
|
@ -1,247 +0,0 @@
|
||||||
<#
|
|
||||||
.Synopsis
|
|
||||||
Activate a Python virtual environment for the current PowerShell session.
|
|
||||||
|
|
||||||
.Description
|
|
||||||
Pushes the python executable for a virtual environment to the front of the
|
|
||||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
|
||||||
in a Python virtual environment. Makes use of the command line switches as
|
|
||||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
|
||||||
|
|
||||||
.Parameter VenvDir
|
|
||||||
Path to the directory that contains the virtual environment to activate. The
|
|
||||||
default value for this is the parent of the directory that the Activate.ps1
|
|
||||||
script is located within.
|
|
||||||
|
|
||||||
.Parameter Prompt
|
|
||||||
The prompt prefix to display when this virtual environment is activated. By
|
|
||||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
|
||||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1
|
|
||||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1 -Verbose
|
|
||||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
|
||||||
and shows extra information about the activation as it executes.
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
|
||||||
Activates the Python virtual environment located in the specified location.
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1 -Prompt "MyPython"
|
|
||||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
|
||||||
and prefixes the current prompt with the specified string (surrounded in
|
|
||||||
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(
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[String]
|
|
||||||
$VenvDir,
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[String]
|
|
||||||
$Prompt
|
|
||||||
)
|
|
||||||
|
|
||||||
<# Function declarations --------------------------------------------------- #>
|
|
||||||
|
|
||||||
<#
|
|
||||||
.Synopsis
|
|
||||||
Remove all shell session elements added by the Activate script, including the
|
|
||||||
addition of the virtual environment's Python executable from the beginning of
|
|
||||||
the PATH variable.
|
|
||||||
|
|
||||||
.Parameter NonDestructive
|
|
||||||
If present, do not remove this function from the global namespace for the
|
|
||||||
session.
|
|
||||||
|
|
||||||
#>
|
|
||||||
function global:deactivate ([switch]$NonDestructive) {
|
|
||||||
# Revert to original values
|
|
||||||
|
|
||||||
# The prior prompt:
|
|
||||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
|
||||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
|
||||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
|
||||||
}
|
|
||||||
|
|
||||||
# The prior PYTHONHOME:
|
|
||||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
|
||||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
|
||||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
|
||||||
}
|
|
||||||
|
|
||||||
# The prior PATH:
|
|
||||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
|
||||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
|
||||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just remove the VIRTUAL_ENV altogether:
|
|
||||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
|
||||||
Remove-Item -Path env:VIRTUAL_ENV
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
|
||||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
|
||||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
|
||||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
|
||||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
# Leave deactivate function in the global namespace if requested:
|
|
||||||
if (-not $NonDestructive) {
|
|
||||||
Remove-Item -Path function:deactivate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<#
|
|
||||||
.Description
|
|
||||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
|
||||||
given folder, and returns them in a map.
|
|
||||||
|
|
||||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
|
||||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
|
||||||
then it is considered a `key = value` line. The left hand string is the key,
|
|
||||||
the right hand is the value.
|
|
||||||
|
|
||||||
If the value starts with a `'` or a `"` then the first and last character is
|
|
||||||
stripped from the value before being captured.
|
|
||||||
|
|
||||||
.Parameter ConfigDir
|
|
||||||
Path to the directory that contains the `pyvenv.cfg` file.
|
|
||||||
#>
|
|
||||||
function Get-PyVenvConfig(
|
|
||||||
[String]
|
|
||||||
$ConfigDir
|
|
||||||
) {
|
|
||||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
|
||||||
|
|
||||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
|
||||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
|
||||||
|
|
||||||
# An empty map will be returned if no config file is found.
|
|
||||||
$pyvenvConfig = @{ }
|
|
||||||
|
|
||||||
if ($pyvenvConfigPath) {
|
|
||||||
|
|
||||||
Write-Verbose "File exists, parse `key = value` lines"
|
|
||||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
|
||||||
|
|
||||||
$pyvenvConfigContent | ForEach-Object {
|
|
||||||
$keyval = $PSItem -split "\s*=\s*", 2
|
|
||||||
if ($keyval[0] -and $keyval[1]) {
|
|
||||||
$val = $keyval[1]
|
|
||||||
|
|
||||||
# Remove extraneous quotations around a string value.
|
|
||||||
if ("'""".Contains($val.Substring(0, 1))) {
|
|
||||||
$val = $val.Substring(1, $val.Length - 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
$pyvenvConfig[$keyval[0]] = $val
|
|
||||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $pyvenvConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
<# Begin Activate script --------------------------------------------------- #>
|
|
||||||
|
|
||||||
# Determine the containing directory of this script
|
|
||||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
|
||||||
|
|
||||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
|
||||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
|
||||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
|
||||||
|
|
||||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
|
||||||
# First, get the location of the virtual environment, it might not be
|
|
||||||
# VenvExecDir if specified on the command line.
|
|
||||||
if ($VenvDir) {
|
|
||||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
|
||||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
|
||||||
Write-Verbose "VenvDir=$VenvDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
|
||||||
# as `prompt`.
|
|
||||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
|
||||||
|
|
||||||
# Next, set the prompt from the command line, or the config file, or
|
|
||||||
# just use the name of the virtual environment folder.
|
|
||||||
if ($Prompt) {
|
|
||||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
|
||||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
|
||||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
|
||||||
$Prompt = $pyvenvCfg['prompt'];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
|
||||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
|
||||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Verbose "Prompt = '$Prompt'"
|
|
||||||
Write-Verbose "VenvDir='$VenvDir'"
|
|
||||||
|
|
||||||
# Deactivate any currently active virtual environment, but leave the
|
|
||||||
# deactivate function in place.
|
|
||||||
deactivate -nondestructive
|
|
||||||
|
|
||||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
|
||||||
# that there is an activated venv.
|
|
||||||
$env:VIRTUAL_ENV = $VenvDir
|
|
||||||
|
|
||||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
|
||||||
|
|
||||||
Write-Verbose "Setting prompt to '$Prompt'"
|
|
||||||
|
|
||||||
# Set the prompt to include the env name
|
|
||||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
|
||||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
|
||||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
|
||||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
|
||||||
|
|
||||||
function global:prompt {
|
|
||||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
|
||||||
_OLD_VIRTUAL_PROMPT
|
|
||||||
}
|
|
||||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clear PYTHONHOME
|
|
||||||
if (Test-Path -Path Env:PYTHONHOME) {
|
|
||||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
|
||||||
Remove-Item -Path Env:PYTHONHOME
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add the venv to the PATH
|
|
||||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
|
||||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
|
|
@ -1,69 +0,0 @@
|
||||||
# This file must be used with "source bin/activate" *from bash*
|
|
||||||
# you cannot run it directly
|
|
||||||
|
|
||||||
deactivate () {
|
|
||||||
# reset old environment variables
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
|
||||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
|
||||||
export PATH
|
|
||||||
unset _OLD_VIRTUAL_PATH
|
|
||||||
fi
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
|
||||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
|
||||||
export PYTHONHOME
|
|
||||||
unset _OLD_VIRTUAL_PYTHONHOME
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This should detect bash and zsh, which have a hash command that must
|
|
||||||
# be called to get it to forget past commands. Without forgetting
|
|
||||||
# past commands the $PATH changes we made may not be respected
|
|
||||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
|
||||||
hash -r 2> /dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
|
||||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
|
||||||
export PS1
|
|
||||||
unset _OLD_VIRTUAL_PS1
|
|
||||||
fi
|
|
||||||
|
|
||||||
unset VIRTUAL_ENV
|
|
||||||
unset VIRTUAL_ENV_PROMPT
|
|
||||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
|
||||||
# Self destruct!
|
|
||||||
unset -f deactivate
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# unset irrelevant variables
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
VIRTUAL_ENV="/home/ricer/projects/vanten-s.com/venv"
|
|
||||||
export VIRTUAL_ENV
|
|
||||||
|
|
||||||
_OLD_VIRTUAL_PATH="$PATH"
|
|
||||||
PATH="$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
export PATH
|
|
||||||
|
|
||||||
# unset PYTHONHOME if set
|
|
||||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
|
||||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
|
||||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
|
||||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
|
||||||
unset PYTHONHOME
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
|
||||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
|
||||||
PS1="(venv) ${PS1:-}"
|
|
||||||
export PS1
|
|
||||||
VIRTUAL_ENV_PROMPT="(venv) "
|
|
||||||
export VIRTUAL_ENV_PROMPT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This should detect bash and zsh, which have a hash command that must
|
|
||||||
# be called to get it to forget past commands. Without forgetting
|
|
||||||
# past commands the $PATH changes we made may not be respected
|
|
||||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
|
||||||
hash -r 2> /dev/null
|
|
||||||
fi
|
|
|
@ -1,26 +0,0 @@
|
||||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
|
||||||
# You cannot run it directly.
|
|
||||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
|
||||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
|
||||||
|
|
||||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
|
||||||
|
|
||||||
# Unset irrelevant variables.
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
setenv VIRTUAL_ENV "/home/ricer/projects/vanten-s.com/venv"
|
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PATH="$PATH"
|
|
||||||
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
|
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
|
||||||
|
|
||||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
|
||||||
set prompt = "(venv) $prompt"
|
|
||||||
setenv VIRTUAL_ENV_PROMPT "(venv) "
|
|
||||||
endif
|
|
||||||
|
|
||||||
alias pydoc python -m pydoc
|
|
||||||
|
|
||||||
rehash
|
|
|
@ -1,69 +0,0 @@
|
||||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
|
||||||
# (https://fishshell.com/); you cannot run it directly.
|
|
||||||
|
|
||||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
|
||||||
# reset old environment variables
|
|
||||||
if test -n "$_OLD_VIRTUAL_PATH"
|
|
||||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
|
||||||
set -e _OLD_VIRTUAL_PATH
|
|
||||||
end
|
|
||||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
|
||||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
|
||||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
|
||||||
end
|
|
||||||
|
|
||||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
|
||||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
|
||||||
# prevents error when using nested fish instances (Issue #93858)
|
|
||||||
if functions -q _old_fish_prompt
|
|
||||||
functions -e fish_prompt
|
|
||||||
functions -c _old_fish_prompt fish_prompt
|
|
||||||
functions -e _old_fish_prompt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
set -e VIRTUAL_ENV
|
|
||||||
set -e VIRTUAL_ENV_PROMPT
|
|
||||||
if test "$argv[1]" != "nondestructive"
|
|
||||||
# Self-destruct!
|
|
||||||
functions -e deactivate
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Unset irrelevant variables.
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
set -gx VIRTUAL_ENV "/home/ricer/projects/vanten-s.com/venv"
|
|
||||||
|
|
||||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
|
||||||
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
|
||||||
|
|
||||||
# Unset PYTHONHOME if set.
|
|
||||||
if set -q PYTHONHOME
|
|
||||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
|
||||||
set -e PYTHONHOME
|
|
||||||
end
|
|
||||||
|
|
||||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
|
||||||
# fish uses a function instead of an env var to generate the prompt.
|
|
||||||
|
|
||||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
|
||||||
functions -c fish_prompt _old_fish_prompt
|
|
||||||
|
|
||||||
# With the original prompt function renamed, we can override with our own.
|
|
||||||
function fish_prompt
|
|
||||||
# Save the return status of the last command.
|
|
||||||
set -l old_status $status
|
|
||||||
|
|
||||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
|
||||||
printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal)
|
|
||||||
|
|
||||||
# Restore the return status of the previous command.
|
|
||||||
echo "exit $old_status" | .
|
|
||||||
# Output the original/"old" prompt.
|
|
||||||
_old_fish_prompt
|
|
||||||
end
|
|
||||||
|
|
||||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
|
||||||
set -gx VIRTUAL_ENV_PROMPT "(venv) "
|
|
||||||
end
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ricer/projects/vanten-s.com/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from markdown.__main__ import run
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(run())
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ricer/projects/vanten-s.com/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pip._internal.cli.main import main
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ricer/projects/vanten-s.com/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pip._internal.cli.main import main
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ricer/projects/vanten-s.com/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pip._internal.cli.main import main
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
|
@ -1 +0,0 @@
|
||||||
/usr/bin/python
|
|
|
@ -1 +0,0 @@
|
||||||
python
|
|
|
@ -1 +0,0 @@
|
||||||
python
|
|
|
@ -1 +0,0 @@
|
||||||
pip
|
|
|
@ -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.
|
|
|
@ -1,141 +0,0 @@
|
||||||
Metadata-Version: 2.1
|
|
||||||
Name: Markdown
|
|
||||||
Version: 3.4.4
|
|
||||||
Summary: Python implementation of John Gruber's Markdown.
|
|
||||||
Author: Manfred Stienstra, Yuri Takhteyev
|
|
||||||
Author-email: Waylan limberg <python.markdown@gmail.com>
|
|
||||||
Maintainer: Isaac Muse
|
|
||||||
Maintainer-email: Waylan Limberg <python.markdown@gmail.com>
|
|
||||||
License: 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.
|
|
||||||
|
|
||||||
Project-URL: Homepage, https://Python-Markdown.github.io/
|
|
||||||
Project-URL: Documentation, https://Python-Markdown.github.io/
|
|
||||||
Project-URL: Repository, https://github.com/Python-Markdown/markdown
|
|
||||||
Project-URL: Issue Tracker, https://github.com/Python-Markdown/markdown/issues
|
|
||||||
Project-URL: Changelog, https://github.com/Python-Markdown/markdown/blob/master/docs/change_log/index.md
|
|
||||||
Keywords: markdown,markdown-parser,python-markdown,markdown-to-html
|
|
||||||
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.7
|
|
||||||
Classifier: Programming Language :: Python :: 3.8
|
|
||||||
Classifier: Programming Language :: Python :: 3.9
|
|
||||||
Classifier: Programming Language :: Python :: 3.10
|
|
||||||
Classifier: Programming Language :: Python :: 3.11
|
|
||||||
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
|
|
||||||
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
||||||
Requires-Python: >=3.7
|
|
||||||
Description-Content-Type: text/markdown
|
|
||||||
License-File: LICENSE.md
|
|
||||||
Requires-Dist: importlib-metadata (>=4.4) ; python_version < "3.10"
|
|
||||||
Provides-Extra: docs
|
|
||||||
Requires-Dist: mkdocs (>=1.0) ; extra == 'docs'
|
|
||||||
Requires-Dist: mkdocs-nature (>=0.4) ; extra == 'docs'
|
|
||||||
Requires-Dist: mdx-gh-links (>=0.2) ; extra == 'docs'
|
|
||||||
Provides-Extra: testing
|
|
||||||
Requires-Dist: coverage ; extra == 'testing'
|
|
||||||
Requires-Dist: pyyaml ; extra == 'testing'
|
|
||||||
|
|
||||||
[Python-Markdown][]
|
|
||||||
===================
|
|
||||||
|
|
||||||
[![Build Status][build-button]][build]
|
|
||||||
[![Coverage Status][codecov-button]][codecov]
|
|
||||||
[![Latest Version][mdversion-button]][md-pypi]
|
|
||||||
[![Python Versions][pyversion-button]][md-pypi]
|
|
||||||
[![BSD License][bsdlicense-button]][bsdlicense]
|
|
||||||
[![Code of Conduct][codeofconduct-button]][Code of Conduct]
|
|
||||||
|
|
||||||
[build-button]: https://github.com/Python-Markdown/markdown/workflows/CI/badge.svg?event=push
|
|
||||||
[build]: https://github.com/Python-Markdown/markdown/actions?query=workflow%3ACI+event%3Apush
|
|
||||||
[codecov-button]: https://codecov.io/gh/Python-Markdown/markdown/branch/master/graph/badge.svg
|
|
||||||
[codecov]: https://codecov.io/gh/Python-Markdown/markdown
|
|
||||||
[mdversion-button]: https://img.shields.io/pypi/v/Markdown.svg
|
|
||||||
[md-pypi]: https://pypi.org/project/Markdown/
|
|
||||||
[pyversion-button]: https://img.shields.io/pypi/pyversions/Markdown.svg
|
|
||||||
[bsdlicense-button]: https://img.shields.io/badge/license-BSD-yellow.svg
|
|
||||||
[bsdlicense]: https://opensource.org/licenses/BSD-3-Clause
|
|
||||||
[codeofconduct-button]: https://img.shields.io/badge/code%20of%20conduct-contributor%20covenant-green.svg?style=flat-square
|
|
||||||
[Code of Conduct]: https://github.com/Python-Markdown/markdown/blob/master/CODE_OF_CONDUCT.md
|
|
||||||
|
|
||||||
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][].
|
|
||||||
|
|
||||||
[Python-Markdown]: https://Python-Markdown.github.io/
|
|
||||||
[Markdown]: https://daringfireball.net/projects/markdown/
|
|
||||||
[Features]: https://Python-Markdown.github.io#Features
|
|
||||||
[Available Extensions]: https://Python-Markdown.github.io/extensions
|
|
||||||
|
|
||||||
Documentation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install markdown
|
|
||||||
```
|
|
||||||
```python
|
|
||||||
import markdown
|
|
||||||
html = markdown.markdown(your_text_string)
|
|
||||||
```
|
|
||||||
|
|
||||||
For more advanced [installation] and [usage] documentation, see the `docs/` directory
|
|
||||||
of the distribution or the project website at <https://Python-Markdown.github.io/>.
|
|
||||||
|
|
||||||
[installation]: https://python-markdown.github.io/install/
|
|
||||||
[usage]: https://python-markdown.github.io/reference/
|
|
||||||
|
|
||||||
See the change log at <https://Python-Markdown.github.io/change_log>.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Code of Conduct
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Everyone interacting in the Python-Markdown project's code bases, issue trackers,
|
|
||||||
and mailing lists is expected to follow the [Code of Conduct].
|
|
|
@ -1,75 +0,0 @@
|
||||||
../../../bin/markdown_py,sha256=xhKvQC-jBfj71MylgNmwZ3DYXBAlA3aLHoLyM3HHRNA,247
|
|
||||||
Markdown-3.4.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
Markdown-3.4.4.dist-info/LICENSE.md,sha256=bxGTy2NHGOZcOlN9biXr1hSCDsDvaTz8EiSBEmONZNo,1645
|
|
||||||
Markdown-3.4.4.dist-info/METADATA,sha256=3t_AJ71xB1ulJsKKME-ecE_WZHv4RGfI-Znm9LDfv1c,6861
|
|
||||||
Markdown-3.4.4.dist-info/RECORD,,
|
|
||||||
Markdown-3.4.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
Markdown-3.4.4.dist-info/WHEEL,sha256=AtBG6SXL3KF_v0NxLf0ehyVOh0cold-JbJYXNGorC6Q,92
|
|
||||||
Markdown-3.4.4.dist-info/entry_points.txt,sha256=lMEyiiA_ZZyfPCBlDviBl-SiU0cfoeuEKpwxw361sKQ,1102
|
|
||||||
Markdown-3.4.4.dist-info/top_level.txt,sha256=IAxs8x618RXoH1uCqeLLxXsDefJvE_mIibr_M4sOlyk,9
|
|
||||||
markdown/__init__.py,sha256=XBsIkhLRPtAIiCAHaiPSu03JaKbHPYqO4bfD0hq6VVo,1013
|
|
||||||
markdown/__main__.py,sha256=LVrZCvarSImaF3nRs7B9JXZDBnsGPfiILoPc0pxPgbI,5805
|
|
||||||
markdown/__meta__.py,sha256=vYlzYL8jGHCvF4Vieh7mL8oEjiIhBg9NH1br1NOGDUA,1654
|
|
||||||
markdown/__pycache__/__init__.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/__main__.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/__meta__.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/blockparser.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/blockprocessors.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/core.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/htmlparser.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/inlinepatterns.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/postprocessors.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/preprocessors.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/serializers.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/test_tools.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/treeprocessors.cpython-311.pyc,,
|
|
||||||
markdown/__pycache__/util.cpython-311.pyc,,
|
|
||||||
markdown/blockparser.py,sha256=LdOe-2JI-veEsanlWnQUrFHQ_xglyI-UUd5BrNtgi5M,4162
|
|
||||||
markdown/blockprocessors.py,sha256=ITWkWUycVERiqZrHS5hEJaZqupiElEpeNF5YC09txwQ,24965
|
|
||||||
markdown/core.py,sha256=HSYh7ATmZNVagq_D4aN69qlc-g_c4LDGimkkPprVffY,14729
|
|
||||||
markdown/extensions/__init__.py,sha256=JJnN0JI4mU7txHrNNpWcRgUWNh22yqN1j6hUk9bxKrs,2869
|
|
||||||
markdown/extensions/__pycache__/__init__.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/abbr.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/admonition.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/attr_list.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/codehilite.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/def_list.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/extra.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/fenced_code.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/footnotes.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/legacy_attrs.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/legacy_em.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/md_in_html.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/meta.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/nl2br.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/sane_lists.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/smarty.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/tables.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/toc.cpython-311.pyc,,
|
|
||||||
markdown/extensions/__pycache__/wikilinks.cpython-311.pyc,,
|
|
||||||
markdown/extensions/abbr.py,sha256=bzUCjh3OlbxwSY7dmoglixM63JM4-Pt-un7cBbEvNRE,3191
|
|
||||||
markdown/extensions/admonition.py,sha256=YuuznonEoYXkytbpCIRP8nqViUOhJlzCXubrpYvW4UY,5892
|
|
||||||
markdown/extensions/attr_list.py,sha256=p9zQjzCszYQ9ExrFGfs56xIxcYal3NvneF5EzBuXfjw,6026
|
|
||||||
markdown/extensions/codehilite.py,sha256=f6ByNCD06Z_MMHGI__wJ5o8CBAZPe5TYSvswfkCAtMA,12966
|
|
||||||
markdown/extensions/def_list.py,sha256=CLLuACOPOShLwiuY1jgWb06RfyOcnFfu464jd5jS5z0,3643
|
|
||||||
markdown/extensions/extra.py,sha256=TRi9DvkSQemA5WALtwd_mpk7qqQFxISLiHaoIpuo8bA,1843
|
|
||||||
markdown/extensions/fenced_code.py,sha256=BeU5iX84l4fuXHJy3ruvCre5ufLQavJiZWZzkj7hGoQ,7240
|
|
||||||
markdown/extensions/footnotes.py,sha256=_hP1VoW4Drxg_wsy2vcxu3ga8RBNy7LhEaKB7Bd05dU,15887
|
|
||||||
markdown/extensions/legacy_attrs.py,sha256=IiqvLJTzVhNSeJIwPVKxCUnB12-qoxyUE4jW-2o_VCg,2548
|
|
||||||
markdown/extensions/legacy_em.py,sha256=A01v5kkTwkQeyE87pfCgOIoc6_vSnspsiv8EJoGgKlA,1584
|
|
||||||
markdown/extensions/md_in_html.py,sha256=rHNMv6MhO3SoK_twjcAkNjMY0Fn-w5yXC1fbjBS9kPs,15884
|
|
||||||
markdown/extensions/meta.py,sha256=DH1Qr2xbvSItnY4EG__NNNhh7JEMUHeFrwDhX3B4wno,2333
|
|
||||||
markdown/extensions/nl2br.py,sha256=CnDxFoVTuD4AIbYMKcs-X8XVIEsTyTQg0YTwOVoXGjw,784
|
|
||||||
markdown/extensions/sane_lists.py,sha256=ZQmCf-247KBexVG0fc62nDvokGkV6W1uavYbieNKSG4,1505
|
|
||||||
markdown/extensions/smarty.py,sha256=Of926JSkkSEj2tgQZ5Vh01gOaHG9oFgchjX41W-4crk,10081
|
|
||||||
markdown/extensions/tables.py,sha256=fzZL40w1jNknE75jfH-f1cCw6Fp3jsjody7350ejzXs,8111
|
|
||||||
markdown/extensions/toc.py,sha256=mBe2hsMWQCE6Jc-PuugxMUIvOzs2iIKqPnNVYQYZ_jg,13972
|
|
||||||
markdown/extensions/wikilinks.py,sha256=l9gONSBXQECNMSKN-o51OTgMTTurEux9NKfisfE8zbk,2814
|
|
||||||
markdown/htmlparser.py,sha256=N2XhpAWhYtv6HvRBEL3x78-YFk8XCzDrJLS9f0GWyGI,13076
|
|
||||||
markdown/inlinepatterns.py,sha256=rmcoBMqA0zVd4Xr6BnP7X6DCydEJvJyYyau-lU7Q22o,30121
|
|
||||||
markdown/postprocessors.py,sha256=anYxZU1BQIe71SBXHpx4O7ev_Ss5EbaU9PFox41aeh8,4320
|
|
||||||
markdown/preprocessors.py,sha256=-s8QGHGlX7JAIJTfCivuc-CVwTLWs0IyEU94YUT2IvQ,2742
|
|
||||||
markdown/serializers.py,sha256=7I3QoVHVXs3uxQAS5WFiP42nPkuUUoy3GsO0DFEAPpU,6519
|
|
||||||
markdown/test_tools.py,sha256=mWmmlDuTzlFDWPWYeeSAWm3vuJSSo00gm-AeUJpCvk0,8392
|
|
||||||
markdown/treeprocessors.py,sha256=aa31Rtjl587HCC7Wiz6weLiR2-knM263oO2bYeW9foU,16345
|
|
||||||
markdown/util.py,sha256=ztabZWmIzs-AZM2DdFowvYJIQSUcUBGAFuRoexbmS00,11678
|
|
|
@ -1,5 +0,0 @@
|
||||||
Wheel-Version: 1.0
|
|
||||||
Generator: bdist_wheel (0.41.0)
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
||||||
|
|
|
@ -1,22 +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
|
|
|
@ -1 +0,0 @@
|
||||||
markdown
|
|
|
@ -1,222 +0,0 @@
|
||||||
# don't import any costly modules
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
is_pypy = '__pypy__' in sys.builtin_module_names
|
|
||||||
|
|
||||||
|
|
||||||
def warn_distutils_present():
|
|
||||||
if 'distutils' not in sys.modules:
|
|
||||||
return
|
|
||||||
if is_pypy and sys.version_info < (3, 7):
|
|
||||||
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
|
|
||||||
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
|
|
||||||
return
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Distutils was imported before Setuptools, but importing Setuptools "
|
|
||||||
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
|
||||||
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
|
||||||
"using distutils directly, ensure that setuptools is installed in the "
|
|
||||||
"traditional way (e.g. not an editable install), and/or make sure "
|
|
||||||
"that setuptools is always imported before distutils."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def clear_distutils():
|
|
||||||
if 'distutils' not in sys.modules:
|
|
||||||
return
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn("Setuptools is replacing distutils.")
|
|
||||||
mods = [
|
|
||||||
name
|
|
||||||
for name in sys.modules
|
|
||||||
if name == "distutils" or name.startswith("distutils.")
|
|
||||||
]
|
|
||||||
for name in mods:
|
|
||||||
del sys.modules[name]
|
|
||||||
|
|
||||||
|
|
||||||
def enabled():
|
|
||||||
"""
|
|
||||||
Allow selection of distutils by environment variable.
|
|
||||||
"""
|
|
||||||
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
|
|
||||||
return which == 'local'
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_local_distutils():
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
clear_distutils()
|
|
||||||
|
|
||||||
# With the DistutilsMetaFinder in place,
|
|
||||||
# perform an import to cause distutils to be
|
|
||||||
# loaded from setuptools._distutils. Ref #2906.
|
|
||||||
with shim():
|
|
||||||
importlib.import_module('distutils')
|
|
||||||
|
|
||||||
# check that submodules load as expected
|
|
||||||
core = importlib.import_module('distutils.core')
|
|
||||||
assert '_distutils' in core.__file__, core.__file__
|
|
||||||
assert 'setuptools._distutils.log' not in sys.modules
|
|
||||||
|
|
||||||
|
|
||||||
def do_override():
|
|
||||||
"""
|
|
||||||
Ensure that the local copy of distutils is preferred over stdlib.
|
|
||||||
|
|
||||||
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
|
||||||
for more motivation.
|
|
||||||
"""
|
|
||||||
if enabled():
|
|
||||||
warn_distutils_present()
|
|
||||||
ensure_local_distutils()
|
|
||||||
|
|
||||||
|
|
||||||
class _TrivialRe:
|
|
||||||
def __init__(self, *patterns):
|
|
||||||
self._patterns = patterns
|
|
||||||
|
|
||||||
def match(self, string):
|
|
||||||
return all(pat in string for pat in self._patterns)
|
|
||||||
|
|
||||||
|
|
||||||
class DistutilsMetaFinder:
|
|
||||||
def find_spec(self, fullname, path, target=None):
|
|
||||||
# optimization: only consider top level modules and those
|
|
||||||
# found in the CPython test suite.
|
|
||||||
if path is not None and not fullname.startswith('test.'):
|
|
||||||
return
|
|
||||||
|
|
||||||
method_name = 'spec_for_{fullname}'.format(**locals())
|
|
||||||
method = getattr(self, method_name, lambda: None)
|
|
||||||
return method()
|
|
||||||
|
|
||||||
def spec_for_distutils(self):
|
|
||||||
if self.is_cpython():
|
|
||||||
return
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
import importlib.abc
|
|
||||||
import importlib.util
|
|
||||||
|
|
||||||
try:
|
|
||||||
mod = importlib.import_module('setuptools._distutils')
|
|
||||||
except Exception:
|
|
||||||
# There are a couple of cases where setuptools._distutils
|
|
||||||
# may not be present:
|
|
||||||
# - An older Setuptools without a local distutils is
|
|
||||||
# taking precedence. Ref #2957.
|
|
||||||
# - Path manipulation during sitecustomize removes
|
|
||||||
# setuptools from the path but only after the hook
|
|
||||||
# has been loaded. Ref #2980.
|
|
||||||
# In either case, fall back to stdlib behavior.
|
|
||||||
return
|
|
||||||
|
|
||||||
class DistutilsLoader(importlib.abc.Loader):
|
|
||||||
def create_module(self, spec):
|
|
||||||
mod.__name__ = 'distutils'
|
|
||||||
return mod
|
|
||||||
|
|
||||||
def exec_module(self, module):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return importlib.util.spec_from_loader(
|
|
||||||
'distutils', DistutilsLoader(), origin=mod.__file__
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_cpython():
|
|
||||||
"""
|
|
||||||
Suppress supplying distutils for CPython (build and tests).
|
|
||||||
Ref #2965 and #3007.
|
|
||||||
"""
|
|
||||||
return os.path.isfile('pybuilddir.txt')
|
|
||||||
|
|
||||||
def spec_for_pip(self):
|
|
||||||
"""
|
|
||||||
Ensure stdlib distutils when running under pip.
|
|
||||||
See pypa/pip#8761 for rationale.
|
|
||||||
"""
|
|
||||||
if self.pip_imported_during_build():
|
|
||||||
return
|
|
||||||
clear_distutils()
|
|
||||||
self.spec_for_distutils = lambda: None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def pip_imported_during_build(cls):
|
|
||||||
"""
|
|
||||||
Detect if pip is being imported in a build script. Ref #2355.
|
|
||||||
"""
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
return any(
|
|
||||||
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def frame_file_is_setup(frame):
|
|
||||||
"""
|
|
||||||
Return True if the indicated frame suggests a setup.py file.
|
|
||||||
"""
|
|
||||||
# some frames may not have __file__ (#2940)
|
|
||||||
return frame.f_globals.get('__file__', '').endswith('setup.py')
|
|
||||||
|
|
||||||
def spec_for_sensitive_tests(self):
|
|
||||||
"""
|
|
||||||
Ensure stdlib distutils when running select tests under CPython.
|
|
||||||
|
|
||||||
python/cpython#91169
|
|
||||||
"""
|
|
||||||
clear_distutils()
|
|
||||||
self.spec_for_distutils = lambda: None
|
|
||||||
|
|
||||||
sensitive_tests = (
|
|
||||||
[
|
|
||||||
'test.test_distutils',
|
|
||||||
'test.test_peg_generator',
|
|
||||||
'test.test_importlib',
|
|
||||||
]
|
|
||||||
if sys.version_info < (3, 10)
|
|
||||||
else [
|
|
||||||
'test.test_distutils',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
for name in DistutilsMetaFinder.sensitive_tests:
|
|
||||||
setattr(
|
|
||||||
DistutilsMetaFinder,
|
|
||||||
f'spec_for_{name}',
|
|
||||||
DistutilsMetaFinder.spec_for_sensitive_tests,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
DISTUTILS_FINDER = DistutilsMetaFinder()
|
|
||||||
|
|
||||||
|
|
||||||
def add_shim():
|
|
||||||
DISTUTILS_FINDER in sys.meta_path or insert_shim()
|
|
||||||
|
|
||||||
|
|
||||||
class shim:
|
|
||||||
def __enter__(self):
|
|
||||||
insert_shim()
|
|
||||||
|
|
||||||
def __exit__(self, exc, value, tb):
|
|
||||||
remove_shim()
|
|
||||||
|
|
||||||
|
|
||||||
def insert_shim():
|
|
||||||
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_shim():
|
|
||||||
try:
|
|
||||||
sys.meta_path.remove(DISTUTILS_FINDER)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
Binary file not shown.
Binary file not shown.
|
@ -1 +0,0 @@
|
||||||
__import__('_distutils_hack').do_override()
|
|
|
@ -1 +0,0 @@
|
||||||
import os; var = 'SETUPTOOLS_USE_DISTUTILS'; enabled = os.environ.get(var, 'local') == 'local'; enabled and __import__('_distutils_hack').add_shim();
|
|
|
@ -1,28 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .core import Markdown, markdown, markdownFromFile
|
|
||||||
from .__meta__ import __version__, __version_info__ # noqa
|
|
||||||
|
|
||||||
# For backward compatibility as some extensions expect it...
|
|
||||||
from .extensions import Extension # noqa
|
|
||||||
|
|
||||||
__all__ = ['Markdown', 'markdown', 'markdownFromFile']
|
|
|
@ -1,151 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import optparse
|
|
||||||
import codecs
|
|
||||||
import warnings
|
|
||||||
import markdown
|
|
||||||
try:
|
|
||||||
# We use `unsafe_load` because users may need to pass in actual Python
|
|
||||||
# objects. As this is only available from the CLI, the user has much
|
|
||||||
# worse problems if an attacker can use this as an attach vector.
|
|
||||||
from yaml import unsafe_load as yaml_load
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
try:
|
|
||||||
# Fall back to PyYAML <5.1
|
|
||||||
from yaml import load as yaml_load
|
|
||||||
except ImportError:
|
|
||||||
# Fall back to JSON
|
|
||||||
from json import load as yaml_load
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from logging import DEBUG, WARNING, CRITICAL
|
|
||||||
|
|
||||||
logger = logging.getLogger('MARKDOWN')
|
|
||||||
|
|
||||||
|
|
||||||
def parse_options(args=None, values=None):
|
|
||||||
"""
|
|
||||||
Define and parse `optparse` options for command-line usage.
|
|
||||||
"""
|
|
||||||
usage = """%prog [options] [INPUTFILE]
|
|
||||||
(STDIN is assumed if no INPUTFILE is given)"""
|
|
||||||
desc = "A Python implementation of John Gruber's Markdown. " \
|
|
||||||
"https://Python-Markdown.github.io/"
|
|
||||||
ver = "%%prog %s" % markdown.__version__
|
|
||||||
|
|
||||||
parser = optparse.OptionParser(usage=usage, description=desc, version=ver)
|
|
||||||
parser.add_option("-f", "--file", dest="filename", default=None,
|
|
||||||
help="Write output to OUTPUT_FILE. Defaults to STDOUT.",
|
|
||||||
metavar="OUTPUT_FILE")
|
|
||||||
parser.add_option("-e", "--encoding", dest="encoding",
|
|
||||||
help="Encoding for input and output files.",)
|
|
||||||
parser.add_option("-o", "--output_format", dest="output_format",
|
|
||||||
default='xhtml', metavar="OUTPUT_FORMAT",
|
|
||||||
help="Use output format 'xhtml' (default) or 'html'.")
|
|
||||||
parser.add_option("-n", "--no_lazy_ol", dest="lazy_ol",
|
|
||||||
action='store_false', default=True,
|
|
||||||
help="Observe number of first item of ordered lists.")
|
|
||||||
parser.add_option("-x", "--extension", action="append", dest="extensions",
|
|
||||||
help="Load extension EXTENSION.", metavar="EXTENSION")
|
|
||||||
parser.add_option("-c", "--extension_configs",
|
|
||||||
dest="configfile", default=None,
|
|
||||||
help="Read extension configurations from CONFIG_FILE. "
|
|
||||||
"CONFIG_FILE must be of JSON or YAML format. YAML "
|
|
||||||
"format requires that a python YAML library be "
|
|
||||||
"installed. The parsed JSON or YAML must result in a "
|
|
||||||
"python dictionary which would be accepted by the "
|
|
||||||
"'extension_configs' keyword on the markdown.Markdown "
|
|
||||||
"class. The extensions must also be loaded with the "
|
|
||||||
"`--extension` option.",
|
|
||||||
metavar="CONFIG_FILE")
|
|
||||||
parser.add_option("-q", "--quiet", default=CRITICAL,
|
|
||||||
action="store_const", const=CRITICAL+10, dest="verbose",
|
|
||||||
help="Suppress all warnings.")
|
|
||||||
parser.add_option("-v", "--verbose",
|
|
||||||
action="store_const", const=WARNING, dest="verbose",
|
|
||||||
help="Print all warnings.")
|
|
||||||
parser.add_option("--noisy",
|
|
||||||
action="store_const", const=DEBUG, dest="verbose",
|
|
||||||
help="Print debug messages.")
|
|
||||||
|
|
||||||
(options, args) = parser.parse_args(args, values)
|
|
||||||
|
|
||||||
if len(args) == 0:
|
|
||||||
input_file = None
|
|
||||||
else:
|
|
||||||
input_file = args[0]
|
|
||||||
|
|
||||||
if not options.extensions:
|
|
||||||
options.extensions = []
|
|
||||||
|
|
||||||
extension_configs = {}
|
|
||||||
if options.configfile:
|
|
||||||
with codecs.open(
|
|
||||||
options.configfile, mode="r", encoding=options.encoding
|
|
||||||
) as fp:
|
|
||||||
try:
|
|
||||||
extension_configs = yaml_load(fp)
|
|
||||||
except Exception as e:
|
|
||||||
message = "Failed parsing extension config file: %s" % \
|
|
||||||
options.configfile
|
|
||||||
e.args = (message,) + e.args[1:]
|
|
||||||
raise
|
|
||||||
|
|
||||||
opts = {
|
|
||||||
'input': input_file,
|
|
||||||
'output': options.filename,
|
|
||||||
'extensions': options.extensions,
|
|
||||||
'extension_configs': extension_configs,
|
|
||||||
'encoding': options.encoding,
|
|
||||||
'output_format': options.output_format,
|
|
||||||
'lazy_ol': options.lazy_ol
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts, options.verbose
|
|
||||||
|
|
||||||
|
|
||||||
def run(): # pragma: no cover
|
|
||||||
"""Run Markdown from the command line."""
|
|
||||||
|
|
||||||
# Parse options and adjust logging level if necessary
|
|
||||||
options, logging_level = parse_options()
|
|
||||||
if not options:
|
|
||||||
sys.exit(2)
|
|
||||||
logger.setLevel(logging_level)
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
logger.addHandler(console_handler)
|
|
||||||
if logging_level <= WARNING:
|
|
||||||
# Ensure deprecation warnings get displayed
|
|
||||||
warnings.filterwarnings('default')
|
|
||||||
logging.captureWarnings(True)
|
|
||||||
warn_logger = logging.getLogger('py.warnings')
|
|
||||||
warn_logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
# Run
|
|
||||||
markdown.markdownFromFile(**options)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__': # pragma: no cover
|
|
||||||
# Support running module as a command line command.
|
|
||||||
# python -m markdown [options] [args]
|
|
||||||
run()
|
|
|
@ -1,49 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
# __version_info__ format:
|
|
||||||
# (major, minor, patch, dev/alpha/beta/rc/final, #)
|
|
||||||
# (1, 1, 2, 'dev', 0) => "1.1.2.dev0"
|
|
||||||
# (1, 1, 2, 'alpha', 1) => "1.1.2a1"
|
|
||||||
# (1, 2, 0, 'beta', 2) => "1.2b2"
|
|
||||||
# (1, 2, 0, 'rc', 4) => "1.2rc4"
|
|
||||||
# (1, 2, 0, 'final', 0) => "1.2"
|
|
||||||
__version_info__ = (3, 4, 4, 'final', 0)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_version(version_info):
|
|
||||||
" Returns a PEP 440-compliant version number from version_info. "
|
|
||||||
assert len(version_info) == 5
|
|
||||||
assert version_info[3] in ('dev', 'alpha', 'beta', 'rc', 'final')
|
|
||||||
|
|
||||||
parts = 2 if version_info[2] == 0 else 3
|
|
||||||
v = '.'.join(map(str, version_info[:parts]))
|
|
||||||
|
|
||||||
if version_info[3] == 'dev':
|
|
||||||
v += '.dev' + str(version_info[4])
|
|
||||||
elif version_info[3] != 'final':
|
|
||||||
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'}
|
|
||||||
v += mapping[version_info[3]] + str(version_info[4])
|
|
||||||
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = _get_version(__version_info__)
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,119 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
from . import util
|
|
||||||
|
|
||||||
|
|
||||||
class State(list):
|
|
||||||
""" Track the current and nested state of the parser.
|
|
||||||
|
|
||||||
This utility class is used to track the state of the `BlockParser` and
|
|
||||||
support multiple levels if nesting. It's just a simple API wrapped around
|
|
||||||
a list. Each time a state is set, that state is appended to the end of the
|
|
||||||
list. Each time a state is reset, that state is removed from the end of
|
|
||||||
the list.
|
|
||||||
|
|
||||||
Therefore, each time a state is set for a nested block, that state must be
|
|
||||||
reset when we back out of that level of nesting or the state could be
|
|
||||||
corrupted.
|
|
||||||
|
|
||||||
While all the methods of a list object are available, only the three
|
|
||||||
defined below need be used.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def set(self, state):
|
|
||||||
""" Set a new state. """
|
|
||||||
self.append(state)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
""" Step back one step in nested state. """
|
|
||||||
self.pop()
|
|
||||||
|
|
||||||
def isstate(self, state):
|
|
||||||
""" Test that top (current) level is of given state. """
|
|
||||||
if len(self):
|
|
||||||
return self[-1] == state
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class BlockParser:
|
|
||||||
""" Parse Markdown blocks into an `ElementTree` object.
|
|
||||||
|
|
||||||
A wrapper class that stitches the various `BlockProcessors` together,
|
|
||||||
looping through them and creating an `ElementTree` object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, md):
|
|
||||||
self.blockprocessors = util.Registry()
|
|
||||||
self.state = State()
|
|
||||||
self.md = md
|
|
||||||
|
|
||||||
def parseDocument(self, lines):
|
|
||||||
""" Parse a markdown document into an ElementTree.
|
|
||||||
|
|
||||||
Given a list of lines, an ElementTree object (not just a parent
|
|
||||||
Element) is created and the root element is passed to the parser
|
|
||||||
as the parent. The ElementTree object is returned.
|
|
||||||
|
|
||||||
This should only be called on an entire document, not pieces.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Create an `ElementTree` from the lines
|
|
||||||
self.root = etree.Element(self.md.doc_tag)
|
|
||||||
self.parseChunk(self.root, '\n'.join(lines))
|
|
||||||
return etree.ElementTree(self.root)
|
|
||||||
|
|
||||||
def parseChunk(self, parent, text):
|
|
||||||
""" Parse a chunk of markdown text and attach to given `etree` node.
|
|
||||||
|
|
||||||
While the `text` argument is generally assumed to contain multiple
|
|
||||||
blocks which will be split on blank lines, it could contain only one
|
|
||||||
block. Generally, this method would be called by extensions when
|
|
||||||
block parsing is required.
|
|
||||||
|
|
||||||
The `parent` `etree` Element passed in is altered in place.
|
|
||||||
Nothing is returned.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.parseBlocks(parent, text.split('\n\n'))
|
|
||||||
|
|
||||||
def parseBlocks(self, parent, blocks):
|
|
||||||
""" Process blocks of markdown text and attach to given `etree` node.
|
|
||||||
|
|
||||||
Given a list of `blocks`, each `blockprocessor` is stepped through
|
|
||||||
until there are no blocks left. While an extension could potentially
|
|
||||||
call this method directly, it's generally expected to be used
|
|
||||||
internally.
|
|
||||||
|
|
||||||
This is a public method as an extension may need to add/alter
|
|
||||||
additional `BlockProcessors` which call this method to recursively
|
|
||||||
parse a nested block.
|
|
||||||
|
|
||||||
"""
|
|
||||||
while blocks:
|
|
||||||
for processor in self.blockprocessors:
|
|
||||||
if processor.test(parent, blocks[0]):
|
|
||||||
if processor.run(parent, blocks) is not False:
|
|
||||||
# run returns True or None
|
|
||||||
break
|
|
|
@ -1,622 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
|
|
||||||
CORE MARKDOWN BLOCKPARSER
|
|
||||||
===========================================================================
|
|
||||||
|
|
||||||
This parser handles basic parsing of Markdown blocks. It doesn't concern
|
|
||||||
itself with inline elements such as **bold** or *italics*, but rather just
|
|
||||||
catches blocks, lists, quotes, etc.
|
|
||||||
|
|
||||||
The `BlockParser` is made up of a bunch of `BlockProcessors`, each handling a
|
|
||||||
different type of block. Extensions may add/replace/remove `BlockProcessors`
|
|
||||||
as they need to alter how markdown blocks are parsed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
from . import util
|
|
||||||
from .blockparser import BlockParser
|
|
||||||
|
|
||||||
logger = logging.getLogger('MARKDOWN')
|
|
||||||
|
|
||||||
|
|
||||||
def build_block_parser(md, **kwargs):
|
|
||||||
""" Build the default block parser used by Markdown. """
|
|
||||||
parser = BlockParser(md)
|
|
||||||
parser.blockprocessors.register(EmptyBlockProcessor(parser), 'empty', 100)
|
|
||||||
parser.blockprocessors.register(ListIndentProcessor(parser), 'indent', 90)
|
|
||||||
parser.blockprocessors.register(CodeBlockProcessor(parser), 'code', 80)
|
|
||||||
parser.blockprocessors.register(HashHeaderProcessor(parser), 'hashheader', 70)
|
|
||||||
parser.blockprocessors.register(SetextHeaderProcessor(parser), 'setextheader', 60)
|
|
||||||
parser.blockprocessors.register(HRProcessor(parser), 'hr', 50)
|
|
||||||
parser.blockprocessors.register(OListProcessor(parser), 'olist', 40)
|
|
||||||
parser.blockprocessors.register(UListProcessor(parser), 'ulist', 30)
|
|
||||||
parser.blockprocessors.register(BlockQuoteProcessor(parser), 'quote', 20)
|
|
||||||
parser.blockprocessors.register(ReferenceProcessor(parser), 'reference', 15)
|
|
||||||
parser.blockprocessors.register(ParagraphProcessor(parser), 'paragraph', 10)
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
class BlockProcessor:
|
|
||||||
""" Base class for block processors.
|
|
||||||
|
|
||||||
Each subclass will provide the methods below to work with the source and
|
|
||||||
tree. Each processor will need to define it's own `test` and `run`
|
|
||||||
methods. The `test` method should return True or False, to indicate
|
|
||||||
whether the current block should be processed by this processor. If the
|
|
||||||
test passes, the parser will call the processors `run` method.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parser):
|
|
||||||
self.parser = parser
|
|
||||||
self.tab_length = parser.md.tab_length
|
|
||||||
|
|
||||||
def lastChild(self, parent):
|
|
||||||
""" Return the last child of an `etree` element. """
|
|
||||||
if len(parent):
|
|
||||||
return parent[-1]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def detab(self, text, length=None):
|
|
||||||
""" Remove a tab from the front of each line of the given text. """
|
|
||||||
if length is None:
|
|
||||||
length = self.tab_length
|
|
||||||
newtext = []
|
|
||||||
lines = text.split('\n')
|
|
||||||
for line in lines:
|
|
||||||
if line.startswith(' ' * length):
|
|
||||||
newtext.append(line[length:])
|
|
||||||
elif not line.strip():
|
|
||||||
newtext.append('')
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return '\n'.join(newtext), '\n'.join(lines[len(newtext):])
|
|
||||||
|
|
||||||
def looseDetab(self, text, level=1):
|
|
||||||
""" Remove a tab from front of lines but allowing dedented lines. """
|
|
||||||
lines = text.split('\n')
|
|
||||||
for i in range(len(lines)):
|
|
||||||
if lines[i].startswith(' '*self.tab_length*level):
|
|
||||||
lines[i] = lines[i][self.tab_length*level:]
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
""" Test for block type. Must be overridden by subclasses.
|
|
||||||
|
|
||||||
As the parser loops through processors, it will call the `test`
|
|
||||||
method on each to determine if the given block of text is of that
|
|
||||||
type. This method must return a boolean `True` or `False`. The
|
|
||||||
actual method of testing is left to the needs of that particular
|
|
||||||
block type. It could be as simple as `block.startswith(some_string)`
|
|
||||||
or a complex regular expression. As the block type may be different
|
|
||||||
depending on the parent of the block (i.e. inside a list), the parent
|
|
||||||
`etree` element is also provided and may be used as part of the test.
|
|
||||||
|
|
||||||
Keywords:
|
|
||||||
|
|
||||||
* ``parent``: An `etree` element which will be the parent of the block.
|
|
||||||
* ``block``: A block of text from the source which has been split at
|
|
||||||
blank lines.
|
|
||||||
"""
|
|
||||||
pass # pragma: no cover
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
""" Run processor. Must be overridden by subclasses.
|
|
||||||
|
|
||||||
When the parser determines the appropriate type of a block, the parser
|
|
||||||
will call the corresponding processor's ``run`` method. This method
|
|
||||||
should parse the individual lines of the block and append them to
|
|
||||||
the `etree`.
|
|
||||||
|
|
||||||
Note that both the ``parent`` and ``etree`` keywords are pointers
|
|
||||||
to instances of the objects which should be edited in place. Each
|
|
||||||
processor must make changes to the existing objects as there is no
|
|
||||||
mechanism to return new/different objects to replace them.
|
|
||||||
|
|
||||||
This means that this method should be adding `SubElements` or adding text
|
|
||||||
to the parent, and should remove (``pop``) or add (``insert``) items to
|
|
||||||
the list of blocks.
|
|
||||||
|
|
||||||
Keywords:
|
|
||||||
|
|
||||||
* ``parent``: An `etree` element which is the parent of the current block.
|
|
||||||
* ``blocks``: A list of all remaining blocks of the document.
|
|
||||||
"""
|
|
||||||
pass # pragma: no cover
|
|
||||||
|
|
||||||
|
|
||||||
class ListIndentProcessor(BlockProcessor):
|
|
||||||
""" Process children of list items.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
* a list item
|
|
||||||
process this part
|
|
||||||
|
|
||||||
or this part
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
ITEM_TYPES = ['li']
|
|
||||||
LIST_TYPES = ['ul', 'ol']
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
super().__init__(*args)
|
|
||||||
self.INDENT_RE = re.compile(r'^(([ ]{%s})+)' % self.tab_length)
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return block.startswith(' '*self.tab_length) and \
|
|
||||||
not self.parser.state.isstate('detabbed') and \
|
|
||||||
(parent.tag in self.ITEM_TYPES or
|
|
||||||
(len(parent) and parent[-1] is not None and
|
|
||||||
(parent[-1].tag in self.LIST_TYPES)))
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
block = blocks.pop(0)
|
|
||||||
level, sibling = self.get_level(parent, block)
|
|
||||||
block = self.looseDetab(block, level)
|
|
||||||
|
|
||||||
self.parser.state.set('detabbed')
|
|
||||||
if parent.tag in self.ITEM_TYPES:
|
|
||||||
# It's possible that this parent has a `ul` or `ol` child list
|
|
||||||
# with a member. If that is the case, then that should be the
|
|
||||||
# parent. This is intended to catch the edge case of an indented
|
|
||||||
# list whose first member was parsed previous to this point
|
|
||||||
# see `OListProcessor`
|
|
||||||
if len(parent) and parent[-1].tag in self.LIST_TYPES:
|
|
||||||
self.parser.parseBlocks(parent[-1], [block])
|
|
||||||
else:
|
|
||||||
# The parent is already a `li`. Just parse the child block.
|
|
||||||
self.parser.parseBlocks(parent, [block])
|
|
||||||
elif sibling.tag in self.ITEM_TYPES:
|
|
||||||
# The sibling is a `li`. Use it as parent.
|
|
||||||
self.parser.parseBlocks(sibling, [block])
|
|
||||||
elif len(sibling) and sibling[-1].tag in self.ITEM_TYPES:
|
|
||||||
# The parent is a list (``ol`` or ``ul``) which has children.
|
|
||||||
# Assume the last child `li` is the parent of this block.
|
|
||||||
if sibling[-1].text:
|
|
||||||
# If the parent `li` has text, that text needs to be moved to a `p`
|
|
||||||
# The `p` must be 'inserted' at beginning of list in the event
|
|
||||||
# that other children already exist i.e.; a nested sub-list.
|
|
||||||
p = etree.Element('p')
|
|
||||||
p.text = sibling[-1].text
|
|
||||||
sibling[-1].text = ''
|
|
||||||
sibling[-1].insert(0, p)
|
|
||||||
self.parser.parseChunk(sibling[-1], block)
|
|
||||||
else:
|
|
||||||
self.create_item(sibling, block)
|
|
||||||
self.parser.state.reset()
|
|
||||||
|
|
||||||
def create_item(self, parent, block):
|
|
||||||
""" Create a new `li` and parse the block with it as the parent. """
|
|
||||||
li = etree.SubElement(parent, 'li')
|
|
||||||
self.parser.parseBlocks(li, [block])
|
|
||||||
|
|
||||||
def get_level(self, parent, block):
|
|
||||||
""" Get level of indent based on list level. """
|
|
||||||
# Get indent level
|
|
||||||
m = self.INDENT_RE.match(block)
|
|
||||||
if m:
|
|
||||||
indent_level = len(m.group(1))/self.tab_length
|
|
||||||
else:
|
|
||||||
indent_level = 0
|
|
||||||
if self.parser.state.isstate('list'):
|
|
||||||
# We're in a tight-list - so we already are at correct parent.
|
|
||||||
level = 1
|
|
||||||
else:
|
|
||||||
# We're in a loose-list - so we need to find parent.
|
|
||||||
level = 0
|
|
||||||
# Step through children of tree to find matching indent level.
|
|
||||||
while indent_level > level:
|
|
||||||
child = self.lastChild(parent)
|
|
||||||
if (child is not None and
|
|
||||||
(child.tag in self.LIST_TYPES or child.tag in self.ITEM_TYPES)):
|
|
||||||
if child.tag in self.LIST_TYPES:
|
|
||||||
level += 1
|
|
||||||
parent = child
|
|
||||||
else:
|
|
||||||
# No more child levels. If we're short of `indent_level`,
|
|
||||||
# we have a code block. So we stop here.
|
|
||||||
break
|
|
||||||
return level, parent
|
|
||||||
|
|
||||||
|
|
||||||
class CodeBlockProcessor(BlockProcessor):
|
|
||||||
""" Process code blocks. """
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return block.startswith(' '*self.tab_length)
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
sibling = self.lastChild(parent)
|
|
||||||
block = blocks.pop(0)
|
|
||||||
theRest = ''
|
|
||||||
if (sibling is not None and sibling.tag == "pre" and
|
|
||||||
len(sibling) and sibling[0].tag == "code"):
|
|
||||||
# The previous block was a code block. As blank lines do not start
|
|
||||||
# new code blocks, append this block to the previous, adding back
|
|
||||||
# line breaks removed from the split into a list.
|
|
||||||
code = sibling[0]
|
|
||||||
block, theRest = self.detab(block)
|
|
||||||
code.text = util.AtomicString(
|
|
||||||
'{}\n{}\n'.format(code.text, util.code_escape(block.rstrip()))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# This is a new code block. Create the elements and insert text.
|
|
||||||
pre = etree.SubElement(parent, 'pre')
|
|
||||||
code = etree.SubElement(pre, 'code')
|
|
||||||
block, theRest = self.detab(block)
|
|
||||||
code.text = util.AtomicString('%s\n' % util.code_escape(block.rstrip()))
|
|
||||||
if theRest:
|
|
||||||
# This block contained unindented line(s) after the first indented
|
|
||||||
# line. Insert these lines as the first block of the master blocks
|
|
||||||
# list for future processing.
|
|
||||||
blocks.insert(0, theRest)
|
|
||||||
|
|
||||||
|
|
||||||
class BlockQuoteProcessor(BlockProcessor):
|
|
||||||
|
|
||||||
RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)')
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return bool(self.RE.search(block)) and not util.nearing_recursion_limit()
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
block = blocks.pop(0)
|
|
||||||
m = self.RE.search(block)
|
|
||||||
if m:
|
|
||||||
before = block[:m.start()] # Lines before blockquote
|
|
||||||
# Pass lines before blockquote in recursively for parsing first.
|
|
||||||
self.parser.parseBlocks(parent, [before])
|
|
||||||
# Remove ``> `` from beginning of each line.
|
|
||||||
block = '\n'.join(
|
|
||||||
[self.clean(line) for line in block[m.start():].split('\n')]
|
|
||||||
)
|
|
||||||
sibling = self.lastChild(parent)
|
|
||||||
if sibling is not None and sibling.tag == "blockquote":
|
|
||||||
# Previous block was a blockquote so set that as this blocks parent
|
|
||||||
quote = sibling
|
|
||||||
else:
|
|
||||||
# This is a new blockquote. Create a new parent element.
|
|
||||||
quote = etree.SubElement(parent, 'blockquote')
|
|
||||||
# Recursively parse block with blockquote as parent.
|
|
||||||
# change parser state so blockquotes embedded in lists use `p` tags
|
|
||||||
self.parser.state.set('blockquote')
|
|
||||||
self.parser.parseChunk(quote, block)
|
|
||||||
self.parser.state.reset()
|
|
||||||
|
|
||||||
def clean(self, line):
|
|
||||||
""" Remove ``>`` from beginning of a line. """
|
|
||||||
m = self.RE.match(line)
|
|
||||||
if line.strip() == ">":
|
|
||||||
return ""
|
|
||||||
elif m:
|
|
||||||
return m.group(2)
|
|
||||||
else:
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
class OListProcessor(BlockProcessor):
|
|
||||||
""" Process ordered list blocks. """
|
|
||||||
|
|
||||||
TAG = 'ol'
|
|
||||||
# The integer (python string) with which the lists starts (default=1)
|
|
||||||
# Example: If list is initialized as: `3. Item`
|
|
||||||
# The `ol` tag will get `starts="3"` attribute
|
|
||||||
STARTSWITH = '1'
|
|
||||||
# Lazy `ol` - ignore `startswith`
|
|
||||||
LAZY_OL = True
|
|
||||||
# List of allowed sibling tags.
|
|
||||||
SIBLING_TAGS = ['ol', 'ul']
|
|
||||||
|
|
||||||
def __init__(self, parser):
|
|
||||||
super().__init__(parser)
|
|
||||||
# Detect an item (``1. item``). ``group(1)`` contains contents of item.
|
|
||||||
self.RE = re.compile(r'^[ ]{0,%d}\d+\.[ ]+(.*)' % (self.tab_length - 1))
|
|
||||||
# Detect items on secondary lines. they can be of either list type.
|
|
||||||
self.CHILD_RE = re.compile(r'^[ ]{0,%d}((\d+\.)|[*+-])[ ]+(.*)' %
|
|
||||||
(self.tab_length - 1))
|
|
||||||
# Detect indented (nested) items of either type
|
|
||||||
self.INDENT_RE = re.compile(r'^[ ]{%d,%d}((\d+\.)|[*+-])[ ]+.*' %
|
|
||||||
(self.tab_length, self.tab_length * 2 - 1))
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return bool(self.RE.match(block))
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
# Check for multiple items in one block.
|
|
||||||
items = self.get_items(blocks.pop(0))
|
|
||||||
sibling = self.lastChild(parent)
|
|
||||||
|
|
||||||
if sibling is not None and sibling.tag in self.SIBLING_TAGS:
|
|
||||||
# Previous block was a list item, so set that as parent
|
|
||||||
lst = sibling
|
|
||||||
# make sure previous item is in a `p` - if the item has text,
|
|
||||||
# then it isn't in a `p`
|
|
||||||
if lst[-1].text:
|
|
||||||
# since it's possible there are other children for this
|
|
||||||
# sibling, we can't just `SubElement` the `p`, we need to
|
|
||||||
# insert it as the first item.
|
|
||||||
p = etree.Element('p')
|
|
||||||
p.text = lst[-1].text
|
|
||||||
lst[-1].text = ''
|
|
||||||
lst[-1].insert(0, p)
|
|
||||||
# if the last item has a tail, then the tail needs to be put in a `p`
|
|
||||||
# likely only when a header is not followed by a blank line
|
|
||||||
lch = self.lastChild(lst[-1])
|
|
||||||
if lch is not None and lch.tail:
|
|
||||||
p = etree.SubElement(lst[-1], 'p')
|
|
||||||
p.text = lch.tail.lstrip()
|
|
||||||
lch.tail = ''
|
|
||||||
|
|
||||||
# parse first block differently as it gets wrapped in a `p`.
|
|
||||||
li = etree.SubElement(lst, 'li')
|
|
||||||
self.parser.state.set('looselist')
|
|
||||||
firstitem = items.pop(0)
|
|
||||||
self.parser.parseBlocks(li, [firstitem])
|
|
||||||
self.parser.state.reset()
|
|
||||||
elif parent.tag in ['ol', 'ul']:
|
|
||||||
# this catches the edge case of a multi-item indented list whose
|
|
||||||
# first item is in a blank parent-list item:
|
|
||||||
# * * subitem1
|
|
||||||
# * subitem2
|
|
||||||
# see also `ListIndentProcessor`
|
|
||||||
lst = parent
|
|
||||||
else:
|
|
||||||
# This is a new list so create parent with appropriate tag.
|
|
||||||
lst = etree.SubElement(parent, self.TAG)
|
|
||||||
# Check if a custom start integer is set
|
|
||||||
if not self.LAZY_OL and self.STARTSWITH != '1':
|
|
||||||
lst.attrib['start'] = self.STARTSWITH
|
|
||||||
|
|
||||||
self.parser.state.set('list')
|
|
||||||
# Loop through items in block, recursively parsing each with the
|
|
||||||
# appropriate parent.
|
|
||||||
for item in items:
|
|
||||||
if item.startswith(' '*self.tab_length):
|
|
||||||
# Item is indented. Parse with last item as parent
|
|
||||||
self.parser.parseBlocks(lst[-1], [item])
|
|
||||||
else:
|
|
||||||
# New item. Create `li` and parse with it as parent
|
|
||||||
li = etree.SubElement(lst, 'li')
|
|
||||||
self.parser.parseBlocks(li, [item])
|
|
||||||
self.parser.state.reset()
|
|
||||||
|
|
||||||
def get_items(self, block):
|
|
||||||
""" Break a block into list items. """
|
|
||||||
items = []
|
|
||||||
for line in block.split('\n'):
|
|
||||||
m = self.CHILD_RE.match(line)
|
|
||||||
if m:
|
|
||||||
# This is a new list item
|
|
||||||
# Check first item for the start index
|
|
||||||
if not items and self.TAG == 'ol':
|
|
||||||
# Detect the integer value of first list item
|
|
||||||
INTEGER_RE = re.compile(r'(\d+)')
|
|
||||||
self.STARTSWITH = INTEGER_RE.match(m.group(1)).group()
|
|
||||||
# Append to the list
|
|
||||||
items.append(m.group(3))
|
|
||||||
elif self.INDENT_RE.match(line):
|
|
||||||
# This is an indented (possibly nested) item.
|
|
||||||
if items[-1].startswith(' '*self.tab_length):
|
|
||||||
# Previous item was indented. Append to that item.
|
|
||||||
items[-1] = '{}\n{}'.format(items[-1], line)
|
|
||||||
else:
|
|
||||||
items.append(line)
|
|
||||||
else:
|
|
||||||
# This is another line of previous item. Append to that item.
|
|
||||||
items[-1] = '{}\n{}'.format(items[-1], line)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
class UListProcessor(OListProcessor):
|
|
||||||
""" Process unordered list blocks. """
|
|
||||||
|
|
||||||
TAG = 'ul'
|
|
||||||
|
|
||||||
def __init__(self, parser):
|
|
||||||
super().__init__(parser)
|
|
||||||
# Detect an item (``1. item``). ``group(1)`` contains contents of item.
|
|
||||||
self.RE = re.compile(r'^[ ]{0,%d}[*+-][ ]+(.*)' % (self.tab_length - 1))
|
|
||||||
|
|
||||||
|
|
||||||
class HashHeaderProcessor(BlockProcessor):
|
|
||||||
""" Process Hash Headers. """
|
|
||||||
|
|
||||||
# Detect a header at start of any line in block
|
|
||||||
RE = re.compile(r'(?:^|\n)(?P<level>#{1,6})(?P<header>(?:\\.|[^\\])*?)#*(?:\n|$)')
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return bool(self.RE.search(block))
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
block = blocks.pop(0)
|
|
||||||
m = self.RE.search(block)
|
|
||||||
if m:
|
|
||||||
before = block[:m.start()] # All lines before header
|
|
||||||
after = block[m.end():] # All lines after header
|
|
||||||
if before:
|
|
||||||
# As the header was not the first line of the block and the
|
|
||||||
# lines before the header must be parsed first,
|
|
||||||
# recursively parse this lines as a block.
|
|
||||||
self.parser.parseBlocks(parent, [before])
|
|
||||||
# Create header using named groups from RE
|
|
||||||
h = etree.SubElement(parent, 'h%d' % len(m.group('level')))
|
|
||||||
h.text = m.group('header').strip()
|
|
||||||
if after:
|
|
||||||
# Insert remaining lines as first block for future parsing.
|
|
||||||
blocks.insert(0, after)
|
|
||||||
else: # pragma: no cover
|
|
||||||
# This should never happen, but just in case...
|
|
||||||
logger.warn("We've got a problem header: %r" % block)
|
|
||||||
|
|
||||||
|
|
||||||
class SetextHeaderProcessor(BlockProcessor):
|
|
||||||
""" Process Setext-style Headers. """
|
|
||||||
|
|
||||||
# Detect Setext-style header. Must be first 2 lines of block.
|
|
||||||
RE = re.compile(r'^.*?\n[=-]+[ ]*(\n|$)', re.MULTILINE)
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return bool(self.RE.match(block))
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
lines = blocks.pop(0).split('\n')
|
|
||||||
# Determine level. ``=`` is 1 and ``-`` is 2.
|
|
||||||
if lines[1].startswith('='):
|
|
||||||
level = 1
|
|
||||||
else:
|
|
||||||
level = 2
|
|
||||||
h = etree.SubElement(parent, 'h%d' % level)
|
|
||||||
h.text = lines[0].strip()
|
|
||||||
if len(lines) > 2:
|
|
||||||
# Block contains additional lines. Add to master blocks for later.
|
|
||||||
blocks.insert(0, '\n'.join(lines[2:]))
|
|
||||||
|
|
||||||
|
|
||||||
class HRProcessor(BlockProcessor):
|
|
||||||
""" Process Horizontal Rules. """
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
SEARCH_RE = re.compile(RE, re.MULTILINE)
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
m = self.SEARCH_RE.search(block)
|
|
||||||
if m:
|
|
||||||
# Save match object on class instance so we can use it later.
|
|
||||||
self.match = m
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
block = blocks.pop(0)
|
|
||||||
match = self.match
|
|
||||||
# Check for lines in block before `hr`.
|
|
||||||
prelines = block[:match.start()].rstrip('\n')
|
|
||||||
if prelines:
|
|
||||||
# Recursively parse lines before `hr` so they get parsed first.
|
|
||||||
self.parser.parseBlocks(parent, [prelines])
|
|
||||||
# create hr
|
|
||||||
etree.SubElement(parent, 'hr')
|
|
||||||
# check for lines in block after `hr`.
|
|
||||||
postlines = block[match.end():].lstrip('\n')
|
|
||||||
if postlines:
|
|
||||||
# Add lines after `hr` to master blocks for later parsing.
|
|
||||||
blocks.insert(0, postlines)
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyBlockProcessor(BlockProcessor):
|
|
||||||
""" Process blocks that are empty or start with an empty line. """
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return not block or block.startswith('\n')
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
block = blocks.pop(0)
|
|
||||||
filler = '\n\n'
|
|
||||||
if block:
|
|
||||||
# Starts with empty line
|
|
||||||
# Only replace a single line.
|
|
||||||
filler = '\n'
|
|
||||||
# Save the rest for later.
|
|
||||||
theRest = block[1:]
|
|
||||||
if theRest:
|
|
||||||
# Add remaining lines to master blocks for later.
|
|
||||||
blocks.insert(0, theRest)
|
|
||||||
sibling = self.lastChild(parent)
|
|
||||||
if (sibling is not None and sibling.tag == 'pre' and
|
|
||||||
len(sibling) and sibling[0].tag == 'code'):
|
|
||||||
# Last block is a code block. Append to preserve whitespace.
|
|
||||||
sibling[0].text = util.AtomicString(
|
|
||||||
'{}{}'.format(sibling[0].text, filler)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
""" Process Paragraph blocks. """
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
block = blocks.pop(0)
|
|
||||||
if block.strip():
|
|
||||||
# Not a blank block. Add to parent, otherwise throw it away.
|
|
||||||
if self.parser.state.isstate('list'):
|
|
||||||
# The parent is a tight-list.
|
|
||||||
#
|
|
||||||
# Check for any children. This will likely only happen in a
|
|
||||||
# tight-list when a header isn't followed by a blank line.
|
|
||||||
# For example:
|
|
||||||
#
|
|
||||||
# * # Header
|
|
||||||
# Line 2 of list item - not part of header.
|
|
||||||
sibling = self.lastChild(parent)
|
|
||||||
if sibling is not None:
|
|
||||||
# Insert after sibling.
|
|
||||||
if sibling.tail:
|
|
||||||
sibling.tail = '{}\n{}'.format(sibling.tail, block)
|
|
||||||
else:
|
|
||||||
sibling.tail = '\n%s' % block
|
|
||||||
else:
|
|
||||||
# Append to parent.text
|
|
||||||
if parent.text:
|
|
||||||
parent.text = '{}\n{}'.format(parent.text, block)
|
|
||||||
else:
|
|
||||||
parent.text = block.lstrip()
|
|
||||||
else:
|
|
||||||
# Create a regular paragraph
|
|
||||||
p = etree.SubElement(parent, 'p')
|
|
||||||
p.text = block.lstrip()
|
|
|
@ -1,397 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import codecs
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import importlib
|
|
||||||
from . import util
|
|
||||||
from .preprocessors import build_preprocessors
|
|
||||||
from .blockprocessors import build_block_parser
|
|
||||||
from .treeprocessors import build_treeprocessors
|
|
||||||
from .inlinepatterns import build_inlinepatterns
|
|
||||||
from .postprocessors import build_postprocessors
|
|
||||||
from .extensions import Extension
|
|
||||||
from .serializers import to_html_string, to_xhtml_string
|
|
||||||
from .util import BLOCK_LEVEL_ELEMENTS
|
|
||||||
|
|
||||||
__all__ = ['Markdown', 'markdown', 'markdownFromFile']
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('MARKDOWN')
|
|
||||||
|
|
||||||
|
|
||||||
class Markdown:
|
|
||||||
"""Convert Markdown to HTML."""
|
|
||||||
|
|
||||||
doc_tag = "div" # Element used to wrap document - later removed
|
|
||||||
|
|
||||||
output_formats = {
|
|
||||||
'html': to_html_string,
|
|
||||||
'xhtml': to_xhtml_string,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Creates a new Markdown instance.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `extensions`: A list of extensions.
|
|
||||||
If an item is an instance of a subclass of `markdown.extension.Extension`, the instance will be used
|
|
||||||
as-is. If an item is of type string, first an entry point will be loaded. If that fails, the string is
|
|
||||||
assumed to use Python dot notation (`path.to.module:ClassName`) to load a `markdown.Extension` subclass. If
|
|
||||||
no class is specified, then a `makeExtension` function is called within the specified module.
|
|
||||||
* `extension_configs`: Configuration settings for extensions.
|
|
||||||
* `output_format`: Format of output. Supported formats are:
|
|
||||||
* `xhtml`: Outputs XHTML style tags. Default.
|
|
||||||
* `html`: Outputs HTML style tags.
|
|
||||||
* `tab_length`: Length of tabs in the source. Default: 4
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.tab_length = kwargs.get('tab_length', 4)
|
|
||||||
|
|
||||||
self.ESCAPED_CHARS = ['\\', '`', '*', '_', '{', '}', '[', ']',
|
|
||||||
'(', ')', '>', '#', '+', '-', '.', '!']
|
|
||||||
|
|
||||||
self.block_level_elements = BLOCK_LEVEL_ELEMENTS.copy()
|
|
||||||
|
|
||||||
self.registeredExtensions = []
|
|
||||||
self.docType = ""
|
|
||||||
self.stripTopLevelTags = True
|
|
||||||
|
|
||||||
self.build_parser()
|
|
||||||
|
|
||||||
self.references = {}
|
|
||||||
self.htmlStash = util.HtmlStash()
|
|
||||||
self.registerExtensions(extensions=kwargs.get('extensions', []),
|
|
||||||
configs=kwargs.get('extension_configs', {}))
|
|
||||||
self.set_output_format(kwargs.get('output_format', 'xhtml'))
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def build_parser(self):
|
|
||||||
""" Build the parser from the various parts. """
|
|
||||||
self.preprocessors = build_preprocessors(self)
|
|
||||||
self.parser = build_block_parser(self)
|
|
||||||
self.inlinePatterns = build_inlinepatterns(self)
|
|
||||||
self.treeprocessors = build_treeprocessors(self)
|
|
||||||
self.postprocessors = build_postprocessors(self)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def registerExtensions(self, extensions, configs):
|
|
||||||
"""
|
|
||||||
Register extensions with this instance of Markdown.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `extensions`: A list of extensions, which can either
|
|
||||||
be strings or objects.
|
|
||||||
* `configs`: A dictionary mapping extension names to `configs` options.
|
|
||||||
|
|
||||||
"""
|
|
||||||
for ext in extensions:
|
|
||||||
if isinstance(ext, str):
|
|
||||||
ext = self.build_extension(ext, configs.get(ext, {}))
|
|
||||||
if isinstance(ext, Extension):
|
|
||||||
ext.extendMarkdown(self)
|
|
||||||
logger.debug(
|
|
||||||
'Successfully loaded extension "%s.%s".'
|
|
||||||
% (ext.__class__.__module__, ext.__class__.__name__)
|
|
||||||
)
|
|
||||||
elif ext is not None:
|
|
||||||
raise TypeError(
|
|
||||||
'Extension "{}.{}" must be of type: "{}.{}"'.format(
|
|
||||||
ext.__class__.__module__, ext.__class__.__name__,
|
|
||||||
Extension.__module__, Extension.__name__
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def build_extension(self, ext_name, configs):
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
`markdown.extensions` group which points to a subclass of the `markdown.extensions.Extension` class.
|
|
||||||
If multiple distributions have registered the same name, the first one found is returned.
|
|
||||||
|
|
||||||
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
|
|
||||||
the `Extension` instance returned by that function.
|
|
||||||
"""
|
|
||||||
configs = dict(configs)
|
|
||||||
|
|
||||||
entry_points = [ep for ep in util.get_installed_extensions() if ep.name == ext_name]
|
|
||||||
if entry_points:
|
|
||||||
ext = entry_points[0].load()
|
|
||||||
return ext(**configs)
|
|
||||||
|
|
||||||
# Get class name (if provided): `path.to.module:ClassName`
|
|
||||||
ext_name, class_name = ext_name.split(':', 1) if ':' in ext_name else (ext_name, '')
|
|
||||||
|
|
||||||
try:
|
|
||||||
module = importlib.import_module(ext_name)
|
|
||||||
logger.debug(
|
|
||||||
'Successfully imported extension module "%s".' % ext_name
|
|
||||||
)
|
|
||||||
except ImportError as e:
|
|
||||||
message = 'Failed loading extension "%s".' % ext_name
|
|
||||||
e.args = (message,) + e.args[1:]
|
|
||||||
raise
|
|
||||||
|
|
||||||
if class_name:
|
|
||||||
# Load given class name from module.
|
|
||||||
return getattr(module, class_name)(**configs)
|
|
||||||
else:
|
|
||||||
# Expect `makeExtension()` function to return a class.
|
|
||||||
try:
|
|
||||||
return module.makeExtension(**configs)
|
|
||||||
except AttributeError as e:
|
|
||||||
message = e.args[0]
|
|
||||||
message = "Failed to initiate extension " \
|
|
||||||
"'%s': %s" % (ext_name, message)
|
|
||||||
e.args = (message,) + e.args[1:]
|
|
||||||
raise
|
|
||||||
|
|
||||||
def registerExtension(self, extension):
|
|
||||||
""" This gets called by the extension """
|
|
||||||
self.registeredExtensions.append(extension)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""
|
|
||||||
Resets all state variables so that we can start with a new text.
|
|
||||||
"""
|
|
||||||
self.htmlStash.reset()
|
|
||||||
self.references.clear()
|
|
||||||
|
|
||||||
for extension in self.registeredExtensions:
|
|
||||||
if hasattr(extension, 'reset'):
|
|
||||||
extension.reset()
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_output_format(self, format):
|
|
||||||
""" Set the output format for the class instance. """
|
|
||||||
self.output_format = format.lower().rstrip('145') # ignore number
|
|
||||||
try:
|
|
||||||
self.serializer = self.output_formats[self.output_format]
|
|
||||||
except KeyError as e:
|
|
||||||
valid_formats = list(self.output_formats.keys())
|
|
||||||
valid_formats.sort()
|
|
||||||
message = 'Invalid Output Format: "%s". Use one of %s.' \
|
|
||||||
% (self.output_format,
|
|
||||||
'"' + '", "'.join(valid_formats) + '"')
|
|
||||||
e.args = (message,) + e.args[1:]
|
|
||||||
raise
|
|
||||||
return self
|
|
||||||
|
|
||||||
def is_block_level(self, tag):
|
|
||||||
"""Check if the tag is a block level HTML tag."""
|
|
||||||
if isinstance(tag, str):
|
|
||||||
return tag.lower().rstrip('/') in self.block_level_elements
|
|
||||||
# Some ElementTree tags are not strings, so return False.
|
|
||||||
return False
|
|
||||||
|
|
||||||
def convert(self, source):
|
|
||||||
"""
|
|
||||||
Convert markdown to serialized XHTML or HTML.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `source`: Source text as a Unicode string.
|
|
||||||
|
|
||||||
Markdown processing takes place in five steps:
|
|
||||||
|
|
||||||
1. A bunch of `preprocessors` munge the input text.
|
|
||||||
2. `BlockParser()` parses the high-level structural elements of the
|
|
||||||
pre-processed text into an `ElementTree`.
|
|
||||||
3. A bunch of `treeprocessors` are run against the `ElementTree`. One
|
|
||||||
such `treeprocessor` runs `InlinePatterns` against the `ElementTree`,
|
|
||||||
detecting inline markup.
|
|
||||||
4. Some post-processors are run against the text after the `ElementTree`
|
|
||||||
has been serialized into text.
|
|
||||||
5. The output is written to a string.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Fix up the source text
|
|
||||||
if not source.strip():
|
|
||||||
return '' # a blank Unicode string
|
|
||||||
|
|
||||||
try:
|
|
||||||
source = str(source)
|
|
||||||
except UnicodeDecodeError as e: # pragma: no cover
|
|
||||||
# Customize error message while maintaining original traceback
|
|
||||||
e.reason += '. -- Note: Markdown only accepts Unicode input!'
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Split into lines and run the line preprocessors.
|
|
||||||
self.lines = source.split("\n")
|
|
||||||
for prep in self.preprocessors:
|
|
||||||
self.lines = prep.run(self.lines)
|
|
||||||
|
|
||||||
# Parse the high-level elements.
|
|
||||||
root = self.parser.parseDocument(self.lines).getroot()
|
|
||||||
|
|
||||||
# Run the tree-processors
|
|
||||||
for treeprocessor in self.treeprocessors:
|
|
||||||
newRoot = treeprocessor.run(root)
|
|
||||||
if newRoot is not None:
|
|
||||||
root = newRoot
|
|
||||||
|
|
||||||
# Serialize _properly_. Strip top-level tags.
|
|
||||||
output = self.serializer(root)
|
|
||||||
if self.stripTopLevelTags:
|
|
||||||
try:
|
|
||||||
start = output.index(
|
|
||||||
'<%s>' % self.doc_tag) + len(self.doc_tag) + 2
|
|
||||||
end = output.rindex('</%s>' % self.doc_tag)
|
|
||||||
output = output[start:end].strip()
|
|
||||||
except ValueError as e: # pragma: no cover
|
|
||||||
if output.strip().endswith('<%s />' % self.doc_tag):
|
|
||||||
# We have an empty document
|
|
||||||
output = ''
|
|
||||||
else:
|
|
||||||
# We have a serious problem
|
|
||||||
raise ValueError('Markdown failed to strip top-level '
|
|
||||||
'tags. Document=%r' % output.strip()) from e
|
|
||||||
|
|
||||||
# Run the text post-processors
|
|
||||||
for pp in self.postprocessors:
|
|
||||||
output = pp.run(output)
|
|
||||||
|
|
||||||
return output.strip()
|
|
||||||
|
|
||||||
def convertFile(self, input=None, output=None, encoding=None):
|
|
||||||
"""Converts a markdown file and returns the HTML as a Unicode string.
|
|
||||||
|
|
||||||
Decodes the file using the provided encoding (defaults to `utf-8`),
|
|
||||||
passes the file content to markdown, and outputs the html to either
|
|
||||||
the provided stream or the file with provided name, using the same
|
|
||||||
encoding as the source file. The `xmlcharrefreplace` error handler is
|
|
||||||
used when encoding the output.
|
|
||||||
|
|
||||||
**Note:** This is the only place that decoding and encoding of Unicode
|
|
||||||
takes place in Python-Markdown. (All other code is Unicode-in /
|
|
||||||
Unicode-out.)
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `input`: File object or path. Reads from `stdin` if `None`.
|
|
||||||
* `output`: File object or path. Writes to `stdout` if `None`.
|
|
||||||
* `encoding`: Encoding of input and output files. Defaults to `utf-8`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
encoding = encoding or "utf-8"
|
|
||||||
|
|
||||||
# Read the source
|
|
||||||
if input:
|
|
||||||
if isinstance(input, str):
|
|
||||||
input_file = codecs.open(input, mode="r", encoding=encoding)
|
|
||||||
else:
|
|
||||||
input_file = codecs.getreader(encoding)(input)
|
|
||||||
text = input_file.read()
|
|
||||||
input_file.close()
|
|
||||||
else:
|
|
||||||
text = sys.stdin.read()
|
|
||||||
if not isinstance(text, str): # pragma: no cover
|
|
||||||
text = text.decode(encoding)
|
|
||||||
|
|
||||||
text = text.lstrip('\ufeff') # remove the byte-order mark
|
|
||||||
|
|
||||||
# Convert
|
|
||||||
html = self.convert(text)
|
|
||||||
|
|
||||||
# Write to file or stdout
|
|
||||||
if output:
|
|
||||||
if isinstance(output, str):
|
|
||||||
output_file = codecs.open(output, "w",
|
|
||||||
encoding=encoding,
|
|
||||||
errors="xmlcharrefreplace")
|
|
||||||
output_file.write(html)
|
|
||||||
output_file.close()
|
|
||||||
else:
|
|
||||||
writer = codecs.getwriter(encoding)
|
|
||||||
output_file = writer(output, errors="xmlcharrefreplace")
|
|
||||||
output_file.write(html)
|
|
||||||
# Don't close here. User may want to write more.
|
|
||||||
else:
|
|
||||||
# Encode manually and write bytes to stdout.
|
|
||||||
html = html.encode(encoding, "xmlcharrefreplace")
|
|
||||||
try:
|
|
||||||
# Write bytes directly to buffer (Python 3).
|
|
||||||
sys.stdout.buffer.write(html)
|
|
||||||
except AttributeError: # pragma: no cover
|
|
||||||
# Probably Python 2, which works with bytes by default.
|
|
||||||
sys.stdout.write(html)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
EXPORTED FUNCTIONS
|
|
||||||
=============================================================================
|
|
||||||
|
|
||||||
Those are the two functions we really mean to export: `markdown()` and
|
|
||||||
`markdownFromFile()`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def markdown(text, **kwargs):
|
|
||||||
"""Convert a markdown string to HTML and return HTML as a Unicode string.
|
|
||||||
|
|
||||||
This is a shortcut function for `Markdown` class to cover the most
|
|
||||||
basic use case. It initializes an instance of `Markdown`, loads the
|
|
||||||
necessary extensions and runs the parser on the given text.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `text`: Markdown formatted text as Unicode or ASCII string.
|
|
||||||
* Any arguments accepted by the Markdown class.
|
|
||||||
|
|
||||||
Returns: An HTML document as a string.
|
|
||||||
|
|
||||||
"""
|
|
||||||
md = Markdown(**kwargs)
|
|
||||||
return md.convert(text)
|
|
||||||
|
|
||||||
|
|
||||||
def markdownFromFile(**kwargs):
|
|
||||||
"""Read markdown code from a file and write it to a file or a stream.
|
|
||||||
|
|
||||||
This is a shortcut function which initializes an instance of `Markdown`,
|
|
||||||
and calls the `convertFile` method rather than `convert`.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `input`: a file name or readable object.
|
|
||||||
* `output`: a file name or writable object.
|
|
||||||
* `encoding`: Encoding of input and output.
|
|
||||||
* Any arguments accepted by the `Markdown` class.
|
|
||||||
|
|
||||||
"""
|
|
||||||
md = Markdown(**kwargs)
|
|
||||||
md.convertFile(kwargs.get('input', None),
|
|
||||||
kwargs.get('output', None),
|
|
||||||
kwargs.get('encoding', None))
|
|
|
@ -1,86 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ..util import parseBoolValue
|
|
||||||
|
|
||||||
|
|
||||||
class Extension:
|
|
||||||
""" Base class for extensions to subclass. """
|
|
||||||
|
|
||||||
# Default configuration -- to be overridden by a subclass
|
|
||||||
# Must be of the following format:
|
|
||||||
# {
|
|
||||||
# 'key': ['value', 'description']
|
|
||||||
# }
|
|
||||||
# Note that `Extension.setConfig` will raise a `KeyError`
|
|
||||||
# if a default is not set here.
|
|
||||||
config = {}
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
""" Initiate Extension and set up configs. """
|
|
||||||
self.setConfigs(kwargs)
|
|
||||||
|
|
||||||
def getConfig(self, key, default=''):
|
|
||||||
""" Return a setting for the given key or an empty string. """
|
|
||||||
if key in self.config:
|
|
||||||
return self.config[key][0]
|
|
||||||
else:
|
|
||||||
return default
|
|
||||||
|
|
||||||
def getConfigs(self):
|
|
||||||
""" Return all configs settings as a dict. """
|
|
||||||
return {key: self.getConfig(key) for key in self.config.keys()}
|
|
||||||
|
|
||||||
def getConfigInfo(self):
|
|
||||||
""" Return all `config` descriptions as a list of tuples. """
|
|
||||||
return [(key, self.config[key][1]) for key in self.config.keys()]
|
|
||||||
|
|
||||||
def setConfig(self, key, value):
|
|
||||||
""" Set a `config` setting for `key` with the given `value`. """
|
|
||||||
if isinstance(self.config[key][0], bool):
|
|
||||||
value = parseBoolValue(value)
|
|
||||||
if self.config[key][0] is None:
|
|
||||||
value = parseBoolValue(value, preserve_none=True)
|
|
||||||
self.config[key][0] = value
|
|
||||||
|
|
||||||
def setConfigs(self, items):
|
|
||||||
""" Set multiple `config` settings given a dict or list of tuples. """
|
|
||||||
if hasattr(items, 'items'):
|
|
||||||
# it's a dict
|
|
||||||
items = items.items()
|
|
||||||
for key, value in items:
|
|
||||||
self.setConfig(key, value)
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
"""
|
|
||||||
Add the various processors and patterns to the Markdown Instance.
|
|
||||||
|
|
||||||
This method must be overridden by every extension.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* md: The Markdown instance.
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Extension "%s.%s" must define an "extendMarkdown"'
|
|
||||||
'method.' % (self.__class__.__module__, self.__class__.__name__)
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,99 +0,0 @@
|
||||||
'''
|
|
||||||
Abbreviation Extension for Python-Markdown
|
|
||||||
==========================================
|
|
||||||
|
|
||||||
This extension adds abbreviation handling to Python-Markdown.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/abbreviations>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/) and
|
|
||||||
[Seemant Kulleen](http://www.kulleen.org/)
|
|
||||||
|
|
||||||
All changes Copyright 2008-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..blockprocessors import BlockProcessor
|
|
||||||
from ..inlinepatterns import InlineProcessor
|
|
||||||
from ..util import AtomicString
|
|
||||||
import re
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
|
|
||||||
|
|
||||||
class AbbrExtension(Extension):
|
|
||||||
""" Abbreviation Extension for Python-Markdown. """
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Insert `AbbrPreprocessor` before `ReferencePreprocessor`. """
|
|
||||||
md.parser.blockprocessors.register(AbbrPreprocessor(md.parser), 'abbr', 16)
|
|
||||||
|
|
||||||
|
|
||||||
class AbbrPreprocessor(BlockProcessor):
|
|
||||||
""" Abbreviation Preprocessor - parse text for abbr references. """
|
|
||||||
|
|
||||||
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.
|
|
||||||
Each reference is set as a new `AbbrPattern` in the markdown instance.
|
|
||||||
|
|
||||||
'''
|
|
||||||
block = blocks.pop(0)
|
|
||||||
m = self.RE.search(block)
|
|
||||||
if m:
|
|
||||||
abbr = m.group('abbr').strip()
|
|
||||||
title = m.group('title').strip()
|
|
||||||
self.parser.md.inlinePatterns.register(
|
|
||||||
AbbrInlineProcessor(self._generate_pattern(abbr), title), 'abbr-%s' % abbr, 2
|
|
||||||
)
|
|
||||||
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
|
|
||||||
|
|
||||||
def _generate_pattern(self, text):
|
|
||||||
'''
|
|
||||||
Given a string, returns an regex pattern to match that string.
|
|
||||||
|
|
||||||
'HTML' -> r'(?P<abbr>[H][T][M][L])'
|
|
||||||
|
|
||||||
Note: we force each char as a literal match (in brackets) as we don't
|
|
||||||
know what they will be beforehand.
|
|
||||||
|
|
||||||
'''
|
|
||||||
chars = list(text)
|
|
||||||
for i in range(len(chars)):
|
|
||||||
chars[i] = r'[%s]' % chars[i]
|
|
||||||
return r'(?P<abbr>\b%s\b)' % (r''.join(chars))
|
|
||||||
|
|
||||||
|
|
||||||
class AbbrInlineProcessor(InlineProcessor):
|
|
||||||
""" Abbreviation inline pattern. """
|
|
||||||
|
|
||||||
def __init__(self, pattern, title):
|
|
||||||
super().__init__(pattern)
|
|
||||||
self.title = title
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
abbr = etree.Element('abbr')
|
|
||||||
abbr.text = AtomicString(m.group('abbr'))
|
|
||||||
abbr.set('title', self.title)
|
|
||||||
return abbr, m.start(0), m.end(0)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return AbbrExtension(**kwargs)
|
|
|
@ -1,170 +0,0 @@
|
||||||
"""
|
|
||||||
Admonition extension for Python-Markdown
|
|
||||||
========================================
|
|
||||||
|
|
||||||
Adds rST-style admonitions. Inspired by [rST][] feature with the same name.
|
|
||||||
|
|
||||||
[rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/admonition>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright [Tiago Serafim](https://www.tiagoserafim.com/).
|
|
||||||
|
|
||||||
All changes Copyright The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..blockprocessors import BlockProcessor
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class AdmonitionExtension(Extension):
|
|
||||||
""" Admonition extension for Python-Markdown. """
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Add Admonition to Markdown instance. """
|
|
||||||
md.registerExtension(self)
|
|
||||||
|
|
||||||
md.parser.blockprocessors.register(AdmonitionProcessor(md.parser), 'admonition', 105)
|
|
||||||
|
|
||||||
|
|
||||||
class AdmonitionProcessor(BlockProcessor):
|
|
||||||
|
|
||||||
CLASSNAME = 'admonition'
|
|
||||||
CLASSNAME_TITLE = 'admonition-title'
|
|
||||||
RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)')
|
|
||||||
RE_SPACES = re.compile(' +')
|
|
||||||
|
|
||||||
def __init__(self, parser):
|
|
||||||
"""Initialization."""
|
|
||||||
|
|
||||||
super().__init__(parser)
|
|
||||||
|
|
||||||
self.current_sibling = None
|
|
||||||
self.content_indention = 0
|
|
||||||
|
|
||||||
def parse_content(self, parent, block):
|
|
||||||
"""Get sibling admonition.
|
|
||||||
|
|
||||||
Retrieve the appropriate sibling element. This can get tricky when
|
|
||||||
dealing with lists.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
old_block = block
|
|
||||||
the_rest = ''
|
|
||||||
|
|
||||||
# We already acquired the block via test
|
|
||||||
if self.current_sibling is not None:
|
|
||||||
sibling = self.current_sibling
|
|
||||||
block, the_rest = self.detab(block, self.content_indent)
|
|
||||||
self.current_sibling = None
|
|
||||||
self.content_indent = 0
|
|
||||||
return sibling, block, the_rest
|
|
||||||
|
|
||||||
sibling = self.lastChild(parent)
|
|
||||||
|
|
||||||
if sibling is None or sibling.get('class', '').find(self.CLASSNAME) == -1:
|
|
||||||
sibling = None
|
|
||||||
else:
|
|
||||||
# If the last child is a list and the content is sufficiently indented
|
|
||||||
# to be under it, then the content's sibling is in the list.
|
|
||||||
last_child = self.lastChild(sibling)
|
|
||||||
indent = 0
|
|
||||||
while last_child is not None:
|
|
||||||
if (
|
|
||||||
sibling is not None and block.startswith(' ' * self.tab_length * 2) and
|
|
||||||
last_child is not None and last_child.tag in ('ul', 'ol', 'dl')
|
|
||||||
):
|
|
||||||
|
|
||||||
# The expectation is that we'll find an `<li>` or `<dt>`.
|
|
||||||
# We should get its last child as well.
|
|
||||||
sibling = self.lastChild(last_child)
|
|
||||||
last_child = self.lastChild(sibling) if sibling is not None else None
|
|
||||||
|
|
||||||
# Context has been lost at this point, so we must adjust the
|
|
||||||
# text's indentation 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:
|
|
||||||
indent += self.tab_length
|
|
||||||
block, the_rest = self.detab(old_block, indent)
|
|
||||||
self.current_sibling = sibling
|
|
||||||
self.content_indent = indent
|
|
||||||
|
|
||||||
return sibling, block, the_rest
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
|
|
||||||
if self.RE.search(block):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return self.parse_content(parent, block)[0] is not None
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
block = blocks.pop(0)
|
|
||||||
m = self.RE.search(block)
|
|
||||||
|
|
||||||
if m:
|
|
||||||
if m.start() > 0:
|
|
||||||
self.parser.parseBlocks(parent, [block[:m.start()]])
|
|
||||||
block = block[m.end():] # removes the first line
|
|
||||||
block, theRest = self.detab(block)
|
|
||||||
else:
|
|
||||||
sibling, block, theRest = self.parse_content(parent, block)
|
|
||||||
|
|
||||||
if m:
|
|
||||||
klass, title = self.get_class_and_title(m)
|
|
||||||
div = etree.SubElement(parent, 'div')
|
|
||||||
div.set('class', '{} {}'.format(self.CLASSNAME, klass))
|
|
||||||
if title:
|
|
||||||
p = etree.SubElement(div, 'p')
|
|
||||||
p.text = title
|
|
||||||
p.set('class', self.CLASSNAME_TITLE)
|
|
||||||
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
|
|
||||||
|
|
||||||
self.parser.parseChunk(div, block)
|
|
||||||
|
|
||||||
if theRest:
|
|
||||||
# This block contained unindented line(s) after the first indented
|
|
||||||
# line. Insert these lines as the first block of the master blocks
|
|
||||||
# list for future processing.
|
|
||||||
blocks.insert(0, theRest)
|
|
||||||
|
|
||||||
def get_class_and_title(self, match):
|
|
||||||
klass, title = match.group(1).lower(), match.group(2)
|
|
||||||
klass = self.RE_SPACES.sub(' ', klass)
|
|
||||||
if title is None:
|
|
||||||
# no title was provided, use the capitalized class name as title
|
|
||||||
# e.g.: `!!! note` will render
|
|
||||||
# `<p class="admonition-title">Note</p>`
|
|
||||||
title = klass.split(' ', 1)[0].capitalize()
|
|
||||||
elif title == '':
|
|
||||||
# an explicit blank title should not be rendered
|
|
||||||
# e.g.: `!!! warning ""` will *not* render `p` with a title
|
|
||||||
title = None
|
|
||||||
return klass, title
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return AdmonitionExtension(**kwargs)
|
|
|
@ -1,166 +0,0 @@
|
||||||
"""
|
|
||||||
Attribute List Extension for Python-Markdown
|
|
||||||
============================================
|
|
||||||
|
|
||||||
Adds attribute list syntax. Inspired by
|
|
||||||
[Maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s
|
|
||||||
feature of the same name.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/attr_list>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2011 [Waylan Limberg](http://achinghead.com/).
|
|
||||||
|
|
||||||
All changes Copyright 2011-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..treeprocessors import Treeprocessor
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_double_quote(s, t):
|
|
||||||
k, v = t.split('=', 1)
|
|
||||||
return k, v.strip('"')
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_single_quote(s, t):
|
|
||||||
k, v = t.split('=', 1)
|
|
||||||
return k, v.strip("'")
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_key_value(s, t):
|
|
||||||
return t.split('=', 1)
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_word(s, t):
|
|
||||||
if t.startswith('.'):
|
|
||||||
return '.', t[1:]
|
|
||||||
if t.startswith('#'):
|
|
||||||
return 'id', t[1:]
|
|
||||||
return t, t
|
|
||||||
|
|
||||||
|
|
||||||
_scanner = re.Scanner([
|
|
||||||
(r'[^ =]+=".*?"', _handle_double_quote),
|
|
||||||
(r"[^ =]+='.*?'", _handle_single_quote),
|
|
||||||
(r'[^ =]+=[^ =]+', _handle_key_value),
|
|
||||||
(r'[^ =]+', _handle_word),
|
|
||||||
(r' ', None)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
def get_attrs(str):
|
|
||||||
""" Parse attribute list and return a list of attribute tuples. """
|
|
||||||
return _scanner.scan(str)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def isheader(elem):
|
|
||||||
return elem.tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
|
|
||||||
|
|
||||||
|
|
||||||
class AttrListTreeprocessor(Treeprocessor):
|
|
||||||
|
|
||||||
BASE_RE = r'\{\:?[ ]*([^\}\n ][^\}\n]*)[ ]*\}'
|
|
||||||
HEADER_RE = re.compile(r'[ ]+{}[ ]*$'.format(BASE_RE))
|
|
||||||
BLOCK_RE = re.compile(r'\n[ ]*{}[ ]*$'.format(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'
|
|
||||||
r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d'
|
|
||||||
r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff'
|
|
||||||
r'\uf900-\ufdcf\ufdf0-\ufffd'
|
|
||||||
r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+')
|
|
||||||
|
|
||||||
def run(self, doc):
|
|
||||||
for elem in doc.iter():
|
|
||||||
if self.md.is_block_level(elem.tag):
|
|
||||||
# Block level: check for `attrs` on last line of text
|
|
||||||
RE = self.BLOCK_RE
|
|
||||||
if isheader(elem) or elem.tag in ['dt', 'td', 'th']:
|
|
||||||
# header, def-term, or table cell: check for attributes at end of element
|
|
||||||
RE = self.HEADER_RE
|
|
||||||
if len(elem) and elem.tag == 'li':
|
|
||||||
# special case list items. children may include a `ul` or `ol`.
|
|
||||||
pos = None
|
|
||||||
# find the `ul` or `ol` position
|
|
||||||
for i, child in enumerate(elem):
|
|
||||||
if child.tag in ['ul', 'ol']:
|
|
||||||
pos = i
|
|
||||||
break
|
|
||||||
if pos is None and elem[-1].tail:
|
|
||||||
# use tail of last child. no `ul` or `ol`.
|
|
||||||
m = RE.search(elem[-1].tail)
|
|
||||||
if m:
|
|
||||||
self.assign_attrs(elem, m.group(1))
|
|
||||||
elem[-1].tail = elem[-1].tail[:m.start()]
|
|
||||||
elif pos is not None and pos > 0 and elem[pos-1].tail:
|
|
||||||
# use tail of last child before `ul` or `ol`
|
|
||||||
m = RE.search(elem[pos-1].tail)
|
|
||||||
if m:
|
|
||||||
self.assign_attrs(elem, m.group(1))
|
|
||||||
elem[pos-1].tail = elem[pos-1].tail[:m.start()]
|
|
||||||
elif elem.text:
|
|
||||||
# use text. `ul` is first child.
|
|
||||||
m = RE.search(elem.text)
|
|
||||||
if m:
|
|
||||||
self.assign_attrs(elem, m.group(1))
|
|
||||||
elem.text = elem.text[:m.start()]
|
|
||||||
elif len(elem) and elem[-1].tail:
|
|
||||||
# has children. Get from tail of last child
|
|
||||||
m = RE.search(elem[-1].tail)
|
|
||||||
if m:
|
|
||||||
self.assign_attrs(elem, m.group(1))
|
|
||||||
elem[-1].tail = elem[-1].tail[:m.start()]
|
|
||||||
if isheader(elem):
|
|
||||||
# clean up trailing #s
|
|
||||||
elem[-1].tail = elem[-1].tail.rstrip('#').rstrip()
|
|
||||||
elif elem.text:
|
|
||||||
# no children. Get from text.
|
|
||||||
m = RE.search(elem.text)
|
|
||||||
if m:
|
|
||||||
self.assign_attrs(elem, m.group(1))
|
|
||||||
elem.text = elem.text[:m.start()]
|
|
||||||
if isheader(elem):
|
|
||||||
# clean up trailing #s
|
|
||||||
elem.text = elem.text.rstrip('#').rstrip()
|
|
||||||
else:
|
|
||||||
# inline: check for `attrs` at start of tail
|
|
||||||
if elem.tail:
|
|
||||||
m = self.INLINE_RE.match(elem.tail)
|
|
||||||
if m:
|
|
||||||
self.assign_attrs(elem, m.group(1))
|
|
||||||
elem.tail = elem.tail[m.end():]
|
|
||||||
|
|
||||||
def assign_attrs(self, elem, attrs):
|
|
||||||
""" Assign `attrs` to element. """
|
|
||||||
for k, v in get_attrs(attrs):
|
|
||||||
if k == '.':
|
|
||||||
# add to class
|
|
||||||
cls = elem.get('class')
|
|
||||||
if cls:
|
|
||||||
elem.set('class', '{} {}'.format(cls, v))
|
|
||||||
else:
|
|
||||||
elem.set('class', v)
|
|
||||||
else:
|
|
||||||
# assign attribute `k` with `v`
|
|
||||||
elem.set(self.sanitize_name(k), v)
|
|
||||||
|
|
||||||
def sanitize_name(self, name):
|
|
||||||
"""
|
|
||||||
Sanitize name as 'an XML Name, minus the ":"'.
|
|
||||||
See https://www.w3.org/TR/REC-xml-names/#NT-NCName
|
|
||||||
"""
|
|
||||||
return self.NAME_RE.sub('_', name)
|
|
||||||
|
|
||||||
|
|
||||||
class AttrListExtension(Extension):
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
md.treeprocessors.register(AttrListTreeprocessor(md), 'attr_list', 8)
|
|
||||||
md.registerExtension(self)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return AttrListExtension(**kwargs)
|
|
|
@ -1,330 +0,0 @@
|
||||||
"""
|
|
||||||
CodeHilite Extension for Python-Markdown
|
|
||||||
========================================
|
|
||||||
|
|
||||||
Adds code/syntax highlighting to standard Python-Markdown code blocks.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/code_hilite>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2006-2008 [Waylan Limberg](http://achinghead.com/).
|
|
||||||
|
|
||||||
All changes Copyright 2008-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..treeprocessors import Treeprocessor
|
|
||||||
from ..util import parseBoolValue
|
|
||||||
|
|
||||||
try: # pragma: no cover
|
|
||||||
from pygments import highlight
|
|
||||||
from pygments.lexers import get_lexer_by_name, guess_lexer
|
|
||||||
from pygments.formatters import get_formatter_by_name
|
|
||||||
from pygments.util import ClassNotFound
|
|
||||||
pygments = True
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
pygments = False
|
|
||||||
|
|
||||||
|
|
||||||
def parse_hl_lines(expr):
|
|
||||||
"""Support our syntax for emphasizing certain lines of code.
|
|
||||||
|
|
||||||
expr should be like '1 2' to emphasize lines 1 and 2 of a code block.
|
|
||||||
Returns a list of ints, the line numbers to emphasize.
|
|
||||||
"""
|
|
||||||
if not expr:
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
|
||||||
return list(map(int, expr.split()))
|
|
||||||
except ValueError: # pragma: no cover
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------ The Main CodeHilite Class ----------------------
|
|
||||||
class CodeHilite:
|
|
||||||
"""
|
|
||||||
Determine language of source code, and pass it on to the Pygments highlighter.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
code = CodeHilite(src=some_code, lang='python')
|
|
||||||
html = code.hilite()
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
* `src`: Source string or any object with a `.readline` attribute.
|
|
||||||
|
|
||||||
* `lang`: String name of Pygments lexer to use for highlighting. Default: `None`.
|
|
||||||
|
|
||||||
* `guess_lang`: Auto-detect which lexer to use. Ignored if `lang` is set to a valid
|
|
||||||
value. Default: `True`.
|
|
||||||
|
|
||||||
* `use_pygments`: Pass code to Pygments for code highlighting. If `False`, the code is
|
|
||||||
instead wrapped for highlighting by a JavaScript library. Default: `True`.
|
|
||||||
|
|
||||||
* `pygments_formatter`: The name of a Pygments formatter or a formatter class used for
|
|
||||||
highlighting the code blocks. Default: `html`.
|
|
||||||
|
|
||||||
* `linenums`: An alias to Pygments `linenos` formatter option. Default: `None`.
|
|
||||||
|
|
||||||
* `css_class`: An alias to Pygments `cssclass` formatter option. Default: 'codehilite'.
|
|
||||||
|
|
||||||
* `lang_prefix`: Prefix prepended to the language. Default: "language-".
|
|
||||||
|
|
||||||
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/
|
|
||||||
|
|
||||||
Additionally, when Pygments is enabled, the code's language is passed to the
|
|
||||||
formatter as an extra option `lang_str`, whose value being `{lang_prefix}{lang}`.
|
|
||||||
This option has no effect to the Pygments' builtin formatters.
|
|
||||||
|
|
||||||
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, **options):
|
|
||||||
self.src = src
|
|
||||||
self.lang = options.pop('lang', None)
|
|
||||||
self.guess_lang = options.pop('guess_lang', True)
|
|
||||||
self.use_pygments = options.pop('use_pygments', True)
|
|
||||||
self.lang_prefix = options.pop('lang_prefix', 'language-')
|
|
||||||
self.pygments_formatter = options.pop('pygments_formatter', 'html')
|
|
||||||
|
|
||||||
if 'linenos' not in options:
|
|
||||||
options['linenos'] = options.pop('linenums', None)
|
|
||||||
if 'cssclass' not in options:
|
|
||||||
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, shebang=True):
|
|
||||||
"""
|
|
||||||
Pass code to the [Pygments](https://pygments.org/) highlighter with
|
|
||||||
optional line numbers. The output should then be styled with CSS to
|
|
||||||
your liking. No styles are applied by default - only styling hooks
|
|
||||||
(i.e.: `<span class="k">`).
|
|
||||||
|
|
||||||
returns : A string of html.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.src = self.src.strip('\n')
|
|
||||||
|
|
||||||
if self.lang is None and shebang:
|
|
||||||
self._parseHeader()
|
|
||||||
|
|
||||||
if pygments and self.use_pygments:
|
|
||||||
try:
|
|
||||||
lexer = get_lexer_by_name(self.lang, **self.options)
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
if self.guess_lang:
|
|
||||||
lexer = guess_lexer(self.src, **self.options)
|
|
||||||
else:
|
|
||||||
lexer = get_lexer_by_name('text', **self.options)
|
|
||||||
except ValueError: # pragma: no cover
|
|
||||||
lexer = get_lexer_by_name('text', **self.options)
|
|
||||||
if not self.lang:
|
|
||||||
# Use the guessed lexer's language instead
|
|
||||||
self.lang = lexer.aliases[0]
|
|
||||||
lang_str = f'{self.lang_prefix}{self.lang}'
|
|
||||||
if isinstance(self.pygments_formatter, str):
|
|
||||||
try:
|
|
||||||
formatter = get_formatter_by_name(self.pygments_formatter, **self.options)
|
|
||||||
except ClassNotFound:
|
|
||||||
formatter = get_formatter_by_name('html', **self.options)
|
|
||||||
else:
|
|
||||||
formatter = self.pygments_formatter(lang_str=lang_str, **self.options)
|
|
||||||
return highlight(self.src, lexer, formatter)
|
|
||||||
else:
|
|
||||||
# just escape and build markup usable by JavaScript highlighting libraries
|
|
||||||
txt = self.src.replace('&', '&')
|
|
||||||
txt = txt.replace('<', '<')
|
|
||||||
txt = txt.replace('>', '>')
|
|
||||||
txt = txt.replace('"', '"')
|
|
||||||
classes = []
|
|
||||||
if self.lang:
|
|
||||||
classes.append('{}{}'.format(self.lang_prefix, self.lang))
|
|
||||||
if self.options['linenos']:
|
|
||||||
classes.append('linenums')
|
|
||||||
class_str = ''
|
|
||||||
if classes:
|
|
||||||
class_str = ' class="{}"'.format(' '.join(classes))
|
|
||||||
return '<pre class="{}"><code{}>{}\n</code></pre>\n'.format(
|
|
||||||
self.options['cssclass'],
|
|
||||||
class_str,
|
|
||||||
txt
|
|
||||||
)
|
|
||||||
|
|
||||||
def _parseHeader(self):
|
|
||||||
"""
|
|
||||||
Determines language of a code block from shebang line and whether the
|
|
||||||
said line should be removed or left in place. If the shebang line
|
|
||||||
contains a path (even a single /) then it is assumed to be a real
|
|
||||||
shebang line and left alone. However, if no path is given
|
|
||||||
(e.i.: `#!python` or `:::python`) then it is assumed to be a mock shebang
|
|
||||||
for language identification of a code fragment and removed from the
|
|
||||||
code block prior to processing for code highlighting. When a mock
|
|
||||||
shebang (e.i: `#!python`) is found, line numbering is turned on. When
|
|
||||||
colons are found in place of a shebang (e.i.: `:::python`), line
|
|
||||||
numbering is left in the current state - off by default.
|
|
||||||
|
|
||||||
Also parses optional list of highlight lines, like:
|
|
||||||
|
|
||||||
:::python hl_lines="1 3"
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
# split text into lines
|
|
||||||
lines = self.src.split("\n")
|
|
||||||
# pull first line to examine
|
|
||||||
fl = lines.pop(0)
|
|
||||||
|
|
||||||
c = re.compile(r'''
|
|
||||||
(?:(?:^::+)|(?P<shebang>^[#]!)) # Shebang or 2 or more colons
|
|
||||||
(?P<path>(?:/\w+)*[/ ])? # Zero or 1 path
|
|
||||||
(?P<lang>[\w#.+-]*) # The language
|
|
||||||
\s* # Arbitrary whitespace
|
|
||||||
# Optional highlight lines, single- or double-quote-delimited
|
|
||||||
(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?
|
|
||||||
''', re.VERBOSE)
|
|
||||||
# search first line for shebang
|
|
||||||
m = c.search(fl)
|
|
||||||
if m:
|
|
||||||
# we have a match
|
|
||||||
try:
|
|
||||||
self.lang = m.group('lang').lower()
|
|
||||||
except IndexError: # pragma: no cover
|
|
||||||
self.lang = None
|
|
||||||
if m.group('path'):
|
|
||||||
# path exists - restore first line
|
|
||||||
lines.insert(0, fl)
|
|
||||||
if self.options['linenos'] is None and m.group('shebang'):
|
|
||||||
# Overridable and Shebang exists - use line numbers
|
|
||||||
self.options['linenos'] = True
|
|
||||||
|
|
||||||
self.options['hl_lines'] = parse_hl_lines(m.group('hl_lines'))
|
|
||||||
else:
|
|
||||||
# No match
|
|
||||||
lines.insert(0, fl)
|
|
||||||
|
|
||||||
self.src = "\n".join(lines).strip("\n")
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------ The Markdown Extension -------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class HiliteTreeprocessor(Treeprocessor):
|
|
||||||
""" Highlight source code in code blocks. """
|
|
||||||
|
|
||||||
def code_unescape(self, text):
|
|
||||||
"""Unescape code."""
|
|
||||||
text = text.replace("<", "<")
|
|
||||||
text = text.replace(">", ">")
|
|
||||||
# Escaped '&' should be replaced at the end to avoid
|
|
||||||
# conflicting with < and >.
|
|
||||||
text = text.replace("&", "&")
|
|
||||||
return text
|
|
||||||
|
|
||||||
def run(self, root):
|
|
||||||
""" Find code blocks and store in `htmlStash`. """
|
|
||||||
blocks = root.iter('pre')
|
|
||||||
for block in blocks:
|
|
||||||
if len(block) == 1 and block[0].tag == 'code':
|
|
||||||
local_config = self.config.copy()
|
|
||||||
code = CodeHilite(
|
|
||||||
self.code_unescape(block[0].text),
|
|
||||||
tab_length=self.md.tab_length,
|
|
||||||
style=local_config.pop('pygments_style', 'default'),
|
|
||||||
**local_config
|
|
||||||
)
|
|
||||||
placeholder = self.md.htmlStash.store(code.hilite())
|
|
||||||
# Clear code block in `etree` instance
|
|
||||||
block.clear()
|
|
||||||
# Change to `p` element which will later
|
|
||||||
# be removed when inserting raw html
|
|
||||||
block.tag = 'p'
|
|
||||||
block.text = placeholder
|
|
||||||
|
|
||||||
|
|
||||||
class CodeHiliteExtension(Extension):
|
|
||||||
""" Add source code highlighting to markdown code blocks. """
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
# define default configs
|
|
||||||
self.config = {
|
|
||||||
'linenums': [None,
|
|
||||||
"Use lines numbers. True|table|inline=yes, False=no, None=auto"],
|
|
||||||
'guess_lang': [True,
|
|
||||||
"Automatic language detection - Default: True"],
|
|
||||||
'css_class': ["codehilite",
|
|
||||||
"Set class name for wrapper <div> - "
|
|
||||||
"Default: codehilite"],
|
|
||||||
'pygments_style': ['default',
|
|
||||||
'Pygments HTML Formatter Style '
|
|
||||||
'(Colorscheme) - Default: default'],
|
|
||||||
'noclasses': [False,
|
|
||||||
'Use inline styles instead of CSS classes - '
|
|
||||||
'Default false'],
|
|
||||||
'use_pygments': [True,
|
|
||||||
'Use Pygments to Highlight code blocks. '
|
|
||||||
'Disable if using a JavaScript library. '
|
|
||||||
'Default: True'],
|
|
||||||
'lang_prefix': [
|
|
||||||
'language-',
|
|
||||||
'Prefix prepended to the language when use_pygments is false. Default: "language-"'
|
|
||||||
],
|
|
||||||
'pygments_formatter': ['html',
|
|
||||||
'Use a specific formatter for Pygments highlighting.'
|
|
||||||
'Default: "html"',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
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 boolean value
|
|
||||||
value = parseBoolValue(value, preserve_none=True)
|
|
||||||
except ValueError:
|
|
||||||
pass # Assume it's not a boolean value. Use as-is.
|
|
||||||
self.config[key] = [value, '']
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Add `HilitePostprocessor` to Markdown instance. """
|
|
||||||
hiliter = HiliteTreeprocessor(md)
|
|
||||||
hiliter.config = self.getConfigs()
|
|
||||||
md.treeprocessors.register(hiliter, 'hilite', 30)
|
|
||||||
|
|
||||||
md.registerExtension(self)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return CodeHiliteExtension(**kwargs)
|
|
|
@ -1,111 +0,0 @@
|
||||||
"""
|
|
||||||
Definition List Extension for Python-Markdown
|
|
||||||
=============================================
|
|
||||||
|
|
||||||
Adds parsing of Definition Lists to Python-Markdown.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/definition_lists>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2008 [Waylan Limberg](http://achinghead.com)
|
|
||||||
|
|
||||||
All changes Copyright 2008-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..blockprocessors import BlockProcessor, ListIndentProcessor
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class DefListProcessor(BlockProcessor):
|
|
||||||
""" Process Definition Lists. """
|
|
||||||
|
|
||||||
RE = re.compile(r'(^|\n)[ ]{0,3}:[ ]{1,3}(.*?)(\n|$)')
|
|
||||||
NO_INDENT_RE = re.compile(r'^[ ]{0,3}[^ :]')
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return bool(self.RE.search(block))
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
|
|
||||||
raw_block = blocks.pop(0)
|
|
||||||
m = self.RE.search(raw_block)
|
|
||||||
terms = [term.strip() for term in
|
|
||||||
raw_block[:m.start()].split('\n') if term.strip()]
|
|
||||||
block = raw_block[m.end():]
|
|
||||||
no_indent = self.NO_INDENT_RE.match(block)
|
|
||||||
if no_indent:
|
|
||||||
d, theRest = (block, None)
|
|
||||||
else:
|
|
||||||
d, theRest = self.detab(block)
|
|
||||||
if d:
|
|
||||||
d = '{}\n{}'.format(m.group(2), d)
|
|
||||||
else:
|
|
||||||
d = m.group(2)
|
|
||||||
sibling = self.lastChild(parent)
|
|
||||||
if not terms and sibling is None:
|
|
||||||
# This is not a definition item. Most likely a paragraph that
|
|
||||||
# starts with a colon at the beginning of a document or list.
|
|
||||||
blocks.insert(0, raw_block)
|
|
||||||
return False
|
|
||||||
if not terms and sibling.tag == 'p':
|
|
||||||
# The previous paragraph contains the terms
|
|
||||||
state = 'looselist'
|
|
||||||
terms = sibling.text.split('\n')
|
|
||||||
parent.remove(sibling)
|
|
||||||
# Acquire new sibling
|
|
||||||
sibling = self.lastChild(parent)
|
|
||||||
else:
|
|
||||||
state = 'list'
|
|
||||||
|
|
||||||
if sibling is not None and sibling.tag == 'dl':
|
|
||||||
# This is another item on an existing list
|
|
||||||
dl = sibling
|
|
||||||
if not terms and len(dl) and dl[-1].tag == 'dd' and len(dl[-1]):
|
|
||||||
state = 'looselist'
|
|
||||||
else:
|
|
||||||
# This is a new list
|
|
||||||
dl = etree.SubElement(parent, 'dl')
|
|
||||||
# Add terms
|
|
||||||
for term in terms:
|
|
||||||
dt = etree.SubElement(dl, 'dt')
|
|
||||||
dt.text = term
|
|
||||||
# Add definition
|
|
||||||
self.parser.state.set(state)
|
|
||||||
dd = etree.SubElement(dl, 'dd')
|
|
||||||
self.parser.parseBlocks(dd, [d])
|
|
||||||
self.parser.state.reset()
|
|
||||||
|
|
||||||
if theRest:
|
|
||||||
blocks.insert(0, theRest)
|
|
||||||
|
|
||||||
|
|
||||||
class DefListIndentProcessor(ListIndentProcessor):
|
|
||||||
""" Process indented children of definition list items. """
|
|
||||||
|
|
||||||
# Definition lists need to be aware of all list types
|
|
||||||
ITEM_TYPES = ['dd', 'li']
|
|
||||||
LIST_TYPES = ['dl', 'ol', 'ul']
|
|
||||||
|
|
||||||
def create_item(self, parent, block):
|
|
||||||
""" Create a new `dd` or `li` (depending on parent) and parse the block with it as the parent. """
|
|
||||||
|
|
||||||
dd = etree.SubElement(parent, 'dd')
|
|
||||||
self.parser.parseBlocks(dd, [block])
|
|
||||||
|
|
||||||
|
|
||||||
class DefListExtension(Extension):
|
|
||||||
""" Add definition lists to Markdown. """
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Add an instance of `DefListProcessor` to `BlockParser`. """
|
|
||||||
md.parser.blockprocessors.register(DefListIndentProcessor(md.parser), 'defindent', 85)
|
|
||||||
md.parser.blockprocessors.register(DefListProcessor(md.parser), 'deflist', 25)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return DefListExtension(**kwargs)
|
|
|
@ -1,58 +0,0 @@
|
||||||
"""
|
|
||||||
Python-Markdown Extra Extension
|
|
||||||
===============================
|
|
||||||
|
|
||||||
A compilation of various Python-Markdown extensions that imitates
|
|
||||||
[PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/).
|
|
||||||
|
|
||||||
Note that each of the individual extensions still need to be available
|
|
||||||
on your PYTHONPATH. This extension simply wraps them all up as a
|
|
||||||
convenience so that only one extension needs to be listed when
|
|
||||||
initiating Markdown. See the documentation for each individual
|
|
||||||
extension for specifics about that extension.
|
|
||||||
|
|
||||||
There may be additional extensions that are distributed with
|
|
||||||
Python-Markdown that are not included here in Extra. Those extensions
|
|
||||||
are not part of PHP Markdown Extra, and therefore, not part of
|
|
||||||
Python-Markdown Extra. If you really would like Extra to include
|
|
||||||
additional extensions, we suggest creating your own clone of Extra
|
|
||||||
under a different name. You could also edit the `extensions` global
|
|
||||||
variable defined below, but be aware that such changes may be lost
|
|
||||||
when you upgrade to any future version of Python-Markdown.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/extra>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Copyright The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
|
|
||||||
extensions = [
|
|
||||||
'fenced_code',
|
|
||||||
'footnotes',
|
|
||||||
'attr_list',
|
|
||||||
'def_list',
|
|
||||||
'tables',
|
|
||||||
'abbr',
|
|
||||||
'md_in_html'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ExtraExtension(Extension):
|
|
||||||
""" Add various extensions to Markdown class."""
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
""" `config` is a dumb holder which gets passed to the actual extension later. """
|
|
||||||
self.config = kwargs
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Register extension instances. """
|
|
||||||
md.registerExtensions(extensions, self.config)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return ExtraExtension(**kwargs)
|
|
|
@ -1,174 +0,0 @@
|
||||||
"""
|
|
||||||
Fenced Code Extension for Python Markdown
|
|
||||||
=========================================
|
|
||||||
|
|
||||||
This extension adds Fenced Code Blocks to Python-Markdown.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/fenced_code_blocks>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
|
|
||||||
|
|
||||||
|
|
||||||
All changes Copyright 2008-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
from textwrap import dedent
|
|
||||||
from . import Extension
|
|
||||||
from ..preprocessors import Preprocessor
|
|
||||||
from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines
|
|
||||||
from .attr_list import get_attrs, AttrListExtension
|
|
||||||
from ..util import parseBoolValue
|
|
||||||
from ..serializers import _escape_attrib_html
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
""" Add `FencedBlockPreprocessor` to the Markdown instance. """
|
|
||||||
md.registerExtension(self)
|
|
||||||
|
|
||||||
md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25)
|
|
||||||
|
|
||||||
|
|
||||||
class FencedBlockPreprocessor(Preprocessor):
|
|
||||||
FENCED_BLOCK_RE = re.compile(
|
|
||||||
dedent(r'''
|
|
||||||
(?P<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence
|
|
||||||
((\{(?P<attrs>[^\}\n]*)\})| # (optional {attrs} or
|
|
||||||
(\.?(?P<lang>[\w#.+-]*)[ ]*)? # optional (.)lang
|
|
||||||
(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines)
|
|
||||||
\n # newline (end of opening fence)
|
|
||||||
(?P<code>.*?)(?<=\n) # the code block
|
|
||||||
(?P=fence)[ ]*$ # closing fence
|
|
||||||
'''),
|
|
||||||
re.MULTILINE | re.DOTALL | re.VERBOSE
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, md, config):
|
|
||||||
super().__init__(md)
|
|
||||||
self.config = config
|
|
||||||
self.checked_for_deps = False
|
|
||||||
self.codehilite_conf = {}
|
|
||||||
self.use_attr_list = False
|
|
||||||
# List of options to convert to boolean values
|
|
||||||
self.bool_options = [
|
|
||||||
'linenums',
|
|
||||||
'guess_lang',
|
|
||||||
'noclasses',
|
|
||||||
'use_pygments'
|
|
||||||
]
|
|
||||||
|
|
||||||
def run(self, lines):
|
|
||||||
""" Match and store Fenced Code Blocks in the `HtmlStash`. """
|
|
||||||
|
|
||||||
# Check for dependent extensions
|
|
||||||
if not self.checked_for_deps:
|
|
||||||
for ext in self.md.registeredExtensions:
|
|
||||||
if isinstance(ext, CodeHiliteExtension):
|
|
||||||
self.codehilite_conf = ext.getConfigs()
|
|
||||||
if isinstance(ext, AttrListExtension):
|
|
||||||
self.use_attr_list = True
|
|
||||||
|
|
||||||
self.checked_for_deps = True
|
|
||||||
|
|
||||||
text = "\n".join(lines)
|
|
||||||
while 1:
|
|
||||||
m = self.FENCED_BLOCK_RE.search(text)
|
|
||||||
if m:
|
|
||||||
lang, id, classes, config = None, '', [], {}
|
|
||||||
if m.group('attrs'):
|
|
||||||
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
|
|
||||||
# is enabled, so we call it to highlight the code
|
|
||||||
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(
|
|
||||||
m.group('code'),
|
|
||||||
lang=lang,
|
|
||||||
style=local_config.pop('pygments_style', 'default'),
|
|
||||||
**local_config
|
|
||||||
)
|
|
||||||
|
|
||||||
code = highliter.hilite(shebang=False)
|
|
||||||
else:
|
|
||||||
id_attr = lang_attr = class_attr = kv_pairs = ''
|
|
||||||
if lang:
|
|
||||||
prefix = self.config.get('lang_prefix', 'language-')
|
|
||||||
lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"'
|
|
||||||
if classes:
|
|
||||||
class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"'
|
|
||||||
if id:
|
|
||||||
id_attr = f' id="{_escape_attrib_html(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` extension 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(
|
|
||||||
f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments'
|
|
||||||
)
|
|
||||||
code = self._escape(m.group('code'))
|
|
||||||
code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>'
|
|
||||||
|
|
||||||
placeholder = self.md.htmlStash.store(code)
|
|
||||||
text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}'
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
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):
|
|
||||||
""" basic html escaping """
|
|
||||||
txt = txt.replace('&', '&')
|
|
||||||
txt = txt.replace('<', '<')
|
|
||||||
txt = txt.replace('>', '>')
|
|
||||||
txt = txt.replace('"', '"')
|
|
||||||
return txt
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return FencedCodeExtension(**kwargs)
|
|
|
@ -1,411 +0,0 @@
|
||||||
"""
|
|
||||||
Footnotes Extension for Python-Markdown
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Adds footnote handling to Python-Markdown.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/footnotes>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Copyright The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..blockprocessors import BlockProcessor
|
|
||||||
from ..inlinepatterns import InlineProcessor
|
|
||||||
from ..treeprocessors import Treeprocessor
|
|
||||||
from ..postprocessors import Postprocessor
|
|
||||||
from .. import util
|
|
||||||
from collections import OrderedDict
|
|
||||||
import re
|
|
||||||
import copy
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
|
|
||||||
FN_BACKLINK_TEXT = util.STX + "zz1337820767766393qq" + util.ETX
|
|
||||||
NBSP_PLACEHOLDER = util.STX + "qq3936677670287331zz" + util.ETX
|
|
||||||
RE_REF_ID = re.compile(r'(fnref)(\d+)')
|
|
||||||
|
|
||||||
|
|
||||||
class FootnoteExtension(Extension):
|
|
||||||
""" Footnote Extension. """
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
""" Setup configs. """
|
|
||||||
|
|
||||||
self.config = {
|
|
||||||
'PLACE_MARKER':
|
|
||||||
["///Footnotes Go Here///",
|
|
||||||
"The text string that marks where the footnotes go"],
|
|
||||||
'UNIQUE_IDS':
|
|
||||||
[False,
|
|
||||||
"Avoid name collisions across "
|
|
||||||
"multiple calls to reset()."],
|
|
||||||
"BACKLINK_TEXT":
|
|
||||||
["↩",
|
|
||||||
"The text string that links from the footnote "
|
|
||||||
"to the reader's place."],
|
|
||||||
"SUPERSCRIPT_TEXT":
|
|
||||||
["{}",
|
|
||||||
"The text string that links from the reader's place "
|
|
||||||
"to the footnote."],
|
|
||||||
"BACKLINK_TITLE":
|
|
||||||
["Jump back to footnote %d in the text",
|
|
||||||
"The text string used for the title HTML attribute "
|
|
||||||
"of the backlink. %d will be replaced by the "
|
|
||||||
"footnote number."],
|
|
||||||
"SEPARATOR":
|
|
||||||
[":",
|
|
||||||
"Footnote separator."]
|
|
||||||
}
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
# In multiple invocations, emit links that don't get tangled.
|
|
||||||
self.unique_prefix = 0
|
|
||||||
self.found_refs = {}
|
|
||||||
self.used_refs = set()
|
|
||||||
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Add pieces to Markdown. """
|
|
||||||
md.registerExtension(self)
|
|
||||||
self.parser = md.parser
|
|
||||||
self.md = md
|
|
||||||
# Insert a `blockprocessor` before `ReferencePreprocessor`
|
|
||||||
md.parser.blockprocessors.register(FootnoteBlockProcessor(self), 'footnote', 17)
|
|
||||||
|
|
||||||
# Insert an inline pattern before `ImageReferencePattern`
|
|
||||||
FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah
|
|
||||||
md.inlinePatterns.register(FootnoteInlineProcessor(FOOTNOTE_RE, self), 'footnote', 175)
|
|
||||||
# Insert a tree-processor that would actually add the footnote div
|
|
||||||
# This must be before all other tree-processors (i.e., `inline` and
|
|
||||||
# `codehilite`) so they can run on the the contents of the div.
|
|
||||||
md.treeprocessors.register(FootnoteTreeprocessor(self), 'footnote', 50)
|
|
||||||
|
|
||||||
# Insert a tree-processor that will run after inline is done.
|
|
||||||
# In this tree-processor we want to check our duplicate footnote tracker
|
|
||||||
# And add additional `backrefs` to the footnote pointing back to the
|
|
||||||
# duplicated references.
|
|
||||||
md.treeprocessors.register(FootnotePostTreeprocessor(self), 'footnote-duplicate', 15)
|
|
||||||
|
|
||||||
# Insert a postprocessor after amp_substitute processor
|
|
||||||
md.postprocessors.register(FootnotePostprocessor(self), 'footnote', 25)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
""" Clear footnotes on reset, and prepare for distinct document. """
|
|
||||||
self.footnotes = OrderedDict()
|
|
||||||
self.unique_prefix += 1
|
|
||||||
self.found_refs = {}
|
|
||||||
self.used_refs = set()
|
|
||||||
|
|
||||||
def unique_ref(self, reference, found=False):
|
|
||||||
""" Get a unique reference if there are duplicates. """
|
|
||||||
if not found:
|
|
||||||
return reference
|
|
||||||
|
|
||||||
original_ref = reference
|
|
||||||
while reference in self.used_refs:
|
|
||||||
ref, rest = reference.split(self.get_separator(), 1)
|
|
||||||
m = RE_REF_ID.match(ref)
|
|
||||||
if m:
|
|
||||||
reference = '%s%d%s%s' % (m.group(1), int(m.group(2))+1, self.get_separator(), rest)
|
|
||||||
else:
|
|
||||||
reference = '%s%d%s%s' % (ref, 2, self.get_separator(), rest)
|
|
||||||
|
|
||||||
self.used_refs.add(reference)
|
|
||||||
if original_ref in self.found_refs:
|
|
||||||
self.found_refs[original_ref] += 1
|
|
||||||
else:
|
|
||||||
self.found_refs[original_ref] = 1
|
|
||||||
return reference
|
|
||||||
|
|
||||||
def findFootnotesPlaceholder(self, root):
|
|
||||||
""" Return ElementTree Element that contains Footnote placeholder. """
|
|
||||||
def finder(element):
|
|
||||||
for child in element:
|
|
||||||
if child.text:
|
|
||||||
if child.text.find(self.getConfig("PLACE_MARKER")) > -1:
|
|
||||||
return child, element, True
|
|
||||||
if child.tail:
|
|
||||||
if child.tail.find(self.getConfig("PLACE_MARKER")) > -1:
|
|
||||||
return child, element, False
|
|
||||||
child_res = finder(child)
|
|
||||||
if child_res is not None:
|
|
||||||
return child_res
|
|
||||||
return None
|
|
||||||
|
|
||||||
res = finder(root)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def setFootnote(self, id, text):
|
|
||||||
""" Store a footnote for later retrieval. """
|
|
||||||
self.footnotes[id] = text
|
|
||||||
|
|
||||||
def get_separator(self):
|
|
||||||
""" Get the footnote separator. """
|
|
||||||
return self.getConfig("SEPARATOR")
|
|
||||||
|
|
||||||
def makeFootnoteId(self, id):
|
|
||||||
""" Return footnote link id. """
|
|
||||||
if self.getConfig("UNIQUE_IDS"):
|
|
||||||
return 'fn%s%d-%s' % (self.get_separator(), self.unique_prefix, id)
|
|
||||||
else:
|
|
||||||
return 'fn{}{}'.format(self.get_separator(), id)
|
|
||||||
|
|
||||||
def makeFootnoteRefId(self, id, found=False):
|
|
||||||
""" Return footnote back-link id. """
|
|
||||||
if self.getConfig("UNIQUE_IDS"):
|
|
||||||
return self.unique_ref('fnref%s%d-%s' % (self.get_separator(), self.unique_prefix, id), found)
|
|
||||||
else:
|
|
||||||
return self.unique_ref('fnref{}{}'.format(self.get_separator(), id), found)
|
|
||||||
|
|
||||||
def makeFootnotesDiv(self, root):
|
|
||||||
""" Return `div` of footnotes as `etree` Element. """
|
|
||||||
|
|
||||||
if not list(self.footnotes.keys()):
|
|
||||||
return None
|
|
||||||
|
|
||||||
div = etree.Element("div")
|
|
||||||
div.set('class', 'footnote')
|
|
||||||
etree.SubElement(div, "hr")
|
|
||||||
ol = etree.SubElement(div, "ol")
|
|
||||||
surrogate_parent = etree.Element("div")
|
|
||||||
|
|
||||||
# Backward compatibility with old '%d' placeholder
|
|
||||||
backlink_title = self.getConfig("BACKLINK_TITLE").replace("%d", "{}")
|
|
||||||
|
|
||||||
for index, id in enumerate(self.footnotes.keys(), start=1):
|
|
||||||
li = etree.SubElement(ol, "li")
|
|
||||||
li.set("id", self.makeFootnoteId(id))
|
|
||||||
# Parse footnote with surrogate parent as `li` cannot be used.
|
|
||||||
# List block handlers have special logic to deal with `li`.
|
|
||||||
# When we are done parsing, we will copy everything over to `li`.
|
|
||||||
self.parser.parseChunk(surrogate_parent, self.footnotes[id])
|
|
||||||
for el in list(surrogate_parent):
|
|
||||||
li.append(el)
|
|
||||||
surrogate_parent.remove(el)
|
|
||||||
backlink = etree.Element("a")
|
|
||||||
backlink.set("href", "#" + self.makeFootnoteRefId(id))
|
|
||||||
backlink.set("class", "footnote-backref")
|
|
||||||
backlink.set(
|
|
||||||
"title",
|
|
||||||
backlink_title.format(index)
|
|
||||||
)
|
|
||||||
backlink.text = FN_BACKLINK_TEXT
|
|
||||||
|
|
||||||
if len(li):
|
|
||||||
node = li[-1]
|
|
||||||
if node.tag == "p":
|
|
||||||
node.text = node.text + NBSP_PLACEHOLDER
|
|
||||||
node.append(backlink)
|
|
||||||
else:
|
|
||||||
p = etree.SubElement(li, "p")
|
|
||||||
p.append(backlink)
|
|
||||||
return div
|
|
||||||
|
|
||||||
|
|
||||||
class FootnoteBlockProcessor(BlockProcessor):
|
|
||||||
""" Find all footnote references and store for later use. """
|
|
||||||
|
|
||||||
RE = re.compile(r'^[ ]{0,3}\[\^([^\]]*)\]:[ ]*(.*)$', re.MULTILINE)
|
|
||||||
|
|
||||||
def __init__(self, footnotes):
|
|
||||||
super().__init__(footnotes.parser)
|
|
||||||
self.footnotes = footnotes
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
return True
|
|
||||||
|
|
||||||
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)]
|
|
||||||
|
|
||||||
# Handle rest of block
|
|
||||||
therest = block[m.end():].lstrip('\n')
|
|
||||||
m2 = self.RE.search(therest)
|
|
||||||
if m2:
|
|
||||||
# Another footnote exists in the rest of this block.
|
|
||||||
# Any content before match is continuation of this footnote, which may be lazily indented.
|
|
||||||
before = therest[:m2.start()].rstrip('\n')
|
|
||||||
fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(before)]).lstrip('\n')
|
|
||||||
# Add back to blocks everything from beginning of match forward for next iteration.
|
|
||||||
blocks.insert(0, therest[m2.start():])
|
|
||||||
else:
|
|
||||||
# All remaining lines of block are continuation of this footnote, which may be lazily indented.
|
|
||||||
fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(therest)]).strip('\n')
|
|
||||||
|
|
||||||
# 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 processing.
|
|
||||||
|
|
||||||
Returns: a list of blocks with indentation removed.
|
|
||||||
"""
|
|
||||||
fn_blocks = []
|
|
||||||
while blocks:
|
|
||||||
if blocks[0].startswith(' '*4):
|
|
||||||
block = blocks.pop(0)
|
|
||||||
# Check for new footnotes within this block and split at new footnote.
|
|
||||||
m = self.RE.search(block)
|
|
||||||
if m:
|
|
||||||
# Another footnote exists in this block.
|
|
||||||
# Any content before match is continuation of this footnote, which may be lazily indented.
|
|
||||||
before = block[:m.start()].rstrip('\n')
|
|
||||||
fn_blocks.append(self.detab(before))
|
|
||||||
# Add back to blocks everything from beginning of match forward for next iteration.
|
|
||||||
blocks.insert(0, block[m.start():])
|
|
||||||
# End of this footnote.
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Entire block is part of this footnote.
|
|
||||||
fn_blocks.append(self.detab(block))
|
|
||||||
else:
|
|
||||||
# End of this footnote.
|
|
||||||
break
|
|
||||||
return fn_blocks
|
|
||||||
|
|
||||||
def detab(self, block):
|
|
||||||
""" Remove one level of indent from a block.
|
|
||||||
|
|
||||||
Preserve lazily indented blocks by only removing indent from indented lines.
|
|
||||||
"""
|
|
||||||
lines = block.split('\n')
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.startswith(' '*4):
|
|
||||||
lines[i] = line[4:]
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
class FootnoteInlineProcessor(InlineProcessor):
|
|
||||||
""" `InlinePattern` for footnote markers in a document's body text. """
|
|
||||||
|
|
||||||
def __init__(self, pattern, footnotes):
|
|
||||||
super().__init__(pattern)
|
|
||||||
self.footnotes = footnotes
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
id = m.group(1)
|
|
||||||
if id in self.footnotes.footnotes.keys():
|
|
||||||
sup = etree.Element("sup")
|
|
||||||
a = etree.SubElement(sup, "a")
|
|
||||||
sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True))
|
|
||||||
a.set('href', '#' + self.footnotes.makeFootnoteId(id))
|
|
||||||
a.set('class', 'footnote-ref')
|
|
||||||
a.text = self.footnotes.getConfig("SUPERSCRIPT_TEXT").format(
|
|
||||||
list(self.footnotes.footnotes.keys()).index(id) + 1
|
|
||||||
)
|
|
||||||
return sup, m.start(0), m.end(0)
|
|
||||||
else:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
|
|
||||||
class FootnotePostTreeprocessor(Treeprocessor):
|
|
||||||
""" Amend footnote div with duplicates. """
|
|
||||||
|
|
||||||
def __init__(self, footnotes):
|
|
||||||
self.footnotes = footnotes
|
|
||||||
|
|
||||||
def add_duplicates(self, li, duplicates):
|
|
||||||
""" Adjust current `li` and add the duplicates: `fnref2`, `fnref3`, etc. """
|
|
||||||
for link in li.iter('a'):
|
|
||||||
# Find the link that needs to be duplicated.
|
|
||||||
if link.attrib.get('class', '') == 'footnote-backref':
|
|
||||||
ref, rest = link.attrib['href'].split(self.footnotes.get_separator(), 1)
|
|
||||||
# Duplicate link the number of times we need to
|
|
||||||
# and point the to the appropriate references.
|
|
||||||
links = []
|
|
||||||
for index in range(2, duplicates + 1):
|
|
||||||
sib_link = copy.deepcopy(link)
|
|
||||||
sib_link.attrib['href'] = '%s%d%s%s' % (ref, index, self.footnotes.get_separator(), rest)
|
|
||||||
links.append(sib_link)
|
|
||||||
self.offset += 1
|
|
||||||
# Add all the new duplicate links.
|
|
||||||
el = list(li)[-1]
|
|
||||||
for link in links:
|
|
||||||
el.append(link)
|
|
||||||
break
|
|
||||||
|
|
||||||
def get_num_duplicates(self, li):
|
|
||||||
""" Get the number of duplicate refs of the footnote. """
|
|
||||||
fn, rest = li.attrib.get('id', '').split(self.footnotes.get_separator(), 1)
|
|
||||||
link_id = '{}ref{}{}'.format(fn, self.footnotes.get_separator(), rest)
|
|
||||||
return self.footnotes.found_refs.get(link_id, 0)
|
|
||||||
|
|
||||||
def handle_duplicates(self, parent):
|
|
||||||
""" Find duplicate footnotes and format and add the duplicates. """
|
|
||||||
for li in list(parent):
|
|
||||||
# Check number of duplicates footnotes and insert
|
|
||||||
# additional links if needed.
|
|
||||||
count = self.get_num_duplicates(li)
|
|
||||||
if count > 1:
|
|
||||||
self.add_duplicates(li, count)
|
|
||||||
|
|
||||||
def run(self, root):
|
|
||||||
""" Crawl the footnote div and add missing duplicate footnotes. """
|
|
||||||
self.offset = 0
|
|
||||||
for div in root.iter('div'):
|
|
||||||
if div.attrib.get('class', '') == 'footnote':
|
|
||||||
# Footnotes should be under the first ordered list under
|
|
||||||
# the footnote div. So once we find it, quit.
|
|
||||||
for ol in div.iter('ol'):
|
|
||||||
self.handle_duplicates(ol)
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
class FootnoteTreeprocessor(Treeprocessor):
|
|
||||||
""" Build and append footnote div to end of document. """
|
|
||||||
|
|
||||||
def __init__(self, footnotes):
|
|
||||||
self.footnotes = footnotes
|
|
||||||
|
|
||||||
def run(self, root):
|
|
||||||
footnotesDiv = self.footnotes.makeFootnotesDiv(root)
|
|
||||||
if footnotesDiv is not None:
|
|
||||||
result = self.footnotes.findFootnotesPlaceholder(root)
|
|
||||||
if result:
|
|
||||||
child, parent, isText = result
|
|
||||||
ind = list(parent).index(child)
|
|
||||||
if isText:
|
|
||||||
parent.remove(child)
|
|
||||||
parent.insert(ind, footnotesDiv)
|
|
||||||
else:
|
|
||||||
parent.insert(ind + 1, footnotesDiv)
|
|
||||||
child.tail = None
|
|
||||||
else:
|
|
||||||
root.append(footnotesDiv)
|
|
||||||
|
|
||||||
|
|
||||||
class FootnotePostprocessor(Postprocessor):
|
|
||||||
""" Replace placeholders with html entities. """
|
|
||||||
def __init__(self, footnotes):
|
|
||||||
self.footnotes = footnotes
|
|
||||||
|
|
||||||
def run(self, text):
|
|
||||||
text = text.replace(
|
|
||||||
FN_BACKLINK_TEXT, self.footnotes.getConfig("BACKLINK_TEXT")
|
|
||||||
)
|
|
||||||
return text.replace(NBSP_PLACEHOLDER, " ")
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
""" Return an instance of the `FootnoteExtension` """
|
|
||||||
return FootnoteExtension(**kwargs)
|
|
|
@ -1,67 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
|
|
||||||
Legacy Attributes Extension
|
|
||||||
===========================
|
|
||||||
|
|
||||||
An extension to Python Markdown which implements legacy attributes.
|
|
||||||
|
|
||||||
Prior to Python-Markdown version 3.0, the Markdown class had an `enable_attributes`
|
|
||||||
keyword which was on by default and provided for attributes to be defined for elements
|
|
||||||
using the format `{@key=value}`. This extension is provided as a replacement for
|
|
||||||
backward compatibility. New documents should be authored using `attr_lists`. However,
|
|
||||||
numerous documents exist which have been using the old attribute format for many
|
|
||||||
years. This extension can be used to continue to render those documents correctly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from markdown.treeprocessors import Treeprocessor, isString
|
|
||||||
from markdown.extensions import Extension
|
|
||||||
|
|
||||||
|
|
||||||
ATTR_RE = re.compile(r'\{@([^\}]*)=([^\}]*)}') # {@id=123}
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyAttrs(Treeprocessor):
|
|
||||||
def run(self, doc):
|
|
||||||
"""Find and set values of attributes ({@key=value}). """
|
|
||||||
for el in doc.iter():
|
|
||||||
alt = el.get('alt', None)
|
|
||||||
if alt is not None:
|
|
||||||
el.set('alt', self.handleAttributes(el, alt))
|
|
||||||
if el.text and isString(el.text):
|
|
||||||
el.text = self.handleAttributes(el, el.text)
|
|
||||||
if el.tail and isString(el.tail):
|
|
||||||
el.tail = self.handleAttributes(el, el.tail)
|
|
||||||
|
|
||||||
def handleAttributes(self, el, txt):
|
|
||||||
""" Set attributes and return text without definitions. """
|
|
||||||
def attributeCallback(match):
|
|
||||||
el.set(match.group(1), match.group(2).replace('\n', ' '))
|
|
||||||
return ATTR_RE.sub(attributeCallback, txt)
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyAttrExtension(Extension):
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
md.treeprocessors.register(LegacyAttrs(md), 'legacyattrs', 15)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return LegacyAttrExtension(**kwargs)
|
|
|
@ -1,49 +0,0 @@
|
||||||
'''
|
|
||||||
Legacy Em Extension for Python-Markdown
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
This extension provides legacy behavior for _connected_words_.
|
|
||||||
|
|
||||||
Copyright 2015-2018 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..inlinepatterns import UnderscoreProcessor, EmStrongItem, EM_STRONG2_RE, STRONG_EM2_RE
|
|
||||||
import re
|
|
||||||
|
|
||||||
# _emphasis_
|
|
||||||
EMPHASIS_RE = r'(_)([^_]+)\1'
|
|
||||||
|
|
||||||
# __strong__
|
|
||||||
STRONG_RE = r'(_{2})(.+?)\1'
|
|
||||||
|
|
||||||
# __strong_em___
|
|
||||||
STRONG_EM_RE = r'(_)\1(?!\1)([^_]+?)\1(?!\1)(.+?)\1{3}'
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyUnderscoreProcessor(UnderscoreProcessor):
|
|
||||||
"""Emphasis processor for handling strong and em matches inside underscores."""
|
|
||||||
|
|
||||||
PATTERNS = [
|
|
||||||
EmStrongItem(re.compile(EM_STRONG2_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'),
|
|
||||||
EmStrongItem(re.compile(STRONG_EM2_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'),
|
|
||||||
EmStrongItem(re.compile(STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'),
|
|
||||||
EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'),
|
|
||||||
EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em')
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyEmExtension(Extension):
|
|
||||||
""" Add legacy_em extension to Markdown class."""
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Modify inline patterns. """
|
|
||||||
md.inlinePatterns.register(LegacyUnderscoreProcessor(r'_'), 'em_strong2', 50)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
""" Return an instance of the `LegacyEmExtension` """
|
|
||||||
return LegacyEmExtension(**kwargs)
|
|
|
@ -1,365 +0,0 @@
|
||||||
"""
|
|
||||||
Python-Markdown Markdown in HTML Extension
|
|
||||||
===============================
|
|
||||||
|
|
||||||
An implementation of [PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/)'s
|
|
||||||
parsing of Markdown syntax in raw HTML.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/raw_html>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Copyright The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..blockprocessors import BlockProcessor
|
|
||||||
from ..preprocessors import Preprocessor
|
|
||||||
from ..postprocessors import RawHtmlPostprocessor
|
|
||||||
from .. import util
|
|
||||||
from ..htmlparser import HTMLExtractor, blank_line_re
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
|
|
||||||
|
|
||||||
class HTMLExtractorExtra(HTMLExtractor):
|
|
||||||
"""
|
|
||||||
Override `HTMLExtractor` and create `etree` `Elements` for any elements which should have content parsed as
|
|
||||||
Markdown.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, md, *args, **kwargs):
|
|
||||||
# All block-level tags.
|
|
||||||
self.block_level_tags = set(md.block_level_elements.copy())
|
|
||||||
# Block-level tags in which the content only gets span level parsing
|
|
||||||
self.span_tags = set(
|
|
||||||
['address', 'dd', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'legend', 'li', 'p', 'summary', 'td', 'th']
|
|
||||||
)
|
|
||||||
# Block-level tags which never get their content parsed.
|
|
||||||
self.raw_tags = set(['canvas', 'math', 'option', 'pre', 'script', 'style', 'textarea'])
|
|
||||||
|
|
||||||
super().__init__(md, *args, **kwargs)
|
|
||||||
|
|
||||||
# Block-level tags in which the content gets parsed as blocks
|
|
||||||
self.block_tags = set(self.block_level_tags) - (self.span_tags | self.raw_tags | self.empty_tags)
|
|
||||||
self.span_and_blocks_tags = self.block_tags | self.span_tags
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""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()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""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])
|
|
||||||
|
|
||||||
def get_element(self):
|
|
||||||
""" Return element from `treebuilder` and reset `treebuilder` for later use. """
|
|
||||||
element = self.treebuilder.close()
|
|
||||||
self.treebuilder = etree.TreeBuilder()
|
|
||||||
return element
|
|
||||||
|
|
||||||
def get_state(self, tag, attrs):
|
|
||||||
""" Return state from tag and `markdown` attribute. One of 'block', 'span', or 'off'. """
|
|
||||||
md_attr = attrs.get('markdown', '0')
|
|
||||||
if md_attr == 'markdown':
|
|
||||||
# `<tag markdown>` is the same as `<tag markdown='1'>`.
|
|
||||||
md_attr = '1'
|
|
||||||
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
|
|
||||||
|
|
||||||
def handle_starttag(self, tag, attrs):
|
|
||||||
# Handle tags that should always be empty and do not specify a closing tag
|
|
||||||
if tag in self.empty_tags and (self.at_line_start() or self.intail):
|
|
||||||
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()
|
|
||||||
self.handle_empty_tag(data, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if tag in self.block_level_tags and (self.at_line_start() or self.intail):
|
|
||||||
# Valueless attribute (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):
|
|
||||||
# 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)
|
|
||||||
if tag in self.CDATA_CONTENT_ELEMENTS:
|
|
||||||
# This is presumably a standalone tag in a code span (see #1036).
|
|
||||||
self.clear_cdata_mode()
|
|
||||||
|
|
||||||
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 = []
|
|
||||||
# Check if element has a tail
|
|
||||||
if not blank_line_re.match(
|
|
||||||
self.rawdata[self.line_offset + self.offset + len(self.get_endtag_text(tag)):]):
|
|
||||||
# More content exists after `endtag`.
|
|
||||||
self.intail = True
|
|
||||||
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.intail and '\n' in data:
|
|
||||||
self.intail = False
|
|
||||||
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:
|
|
||||||
self.handle_data(self.md.htmlStash.store(data))
|
|
||||||
|
|
||||||
def parse_pi(self, i):
|
|
||||||
if self.at_line_start() or self.intail or self.mdstack:
|
|
||||||
# The same override exists in `HTMLExtractor` without the check
|
|
||||||
# for `mdstack`. Therefore, use parent of `HTMLExtractor` instead.
|
|
||||||
return super(HTMLExtractor, self).parse_pi(i)
|
|
||||||
# This is not the beginning of a raw block so treat as plain data
|
|
||||||
# and avoid consuming any tags which may follow (see #1066).
|
|
||||||
self.handle_data('<?')
|
|
||||||
return i + 2
|
|
||||||
|
|
||||||
def parse_html_declaration(self, i):
|
|
||||||
if self.at_line_start() or self.intail or self.mdstack:
|
|
||||||
# The same override exists in `HTMLExtractor` without the check
|
|
||||||
# for `mdstack`. Therefore, use parent of `HTMLExtractor` instead.
|
|
||||||
return super(HTMLExtractor, self).parse_html_declaration(i)
|
|
||||||
# This is not the beginning of a raw block so treat as plain data
|
|
||||||
# and avoid consuming any tags which may follow (see #1066).
|
|
||||||
self.handle_data('<!')
|
|
||||||
return i + 2
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
Recursively 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 disabled, the text content of it and its children 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 separate 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 inline processors.
|
|
||||||
# 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, '')
|
|
||||||
# Confirm 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):
|
|
||||||
"""Add Markdown parsing in HTML to Markdown class."""
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Register extension instances. """
|
|
||||||
|
|
||||||
# Replace raw HTML preprocessor
|
|
||||||
md.preprocessors.register(HtmlBlockPreprocessor(md), 'html_block', 20)
|
|
||||||
# Add `blockprocessor` which handles the placeholders for `etree` elements
|
|
||||||
md.parser.blockprocessors.register(
|
|
||||||
MarkdownInHtmlProcessor(md.parser), 'markdown_block', 105
|
|
||||||
)
|
|
||||||
# Replace raw HTML postprocessor
|
|
||||||
md.postprocessors.register(MarkdownInHTMLPostprocessor(md), 'raw_html', 30)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return MarkdownInHtmlExtension(**kwargs)
|
|
|
@ -1,79 +0,0 @@
|
||||||
"""
|
|
||||||
Meta Data Extension for Python-Markdown
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
This extension adds Meta Data handling to markdown.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/meta_data>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com).
|
|
||||||
|
|
||||||
All changes Copyright 2008-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..preprocessors import Preprocessor
|
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
|
|
||||||
log = logging.getLogger('MARKDOWN')
|
|
||||||
|
|
||||||
# Global Vars
|
|
||||||
META_RE = re.compile(r'^[ ]{0,3}(?P<key>[A-Za-z0-9_-]+):\s*(?P<value>.*)')
|
|
||||||
META_MORE_RE = re.compile(r'^[ ]{4,}(?P<value>.*)')
|
|
||||||
BEGIN_RE = re.compile(r'^-{3}(\s.*)?')
|
|
||||||
END_RE = re.compile(r'^(-{3}|\.{3})(\s.*)?')
|
|
||||||
|
|
||||||
|
|
||||||
class MetaExtension (Extension):
|
|
||||||
""" Meta-Data extension for Python-Markdown. """
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Add `MetaPreprocessor` to Markdown instance. """
|
|
||||||
md.registerExtension(self)
|
|
||||||
self.md = md
|
|
||||||
md.preprocessors.register(MetaPreprocessor(md), 'meta', 27)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.md.Meta = {}
|
|
||||||
|
|
||||||
|
|
||||||
class MetaPreprocessor(Preprocessor):
|
|
||||||
""" Get Meta-Data. """
|
|
||||||
|
|
||||||
def run(self, lines):
|
|
||||||
""" Parse Meta-Data and store in Markdown.Meta. """
|
|
||||||
meta = {}
|
|
||||||
key = None
|
|
||||||
if lines and BEGIN_RE.match(lines[0]):
|
|
||||||
lines.pop(0)
|
|
||||||
while lines:
|
|
||||||
line = lines.pop(0)
|
|
||||||
m1 = META_RE.match(line)
|
|
||||||
if line.strip() == '' or END_RE.match(line):
|
|
||||||
break # blank line or end of YAML header - done
|
|
||||||
if m1:
|
|
||||||
key = m1.group('key').lower().strip()
|
|
||||||
value = m1.group('value').strip()
|
|
||||||
try:
|
|
||||||
meta[key].append(value)
|
|
||||||
except KeyError:
|
|
||||||
meta[key] = [value]
|
|
||||||
else:
|
|
||||||
m2 = META_MORE_RE.match(line)
|
|
||||||
if m2 and key:
|
|
||||||
# Add another line to existing key
|
|
||||||
meta[key].append(m2.group('value').strip())
|
|
||||||
else:
|
|
||||||
lines.insert(0, line)
|
|
||||||
break # no meta data - done
|
|
||||||
self.md.Meta = meta
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return MetaExtension(**kwargs)
|
|
|
@ -1,33 +0,0 @@
|
||||||
"""
|
|
||||||
`NL2BR` Extension
|
|
||||||
===============
|
|
||||||
|
|
||||||
A Python-Markdown extension to treat newlines as hard breaks; like
|
|
||||||
GitHub-flavored Markdown does.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/nl2br>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2011 [Brian Neal](https://deathofagremmie.com/)
|
|
||||||
|
|
||||||
All changes Copyright 2011-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..inlinepatterns import SubstituteTagInlineProcessor
|
|
||||||
|
|
||||||
BR_RE = r'\n'
|
|
||||||
|
|
||||||
|
|
||||||
class Nl2BrExtension(Extension):
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
br_tag = SubstituteTagInlineProcessor(BR_RE, 'br')
|
|
||||||
md.inlinePatterns.register(br_tag, 'nl', 5)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return Nl2BrExtension(**kwargs)
|
|
|
@ -1,54 +0,0 @@
|
||||||
"""
|
|
||||||
Sane List Extension for Python-Markdown
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Modify the behavior of Lists in Python-Markdown to act in a sane manor.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/sane_lists>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2011 [Waylan Limberg](http://achinghead.com)
|
|
||||||
|
|
||||||
All changes Copyright 2011-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..blockprocessors import OListProcessor, UListProcessor
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class SaneOListProcessor(OListProcessor):
|
|
||||||
|
|
||||||
SIBLING_TAGS = ['ol']
|
|
||||||
LAZY_OL = False
|
|
||||||
|
|
||||||
def __init__(self, parser):
|
|
||||||
super().__init__(parser)
|
|
||||||
self.CHILD_RE = re.compile(r'^[ ]{0,%d}((\d+\.))[ ]+(.*)' %
|
|
||||||
(self.tab_length - 1))
|
|
||||||
|
|
||||||
|
|
||||||
class SaneUListProcessor(UListProcessor):
|
|
||||||
|
|
||||||
SIBLING_TAGS = ['ul']
|
|
||||||
|
|
||||||
def __init__(self, parser):
|
|
||||||
super().__init__(parser)
|
|
||||||
self.CHILD_RE = re.compile(r'^[ ]{0,%d}(([*+-]))[ ]+(.*)' %
|
|
||||||
(self.tab_length - 1))
|
|
||||||
|
|
||||||
|
|
||||||
class SaneListExtension(Extension):
|
|
||||||
""" Add sane lists to Markdown. """
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Override existing Processors. """
|
|
||||||
md.parser.blockprocessors.register(SaneOListProcessor(md.parser), 'olist', 40)
|
|
||||||
md.parser.blockprocessors.register(SaneUListProcessor(md.parser), 'ulist', 30)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return SaneListExtension(**kwargs)
|
|
|
@ -1,257 +0,0 @@
|
||||||
'''
|
|
||||||
Smarty extension for Python-Markdown
|
|
||||||
====================================
|
|
||||||
|
|
||||||
Adds conversion of ASCII dashes, quotes and ellipses to their HTML
|
|
||||||
entity equivalents.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/smarty>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Author: 2013, Dmitry Shachnev <mitya57@gmail.com>
|
|
||||||
|
|
||||||
All changes Copyright 2013-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
SmartyPants license:
|
|
||||||
|
|
||||||
Copyright (c) 2003 John Gruber <https://daringfireball.net/>
|
|
||||||
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 "SmartyPants" 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 copyright holders and contributors "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 the copyright
|
|
||||||
owner or contributors 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.
|
|
||||||
|
|
||||||
|
|
||||||
`smartypants.py` license:
|
|
||||||
|
|
||||||
`smartypants.py` is a derivative work of SmartyPants.
|
|
||||||
Copyright (c) 2004, 2007 Chad Miller <http://web.chad.org/>
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
This software is provided by the copyright holders and contributors "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 the copyright
|
|
||||||
owner or contributors 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.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..inlinepatterns import HtmlInlineProcessor, HTML_RE
|
|
||||||
from ..treeprocessors import InlineProcessor
|
|
||||||
from ..util import Registry
|
|
||||||
|
|
||||||
|
|
||||||
# Constants for quote education.
|
|
||||||
punctClass = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]"""
|
|
||||||
endOfWordClass = r"[\s.,;:!?)]"
|
|
||||||
closeClass = r"[^\ \t\r\n\[\{\(\-\u0002\u0003]"
|
|
||||||
|
|
||||||
openingQuotesBase = (
|
|
||||||
r'(\s' # a whitespace char
|
|
||||||
r'| ' # or a non-breaking space entity
|
|
||||||
r'|--' # or dashes
|
|
||||||
r'|–|—' # or Unicode
|
|
||||||
r'|&[mn]dash;' # or named dash entities
|
|
||||||
r'|–|—' # or decimal entities
|
|
||||||
r')'
|
|
||||||
)
|
|
||||||
|
|
||||||
substitutions = {
|
|
||||||
'mdash': '—',
|
|
||||||
'ndash': '–',
|
|
||||||
'ellipsis': '…',
|
|
||||||
'left-angle-quote': '«',
|
|
||||||
'right-angle-quote': '»',
|
|
||||||
'left-single-quote': '‘',
|
|
||||||
'right-single-quote': '’',
|
|
||||||
'left-double-quote': '“',
|
|
||||||
'right-double-quote': '”',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Special case if the very first character is a quote
|
|
||||||
# followed by punctuation at a non-word-break. Close the quotes by brute force:
|
|
||||||
singleQuoteStartRe = r"^'(?=%s\B)" % punctClass
|
|
||||||
doubleQuoteStartRe = r'^"(?=%s\B)' % punctClass
|
|
||||||
|
|
||||||
# Special case for double sets of quotes, e.g.:
|
|
||||||
# <p>He said, "'Quoted' words in a larger quote."</p>
|
|
||||||
doubleQuoteSetsRe = r""""'(?=\w)"""
|
|
||||||
singleQuoteSetsRe = r"""'"(?=\w)"""
|
|
||||||
|
|
||||||
# Special case for decade abbreviations (the '80s):
|
|
||||||
decadeAbbrRe = r"(?<!\w)'(?=\d{2}s)"
|
|
||||||
|
|
||||||
# Get most opening double quotes:
|
|
||||||
openingDoubleQuotesRegex = r'%s"(?=\w)' % openingQuotesBase
|
|
||||||
|
|
||||||
# Double closing quotes:
|
|
||||||
closingDoubleQuotesRegex = r'"(?=\s)'
|
|
||||||
closingDoubleQuotesRegex2 = '(?<=%s)"' % closeClass
|
|
||||||
|
|
||||||
# Get most opening single quotes:
|
|
||||||
openingSingleQuotesRegex = r"%s'(?=\w)" % openingQuotesBase
|
|
||||||
|
|
||||||
# Single closing quotes:
|
|
||||||
closingSingleQuotesRegex = r"(?<=%s)'(?!\s|s\b|\d)" % closeClass
|
|
||||||
closingSingleQuotesRegex2 = r"'(\s|s\b)"
|
|
||||||
|
|
||||||
# All remaining quotes should be opening ones
|
|
||||||
remainingSingleQuotesRegex = r"'"
|
|
||||||
remainingDoubleQuotesRegex = r'"'
|
|
||||||
|
|
||||||
HTML_STRICT_RE = HTML_RE + r'(?!\>)'
|
|
||||||
|
|
||||||
|
|
||||||
class SubstituteTextPattern(HtmlInlineProcessor):
|
|
||||||
def __init__(self, pattern, replace, md):
|
|
||||||
""" Replaces matches with some text. """
|
|
||||||
HtmlInlineProcessor.__init__(self, pattern)
|
|
||||||
self.replace = replace
|
|
||||||
self.md = md
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
result = ''
|
|
||||||
for part in self.replace:
|
|
||||||
if isinstance(part, int):
|
|
||||||
result += m.group(part)
|
|
||||||
else:
|
|
||||||
result += self.md.htmlStash.store(part)
|
|
||||||
return result, m.start(0), m.end(0)
|
|
||||||
|
|
||||||
|
|
||||||
class SmartyExtension(Extension):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.config = {
|
|
||||||
'smart_quotes': [True, 'Educate quotes'],
|
|
||||||
'smart_angled_quotes': [False, 'Educate angled quotes'],
|
|
||||||
'smart_dashes': [True, 'Educate dashes'],
|
|
||||||
'smart_ellipses': [True, 'Educate ellipses'],
|
|
||||||
'substitutions': [{}, 'Overwrite default substitutions'],
|
|
||||||
}
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.substitutions = dict(substitutions)
|
|
||||||
self.substitutions.update(self.getConfig('substitutions', default={}))
|
|
||||||
|
|
||||||
def _addPatterns(self, md, patterns, serie, priority):
|
|
||||||
for ind, pattern in enumerate(patterns):
|
|
||||||
pattern += (md,)
|
|
||||||
pattern = SubstituteTextPattern(*pattern)
|
|
||||||
name = 'smarty-%s-%d' % (serie, ind)
|
|
||||||
self.inlinePatterns.register(pattern, name, priority-ind)
|
|
||||||
|
|
||||||
def educateDashes(self, md):
|
|
||||||
emDashesPattern = SubstituteTextPattern(
|
|
||||||
r'(?<!-)---(?!-)', (self.substitutions['mdash'],), md
|
|
||||||
)
|
|
||||||
enDashesPattern = SubstituteTextPattern(
|
|
||||||
r'(?<!-)--(?!-)', (self.substitutions['ndash'],), md
|
|
||||||
)
|
|
||||||
self.inlinePatterns.register(emDashesPattern, 'smarty-em-dashes', 50)
|
|
||||||
self.inlinePatterns.register(enDashesPattern, 'smarty-en-dashes', 45)
|
|
||||||
|
|
||||||
def educateEllipses(self, md):
|
|
||||||
ellipsesPattern = SubstituteTextPattern(
|
|
||||||
r'(?<!\.)\.{3}(?!\.)', (self.substitutions['ellipsis'],), md
|
|
||||||
)
|
|
||||||
self.inlinePatterns.register(ellipsesPattern, 'smarty-ellipses', 10)
|
|
||||||
|
|
||||||
def educateAngledQuotes(self, md):
|
|
||||||
leftAngledQuotePattern = SubstituteTextPattern(
|
|
||||||
r'\<\<', (self.substitutions['left-angle-quote'],), md
|
|
||||||
)
|
|
||||||
rightAngledQuotePattern = SubstituteTextPattern(
|
|
||||||
r'\>\>', (self.substitutions['right-angle-quote'],), md
|
|
||||||
)
|
|
||||||
self.inlinePatterns.register(leftAngledQuotePattern, 'smarty-left-angle-quotes', 40)
|
|
||||||
self.inlinePatterns.register(rightAngledQuotePattern, 'smarty-right-angle-quotes', 35)
|
|
||||||
|
|
||||||
def educateQuotes(self, md):
|
|
||||||
lsquo = self.substitutions['left-single-quote']
|
|
||||||
rsquo = self.substitutions['right-single-quote']
|
|
||||||
ldquo = self.substitutions['left-double-quote']
|
|
||||||
rdquo = self.substitutions['right-double-quote']
|
|
||||||
patterns = (
|
|
||||||
(singleQuoteStartRe, (rsquo,)),
|
|
||||||
(doubleQuoteStartRe, (rdquo,)),
|
|
||||||
(doubleQuoteSetsRe, (ldquo + lsquo,)),
|
|
||||||
(singleQuoteSetsRe, (lsquo + ldquo,)),
|
|
||||||
(decadeAbbrRe, (rsquo,)),
|
|
||||||
(openingSingleQuotesRegex, (1, lsquo)),
|
|
||||||
(closingSingleQuotesRegex, (rsquo,)),
|
|
||||||
(closingSingleQuotesRegex2, (rsquo, 1)),
|
|
||||||
(remainingSingleQuotesRegex, (lsquo,)),
|
|
||||||
(openingDoubleQuotesRegex, (1, ldquo)),
|
|
||||||
(closingDoubleQuotesRegex, (rdquo,)),
|
|
||||||
(closingDoubleQuotesRegex2, (rdquo,)),
|
|
||||||
(remainingDoubleQuotesRegex, (ldquo,))
|
|
||||||
)
|
|
||||||
self._addPatterns(md, patterns, 'quotes', 30)
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
configs = self.getConfigs()
|
|
||||||
self.inlinePatterns = Registry()
|
|
||||||
if configs['smart_ellipses']:
|
|
||||||
self.educateEllipses(md)
|
|
||||||
if configs['smart_quotes']:
|
|
||||||
self.educateQuotes(md)
|
|
||||||
if configs['smart_angled_quotes']:
|
|
||||||
self.educateAngledQuotes(md)
|
|
||||||
# Override `HTML_RE` from `inlinepatterns.py` so that it does not
|
|
||||||
# process tags with duplicate closing quotes.
|
|
||||||
md.inlinePatterns.register(HtmlInlineProcessor(HTML_STRICT_RE, md), 'html', 90)
|
|
||||||
if configs['smart_dashes']:
|
|
||||||
self.educateDashes(md)
|
|
||||||
inlineProcessor = InlineProcessor(md)
|
|
||||||
inlineProcessor.inlinePatterns = self.inlinePatterns
|
|
||||||
md.treeprocessors.register(inlineProcessor, 'smarty', 2)
|
|
||||||
md.ESCAPED_CHARS.extend(['"', "'"])
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return SmartyExtension(**kwargs)
|
|
|
@ -1,236 +0,0 @@
|
||||||
"""
|
|
||||||
Tables Extension for Python-Markdown
|
|
||||||
====================================
|
|
||||||
|
|
||||||
Added parsing of tables to Python-Markdown.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/tables>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2009 [Waylan Limberg](http://achinghead.com)
|
|
||||||
|
|
||||||
All changes Copyright 2008-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..blockprocessors import BlockProcessor
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
import re
|
|
||||||
PIPE_NONE = 0
|
|
||||||
PIPE_LEFT = 1
|
|
||||||
PIPE_RIGHT = 2
|
|
||||||
|
|
||||||
|
|
||||||
class TableProcessor(BlockProcessor):
|
|
||||||
""" Process Tables. """
|
|
||||||
|
|
||||||
RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`+)|(`+)|(\\\|)|(\|))')
|
|
||||||
RE_END_BORDER = re.compile(r'(?<!\\)(?:\\\\)*\|$')
|
|
||||||
|
|
||||||
def __init__(self, parser, config):
|
|
||||||
self.border = False
|
|
||||||
self.separator = ''
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
super().__init__(parser)
|
|
||||||
|
|
||||||
def test(self, parent, block):
|
|
||||||
"""
|
|
||||||
Ensure first two rows (column header and separator row) are valid table rows.
|
|
||||||
|
|
||||||
Keep border check and separator row do avoid repeating the work.
|
|
||||||
"""
|
|
||||||
is_table = False
|
|
||||||
rows = [row.strip(' ') for row in block.split('\n')]
|
|
||||||
if len(rows) > 1:
|
|
||||||
header0 = rows[0]
|
|
||||||
self.border = PIPE_NONE
|
|
||||||
if header0.startswith('|'):
|
|
||||||
self.border |= PIPE_LEFT
|
|
||||||
if self.RE_END_BORDER.search(header0) is not None:
|
|
||||||
self.border |= PIPE_RIGHT
|
|
||||||
row = self._split_row(header0)
|
|
||||||
row0_len = len(row)
|
|
||||||
is_table = row0_len > 1
|
|
||||||
|
|
||||||
# Each row in a single column table needs at least one pipe.
|
|
||||||
if not is_table and row0_len == 1 and self.border:
|
|
||||||
for index in range(1, len(rows)):
|
|
||||||
is_table = rows[index].startswith('|')
|
|
||||||
if not is_table:
|
|
||||||
is_table = self.RE_END_BORDER.search(rows[index]) is not None
|
|
||||||
if not is_table:
|
|
||||||
break
|
|
||||||
|
|
||||||
if is_table:
|
|
||||||
row = self._split_row(rows[1])
|
|
||||||
is_table = (len(row) == row0_len) and set(''.join(row)) <= set('|:- ')
|
|
||||||
if is_table:
|
|
||||||
self.separator = row
|
|
||||||
|
|
||||||
return is_table
|
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
|
||||||
""" Parse a table block and build table. """
|
|
||||||
block = blocks.pop(0).split('\n')
|
|
||||||
header = block[0].strip(' ')
|
|
||||||
rows = [] if len(block) < 3 else block[2:]
|
|
||||||
|
|
||||||
# Get alignment of columns
|
|
||||||
align = []
|
|
||||||
for c in self.separator:
|
|
||||||
c = c.strip(' ')
|
|
||||||
if c.startswith(':') and c.endswith(':'):
|
|
||||||
align.append('center')
|
|
||||||
elif c.startswith(':'):
|
|
||||||
align.append('left')
|
|
||||||
elif c.endswith(':'):
|
|
||||||
align.append('right')
|
|
||||||
else:
|
|
||||||
align.append(None)
|
|
||||||
|
|
||||||
# Build table
|
|
||||||
table = etree.SubElement(parent, 'table')
|
|
||||||
thead = etree.SubElement(table, 'thead')
|
|
||||||
self._build_row(header, thead, align)
|
|
||||||
tbody = etree.SubElement(table, 'tbody')
|
|
||||||
if len(rows) == 0:
|
|
||||||
# Handle empty table
|
|
||||||
self._build_empty_row(tbody, align)
|
|
||||||
else:
|
|
||||||
for row in rows:
|
|
||||||
self._build_row(row.strip(' '), tbody, align)
|
|
||||||
|
|
||||||
def _build_empty_row(self, parent, align):
|
|
||||||
"""Build an empty row."""
|
|
||||||
tr = etree.SubElement(parent, 'tr')
|
|
||||||
count = len(align)
|
|
||||||
while count:
|
|
||||||
etree.SubElement(tr, 'td')
|
|
||||||
count -= 1
|
|
||||||
|
|
||||||
def _build_row(self, row, parent, align):
|
|
||||||
""" Given a row of text, build table cells. """
|
|
||||||
tr = etree.SubElement(parent, 'tr')
|
|
||||||
tag = 'td'
|
|
||||||
if parent.tag == 'thead':
|
|
||||||
tag = 'th'
|
|
||||||
cells = self._split_row(row)
|
|
||||||
# We use align here rather than cells to ensure every row
|
|
||||||
# contains the same number of columns.
|
|
||||||
for i, a in enumerate(align):
|
|
||||||
c = etree.SubElement(tr, tag)
|
|
||||||
try:
|
|
||||||
c.text = cells[i].strip(' ')
|
|
||||||
except IndexError: # pragma: no cover
|
|
||||||
c.text = ""
|
|
||||||
if a:
|
|
||||||
if self.config['use_align_attribute']:
|
|
||||||
c.set('align', a)
|
|
||||||
else:
|
|
||||||
c.set('style', f'text-align: {a};')
|
|
||||||
|
|
||||||
def _split_row(self, row):
|
|
||||||
""" split a row of text into list of cells. """
|
|
||||||
if self.border:
|
|
||||||
if row.startswith('|'):
|
|
||||||
row = row[1:]
|
|
||||||
row = self.RE_END_BORDER.sub('', row)
|
|
||||||
return self._split(row)
|
|
||||||
|
|
||||||
def _split(self, row):
|
|
||||||
""" split a row of text with some code into a list of cells. """
|
|
||||||
elements = []
|
|
||||||
pipes = []
|
|
||||||
tics = []
|
|
||||||
tic_points = []
|
|
||||||
tic_region = []
|
|
||||||
good_pipes = []
|
|
||||||
|
|
||||||
# Parse row
|
|
||||||
# Throw out \\, and \|
|
|
||||||
for m in self.RE_CODE_PIPES.finditer(row):
|
|
||||||
# Store ` data (len, start_pos, end_pos)
|
|
||||||
if m.group(2):
|
|
||||||
# \`+
|
|
||||||
# Store length of each tic group: subtract \
|
|
||||||
tics.append(len(m.group(2)) - 1)
|
|
||||||
# Store start of group, end of group, and escape length
|
|
||||||
tic_points.append((m.start(2), m.end(2) - 1, 1))
|
|
||||||
elif m.group(3):
|
|
||||||
# `+
|
|
||||||
# Store length of each tic group
|
|
||||||
tics.append(len(m.group(3)))
|
|
||||||
# Store start of group, end of group, and escape length
|
|
||||||
tic_points.append((m.start(3), m.end(3) - 1, 0))
|
|
||||||
# Store pipe location
|
|
||||||
elif m.group(5):
|
|
||||||
pipes.append(m.start(5))
|
|
||||||
|
|
||||||
# Pair up tics according to size if possible
|
|
||||||
# Subtract the escape length *only* from the opening.
|
|
||||||
# Walk through tic list and see if tic has a close.
|
|
||||||
# Store the tic region (start of region, end of region).
|
|
||||||
pos = 0
|
|
||||||
tic_len = len(tics)
|
|
||||||
while pos < tic_len:
|
|
||||||
try:
|
|
||||||
tic_size = tics[pos] - tic_points[pos][2]
|
|
||||||
if tic_size == 0:
|
|
||||||
raise ValueError
|
|
||||||
index = tics[pos + 1:].index(tic_size) + 1
|
|
||||||
tic_region.append((tic_points[pos][0], tic_points[pos + index][1]))
|
|
||||||
pos += index + 1
|
|
||||||
except ValueError:
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
# Resolve pipes. Check if they are within a tic pair region.
|
|
||||||
# Walk through pipes comparing them to each region.
|
|
||||||
# - If pipe position is less that a region, it isn't in a region
|
|
||||||
# - If it is within a region, we don't want it, so throw it out
|
|
||||||
# - If we didn't throw it out, it must be a table pipe
|
|
||||||
for pipe in pipes:
|
|
||||||
throw_out = False
|
|
||||||
for region in tic_region:
|
|
||||||
if pipe < region[0]:
|
|
||||||
# Pipe is not in a region
|
|
||||||
break
|
|
||||||
elif region[0] <= pipe <= region[1]:
|
|
||||||
# Pipe is within a code region. Throw it out.
|
|
||||||
throw_out = True
|
|
||||||
break
|
|
||||||
if not throw_out:
|
|
||||||
good_pipes.append(pipe)
|
|
||||||
|
|
||||||
# Split row according to table delimiters.
|
|
||||||
pos = 0
|
|
||||||
for pipe in good_pipes:
|
|
||||||
elements.append(row[pos:pipe])
|
|
||||||
pos = pipe + 1
|
|
||||||
elements.append(row[pos:])
|
|
||||||
return elements
|
|
||||||
|
|
||||||
|
|
||||||
class TableExtension(Extension):
|
|
||||||
""" Add tables to Markdown. """
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.config = {
|
|
||||||
'use_align_attribute': [False, 'True to use align attribute instead of style.'],
|
|
||||||
}
|
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
""" Add an instance of `TableProcessor` to `BlockParser`. """
|
|
||||||
if '|' not in md.ESCAPED_CHARS:
|
|
||||||
md.ESCAPED_CHARS.append('|')
|
|
||||||
processor = TableProcessor(md.parser, self.getConfigs())
|
|
||||||
md.parser.blockprocessors.register(processor, 'table', 75)
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return TableExtension(**kwargs)
|
|
|
@ -1,379 +0,0 @@
|
||||||
"""
|
|
||||||
Table of Contents Extension for Python-Markdown
|
|
||||||
===============================================
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/toc>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright 2008 [Jack Miller](https://codezen.org/)
|
|
||||||
|
|
||||||
All changes Copyright 2008-2014 The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..treeprocessors import Treeprocessor
|
|
||||||
from ..util import code_escape, parseBoolValue, AMP_SUBSTITUTE, HTML_PLACEHOLDER_RE, AtomicString
|
|
||||||
from ..treeprocessors import UnescapeTreeprocessor
|
|
||||||
import re
|
|
||||||
import html
|
|
||||||
import unicodedata
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
|
|
||||||
|
|
||||||
def slugify(value, separator, unicode=False):
|
|
||||||
""" Slugify a string, to make it URL friendly. """
|
|
||||||
if not unicode:
|
|
||||||
# Replace Extended Latin characters with ASCII, i.e. `žlutý` => `zluty`
|
|
||||||
value = unicodedata.normalize('NFKD', value)
|
|
||||||
value = value.encode('ascii', 'ignore').decode('ascii')
|
|
||||||
value = re.sub(r'[^\w\s-]', '', value).strip().lower()
|
|
||||||
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, unicode=True)
|
|
||||||
|
|
||||||
|
|
||||||
IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$')
|
|
||||||
|
|
||||||
|
|
||||||
def unique(id, ids):
|
|
||||||
""" Ensure id is unique in set of ids. Append '_1', '_2'... if not """
|
|
||||||
while id in ids or not id:
|
|
||||||
m = IDCOUNT_RE.match(id)
|
|
||||||
if m:
|
|
||||||
id = '%s_%d' % (m.group(1), int(m.group(2))+1)
|
|
||||||
else:
|
|
||||||
id = '%s_%d' % (id, 1)
|
|
||||||
ids.add(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):
|
|
||||||
""" Extract raw HTML from stash, reduce to plain text and swap with placeholder. """
|
|
||||||
def _html_sub(m):
|
|
||||||
""" Substitute raw html with plain text. """
|
|
||||||
try:
|
|
||||||
raw = md.htmlStash.rawHtmlBlocks[int(m.group(1))]
|
|
||||||
except (IndexError, TypeError): # pragma: no cover
|
|
||||||
return m.group(0)
|
|
||||||
# Strip out tags and/or entities - leaving text
|
|
||||||
res = re.sub(r'(<[^>]+>)', '', raw)
|
|
||||||
if strip_entities:
|
|
||||||
res = re.sub(r'(&[\#a-zA-Z0-9]+;)', '', res)
|
|
||||||
return res
|
|
||||||
|
|
||||||
return HTML_PLACEHOLDER_RE.sub(_html_sub, text)
|
|
||||||
|
|
||||||
|
|
||||||
def unescape(text):
|
|
||||||
""" Unescape escaped text. """
|
|
||||||
c = UnescapeTreeprocessor()
|
|
||||||
return c.unescape(text)
|
|
||||||
|
|
||||||
|
|
||||||
def nest_toc_tokens(toc_list):
|
|
||||||
"""Given an unsorted list with errors and skips, return a nested one.
|
|
||||||
[{'level': 1}, {'level': 2}]
|
|
||||||
=>
|
|
||||||
[{'level': 1, 'children': [{'level': 2, 'children': []}]}]
|
|
||||||
|
|
||||||
A wrong list is also converted:
|
|
||||||
[{'level': 2}, {'level': 1}]
|
|
||||||
=>
|
|
||||||
[{'level': 2, 'children': []}, {'level': 1, 'children': []}]
|
|
||||||
"""
|
|
||||||
|
|
||||||
ordered_list = []
|
|
||||||
if len(toc_list):
|
|
||||||
# Initialize everything by processing the first entry
|
|
||||||
last = toc_list.pop(0)
|
|
||||||
last['children'] = []
|
|
||||||
levels = [last['level']]
|
|
||||||
ordered_list.append(last)
|
|
||||||
parents = []
|
|
||||||
|
|
||||||
# Walk the rest nesting the entries properly
|
|
||||||
while toc_list:
|
|
||||||
t = toc_list.pop(0)
|
|
||||||
current_level = t['level']
|
|
||||||
t['children'] = []
|
|
||||||
|
|
||||||
# Reduce depth if current level < last item's level
|
|
||||||
if current_level < levels[-1]:
|
|
||||||
# Pop last level since we know we are less than it
|
|
||||||
levels.pop()
|
|
||||||
|
|
||||||
# Pop parents and levels we are less than or equal to
|
|
||||||
to_pop = 0
|
|
||||||
for p in reversed(parents):
|
|
||||||
if current_level <= p['level']:
|
|
||||||
to_pop += 1
|
|
||||||
else: # pragma: no cover
|
|
||||||
break
|
|
||||||
if to_pop:
|
|
||||||
levels = levels[:-to_pop]
|
|
||||||
parents = parents[:-to_pop]
|
|
||||||
|
|
||||||
# Note current level as last
|
|
||||||
levels.append(current_level)
|
|
||||||
|
|
||||||
# Level is the same, so append to
|
|
||||||
# the current parent (if available)
|
|
||||||
if current_level == levels[-1]:
|
|
||||||
(parents[-1]['children'] if parents
|
|
||||||
else ordered_list).append(t)
|
|
||||||
|
|
||||||
# Current level is > last item's level,
|
|
||||||
# So make last item a parent and append current as child
|
|
||||||
else:
|
|
||||||
last['children'].append(t)
|
|
||||||
parents.append(last)
|
|
||||||
levels.append(current_level)
|
|
||||||
last = t
|
|
||||||
|
|
||||||
return ordered_list
|
|
||||||
|
|
||||||
|
|
||||||
class TocTreeprocessor(Treeprocessor):
|
|
||||||
def __init__(self, md, config):
|
|
||||||
super().__init__(md)
|
|
||||||
|
|
||||||
self.marker = config["marker"]
|
|
||||||
self.title = config["title"]
|
|
||||||
self.base_level = int(config["baselevel"]) - 1
|
|
||||||
self.slugify = config["slugify"]
|
|
||||||
self.sep = config["separator"]
|
|
||||||
self.toc_class = config["toc_class"]
|
|
||||||
self.use_anchors = parseBoolValue(config["anchorlink"])
|
|
||||||
self.anchorlink_class = config["anchorlink_class"]
|
|
||||||
self.use_permalinks = parseBoolValue(config["permalink"], False)
|
|
||||||
if self.use_permalinks is None:
|
|
||||||
self.use_permalinks = config["permalink"]
|
|
||||||
self.permalink_class = config["permalink_class"]
|
|
||||||
self.permalink_title = config["permalink_title"]
|
|
||||||
self.header_rgx = re.compile("[Hh][123456]")
|
|
||||||
if isinstance(config["toc_depth"], str) and '-' in config["toc_depth"]:
|
|
||||||
self.toc_top, self.toc_bottom = [int(x) for x in config["toc_depth"].split('-')]
|
|
||||||
else:
|
|
||||||
self.toc_top = 1
|
|
||||||
self.toc_bottom = int(config["toc_depth"])
|
|
||||||
|
|
||||||
def iterparent(self, node):
|
|
||||||
''' Iterator wrapper to get allowed parent and child all at once. '''
|
|
||||||
|
|
||||||
# We do not allow the marker inside a header as that
|
|
||||||
# would causes an endless loop of placing a new TOC
|
|
||||||
# inside previously generated TOC.
|
|
||||||
for child in node:
|
|
||||||
if not self.header_rgx.match(child.tag) and child.tag not in ['pre', 'code']:
|
|
||||||
yield node, child
|
|
||||||
yield from self.iterparent(child)
|
|
||||||
|
|
||||||
def replace_marker(self, root, elem):
|
|
||||||
''' Replace marker with elem. '''
|
|
||||||
for (p, c) in self.iterparent(root):
|
|
||||||
text = ''.join(c.itertext()).strip()
|
|
||||||
if not text:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# To keep the output from screwing up the
|
|
||||||
# validation by putting a `<div>` inside of a `<p>`
|
|
||||||
# we actually replace the `<p>` in its entirety.
|
|
||||||
|
|
||||||
# The `<p>` element may contain more than a single text content
|
|
||||||
# (`nl2br` can introduce a `<br>`). In this situation, `c.text` returns
|
|
||||||
# the very first content, ignore children contents or tail content.
|
|
||||||
# `len(c) == 0` is here to ensure there is only text in the `<p>`.
|
|
||||||
if c.text and c.text.strip() == self.marker and len(c) == 0:
|
|
||||||
for i in range(len(p)):
|
|
||||||
if p[i] == c:
|
|
||||||
p[i] = elem
|
|
||||||
break
|
|
||||||
|
|
||||||
def set_level(self, elem):
|
|
||||||
''' Adjust header level according to base level. '''
|
|
||||||
level = int(elem.tag[-1]) + self.base_level
|
|
||||||
if level > 6:
|
|
||||||
level = 6
|
|
||||||
elem.tag = 'h%d' % level
|
|
||||||
|
|
||||||
def add_anchor(self, c, elem_id):
|
|
||||||
anchor = etree.Element("a")
|
|
||||||
anchor.text = c.text
|
|
||||||
anchor.attrib["href"] = "#" + elem_id
|
|
||||||
anchor.attrib["class"] = self.anchorlink_class
|
|
||||||
c.text = ""
|
|
||||||
for elem in c:
|
|
||||||
anchor.append(elem)
|
|
||||||
while len(c):
|
|
||||||
c.remove(c[0])
|
|
||||||
c.append(anchor)
|
|
||||||
|
|
||||||
def add_permalink(self, c, elem_id):
|
|
||||||
permalink = etree.Element("a")
|
|
||||||
permalink.text = ("%spara;" % AMP_SUBSTITUTE
|
|
||||||
if self.use_permalinks is True
|
|
||||||
else self.use_permalinks)
|
|
||||||
permalink.attrib["href"] = "#" + elem_id
|
|
||||||
permalink.attrib["class"] = self.permalink_class
|
|
||||||
if self.permalink_title:
|
|
||||||
permalink.attrib["title"] = self.permalink_title
|
|
||||||
c.append(permalink)
|
|
||||||
|
|
||||||
def build_toc_div(self, toc_list):
|
|
||||||
""" Return a string div given a toc list. """
|
|
||||||
div = etree.Element("div")
|
|
||||||
div.attrib["class"] = self.toc_class
|
|
||||||
|
|
||||||
# Add title to the div
|
|
||||||
if self.title:
|
|
||||||
header = etree.SubElement(div, "span")
|
|
||||||
header.attrib["class"] = "toctitle"
|
|
||||||
header.text = self.title
|
|
||||||
|
|
||||||
def build_etree_ul(toc_list, parent):
|
|
||||||
ul = etree.SubElement(parent, "ul")
|
|
||||||
for item in toc_list:
|
|
||||||
# List item link, to be inserted into the toc div
|
|
||||||
li = etree.SubElement(ul, "li")
|
|
||||||
link = etree.SubElement(li, "a")
|
|
||||||
link.text = item.get('name', '')
|
|
||||||
link.attrib["href"] = '#' + item.get('id', '')
|
|
||||||
if item['children']:
|
|
||||||
build_etree_ul(item['children'], li)
|
|
||||||
return ul
|
|
||||||
|
|
||||||
build_etree_ul(toc_list, div)
|
|
||||||
|
|
||||||
if 'prettify' in self.md.treeprocessors:
|
|
||||||
self.md.treeprocessors['prettify'].run(div)
|
|
||||||
|
|
||||||
return div
|
|
||||||
|
|
||||||
def run(self, doc):
|
|
||||||
# Get a list of id attributes
|
|
||||||
used_ids = set()
|
|
||||||
for el in doc.iter():
|
|
||||||
if "id" in el.attrib:
|
|
||||||
used_ids.add(el.attrib["id"])
|
|
||||||
|
|
||||||
toc_tokens = []
|
|
||||||
for el in doc.iter():
|
|
||||||
if isinstance(el.tag, str) and self.header_rgx.match(el.tag):
|
|
||||||
self.set_level(el)
|
|
||||||
text = get_name(el)
|
|
||||||
|
|
||||||
# Do not override pre-existing ids
|
|
||||||
if "id" not in el.attrib:
|
|
||||||
innertext = unescape(stashedHTML2text(text, self.md))
|
|
||||||
el.attrib["id"] = unique(self.slugify(innertext, self.sep), used_ids)
|
|
||||||
|
|
||||||
if int(el.tag[-1]) >= self.toc_top and int(el.tag[-1]) <= self.toc_bottom:
|
|
||||||
toc_tokens.append({
|
|
||||||
'level': int(el.tag[-1]),
|
|
||||||
'id': el.attrib["id"],
|
|
||||||
'name': unescape(stashedHTML2text(
|
|
||||||
code_escape(el.attrib.get('data-toc-label', text)),
|
|
||||||
self.md, strip_entities=False
|
|
||||||
))
|
|
||||||
})
|
|
||||||
|
|
||||||
# Remove the data-toc-label attribute as it is no longer needed
|
|
||||||
if 'data-toc-label' in el.attrib:
|
|
||||||
del el.attrib['data-toc-label']
|
|
||||||
|
|
||||||
if self.use_anchors:
|
|
||||||
self.add_anchor(el, el.attrib["id"])
|
|
||||||
if self.use_permalinks not in [False, None]:
|
|
||||||
self.add_permalink(el, el.attrib["id"])
|
|
||||||
|
|
||||||
toc_tokens = nest_toc_tokens(toc_tokens)
|
|
||||||
div = self.build_toc_div(toc_tokens)
|
|
||||||
if self.marker:
|
|
||||||
self.replace_marker(doc, div)
|
|
||||||
|
|
||||||
# serialize and attach to markdown instance.
|
|
||||||
toc = self.md.serializer(div)
|
|
||||||
for pp in self.md.postprocessors:
|
|
||||||
toc = pp.run(toc)
|
|
||||||
self.md.toc_tokens = toc_tokens
|
|
||||||
self.md.toc = toc
|
|
||||||
|
|
||||||
|
|
||||||
class TocExtension(Extension):
|
|
||||||
|
|
||||||
TreeProcessorClass = TocTreeprocessor
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.config = {
|
|
||||||
"marker": ['[TOC]',
|
|
||||||
'Text to find and replace with Table of Contents - '
|
|
||||||
'Set to an empty string to disable. Defaults to "[TOC]"'],
|
|
||||||
"title": ["",
|
|
||||||
"Title to insert into TOC <div> - "
|
|
||||||
"Defaults to an empty string"],
|
|
||||||
"toc_class": ['toc',
|
|
||||||
'CSS class(es) used for the link. '
|
|
||||||
'Defaults to "toclink"'],
|
|
||||||
"anchorlink": [False,
|
|
||||||
"True if header should be a self link - "
|
|
||||||
"Defaults to False"],
|
|
||||||
"anchorlink_class": ['toclink',
|
|
||||||
'CSS class(es) used for the link. '
|
|
||||||
'Defaults to "toclink"'],
|
|
||||||
"permalink": [0,
|
|
||||||
"True or link text if a Sphinx-style permalink should "
|
|
||||||
"be added - Defaults to False"],
|
|
||||||
"permalink_class": ['headerlink',
|
|
||||||
'CSS class(es) used for the link. '
|
|
||||||
'Defaults to "headerlink"'],
|
|
||||||
"permalink_title": ["Permanent link",
|
|
||||||
"Title attribute of the permalink - "
|
|
||||||
"Defaults to 'Permanent link'"],
|
|
||||||
"baselevel": ['1', 'Base level for headers.'],
|
|
||||||
"slugify": [slugify,
|
|
||||||
"Function to generate anchors based on header text - "
|
|
||||||
"Defaults to the headerid ext's slugify function."],
|
|
||||||
'separator': ['-', 'Word separator. Defaults to "-".'],
|
|
||||||
"toc_depth": [6,
|
|
||||||
'Define the range of section levels to include in'
|
|
||||||
'the Table of Contents. A single integer (b) defines'
|
|
||||||
'the bottom section level (<h1>..<hb>) only.'
|
|
||||||
'A string consisting of two digits separated by a hyphen'
|
|
||||||
'in between ("2-5"), define the top (t) and the'
|
|
||||||
'bottom (b) (<ht>..<hb>). Defaults to `6` (bottom).'],
|
|
||||||
}
|
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
md.registerExtension(self)
|
|
||||||
self.md = md
|
|
||||||
self.reset()
|
|
||||||
tocext = self.TreeProcessorClass(md, self.getConfigs())
|
|
||||||
md.treeprocessors.register(tocext, 'toc', 5)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.md.toc = ''
|
|
||||||
self.md.toc_tokens = []
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return TocExtension(**kwargs)
|
|
|
@ -1,87 +0,0 @@
|
||||||
'''
|
|
||||||
WikiLinks Extension for Python-Markdown
|
|
||||||
======================================
|
|
||||||
|
|
||||||
Converts [[WikiLinks]] to relative links.
|
|
||||||
|
|
||||||
See <https://Python-Markdown.github.io/extensions/wikilinks>
|
|
||||||
for documentation.
|
|
||||||
|
|
||||||
Original code Copyright [Waylan Limberg](http://achinghead.com/).
|
|
||||||
|
|
||||||
All changes Copyright The Python Markdown Project
|
|
||||||
|
|
||||||
License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
from . import Extension
|
|
||||||
from ..inlinepatterns import InlineProcessor
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def build_url(label, base, end):
|
|
||||||
""" Build a URL from the label, a base, and an end. """
|
|
||||||
clean_label = re.sub(r'([ ]+_)|(_[ ]+)|([ ]+)', '_', label)
|
|
||||||
return '{}{}{}'.format(base, clean_label, end)
|
|
||||||
|
|
||||||
|
|
||||||
class WikiLinkExtension(Extension):
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.config = {
|
|
||||||
'base_url': ['/', 'String to append to beginning or URL.'],
|
|
||||||
'end_url': ['/', 'String to append to end of URL.'],
|
|
||||||
'html_class': ['wikilink', 'CSS hook. Leave blank for none.'],
|
|
||||||
'build_url': [build_url, 'Callable formats URL from label.'],
|
|
||||||
}
|
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
def extendMarkdown(self, md):
|
|
||||||
self.md = md
|
|
||||||
|
|
||||||
# append to end of inline patterns
|
|
||||||
WIKILINK_RE = r'\[\[([\w0-9_ -]+)\]\]'
|
|
||||||
wikilinkPattern = WikiLinksInlineProcessor(WIKILINK_RE, self.getConfigs())
|
|
||||||
wikilinkPattern.md = md
|
|
||||||
md.inlinePatterns.register(wikilinkPattern, 'wikilink', 75)
|
|
||||||
|
|
||||||
|
|
||||||
class WikiLinksInlineProcessor(InlineProcessor):
|
|
||||||
def __init__(self, pattern, config):
|
|
||||||
super().__init__(pattern)
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
if m.group(1).strip():
|
|
||||||
base_url, end_url, html_class = self._getMeta()
|
|
||||||
label = m.group(1).strip()
|
|
||||||
url = self.config['build_url'](label, base_url, end_url)
|
|
||||||
a = etree.Element('a')
|
|
||||||
a.text = label
|
|
||||||
a.set('href', url)
|
|
||||||
if html_class:
|
|
||||||
a.set('class', html_class)
|
|
||||||
else:
|
|
||||||
a = ''
|
|
||||||
return a, m.start(0), m.end(0)
|
|
||||||
|
|
||||||
def _getMeta(self):
|
|
||||||
""" Return meta data or `config` data. """
|
|
||||||
base_url = self.config['base_url']
|
|
||||||
end_url = self.config['end_url']
|
|
||||||
html_class = self.config['html_class']
|
|
||||||
if hasattr(self.md, 'Meta'):
|
|
||||||
if 'wiki_base_url' in self.md.Meta:
|
|
||||||
base_url = self.md.Meta['wiki_base_url'][0]
|
|
||||||
if 'wiki_end_url' in self.md.Meta:
|
|
||||||
end_url = self.md.Meta['wiki_end_url'][0]
|
|
||||||
if 'wiki_html_class' in self.md.Meta:
|
|
||||||
html_class = self.md.Meta['wiki_html_class'][0]
|
|
||||||
return base_url, end_url, html_class
|
|
||||||
|
|
||||||
|
|
||||||
def makeExtension(**kwargs): # pragma: no cover
|
|
||||||
return WikiLinkExtension(**kwargs)
|
|
|
@ -1,323 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2020 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import importlib.util
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
# Import a copy of the html.parser lib as `htmlparser` so we can monkeypatch it.
|
|
||||||
# Users can still do `from html import parser` and get the default behavior.
|
|
||||||
spec = importlib.util.find_spec('html.parser')
|
|
||||||
htmlparser = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(htmlparser)
|
|
||||||
sys.modules['htmlparser'] = htmlparser
|
|
||||||
|
|
||||||
# Monkeypatch `HTMLParser` to only accept `?>` to close Processing Instructions.
|
|
||||||
htmlparser.piclose = re.compile(r'\?>')
|
|
||||||
# Monkeypatch `HTMLParser` to only recognize entity references with a closing semicolon.
|
|
||||||
htmlparser.entityref = re.compile(r'&([a-zA-Z][-.a-zA-Z0-9]*);')
|
|
||||||
# Monkeypatch `HTMLParser` to no longer support partial entities. We are always feeding a complete block,
|
|
||||||
# so the 'incomplete' functionality is unnecessary. As the `entityref` regex is run right before incomplete,
|
|
||||||
# and the two regex are the same, then incomplete will simply never match and we avoid the logic within.
|
|
||||||
htmlparser.incomplete = htmlparser.entityref
|
|
||||||
# Monkeypatch `HTMLParser` to not accept a backtick in a tag name, attribute name, or bare value.
|
|
||||||
htmlparser.locatestarttagend_tolerant = re.compile(r"""
|
|
||||||
<[a-zA-Z][^`\t\n\r\f />\x00]* # tag name <= added backtick here
|
|
||||||
(?:[\s/]* # optional whitespace before attribute name
|
|
||||||
(?:(?<=['"\s/])[^`\s/>][^\s/=>]* # attribute name <= added backtick here
|
|
||||||
(?:\s*=+\s* # value indicator
|
|
||||||
(?:'[^']*' # LITA-enclosed value
|
|
||||||
|"[^"]*" # LIT-enclosed value
|
|
||||||
|(?!['"])[^`>\s]* # bare value <= added backtick here
|
|
||||||
)
|
|
||||||
(?:\s*,)* # possibly followed by a comma
|
|
||||||
)?(?:\s|/(?!>))*
|
|
||||||
)*
|
|
||||||
)?
|
|
||||||
\s* # trailing whitespace
|
|
||||||
""", re.VERBOSE)
|
|
||||||
|
|
||||||
# Match a blank line at the start of a block of text (two newlines).
|
|
||||||
# The newlines may be preceded by additional whitespace.
|
|
||||||
blank_line_re = re.compile(r'^([ ]*\n){2}')
|
|
||||||
|
|
||||||
|
|
||||||
class HTMLExtractor(htmlparser.HTMLParser):
|
|
||||||
"""
|
|
||||||
Extract raw HTML from text.
|
|
||||||
|
|
||||||
The raw HTML is stored in the `htmlStash` of the Markdown instance passed
|
|
||||||
to `md` and the remaining text is stored in `cleandoc` as a list of strings.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, md, *args, **kwargs):
|
|
||||||
if 'convert_charrefs' not in kwargs:
|
|
||||||
kwargs['convert_charrefs'] = False
|
|
||||||
|
|
||||||
# Block tags that should contain no content (self closing)
|
|
||||||
self.empty_tags = set(['hr'])
|
|
||||||
|
|
||||||
# This calls self.reset
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.md = md
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""Reset this instance. Loses all unprocessed data."""
|
|
||||||
self.inraw = False
|
|
||||||
self.intail = False
|
|
||||||
self.stack = [] # When `inraw==True`, stack contains a list of tags
|
|
||||||
self._cache = []
|
|
||||||
self.cleandoc = []
|
|
||||||
super().reset()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Handle any buffered data."""
|
|
||||||
super().close()
|
|
||||||
if len(self.rawdata):
|
|
||||||
# Temp fix for https://bugs.python.org/issue41989
|
|
||||||
# TODO: remove this when the bug is fixed in all supported Python versions.
|
|
||||||
if self.convert_charrefs and not self.cdata_elem: # pragma: no cover
|
|
||||||
self.handle_data(htmlparser.unescape(self.rawdata))
|
|
||||||
else:
|
|
||||||
self.handle_data(self.rawdata)
|
|
||||||
# Handle any unclosed tags.
|
|
||||||
if len(self._cache):
|
|
||||||
self.cleandoc.append(self.md.htmlStash.store(''.join(self._cache)))
|
|
||||||
self._cache = []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def line_offset(self):
|
|
||||||
"""Returns char index in `self.rawdata` for the start of the current line. """
|
|
||||||
if self.lineno > 1 and '\n' in self.rawdata:
|
|
||||||
m = re.match(r'([^\n]*\n){{{}}}'.format(self.lineno-1), self.rawdata)
|
|
||||||
if m:
|
|
||||||
return m.end()
|
|
||||||
else: # pragma: no cover
|
|
||||||
# Value of `self.lineno` must exceed total number of lines.
|
|
||||||
# Find index of beginning of last line.
|
|
||||||
return self.rawdata.rfind('\n')
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def at_line_start(self):
|
|
||||||
"""
|
|
||||||
Returns True if current position is at start of line.
|
|
||||||
|
|
||||||
Allows for up to three blank spaces at start of line.
|
|
||||||
"""
|
|
||||||
if self.offset == 0:
|
|
||||||
return True
|
|
||||||
if self.offset > 3:
|
|
||||||
return False
|
|
||||||
# Confirm up to first 3 chars are whitespace
|
|
||||||
return self.rawdata[self.line_offset:self.line_offset + self.offset].strip() == ''
|
|
||||||
|
|
||||||
def get_endtag_text(self, tag):
|
|
||||||
"""
|
|
||||||
Returns the text of the end tag.
|
|
||||||
|
|
||||||
If it fails to extract the actual text from the raw data, it builds a closing tag with `tag`.
|
|
||||||
"""
|
|
||||||
# Attempt to extract actual tag from raw source text
|
|
||||||
start = self.line_offset + self.offset
|
|
||||||
m = htmlparser.endendtag.search(self.rawdata, start)
|
|
||||||
if m:
|
|
||||||
return self.rawdata[start:m.end()]
|
|
||||||
else: # pragma: no cover
|
|
||||||
# Failed to extract from raw data. Assume well formed and lowercase.
|
|
||||||
return '</{}>'.format(tag)
|
|
||||||
|
|
||||||
def handle_starttag(self, tag, attrs):
|
|
||||||
# Handle tags that should always be empty and do not specify a closing tag
|
|
||||||
if tag in self.empty_tags:
|
|
||||||
self.handle_startendtag(tag, attrs)
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.md.is_block_level(tag) and (self.intail or (self.at_line_start() and not self.inraw)):
|
|
||||||
# Started a new raw block. Prepare stack.
|
|
||||||
self.inraw = True
|
|
||||||
self.cleandoc.append('\n')
|
|
||||||
|
|
||||||
text = self.get_starttag_text()
|
|
||||||
if self.inraw:
|
|
||||||
self.stack.append(tag)
|
|
||||||
self._cache.append(text)
|
|
||||||
else:
|
|
||||||
self.cleandoc.append(text)
|
|
||||||
if tag in self.CDATA_CONTENT_ELEMENTS:
|
|
||||||
# This is presumably a standalone tag in a code span (see #1036).
|
|
||||||
self.clear_cdata_mode()
|
|
||||||
|
|
||||||
def handle_endtag(self, tag):
|
|
||||||
text = self.get_endtag_text(tag)
|
|
||||||
|
|
||||||
if self.inraw:
|
|
||||||
self._cache.append(text)
|
|
||||||
if tag in self.stack:
|
|
||||||
# Remove tag from stack
|
|
||||||
while self.stack:
|
|
||||||
if self.stack.pop() == tag:
|
|
||||||
break
|
|
||||||
if len(self.stack) == 0:
|
|
||||||
# End of raw block.
|
|
||||||
if blank_line_re.match(self.rawdata[self.line_offset + self.offset + len(text):]):
|
|
||||||
# Preserve blank line and end of raw block.
|
|
||||||
self._cache.append('\n')
|
|
||||||
else:
|
|
||||||
# More content exists after `endtag`.
|
|
||||||
self.intail = True
|
|
||||||
# Reset stack.
|
|
||||||
self.inraw = False
|
|
||||||
self.cleandoc.append(self.md.htmlStash.store(''.join(self._cache)))
|
|
||||||
# Insert blank line between this and next line.
|
|
||||||
self.cleandoc.append('\n\n')
|
|
||||||
self._cache = []
|
|
||||||
else:
|
|
||||||
self.cleandoc.append(text)
|
|
||||||
|
|
||||||
def handle_data(self, data):
|
|
||||||
if self.intail and '\n' in data:
|
|
||||||
self.intail = False
|
|
||||||
if self.inraw:
|
|
||||||
self._cache.append(data)
|
|
||||||
else:
|
|
||||||
self.cleandoc.append(data)
|
|
||||||
|
|
||||||
def handle_empty_tag(self, data, is_block):
|
|
||||||
""" Handle empty tags (`<data>`). """
|
|
||||||
if self.inraw or self.intail:
|
|
||||||
# Append this to the existing raw block
|
|
||||||
self._cache.append(data)
|
|
||||||
elif self.at_line_start() and is_block:
|
|
||||||
# Handle this as a standalone raw block
|
|
||||||
if blank_line_re.match(self.rawdata[self.line_offset + self.offset + len(data):]):
|
|
||||||
# Preserve blank line after tag in raw block.
|
|
||||||
data += '\n'
|
|
||||||
else:
|
|
||||||
# More content exists after tag.
|
|
||||||
self.intail = True
|
|
||||||
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(data))
|
|
||||||
# Insert blank line between this and next line.
|
|
||||||
self.cleandoc.append('\n\n')
|
|
||||||
else:
|
|
||||||
self.cleandoc.append(data)
|
|
||||||
|
|
||||||
def handle_startendtag(self, tag, attrs):
|
|
||||||
self.handle_empty_tag(self.get_starttag_text(), is_block=self.md.is_block_level(tag))
|
|
||||||
|
|
||||||
def handle_charref(self, name):
|
|
||||||
self.handle_empty_tag('&#{};'.format(name), is_block=False)
|
|
||||||
|
|
||||||
def handle_entityref(self, name):
|
|
||||||
self.handle_empty_tag('&{};'.format(name), is_block=False)
|
|
||||||
|
|
||||||
def handle_comment(self, data):
|
|
||||||
self.handle_empty_tag('<!--{}-->'.format(data), is_block=True)
|
|
||||||
|
|
||||||
def handle_decl(self, data):
|
|
||||||
self.handle_empty_tag('<!{}>'.format(data), is_block=True)
|
|
||||||
|
|
||||||
def handle_pi(self, data):
|
|
||||||
self.handle_empty_tag('<?{}?>'.format(data), is_block=True)
|
|
||||||
|
|
||||||
def unknown_decl(self, data):
|
|
||||||
end = ']]>' if data.startswith('CDATA[') else ']>'
|
|
||||||
self.handle_empty_tag('<![{}{}'.format(data, end), is_block=True)
|
|
||||||
|
|
||||||
def parse_pi(self, i):
|
|
||||||
if self.at_line_start() or self.intail:
|
|
||||||
return super().parse_pi(i)
|
|
||||||
# This is not the beginning of a raw block so treat as plain data
|
|
||||||
# and avoid consuming any tags which may follow (see #1066).
|
|
||||||
self.handle_data('<?')
|
|
||||||
return i + 2
|
|
||||||
|
|
||||||
def parse_html_declaration(self, i):
|
|
||||||
if self.at_line_start() or self.intail:
|
|
||||||
return super().parse_html_declaration(i)
|
|
||||||
# This is not the beginning of a raw block so treat as plain data
|
|
||||||
# and avoid consuming any tags which may follow (see #1066).
|
|
||||||
self.handle_data('<!')
|
|
||||||
return i + 2
|
|
||||||
|
|
||||||
# The rest has been copied from base class in standard lib to address #1036.
|
|
||||||
# As `__startag_text` is private, all references to it must be in this subclass.
|
|
||||||
# The last few lines of `parse_starttag` are reversed so that `handle_starttag`
|
|
||||||
# can override `cdata_mode` in certain situations (in a code span).
|
|
||||||
__starttag_text = None
|
|
||||||
|
|
||||||
def get_starttag_text(self):
|
|
||||||
"""Return full source of start tag: `<...>`."""
|
|
||||||
return self.__starttag_text
|
|
||||||
|
|
||||||
def parse_starttag(self, i): # pragma: no cover
|
|
||||||
self.__starttag_text = None
|
|
||||||
endpos = self.check_for_whole_start_tag(i)
|
|
||||||
if endpos < 0:
|
|
||||||
return endpos
|
|
||||||
rawdata = self.rawdata
|
|
||||||
self.__starttag_text = rawdata[i:endpos]
|
|
||||||
|
|
||||||
# Now parse the data between `i+1` and `j` into a tag and `attrs`
|
|
||||||
attrs = []
|
|
||||||
match = htmlparser.tagfind_tolerant.match(rawdata, i+1)
|
|
||||||
assert match, 'unexpected call to parse_starttag()'
|
|
||||||
k = match.end()
|
|
||||||
self.lasttag = tag = match.group(1).lower()
|
|
||||||
while k < endpos:
|
|
||||||
m = htmlparser.attrfind_tolerant.match(rawdata, k)
|
|
||||||
if not m:
|
|
||||||
break
|
|
||||||
attrname, rest, attrvalue = m.group(1, 2, 3)
|
|
||||||
if not rest:
|
|
||||||
attrvalue = None
|
|
||||||
elif attrvalue[:1] == '\'' == attrvalue[-1:] or \
|
|
||||||
attrvalue[:1] == '"' == attrvalue[-1:]: # noqa: E127
|
|
||||||
attrvalue = attrvalue[1:-1]
|
|
||||||
if attrvalue:
|
|
||||||
attrvalue = htmlparser.unescape(attrvalue)
|
|
||||||
attrs.append((attrname.lower(), attrvalue))
|
|
||||||
k = m.end()
|
|
||||||
|
|
||||||
end = rawdata[k:endpos].strip()
|
|
||||||
if end not in (">", "/>"):
|
|
||||||
lineno, offset = self.getpos()
|
|
||||||
if "\n" in self.__starttag_text:
|
|
||||||
lineno = lineno + self.__starttag_text.count("\n")
|
|
||||||
offset = len(self.__starttag_text) \
|
|
||||||
- self.__starttag_text.rfind("\n") # noqa: E127
|
|
||||||
else:
|
|
||||||
offset = offset + len(self.__starttag_text)
|
|
||||||
self.handle_data(rawdata[i:endpos])
|
|
||||||
return endpos
|
|
||||||
if end.endswith('/>'):
|
|
||||||
# XHTML-style empty tag: `<span attr="value" />`
|
|
||||||
self.handle_startendtag(tag, attrs)
|
|
||||||
else:
|
|
||||||
# *** set `cdata_mode` first so we can override it in `handle_starttag` (see #1036) ***
|
|
||||||
if tag in self.CDATA_CONTENT_ELEMENTS:
|
|
||||||
self.set_cdata_mode(tag)
|
|
||||||
self.handle_starttag(tag, attrs)
|
|
||||||
return endpos
|
|
|
@ -1,898 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
|
|
||||||
INLINE PATTERNS
|
|
||||||
=============================================================================
|
|
||||||
|
|
||||||
Inline patterns such as *emphasis* are handled by means of auxiliary
|
|
||||||
objects, one per pattern. Pattern objects must be instances of classes
|
|
||||||
that extend markdown.Pattern. Each pattern object uses a single regular
|
|
||||||
expression and needs support the following methods:
|
|
||||||
|
|
||||||
pattern.getCompiledRegExp() # returns a regular expression
|
|
||||||
|
|
||||||
pattern.handleMatch(m) # takes a match object and returns
|
|
||||||
# an ElementTree element or just plain text
|
|
||||||
|
|
||||||
All of python markdown's built-in patterns subclass from Pattern,
|
|
||||||
but you can add additional patterns that don't.
|
|
||||||
|
|
||||||
Also note that all the regular expressions used by inline must
|
|
||||||
capture the whole block. For this reason, they all start with
|
|
||||||
`^(.*)` and end with `(.*)!`. In case with built-in expression
|
|
||||||
Pattern takes care of adding the `^(.*)` and `(.*)!`.
|
|
||||||
|
|
||||||
Finally, the order in which regular expressions are applied is very
|
|
||||||
important - e.g. if we first replace `http://.../` links with `<a>` tags
|
|
||||||
and _then_ try to replace inline html, we would end up with a mess.
|
|
||||||
So, we apply the expressions in the following order:
|
|
||||||
|
|
||||||
* escape and backticks have to go before everything else, so
|
|
||||||
that we can preempt any markdown patterns by escaping them.
|
|
||||||
|
|
||||||
* then we handle auto-links (must be done before inline html)
|
|
||||||
|
|
||||||
* then we handle inline HTML. At this point we will simply
|
|
||||||
replace all inline HTML strings with a placeholder and add
|
|
||||||
the actual HTML to a hash.
|
|
||||||
|
|
||||||
* then inline images (must be done before links)
|
|
||||||
|
|
||||||
* then bracketed links, first regular then reference-style
|
|
||||||
|
|
||||||
* finally we apply strong and emphasis
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import util
|
|
||||||
from collections import namedtuple
|
|
||||||
import re
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
try: # pragma: no cover
|
|
||||||
from html import entities
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
import htmlentitydefs as entities
|
|
||||||
|
|
||||||
|
|
||||||
def build_inlinepatterns(md, **kwargs):
|
|
||||||
""" Build the default set of inline patterns for Markdown. """
|
|
||||||
inlinePatterns = util.Registry()
|
|
||||||
inlinePatterns.register(BacktickInlineProcessor(BACKTICK_RE), 'backtick', 190)
|
|
||||||
inlinePatterns.register(EscapeInlineProcessor(ESCAPE_RE, md), 'escape', 180)
|
|
||||||
inlinePatterns.register(ReferenceInlineProcessor(REFERENCE_RE, md), 'reference', 170)
|
|
||||||
inlinePatterns.register(LinkInlineProcessor(LINK_RE, md), 'link', 160)
|
|
||||||
inlinePatterns.register(ImageInlineProcessor(IMAGE_LINK_RE, md), 'image_link', 150)
|
|
||||||
inlinePatterns.register(
|
|
||||||
ImageReferenceInlineProcessor(IMAGE_REFERENCE_RE, md), 'image_reference', 140
|
|
||||||
)
|
|
||||||
inlinePatterns.register(
|
|
||||||
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(AutomailInlineProcessor(AUTOMAIL_RE, md), 'automail', 110)
|
|
||||||
inlinePatterns.register(SubstituteTagInlineProcessor(LINE_BREAK_RE, 'br'), 'linebreak', 100)
|
|
||||||
inlinePatterns.register(HtmlInlineProcessor(HTML_RE, md), 'html', 90)
|
|
||||||
inlinePatterns.register(HtmlInlineProcessor(ENTITY_RE, md), 'entity', 80)
|
|
||||||
inlinePatterns.register(SimpleTextInlineProcessor(NOT_STRONG_RE), 'not_strong', 70)
|
|
||||||
inlinePatterns.register(AsteriskProcessor(r'\*'), 'em_strong', 60)
|
|
||||||
inlinePatterns.register(UnderscoreProcessor(r'_'), 'em_strong2', 50)
|
|
||||||
return inlinePatterns
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
The actual regular expressions for patterns
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
"""
|
|
||||||
|
|
||||||
NOIMG = r'(?<!\!)'
|
|
||||||
|
|
||||||
# `e=f()` or ``e=f("`")``
|
|
||||||
BACKTICK_RE = r'(?:(?<!\\)((?:\\{2})+)(?=`+)|(?<!\\)(`+)(.+?)(?<!`)\2(?!`))'
|
|
||||||
|
|
||||||
# `\<`
|
|
||||||
ESCAPE_RE = r'\\(.)'
|
|
||||||
|
|
||||||
# `*emphasis*`
|
|
||||||
EMPHASIS_RE = r'(\*)([^\*]+)\1'
|
|
||||||
|
|
||||||
# `**strong**`
|
|
||||||
STRONG_RE = r'(\*{2})(.+?)\1'
|
|
||||||
|
|
||||||
# `__smart__strong__`
|
|
||||||
SMART_STRONG_RE = r'(?<!\w)(_{2})(?!_)(.+?)(?<!_)\1(?!\w)'
|
|
||||||
|
|
||||||
# `_smart_emphasis_`
|
|
||||||
SMART_EMPHASIS_RE = r'(?<!\w)(_)(?!_)(.+?)(?<!_)\1(?!\w)'
|
|
||||||
|
|
||||||
# `__strong _em__`
|
|
||||||
SMART_STRONG_EM_RE = r'(?<!\w)(\_)\1(?!\1)(.+?)(?<!\w)\1(?!\1)(.+?)\1{3}(?!\w)'
|
|
||||||
|
|
||||||
# `***strongem***` or `***em*strong**`
|
|
||||||
EM_STRONG_RE = r'(\*)\1{2}(.+?)\1(.*?)\1{2}'
|
|
||||||
|
|
||||||
# `___strongem___` or `___em_strong__`
|
|
||||||
EM_STRONG2_RE = r'(_)\1{2}(.+?)\1(.*?)\1{2}'
|
|
||||||
|
|
||||||
# `***strong**em*`
|
|
||||||
STRONG_EM_RE = r'(\*)\1{2}(.+?)\1{2}(.*?)\1'
|
|
||||||
|
|
||||||
# `___strong__em_`
|
|
||||||
STRONG_EM2_RE = r'(_)\1{2}(.+?)\1{2}(.*?)\1'
|
|
||||||
|
|
||||||
# `**strong*em***`
|
|
||||||
STRONG_EM3_RE = r'(\*)\1(?!\1)([^*]+?)\1(?!\1)(.+?)\1{3}'
|
|
||||||
|
|
||||||
# `[text](url)` or `[text](<url>)` or `[text](url "title")`
|
|
||||||
LINK_RE = NOIMG + r'\['
|
|
||||||
|
|
||||||
# `![alttxt](http://x.com/)` or `![alttxt](<http://x.com/>)`
|
|
||||||
IMAGE_LINK_RE = r'\!\['
|
|
||||||
|
|
||||||
# `[Google][3]`
|
|
||||||
REFERENCE_RE = LINK_RE
|
|
||||||
|
|
||||||
# `![alt text][2]`
|
|
||||||
IMAGE_REFERENCE_RE = IMAGE_LINK_RE
|
|
||||||
|
|
||||||
# stand-alone `*` or `_`
|
|
||||||
NOT_STRONG_RE = r'((^|(?<=\s))(\*{1,3}|_{1,3})(?=\s|$))'
|
|
||||||
|
|
||||||
# `<http://www.123.com>`
|
|
||||||
AUTOLINK_RE = r'<((?:[Ff]|[Hh][Tt])[Tt][Pp][Ss]?://[^<>]*)>'
|
|
||||||
|
|
||||||
# `<me@example.com>`
|
|
||||||
AUTOMAIL_RE = r'<([^<> !]+@[^@<> ]+)>'
|
|
||||||
|
|
||||||
# `<...>`
|
|
||||||
HTML_RE = r'(<(\/?[a-zA-Z][^<>@ ]*( [^<>]*)?|!--(?:(?!<!--|-->).)*--)>)'
|
|
||||||
|
|
||||||
# `&` (decimal) or `&` (hex) or `&` (named)
|
|
||||||
ENTITY_RE = r'(&(?:\#[0-9]+|\#x[0-9a-fA-F]+|[a-zA-Z0-9]+);)'
|
|
||||||
|
|
||||||
# two spaces at end of line
|
|
||||||
LINE_BREAK_RE = r' \n'
|
|
||||||
|
|
||||||
|
|
||||||
def dequote(string):
|
|
||||||
"""Remove quotes from around a string."""
|
|
||||||
if ((string.startswith('"') and string.endswith('"')) or
|
|
||||||
(string.startswith("'") and string.endswith("'"))):
|
|
||||||
return string[1:-1]
|
|
||||||
else:
|
|
||||||
return string
|
|
||||||
|
|
||||||
|
|
||||||
class EmStrongItem(namedtuple('EmStrongItem', ['pattern', 'builder', 'tags'])):
|
|
||||||
"""Emphasis/strong pattern item."""
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
The pattern classes
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Pattern: # pragma: no cover
|
|
||||||
"""Base class that inline patterns subclass. """
|
|
||||||
|
|
||||||
ANCESTOR_EXCLUDES = tuple()
|
|
||||||
|
|
||||||
def __init__(self, pattern, md=None):
|
|
||||||
"""
|
|
||||||
Create an instant of an inline pattern.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* pattern: A regular expression that matches a pattern
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.pattern = pattern
|
|
||||||
self.compiled_re = re.compile(r"^(.*?)%s(.*)$" % pattern,
|
|
||||||
re.DOTALL | re.UNICODE)
|
|
||||||
|
|
||||||
self.md = md
|
|
||||||
|
|
||||||
def getCompiledRegExp(self):
|
|
||||||
""" Return a compiled regular expression. """
|
|
||||||
return self.compiled_re
|
|
||||||
|
|
||||||
def handleMatch(self, m):
|
|
||||||
"""Return a ElementTree element from the given match.
|
|
||||||
|
|
||||||
Subclasses should override this method.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* m: A re match object containing a match of the pattern.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass # pragma: no cover
|
|
||||||
|
|
||||||
def type(self):
|
|
||||||
""" Return class name, to define pattern type """
|
|
||||||
return self.__class__.__name__
|
|
||||||
|
|
||||||
def unescape(self, text):
|
|
||||||
""" Return unescaped text given text with an inline placeholder. """
|
|
||||||
try:
|
|
||||||
stash = self.md.treeprocessors['inline'].stashed_nodes
|
|
||||||
except KeyError: # pragma: no cover
|
|
||||||
return text
|
|
||||||
|
|
||||||
def get_stash(m):
|
|
||||||
id = m.group(1)
|
|
||||||
if id in stash:
|
|
||||||
value = stash.get(id)
|
|
||||||
if isinstance(value, str):
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
# An `etree` Element - return text content only
|
|
||||||
return ''.join(value.itertext())
|
|
||||||
return util.INLINE_PLACEHOLDER_RE.sub(get_stash, text)
|
|
||||||
|
|
||||||
|
|
||||||
class InlineProcessor(Pattern):
|
|
||||||
"""
|
|
||||||
Base class that inline patterns subclass.
|
|
||||||
|
|
||||||
This is the newer style inline processor that uses a more
|
|
||||||
efficient and flexible search approach.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, pattern, md=None):
|
|
||||||
"""
|
|
||||||
Create an instant of an inline pattern.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* pattern: A regular expression that matches a pattern
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.pattern = pattern
|
|
||||||
self.compiled_re = re.compile(pattern, re.DOTALL | re.UNICODE)
|
|
||||||
|
|
||||||
# API for Markdown to pass `safe_mode` into instance
|
|
||||||
self.safe_mode = False
|
|
||||||
self.md = md
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
"""Return a ElementTree element from the given match and the
|
|
||||||
start and end index of the matched text.
|
|
||||||
|
|
||||||
If `start` and/or `end` are returned as `None`, it will be
|
|
||||||
assumed that the processor did not find a valid region of text.
|
|
||||||
|
|
||||||
Subclasses should override this method.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* m: A re match object containing a match of the pattern.
|
|
||||||
* data: The buffer current under analysis
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
* el: The ElementTree element, text or None.
|
|
||||||
* start: The start of the region that has been matched or None.
|
|
||||||
* end: The end of the region that has been matched or None.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass # pragma: no cover
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleTextPattern(Pattern): # pragma: no cover
|
|
||||||
""" Return a simple text of group(2) of a Pattern. """
|
|
||||||
def handleMatch(self, m):
|
|
||||||
return m.group(2)
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleTextInlineProcessor(InlineProcessor):
|
|
||||||
""" Return a simple text of group(1) of a Pattern. """
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
return m.group(1), m.start(0), m.end(0)
|
|
||||||
|
|
||||||
|
|
||||||
class EscapeInlineProcessor(InlineProcessor):
|
|
||||||
""" Return an escaped character. """
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
char = m.group(1)
|
|
||||||
if char in self.md.ESCAPED_CHARS:
|
|
||||||
return '{}{}{}'.format(util.STX, ord(char), util.ETX), m.start(0), m.end(0)
|
|
||||||
else:
|
|
||||||
return None, m.start(0), m.end(0)
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleTagPattern(Pattern): # pragma: no cover
|
|
||||||
"""
|
|
||||||
Return element of type `tag` with a text attribute of group(3)
|
|
||||||
of a Pattern.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, pattern, tag):
|
|
||||||
Pattern.__init__(self, pattern)
|
|
||||||
self.tag = tag
|
|
||||||
|
|
||||||
def handleMatch(self, m):
|
|
||||||
el = etree.Element(self.tag)
|
|
||||||
el.text = m.group(3)
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleTagInlineProcessor(InlineProcessor):
|
|
||||||
"""
|
|
||||||
Return element of type `tag` with a text attribute of group(2)
|
|
||||||
of a Pattern.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, pattern, tag):
|
|
||||||
InlineProcessor.__init__(self, pattern)
|
|
||||||
self.tag = tag
|
|
||||||
|
|
||||||
def handleMatch(self, m, data): # pragma: no cover
|
|
||||||
el = etree.Element(self.tag)
|
|
||||||
el.text = m.group(2)
|
|
||||||
return el, m.start(0), m.end(0)
|
|
||||||
|
|
||||||
|
|
||||||
class SubstituteTagPattern(SimpleTagPattern): # pragma: no cover
|
|
||||||
""" Return an element of type `tag` with no children. """
|
|
||||||
def handleMatch(self, m):
|
|
||||||
return etree.Element(self.tag)
|
|
||||||
|
|
||||||
|
|
||||||
class SubstituteTagInlineProcessor(SimpleTagInlineProcessor):
|
|
||||||
""" Return an element of type `tag` with no children. """
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
return etree.Element(self.tag), m.start(0), m.end(0)
|
|
||||||
|
|
||||||
|
|
||||||
class BacktickInlineProcessor(InlineProcessor):
|
|
||||||
""" Return a `<code>` element containing the matching text. """
|
|
||||||
def __init__(self, pattern):
|
|
||||||
InlineProcessor.__init__(self, pattern)
|
|
||||||
self.ESCAPED_BSLASH = '{}{}{}'.format(util.STX, ord('\\'), util.ETX)
|
|
||||||
self.tag = 'code'
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
if m.group(3):
|
|
||||||
el = etree.Element(self.tag)
|
|
||||||
el.text = util.AtomicString(util.code_escape(m.group(3).strip()))
|
|
||||||
return el, m.start(0), m.end(0)
|
|
||||||
else:
|
|
||||||
return m.group(1).replace('\\\\', self.ESCAPED_BSLASH), m.start(0), m.end(0)
|
|
||||||
|
|
||||||
|
|
||||||
class DoubleTagPattern(SimpleTagPattern): # pragma: no cover
|
|
||||||
"""Return a ElementTree element nested in tag2 nested in tag1.
|
|
||||||
|
|
||||||
Useful for strong emphasis etc.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def handleMatch(self, m):
|
|
||||||
tag1, tag2 = self.tag.split(",")
|
|
||||||
el1 = etree.Element(tag1)
|
|
||||||
el2 = etree.SubElement(el1, tag2)
|
|
||||||
el2.text = m.group(3)
|
|
||||||
if len(m.groups()) == 5:
|
|
||||||
el2.tail = m.group(4)
|
|
||||||
return el1
|
|
||||||
|
|
||||||
|
|
||||||
class DoubleTagInlineProcessor(SimpleTagInlineProcessor):
|
|
||||||
"""Return a ElementTree element nested in tag2 nested in tag1.
|
|
||||||
|
|
||||||
Useful for strong emphasis etc.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def handleMatch(self, m, data): # pragma: no cover
|
|
||||||
tag1, tag2 = self.tag.split(",")
|
|
||||||
el1 = etree.Element(tag1)
|
|
||||||
el2 = etree.SubElement(el1, tag2)
|
|
||||||
el2.text = m.group(2)
|
|
||||||
if len(m.groups()) == 3:
|
|
||||||
el2.tail = m.group(3)
|
|
||||||
return el1, m.start(0), m.end(0)
|
|
||||||
|
|
||||||
|
|
||||||
class HtmlInlineProcessor(InlineProcessor):
|
|
||||||
""" Store raw inline html and return a placeholder. """
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
rawhtml = self.backslash_unescape(self.unescape(m.group(1)))
|
|
||||||
place_holder = self.md.htmlStash.store(rawhtml)
|
|
||||||
return place_holder, m.start(0), m.end(0)
|
|
||||||
|
|
||||||
def unescape(self, text):
|
|
||||||
""" Return unescaped text given text with an inline placeholder. """
|
|
||||||
try:
|
|
||||||
stash = self.md.treeprocessors['inline'].stashed_nodes
|
|
||||||
except KeyError: # pragma: no cover
|
|
||||||
return text
|
|
||||||
|
|
||||||
def get_stash(m):
|
|
||||||
id = m.group(1)
|
|
||||||
value = stash.get(id)
|
|
||||||
if value is not None:
|
|
||||||
try:
|
|
||||||
return self.md.serializer(value)
|
|
||||||
except Exception:
|
|
||||||
return r'\%s' % value
|
|
||||||
|
|
||||||
return util.INLINE_PLACEHOLDER_RE.sub(get_stash, text)
|
|
||||||
|
|
||||||
def backslash_unescape(self, text):
|
|
||||||
""" Return text with backslash escapes undone (backslashes are restored). """
|
|
||||||
try:
|
|
||||||
RE = self.md.treeprocessors['unescape'].RE
|
|
||||||
except KeyError: # pragma: no cover
|
|
||||||
return text
|
|
||||||
|
|
||||||
def _unescape(m):
|
|
||||||
return chr(int(m.group(1)))
|
|
||||||
|
|
||||||
return RE.sub(_unescape, text)
|
|
||||||
|
|
||||||
|
|
||||||
class AsteriskProcessor(InlineProcessor):
|
|
||||||
"""Emphasis processor for handling strong and em matches inside asterisks."""
|
|
||||||
|
|
||||||
PATTERNS = [
|
|
||||||
EmStrongItem(re.compile(EM_STRONG_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'),
|
|
||||||
EmStrongItem(re.compile(STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'),
|
|
||||||
EmStrongItem(re.compile(STRONG_EM3_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'),
|
|
||||||
EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'),
|
|
||||||
EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em')
|
|
||||||
]
|
|
||||||
|
|
||||||
def build_single(self, m, tag, idx):
|
|
||||||
"""Return single tag."""
|
|
||||||
el1 = etree.Element(tag)
|
|
||||||
text = m.group(2)
|
|
||||||
self.parse_sub_patterns(text, el1, None, idx)
|
|
||||||
return el1
|
|
||||||
|
|
||||||
def build_double(self, m, tags, idx):
|
|
||||||
"""Return double tag."""
|
|
||||||
|
|
||||||
tag1, tag2 = tags.split(",")
|
|
||||||
el1 = etree.Element(tag1)
|
|
||||||
el2 = etree.Element(tag2)
|
|
||||||
text = m.group(2)
|
|
||||||
self.parse_sub_patterns(text, el2, None, idx)
|
|
||||||
el1.append(el2)
|
|
||||||
if len(m.groups()) == 3:
|
|
||||||
text = m.group(3)
|
|
||||||
self.parse_sub_patterns(text, el1, el2, idx)
|
|
||||||
return el1
|
|
||||||
|
|
||||||
def build_double2(self, m, tags, idx):
|
|
||||||
"""Return double tags (variant 2): `<strong>text <em>text</em></strong>`."""
|
|
||||||
|
|
||||||
tag1, tag2 = tags.split(",")
|
|
||||||
el1 = etree.Element(tag1)
|
|
||||||
el2 = etree.Element(tag2)
|
|
||||||
text = m.group(2)
|
|
||||||
self.parse_sub_patterns(text, el1, None, idx)
|
|
||||||
text = m.group(3)
|
|
||||||
el1.append(el2)
|
|
||||||
self.parse_sub_patterns(text, el2, None, idx)
|
|
||||||
return el1
|
|
||||||
|
|
||||||
def parse_sub_patterns(self, data, parent, last, idx):
|
|
||||||
"""
|
|
||||||
Parses sub patterns.
|
|
||||||
|
|
||||||
`data` (`str`):
|
|
||||||
text to evaluate.
|
|
||||||
|
|
||||||
`parent` (`etree.Element`):
|
|
||||||
Parent to attach text and sub elements to.
|
|
||||||
|
|
||||||
`last` (`etree.Element`):
|
|
||||||
Last appended child to parent. Can also be None if parent has no children.
|
|
||||||
|
|
||||||
`idx` (`int`):
|
|
||||||
Current pattern index that was used to evaluate the parent.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
offset = 0
|
|
||||||
pos = 0
|
|
||||||
|
|
||||||
length = len(data)
|
|
||||||
while pos < length:
|
|
||||||
# Find the start of potential emphasis or strong tokens
|
|
||||||
if self.compiled_re.match(data, pos):
|
|
||||||
matched = False
|
|
||||||
# See if the we can match an emphasis/strong pattern
|
|
||||||
for index, item in enumerate(self.PATTERNS):
|
|
||||||
# Only evaluate patterns that are after what was used on the parent
|
|
||||||
if index <= idx:
|
|
||||||
continue
|
|
||||||
m = item.pattern.match(data, pos)
|
|
||||||
if m:
|
|
||||||
# Append child nodes to parent
|
|
||||||
# Text nodes should be appended to the last
|
|
||||||
# child if present, and if not, it should
|
|
||||||
# be added as the parent's text node.
|
|
||||||
text = data[offset:m.start(0)]
|
|
||||||
if text:
|
|
||||||
if last is not None:
|
|
||||||
last.tail = text
|
|
||||||
else:
|
|
||||||
parent.text = text
|
|
||||||
el = self.build_element(m, item.builder, item.tags, index)
|
|
||||||
parent.append(el)
|
|
||||||
last = el
|
|
||||||
# Move our position past the matched hunk
|
|
||||||
offset = pos = m.end(0)
|
|
||||||
matched = True
|
|
||||||
if not matched:
|
|
||||||
# We matched nothing, move on to the next character
|
|
||||||
pos += 1
|
|
||||||
else:
|
|
||||||
# Increment position as no potential emphasis start was found.
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
# Append any leftover text as a text node.
|
|
||||||
text = data[offset:]
|
|
||||||
if text:
|
|
||||||
if last is not None:
|
|
||||||
last.tail = text
|
|
||||||
else:
|
|
||||||
parent.text = text
|
|
||||||
|
|
||||||
def build_element(self, m, builder, tags, index):
|
|
||||||
"""Element builder."""
|
|
||||||
|
|
||||||
if builder == 'double2':
|
|
||||||
return self.build_double2(m, tags, index)
|
|
||||||
elif builder == 'double':
|
|
||||||
return self.build_double(m, tags, index)
|
|
||||||
else:
|
|
||||||
return self.build_single(m, tags, index)
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
"""Parse patterns."""
|
|
||||||
|
|
||||||
el = None
|
|
||||||
start = None
|
|
||||||
end = None
|
|
||||||
|
|
||||||
for index, item in enumerate(self.PATTERNS):
|
|
||||||
m1 = item.pattern.match(data, m.start(0))
|
|
||||||
if m1:
|
|
||||||
start = m1.start(0)
|
|
||||||
end = m1.end(0)
|
|
||||||
el = self.build_element(m1, item.builder, item.tags, index)
|
|
||||||
break
|
|
||||||
return el, start, end
|
|
||||||
|
|
||||||
|
|
||||||
class UnderscoreProcessor(AsteriskProcessor):
|
|
||||||
"""Emphasis processor for handling strong and em matches inside underscores."""
|
|
||||||
|
|
||||||
PATTERNS = [
|
|
||||||
EmStrongItem(re.compile(EM_STRONG2_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'),
|
|
||||||
EmStrongItem(re.compile(STRONG_EM2_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'),
|
|
||||||
EmStrongItem(re.compile(SMART_STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'),
|
|
||||||
EmStrongItem(re.compile(SMART_STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'),
|
|
||||||
EmStrongItem(re.compile(SMART_EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em')
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class LinkInlineProcessor(InlineProcessor):
|
|
||||||
""" Return a link element from the given match. """
|
|
||||||
RE_LINK = re.compile(r'''\(\s*(?:(<[^<>]*>)\s*(?:('[^']*'|"[^"]*")\s*)?\))?''', re.DOTALL | re.UNICODE)
|
|
||||||
RE_TITLE_CLEAN = re.compile(r'\s')
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
text, index, handled = self.getText(data, m.end(0))
|
|
||||||
|
|
||||||
if not handled:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
href, title, index, handled = self.getLink(data, index)
|
|
||||||
if not handled:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
el = etree.Element("a")
|
|
||||||
el.text = text
|
|
||||||
|
|
||||||
el.set("href", href)
|
|
||||||
|
|
||||||
if title is not None:
|
|
||||||
el.set("title", title)
|
|
||||||
|
|
||||||
return el, m.start(0), index
|
|
||||||
|
|
||||||
def getLink(self, data, index):
|
|
||||||
"""Parse data between `()` of `[Text]()` allowing recursive `()`. """
|
|
||||||
|
|
||||||
href = ''
|
|
||||||
title = None
|
|
||||||
handled = False
|
|
||||||
|
|
||||||
m = self.RE_LINK.match(data, pos=index)
|
|
||||||
if m and m.group(1):
|
|
||||||
# Matches [Text](<link> "title")
|
|
||||||
href = m.group(1)[1:-1].strip()
|
|
||||||
if m.group(2):
|
|
||||||
title = m.group(2)[1:-1]
|
|
||||||
index = m.end(0)
|
|
||||||
handled = True
|
|
||||||
elif m:
|
|
||||||
# Track bracket nesting and index in string
|
|
||||||
bracket_count = 1
|
|
||||||
backtrack_count = 1
|
|
||||||
start_index = m.end()
|
|
||||||
index = start_index
|
|
||||||
last_bracket = -1
|
|
||||||
|
|
||||||
# Primary (first found) quote tracking.
|
|
||||||
quote = None
|
|
||||||
start_quote = -1
|
|
||||||
exit_quote = -1
|
|
||||||
ignore_matches = False
|
|
||||||
|
|
||||||
# Secondary (second found) quote tracking.
|
|
||||||
alt_quote = None
|
|
||||||
start_alt_quote = -1
|
|
||||||
exit_alt_quote = -1
|
|
||||||
|
|
||||||
# Track last character
|
|
||||||
last = ''
|
|
||||||
|
|
||||||
for pos in range(index, len(data)):
|
|
||||||
c = data[pos]
|
|
||||||
if c == '(':
|
|
||||||
# Count nested (
|
|
||||||
# Don't increment the bracket count if we are sure we're in a title.
|
|
||||||
if not ignore_matches:
|
|
||||||
bracket_count += 1
|
|
||||||
elif backtrack_count > 0:
|
|
||||||
backtrack_count -= 1
|
|
||||||
elif c == ')':
|
|
||||||
# Match nested ) to (
|
|
||||||
# Don't decrement if we are sure we are in a title that is unclosed.
|
|
||||||
if ((exit_quote != -1 and quote == last) or (exit_alt_quote != -1 and alt_quote == last)):
|
|
||||||
bracket_count = 0
|
|
||||||
elif not ignore_matches:
|
|
||||||
bracket_count -= 1
|
|
||||||
elif backtrack_count > 0:
|
|
||||||
backtrack_count -= 1
|
|
||||||
# We've found our backup end location if the title doesn't resolve.
|
|
||||||
if backtrack_count == 0:
|
|
||||||
last_bracket = index + 1
|
|
||||||
|
|
||||||
elif c in ("'", '"'):
|
|
||||||
# Quote has started
|
|
||||||
if not quote:
|
|
||||||
# We'll assume we are now in a title.
|
|
||||||
# Brackets are quoted, so no need to match them (except for the final one).
|
|
||||||
ignore_matches = True
|
|
||||||
backtrack_count = bracket_count
|
|
||||||
bracket_count = 1
|
|
||||||
start_quote = index + 1
|
|
||||||
quote = c
|
|
||||||
# Secondary quote (in case the first doesn't resolve): [text](link'"title")
|
|
||||||
elif c != quote and not alt_quote:
|
|
||||||
start_alt_quote = index + 1
|
|
||||||
alt_quote = c
|
|
||||||
# Update primary quote match
|
|
||||||
elif c == quote:
|
|
||||||
exit_quote = index + 1
|
|
||||||
# Update secondary quote match
|
|
||||||
elif alt_quote and c == alt_quote:
|
|
||||||
exit_alt_quote = index + 1
|
|
||||||
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
# Link is closed, so let's break out of the loop
|
|
||||||
if bracket_count == 0:
|
|
||||||
# Get the title if we closed a title string right before link closed
|
|
||||||
if exit_quote >= 0 and quote == last:
|
|
||||||
href = data[start_index:start_quote - 1]
|
|
||||||
title = ''.join(data[start_quote:exit_quote - 1])
|
|
||||||
elif exit_alt_quote >= 0 and alt_quote == last:
|
|
||||||
href = data[start_index:start_alt_quote - 1]
|
|
||||||
title = ''.join(data[start_alt_quote:exit_alt_quote - 1])
|
|
||||||
else:
|
|
||||||
href = data[start_index:index - 1]
|
|
||||||
break
|
|
||||||
|
|
||||||
if c != ' ':
|
|
||||||
last = c
|
|
||||||
|
|
||||||
# We have a scenario: `[test](link"notitle)`
|
|
||||||
# When we enter a string, we stop tracking bracket resolution in the main counter,
|
|
||||||
# but we do keep a backup counter up until we discover where we might resolve all brackets
|
|
||||||
# if the title string fails to resolve.
|
|
||||||
if bracket_count != 0 and backtrack_count == 0:
|
|
||||||
href = data[start_index:last_bracket - 1]
|
|
||||||
index = last_bracket
|
|
||||||
bracket_count = 0
|
|
||||||
|
|
||||||
handled = bracket_count == 0
|
|
||||||
|
|
||||||
if title is not None:
|
|
||||||
title = self.RE_TITLE_CLEAN.sub(' ', dequote(self.unescape(title.strip())))
|
|
||||||
|
|
||||||
href = self.unescape(href).strip()
|
|
||||||
|
|
||||||
return href, title, index, handled
|
|
||||||
|
|
||||||
def getText(self, data, index):
|
|
||||||
"""Parse the content between `[]` of the start of an image or link
|
|
||||||
resolving nested square brackets.
|
|
||||||
|
|
||||||
"""
|
|
||||||
bracket_count = 1
|
|
||||||
text = []
|
|
||||||
for pos in range(index, len(data)):
|
|
||||||
c = data[pos]
|
|
||||||
if c == ']':
|
|
||||||
bracket_count -= 1
|
|
||||||
elif c == '[':
|
|
||||||
bracket_count += 1
|
|
||||||
index += 1
|
|
||||||
if bracket_count == 0:
|
|
||||||
break
|
|
||||||
text.append(c)
|
|
||||||
return ''.join(text), index, bracket_count == 0
|
|
||||||
|
|
||||||
|
|
||||||
class ImageInlineProcessor(LinkInlineProcessor):
|
|
||||||
""" Return a `img` element from the given match. """
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
text, index, handled = self.getText(data, m.end(0))
|
|
||||||
if not handled:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
src, title, index, handled = self.getLink(data, index)
|
|
||||||
if not handled:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
el = etree.Element("img")
|
|
||||||
|
|
||||||
el.set("src", src)
|
|
||||||
|
|
||||||
if title is not None:
|
|
||||||
el.set("title", title)
|
|
||||||
|
|
||||||
el.set('alt', self.unescape(text))
|
|
||||||
return el, m.start(0), index
|
|
||||||
|
|
||||||
|
|
||||||
class ReferenceInlineProcessor(LinkInlineProcessor):
|
|
||||||
""" Match to a stored reference and return link element. """
|
|
||||||
NEWLINE_CLEANUP_RE = re.compile(r'\s+', re.MULTILINE)
|
|
||||||
|
|
||||||
RE_LINK = re.compile(r'\s?\[([^\]]*)\]', re.DOTALL | re.UNICODE)
|
|
||||||
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
text, index, handled = self.getText(data, m.end(0))
|
|
||||||
if not handled:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
id, end, handled = self.evalId(data, index, text)
|
|
||||||
if not handled:
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
# Clean up line breaks in id
|
|
||||||
id = self.NEWLINE_CLEANUP_RE.sub(' ', id)
|
|
||||||
if id not in self.md.references: # ignore undefined refs
|
|
||||||
return None, m.start(0), end
|
|
||||||
|
|
||||||
href, title = self.md.references[id]
|
|
||||||
|
|
||||||
return self.makeTag(href, title, text), m.start(0), end
|
|
||||||
|
|
||||||
def evalId(self, data, index, text):
|
|
||||||
"""
|
|
||||||
Evaluate the id portion of [ref][id].
|
|
||||||
|
|
||||||
If [ref][] use [ref].
|
|
||||||
"""
|
|
||||||
m = self.RE_LINK.match(data, pos=index)
|
|
||||||
if not m:
|
|
||||||
return None, index, False
|
|
||||||
else:
|
|
||||||
id = m.group(1).lower()
|
|
||||||
end = m.end(0)
|
|
||||||
if not id:
|
|
||||||
id = text.lower()
|
|
||||||
return id, end, True
|
|
||||||
|
|
||||||
def makeTag(self, href, title, text):
|
|
||||||
el = etree.Element('a')
|
|
||||||
|
|
||||||
el.set('href', href)
|
|
||||||
if title:
|
|
||||||
el.set('title', title)
|
|
||||||
|
|
||||||
el.text = text
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
class ShortReferenceInlineProcessor(ReferenceInlineProcessor):
|
|
||||||
"""Short form of reference: [google]. """
|
|
||||||
def evalId(self, data, index, text):
|
|
||||||
"""Evaluate the id from of [ref] """
|
|
||||||
|
|
||||||
return text.lower(), index, True
|
|
||||||
|
|
||||||
|
|
||||||
class ImageReferenceInlineProcessor(ReferenceInlineProcessor):
|
|
||||||
""" Match to a stored reference and return `img` element. """
|
|
||||||
def makeTag(self, href, title, text):
|
|
||||||
el = etree.Element("img")
|
|
||||||
el.set("src", href)
|
|
||||||
if title:
|
|
||||||
el.set("title", title)
|
|
||||||
el.set("alt", self.unescape(text))
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
class ShortImageReferenceInlineProcessor(ImageReferenceInlineProcessor):
|
|
||||||
""" Short form of image reference: `![ref]`. """
|
|
||||||
def evalId(self, data, index, text):
|
|
||||||
"""Evaluate the id from of [ref] """
|
|
||||||
|
|
||||||
return text.lower(), index, True
|
|
||||||
|
|
||||||
|
|
||||||
class AutolinkInlineProcessor(InlineProcessor):
|
|
||||||
""" Return a link Element given an auto-link (`<http://example/com>`). """
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
el = etree.Element("a")
|
|
||||||
el.set('href', self.unescape(m.group(1)))
|
|
||||||
el.text = util.AtomicString(m.group(1))
|
|
||||||
return el, m.start(0), m.end(0)
|
|
||||||
|
|
||||||
|
|
||||||
class AutomailInlineProcessor(InlineProcessor):
|
|
||||||
"""
|
|
||||||
Return a `mailto` link Element given an auto-mail link (`<foo@example.com>`).
|
|
||||||
"""
|
|
||||||
def handleMatch(self, m, data):
|
|
||||||
el = etree.Element('a')
|
|
||||||
email = self.unescape(m.group(1))
|
|
||||||
if email.startswith("mailto:"):
|
|
||||||
email = email[len("mailto:"):]
|
|
||||||
|
|
||||||
def codepoint2name(code):
|
|
||||||
"""Return entity definition by code, or the code if not defined."""
|
|
||||||
entity = entities.codepoint2name.get(code)
|
|
||||||
if entity:
|
|
||||||
return "{}{};".format(util.AMP_SUBSTITUTE, entity)
|
|
||||||
else:
|
|
||||||
return "%s#%d;" % (util.AMP_SUBSTITUTE, code)
|
|
||||||
|
|
||||||
letters = [codepoint2name(ord(letter)) for letter in email]
|
|
||||||
el.text = util.AtomicString(''.join(letters))
|
|
||||||
|
|
||||||
mailto = "mailto:" + email
|
|
||||||
mailto = "".join([util.AMP_SUBSTITUTE + '#%d;' %
|
|
||||||
ord(letter) for letter in mailto])
|
|
||||||
el.set('href', mailto)
|
|
||||||
return el, m.start(0), m.end(0)
|
|
|
@ -1,137 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
|
|
||||||
POST-PROCESSORS
|
|
||||||
=============================================================================
|
|
||||||
|
|
||||||
Markdown also allows post-processors, which are similar to preprocessors in
|
|
||||||
that they need to implement a "run" method. However, they are run after core
|
|
||||||
processing.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
from . import util
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def build_postprocessors(md, **kwargs):
|
|
||||||
""" Build the default postprocessors for Markdown. """
|
|
||||||
postprocessors = util.Registry()
|
|
||||||
postprocessors.register(RawHtmlPostprocessor(md), 'raw_html', 30)
|
|
||||||
postprocessors.register(AndSubstitutePostprocessor(), 'amp_substitute', 20)
|
|
||||||
return postprocessors
|
|
||||||
|
|
||||||
|
|
||||||
class Postprocessor(util.Processor):
|
|
||||||
"""
|
|
||||||
Postprocessors are run after the ElementTree it converted back into text.
|
|
||||||
|
|
||||||
Each Postprocessor implements a "run" method that takes a pointer to a
|
|
||||||
text string, modifies it as necessary and returns a text string.
|
|
||||||
|
|
||||||
Postprocessors must extend markdown.Postprocessor.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def run(self, text):
|
|
||||||
"""
|
|
||||||
Subclasses of Postprocessor should implement a `run` method, which
|
|
||||||
takes the html document as a single text string and returns a
|
|
||||||
(possibly modified) string.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass # pragma: no cover
|
|
||||||
|
|
||||||
|
|
||||||
class RawHtmlPostprocessor(Postprocessor):
|
|
||||||
""" Restore raw html to the document. """
|
|
||||||
|
|
||||||
BLOCK_LEVEL_REGEX = re.compile(r'^\<\/?([^ >]+)')
|
|
||||||
|
|
||||||
def run(self, text):
|
|
||||||
""" Iterate over html stash and restore html. """
|
|
||||||
replacements = OrderedDict()
|
|
||||||
for i in range(self.md.htmlStash.html_counter):
|
|
||||||
html = self.stash_to_string(self.md.htmlStash.rawHtmlBlocks[i])
|
|
||||||
if self.isblocklevel(html):
|
|
||||||
replacements["<p>{}</p>".format(
|
|
||||||
self.md.htmlStash.get_placeholder(i))] = html
|
|
||||||
replacements[self.md.htmlStash.get_placeholder(i)] = html
|
|
||||||
|
|
||||||
def substitute_match(m):
|
|
||||||
key = m.group(0)
|
|
||||||
|
|
||||||
if key not in replacements:
|
|
||||||
if key[3:-4] in replacements:
|
|
||||||
return f'<p>{ replacements[key[3:-4]] }</p>'
|
|
||||||
else:
|
|
||||||
return key
|
|
||||||
|
|
||||||
return replacements[key]
|
|
||||||
|
|
||||||
if replacements:
|
|
||||||
base_placeholder = util.HTML_PLACEHOLDER % r'([0-9]+)'
|
|
||||||
pattern = re.compile(f'<p>{ base_placeholder }</p>|{ base_placeholder }')
|
|
||||||
processed_text = pattern.sub(substitute_match, text)
|
|
||||||
else:
|
|
||||||
return text
|
|
||||||
|
|
||||||
if processed_text == text:
|
|
||||||
return processed_text
|
|
||||||
else:
|
|
||||||
return self.run(processed_text)
|
|
||||||
|
|
||||||
def isblocklevel(self, html):
|
|
||||||
m = self.BLOCK_LEVEL_REGEX.match(html)
|
|
||||||
if m:
|
|
||||||
if m.group(1)[0] in ('!', '?', '@', '%'):
|
|
||||||
# Comment, PHP etc...
|
|
||||||
return True
|
|
||||||
return self.md.is_block_level(m.group(1))
|
|
||||||
return False
|
|
||||||
|
|
||||||
def stash_to_string(self, text):
|
|
||||||
""" Convert a stashed object to a string. """
|
|
||||||
return str(text)
|
|
||||||
|
|
||||||
|
|
||||||
class AndSubstitutePostprocessor(Postprocessor):
|
|
||||||
""" Restore valid entities """
|
|
||||||
|
|
||||||
def run(self, text):
|
|
||||||
text = text.replace(util.AMP_SUBSTITUTE, "&")
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
@util.deprecated(
|
|
||||||
"This class will be removed in the future; "
|
|
||||||
"use 'treeprocessors.UnescapeTreeprocessor' instead."
|
|
||||||
)
|
|
||||||
class UnescapePostprocessor(Postprocessor):
|
|
||||||
""" Restore escaped chars """
|
|
||||||
|
|
||||||
RE = re.compile(r'{}(\d+){}'.format(util.STX, util.ETX))
|
|
||||||
|
|
||||||
def unescape(self, m):
|
|
||||||
return chr(int(m.group(1)))
|
|
||||||
|
|
||||||
def run(self, text):
|
|
||||||
return self.RE.sub(self.unescape, text)
|
|
|
@ -1,82 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
|
|
||||||
PRE-PROCESSORS
|
|
||||||
=============================================================================
|
|
||||||
|
|
||||||
Preprocessors work on source text before we start doing anything too
|
|
||||||
complicated.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import util
|
|
||||||
from .htmlparser import HTMLExtractor
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def build_preprocessors(md, **kwargs):
|
|
||||||
""" Build the default set of preprocessors used by Markdown. """
|
|
||||||
preprocessors = util.Registry()
|
|
||||||
preprocessors.register(NormalizeWhitespace(md), 'normalize_whitespace', 30)
|
|
||||||
preprocessors.register(HtmlBlockPreprocessor(md), 'html_block', 20)
|
|
||||||
return preprocessors
|
|
||||||
|
|
||||||
|
|
||||||
class Preprocessor(util.Processor):
|
|
||||||
"""
|
|
||||||
Preprocessors are run after the text is broken into lines.
|
|
||||||
|
|
||||||
Each preprocessor implements a "run" method that takes a pointer to a
|
|
||||||
list of lines of the document, modifies it as necessary and returns
|
|
||||||
either the same pointer or a pointer to a new list.
|
|
||||||
|
|
||||||
Preprocessors must extend markdown.Preprocessor.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def run(self, lines):
|
|
||||||
"""
|
|
||||||
Each subclass of Preprocessor should override the `run` method, which
|
|
||||||
takes the document as a list of strings split by newlines and returns
|
|
||||||
the (possibly modified) list of lines.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass # pragma: no cover
|
|
||||||
|
|
||||||
|
|
||||||
class NormalizeWhitespace(Preprocessor):
|
|
||||||
""" Normalize whitespace for consistent parsing. """
|
|
||||||
|
|
||||||
def run(self, lines):
|
|
||||||
source = '\n'.join(lines)
|
|
||||||
source = source.replace(util.STX, "").replace(util.ETX, "")
|
|
||||||
source = source.replace("\r\n", "\n").replace("\r", "\n") + "\n\n"
|
|
||||||
source = source.expandtabs(self.md.tab_length)
|
|
||||||
source = re.sub(r'(?<=\n) +\n', '\n', source)
|
|
||||||
return source.split('\n')
|
|
||||||
|
|
||||||
|
|
||||||
class HtmlBlockPreprocessor(Preprocessor):
|
|
||||||
"""Remove html blocks from the text and store them for later retrieval."""
|
|
||||||
|
|
||||||
def run(self, lines):
|
|
||||||
source = '\n'.join(lines)
|
|
||||||
parser = HTMLExtractor(self.md)
|
|
||||||
parser.feed(source)
|
|
||||||
parser.close()
|
|
||||||
return ''.join(parser.cleandoc).split('\n')
|
|
|
@ -1,187 +0,0 @@
|
||||||
# Add x/html serialization to `Elementree`
|
|
||||||
# Taken from ElementTree 1.3 preview with slight modifications
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999-2007 by Fredrik Lundh. All rights reserved.
|
|
||||||
#
|
|
||||||
# fredrik@pythonware.com
|
|
||||||
# https://www.pythonware.com/
|
|
||||||
#
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
# The ElementTree toolkit is
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999-2007 by Fredrik Lundh
|
|
||||||
#
|
|
||||||
# By obtaining, using, and/or copying this software and/or its
|
|
||||||
# associated documentation, you agree that you have read, understood,
|
|
||||||
# and will comply with the following terms and conditions:
|
|
||||||
#
|
|
||||||
# Permission to use, copy, modify, and distribute this software and
|
|
||||||
# its associated documentation for any purpose and without fee is
|
|
||||||
# hereby granted, provided that the above copyright notice appears in
|
|
||||||
# all copies, and that both that copyright notice and this permission
|
|
||||||
# notice appear in supporting documentation, and that the name of
|
|
||||||
# Secret Labs AB or the author not be used in advertising or publicity
|
|
||||||
# pertaining to distribution of the software without specific, written
|
|
||||||
# prior permission.
|
|
||||||
#
|
|
||||||
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
|
||||||
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
|
||||||
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
|
||||||
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
|
||||||
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
||||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
|
||||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
|
||||||
# OF THIS SOFTWARE.
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
from xml.etree.ElementTree import ProcessingInstruction
|
|
||||||
from xml.etree.ElementTree import Comment, ElementTree, QName
|
|
||||||
import re
|
|
||||||
|
|
||||||
__all__ = ['to_html_string', 'to_xhtml_string']
|
|
||||||
|
|
||||||
HTML_EMPTY = ("area", "base", "basefont", "br", "col", "frame", "hr",
|
|
||||||
"img", "input", "isindex", "link", "meta", "param")
|
|
||||||
RE_AMP = re.compile(r'&(?!(?:\#[0-9]+|\#x[0-9a-f]+|[0-9a-z]+);)', re.I)
|
|
||||||
|
|
||||||
try:
|
|
||||||
HTML_EMPTY = set(HTML_EMPTY)
|
|
||||||
except NameError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _raise_serialization_error(text): # pragma: no cover
|
|
||||||
raise TypeError(
|
|
||||||
"cannot serialize {!r} (type {})".format(text, type(text).__name__)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _escape_cdata(text):
|
|
||||||
# escape character data
|
|
||||||
try:
|
|
||||||
# it's worth avoiding do-nothing calls for strings that are
|
|
||||||
# shorter than 500 character, or so. assume that's, by far,
|
|
||||||
# the most common case in most applications.
|
|
||||||
if "&" in text:
|
|
||||||
# Only replace & when not part of an entity
|
|
||||||
text = RE_AMP.sub('&', text)
|
|
||||||
if "<" in text:
|
|
||||||
text = text.replace("<", "<")
|
|
||||||
if ">" in text:
|
|
||||||
text = text.replace(">", ">")
|
|
||||||
return text
|
|
||||||
except (TypeError, AttributeError): # pragma: no cover
|
|
||||||
_raise_serialization_error(text)
|
|
||||||
|
|
||||||
|
|
||||||
def _escape_attrib(text):
|
|
||||||
# escape attribute value
|
|
||||||
try:
|
|
||||||
if "&" in text:
|
|
||||||
# Only replace & when not part of an entity
|
|
||||||
text = RE_AMP.sub('&', text)
|
|
||||||
if "<" in text:
|
|
||||||
text = text.replace("<", "<")
|
|
||||||
if ">" in text:
|
|
||||||
text = text.replace(">", ">")
|
|
||||||
if "\"" in text:
|
|
||||||
text = text.replace("\"", """)
|
|
||||||
if "\n" in text:
|
|
||||||
text = text.replace("\n", " ")
|
|
||||||
return text
|
|
||||||
except (TypeError, AttributeError): # pragma: no cover
|
|
||||||
_raise_serialization_error(text)
|
|
||||||
|
|
||||||
|
|
||||||
def _escape_attrib_html(text):
|
|
||||||
# escape attribute value
|
|
||||||
try:
|
|
||||||
if "&" in text:
|
|
||||||
# Only replace & when not part of an entity
|
|
||||||
text = RE_AMP.sub('&', text)
|
|
||||||
if "<" in text:
|
|
||||||
text = text.replace("<", "<")
|
|
||||||
if ">" in text:
|
|
||||||
text = text.replace(">", ">")
|
|
||||||
if "\"" in text:
|
|
||||||
text = text.replace("\"", """)
|
|
||||||
return text
|
|
||||||
except (TypeError, AttributeError): # pragma: no cover
|
|
||||||
_raise_serialization_error(text)
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_html(write, elem, format):
|
|
||||||
tag = elem.tag
|
|
||||||
text = elem.text
|
|
||||||
if tag is Comment:
|
|
||||||
write("<!--%s-->" % _escape_cdata(text))
|
|
||||||
elif tag is ProcessingInstruction:
|
|
||||||
write("<?%s?>" % _escape_cdata(text))
|
|
||||||
elif tag is None:
|
|
||||||
if text:
|
|
||||||
write(_escape_cdata(text))
|
|
||||||
for e in elem:
|
|
||||||
_serialize_html(write, e, format)
|
|
||||||
else:
|
|
||||||
namespace_uri = None
|
|
||||||
if isinstance(tag, QName):
|
|
||||||
# `QNAME` objects store their data as a string: `{uri}tag`
|
|
||||||
if tag.text[:1] == "{":
|
|
||||||
namespace_uri, tag = tag.text[1:].split("}", 1)
|
|
||||||
else:
|
|
||||||
raise ValueError('QName objects must define a tag.')
|
|
||||||
write("<" + tag)
|
|
||||||
items = elem.items()
|
|
||||||
if items:
|
|
||||||
items = sorted(items) # lexical order
|
|
||||||
for k, v in items:
|
|
||||||
if isinstance(k, QName):
|
|
||||||
# Assume a text only `QName`
|
|
||||||
k = k.text
|
|
||||||
if isinstance(v, QName):
|
|
||||||
# Assume a text only `QName`
|
|
||||||
v = v.text
|
|
||||||
else:
|
|
||||||
v = _escape_attrib_html(v)
|
|
||||||
if k == v and format == 'html':
|
|
||||||
# handle boolean attributes
|
|
||||||
write(" %s" % v)
|
|
||||||
else:
|
|
||||||
write(' {}="{}"'.format(k, v))
|
|
||||||
if namespace_uri:
|
|
||||||
write(' xmlns="%s"' % (_escape_attrib(namespace_uri)))
|
|
||||||
if format == "xhtml" and tag.lower() in HTML_EMPTY:
|
|
||||||
write(" />")
|
|
||||||
else:
|
|
||||||
write(">")
|
|
||||||
if text:
|
|
||||||
if tag.lower() in ["script", "style"]:
|
|
||||||
write(text)
|
|
||||||
else:
|
|
||||||
write(_escape_cdata(text))
|
|
||||||
for e in elem:
|
|
||||||
_serialize_html(write, e, format)
|
|
||||||
if tag.lower() not in HTML_EMPTY:
|
|
||||||
write("</" + tag + ">")
|
|
||||||
if elem.tail:
|
|
||||||
write(_escape_cdata(elem.tail))
|
|
||||||
|
|
||||||
|
|
||||||
def _write_html(root, format="html"):
|
|
||||||
assert root is not None
|
|
||||||
data = []
|
|
||||||
write = data.append
|
|
||||||
_serialize_html(write, root, format)
|
|
||||||
return "".join(data)
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
# public functions
|
|
||||||
|
|
||||||
def to_html_string(element):
|
|
||||||
return _write_html(ElementTree(element).getroot(), format="html")
|
|
||||||
|
|
||||||
|
|
||||||
def to_xhtml_string(element):
|
|
||||||
return _write_html(ElementTree(element).getroot(), format="xhtml")
|
|
|
@ -1,220 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
import textwrap
|
|
||||||
from . import markdown, Markdown, util
|
|
||||||
|
|
||||||
try:
|
|
||||||
import tidylib
|
|
||||||
except ImportError:
|
|
||||||
tidylib = None
|
|
||||||
|
|
||||||
__all__ = ['TestCase', 'LegacyTestCase', 'Kwargs']
|
|
||||||
|
|
||||||
|
|
||||||
class TestCase(unittest.TestCase):
|
|
||||||
"""
|
|
||||||
A `unittest.TestCase` subclass with helpers for testing Markdown output.
|
|
||||||
|
|
||||||
Define `default_kwargs` as a dict of keywords to pass to Markdown for each
|
|
||||||
test. The defaults can be overridden on individual tests.
|
|
||||||
|
|
||||||
The `assertMarkdownRenders` method accepts the source text, the expected
|
|
||||||
output, and any keywords to pass to Markdown. The `default_kwargs` are used
|
|
||||||
except where overridden by `kwargs`. The output and expected output are passed
|
|
||||||
to `TestCase.assertMultiLineEqual`. An `AssertionError` is raised with a diff
|
|
||||||
if the actual output does not equal the expected output.
|
|
||||||
|
|
||||||
The `dedent` method is available to dedent triple-quoted strings if
|
|
||||||
necessary.
|
|
||||||
|
|
||||||
In all other respects, behaves as `unittest.TestCase`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
default_kwargs = {}
|
|
||||||
|
|
||||||
def assertMarkdownRenders(self, source, expected, expected_attrs=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Test that source Markdown text renders to expected output with given keywords.
|
|
||||||
|
|
||||||
`expected_attrs` accepts a dict. Each key should be the name of an attribute
|
|
||||||
on the `Markdown` instance and the value should be the expected value after
|
|
||||||
the source text is parsed by Markdown. After the expected output is tested,
|
|
||||||
the expected value for each attribute is compared against the actual
|
|
||||||
attribute of the `Markdown` instance using `TestCase.assertEqual`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
expected_attrs = expected_attrs or {}
|
|
||||||
kws = self.default_kwargs.copy()
|
|
||||||
kws.update(kwargs)
|
|
||||||
md = Markdown(**kws)
|
|
||||||
output = md.convert(source)
|
|
||||||
self.assertMultiLineEqual(output, expected)
|
|
||||||
for key, value in expected_attrs.items():
|
|
||||||
self.assertEqual(getattr(md, key), value)
|
|
||||||
|
|
||||||
def dedent(self, text):
|
|
||||||
"""
|
|
||||||
Dedent text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO: If/when actual output ends with a newline, then use:
|
|
||||||
# return textwrap.dedent(text.strip('/n'))
|
|
||||||
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 #
|
|
||||||
#########################
|
|
||||||
|
|
||||||
|
|
||||||
class Kwargs(dict):
|
|
||||||
""" A dict like class for holding keyword arguments. """
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_whitespace(text):
|
|
||||||
""" Normalize whitespace for a string of html using `tidylib`. """
|
|
||||||
output, errors = tidylib.tidy_fragment(text, options={
|
|
||||||
'drop_empty_paras': 0,
|
|
||||||
'fix_backslash': 0,
|
|
||||||
'fix_bad_comments': 0,
|
|
||||||
'fix_uri': 0,
|
|
||||||
'join_styles': 0,
|
|
||||||
'lower_literals': 0,
|
|
||||||
'merge_divs': 0,
|
|
||||||
'output_xhtml': 1,
|
|
||||||
'quote_ampersand': 0,
|
|
||||||
'newline': 'LF'
|
|
||||||
})
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyTestMeta(type):
|
|
||||||
def __new__(cls, name, bases, dct):
|
|
||||||
|
|
||||||
def generate_test(infile, outfile, normalize, kwargs):
|
|
||||||
def test(self):
|
|
||||||
with open(infile, encoding="utf-8") as f:
|
|
||||||
input = f.read()
|
|
||||||
with open(outfile, encoding="utf-8") as f:
|
|
||||||
# Normalize line endings
|
|
||||||
# (on Windows, git may have altered line endings).
|
|
||||||
expected = f.read().replace("\r\n", "\n")
|
|
||||||
output = markdown(input, **kwargs)
|
|
||||||
if tidylib and normalize:
|
|
||||||
try:
|
|
||||||
expected = _normalize_whitespace(expected)
|
|
||||||
output = _normalize_whitespace(output)
|
|
||||||
except OSError:
|
|
||||||
self.skipTest("Tidylib's c library not available.")
|
|
||||||
elif normalize:
|
|
||||||
self.skipTest('Tidylib not available.')
|
|
||||||
self.assertMultiLineEqual(output, expected)
|
|
||||||
return test
|
|
||||||
|
|
||||||
location = dct.get('location', '')
|
|
||||||
exclude = dct.get('exclude', [])
|
|
||||||
normalize = dct.get('normalize', False)
|
|
||||||
input_ext = dct.get('input_ext', '.txt')
|
|
||||||
output_ext = dct.get('output_ext', '.html')
|
|
||||||
kwargs = dct.get('default_kwargs', Kwargs())
|
|
||||||
|
|
||||||
if os.path.isdir(location):
|
|
||||||
for file in os.listdir(location):
|
|
||||||
infile = os.path.join(location, file)
|
|
||||||
if os.path.isfile(infile):
|
|
||||||
tname, ext = os.path.splitext(file)
|
|
||||||
if ext == input_ext:
|
|
||||||
outfile = os.path.join(location, tname + output_ext)
|
|
||||||
tname = tname.replace(' ', '_').replace('-', '_')
|
|
||||||
kws = kwargs.copy()
|
|
||||||
if tname in dct:
|
|
||||||
kws.update(dct[tname])
|
|
||||||
test_name = 'test_%s' % tname
|
|
||||||
if tname not in exclude:
|
|
||||||
dct[test_name] = generate_test(infile, outfile, normalize, kws)
|
|
||||||
else:
|
|
||||||
dct[test_name] = unittest.skip('Excluded')(lambda: None)
|
|
||||||
|
|
||||||
return type.__new__(cls, name, bases, dct)
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyTestCase(unittest.TestCase, metaclass=LegacyTestMeta):
|
|
||||||
"""
|
|
||||||
A `unittest.TestCase` subclass for running Markdown's legacy file-based tests.
|
|
||||||
|
|
||||||
A subclass should define various properties which point to a directory of
|
|
||||||
text-based test files and define various behaviors/defaults for those tests.
|
|
||||||
The following properties are supported:
|
|
||||||
|
|
||||||
`location`: A path to the directory of test files. An absolute path is preferred.
|
|
||||||
`exclude`: A list of tests to exclude. Each test name should comprise the filename
|
|
||||||
without an extension.
|
|
||||||
`normalize`: A boolean value indicating if the HTML should be normalized.
|
|
||||||
Default: `False`.
|
|
||||||
`input_ext`: A string containing the file extension of input files. Default: `.txt`.
|
|
||||||
`output_ext`: A string containing the file extension of expected output files.
|
|
||||||
Default: `html`.
|
|
||||||
`default_kwargs`: A `Kwargs` instance which stores the default set of keyword
|
|
||||||
arguments for all test files in the directory.
|
|
||||||
|
|
||||||
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 without the file
|
|
||||||
extension. Any spaces and dashes in the filename should be replaced with
|
|
||||||
underscores. The value of the property should be a `Kwargs` instance which
|
|
||||||
contains the keyword arguments that should be passed to `Markdown` for that
|
|
||||||
test file. The keyword arguments will "update" the `default_kwargs`.
|
|
||||||
|
|
||||||
When the class instance is created, it will walk the given directory and create
|
|
||||||
a separate `Unitttest` for each set of test files using the naming scheme:
|
|
||||||
`test_filename`. One `Unittest` will be run for each set of input and output files.
|
|
||||||
"""
|
|
||||||
pass
|
|
|
@ -1,458 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
from . import util
|
|
||||||
from . import inlinepatterns
|
|
||||||
|
|
||||||
|
|
||||||
def build_treeprocessors(md, **kwargs):
|
|
||||||
""" Build the default `treeprocessors` for Markdown. """
|
|
||||||
treeprocessors = util.Registry()
|
|
||||||
treeprocessors.register(InlineProcessor(md), 'inline', 20)
|
|
||||||
treeprocessors.register(PrettifyTreeprocessor(md), 'prettify', 10)
|
|
||||||
treeprocessors.register(UnescapeTreeprocessor(md), 'unescape', 0)
|
|
||||||
return treeprocessors
|
|
||||||
|
|
||||||
|
|
||||||
def isString(s):
|
|
||||||
""" Check if it's string """
|
|
||||||
if not isinstance(s, util.AtomicString):
|
|
||||||
return isinstance(s, str)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Treeprocessor(util.Processor):
|
|
||||||
"""
|
|
||||||
`Treeprocessor`s are run on the `ElementTree` object before serialization.
|
|
||||||
|
|
||||||
Each `Treeprocessor` implements a `run` method that takes a pointer to an
|
|
||||||
`ElementTree`, modifies it as necessary and returns an `ElementTree`
|
|
||||||
object.
|
|
||||||
|
|
||||||
`Treeprocessors` must extend `markdown.Treeprocessor`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def run(self, root):
|
|
||||||
"""
|
|
||||||
Subclasses of `Treeprocessor` should implement a `run` method, which
|
|
||||||
takes a root `ElementTree`. This method can return another `ElementTree`
|
|
||||||
object, and the existing root `ElementTree `will be replaced, or it can
|
|
||||||
modify the current tree and return `None`.
|
|
||||||
"""
|
|
||||||
pass # pragma: no cover
|
|
||||||
|
|
||||||
|
|
||||||
class InlineProcessor(Treeprocessor):
|
|
||||||
"""
|
|
||||||
A `Treeprocessor` that traverses a tree, applying inline patterns.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, md):
|
|
||||||
self.__placeholder_prefix = util.INLINE_PLACEHOLDER_PREFIX
|
|
||||||
self.__placeholder_suffix = util.ETX
|
|
||||||
self.__placeholder_length = 4 + len(self.__placeholder_prefix) \
|
|
||||||
+ len(self.__placeholder_suffix)
|
|
||||||
self.__placeholder_re = util.INLINE_PLACEHOLDER_RE
|
|
||||||
self.md = md
|
|
||||||
self.inlinePatterns = md.inlinePatterns
|
|
||||||
self.ancestors = []
|
|
||||||
|
|
||||||
def __makePlaceholder(self, type):
|
|
||||||
""" Generate a placeholder """
|
|
||||||
id = "%04d" % len(self.stashed_nodes)
|
|
||||||
hash = util.INLINE_PLACEHOLDER % id
|
|
||||||
return hash, id
|
|
||||||
|
|
||||||
def __findPlaceholder(self, data, index):
|
|
||||||
"""
|
|
||||||
Extract id from data string, start from index
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `data`: string
|
|
||||||
* `index`: index, from which we start search
|
|
||||||
|
|
||||||
Returns: placeholder id and string index, after the found placeholder.
|
|
||||||
|
|
||||||
"""
|
|
||||||
m = self.__placeholder_re.search(data, index)
|
|
||||||
if m:
|
|
||||||
return m.group(1), m.end()
|
|
||||||
else:
|
|
||||||
return None, index + 1
|
|
||||||
|
|
||||||
def __stashNode(self, node, type):
|
|
||||||
""" Add node to stash """
|
|
||||||
placeholder, id = self.__makePlaceholder(type)
|
|
||||||
self.stashed_nodes[id] = node
|
|
||||||
return placeholder
|
|
||||||
|
|
||||||
def __handleInline(self, data, patternIndex=0):
|
|
||||||
"""
|
|
||||||
Process string with inline patterns and replace it
|
|
||||||
with placeholders
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `data`: A line of Markdown text
|
|
||||||
* `patternIndex`: The index of the `inlinePattern` to start with
|
|
||||||
|
|
||||||
Returns: String with placeholders.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not isinstance(data, util.AtomicString):
|
|
||||||
startIndex = 0
|
|
||||||
count = len(self.inlinePatterns)
|
|
||||||
while patternIndex < count:
|
|
||||||
data, matched, startIndex = self.__applyPattern(
|
|
||||||
self.inlinePatterns[patternIndex], data, patternIndex, startIndex
|
|
||||||
)
|
|
||||||
if not matched:
|
|
||||||
patternIndex += 1
|
|
||||||
return data
|
|
||||||
|
|
||||||
def __processElementText(self, node, subnode, isText=True):
|
|
||||||
"""
|
|
||||||
Process placeholders in `Element.text` or `Element.tail`
|
|
||||||
of Elements popped from `self.stashed_nodes`.
|
|
||||||
|
|
||||||
Keywords arguments:
|
|
||||||
|
|
||||||
* `node`: parent node
|
|
||||||
* `subnode`: processing node
|
|
||||||
* `isText`: boolean variable, True - it's text, False - it's a tail
|
|
||||||
|
|
||||||
Returns: None
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isText:
|
|
||||||
text = subnode.text
|
|
||||||
subnode.text = None
|
|
||||||
else:
|
|
||||||
text = subnode.tail
|
|
||||||
subnode.tail = None
|
|
||||||
|
|
||||||
childResult = self.__processPlaceholders(text, subnode, isText)
|
|
||||||
|
|
||||||
if not isText and node is not subnode:
|
|
||||||
pos = list(node).index(subnode) + 1
|
|
||||||
else:
|
|
||||||
pos = 0
|
|
||||||
|
|
||||||
childResult.reverse()
|
|
||||||
for newChild in childResult:
|
|
||||||
node.insert(pos, newChild[0])
|
|
||||||
|
|
||||||
def __processPlaceholders(self, data, parent, isText=True):
|
|
||||||
"""
|
|
||||||
Process string with placeholders and generate `ElementTree` tree.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `data`: string with placeholders instead of `ElementTree` elements.
|
|
||||||
* `parent`: Element, which contains processing inline data
|
|
||||||
|
|
||||||
Returns: list with `ElementTree` elements with applied inline patterns.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def linkText(text):
|
|
||||||
if text:
|
|
||||||
if result:
|
|
||||||
if result[-1][0].tail:
|
|
||||||
result[-1][0].tail += text
|
|
||||||
else:
|
|
||||||
result[-1][0].tail = text
|
|
||||||
elif not isText:
|
|
||||||
if parent.tail:
|
|
||||||
parent.tail += text
|
|
||||||
else:
|
|
||||||
parent.tail = text
|
|
||||||
else:
|
|
||||||
if parent.text:
|
|
||||||
parent.text += text
|
|
||||||
else:
|
|
||||||
parent.text = text
|
|
||||||
result = []
|
|
||||||
strartIndex = 0
|
|
||||||
while data:
|
|
||||||
index = data.find(self.__placeholder_prefix, strartIndex)
|
|
||||||
if index != -1:
|
|
||||||
id, phEndIndex = self.__findPlaceholder(data, index)
|
|
||||||
|
|
||||||
if id in self.stashed_nodes:
|
|
||||||
node = self.stashed_nodes.get(id)
|
|
||||||
|
|
||||||
if index > 0:
|
|
||||||
text = data[strartIndex:index]
|
|
||||||
linkText(text)
|
|
||||||
|
|
||||||
if not isString(node): # it's Element
|
|
||||||
for child in [node] + list(node):
|
|
||||||
if child.tail:
|
|
||||||
if child.tail.strip():
|
|
||||||
self.__processElementText(
|
|
||||||
node, child, False
|
|
||||||
)
|
|
||||||
if child.text:
|
|
||||||
if child.text.strip():
|
|
||||||
self.__processElementText(child, child)
|
|
||||||
else: # it's just a string
|
|
||||||
linkText(node)
|
|
||||||
strartIndex = phEndIndex
|
|
||||||
continue
|
|
||||||
|
|
||||||
strartIndex = phEndIndex
|
|
||||||
result.append((node, self.ancestors[:]))
|
|
||||||
|
|
||||||
else: # wrong placeholder
|
|
||||||
end = index + len(self.__placeholder_prefix)
|
|
||||||
linkText(data[strartIndex:end])
|
|
||||||
strartIndex = end
|
|
||||||
else:
|
|
||||||
text = data[strartIndex:]
|
|
||||||
if isinstance(data, util.AtomicString):
|
|
||||||
# We don't want to loose the `AtomicString`
|
|
||||||
text = util.AtomicString(text)
|
|
||||||
linkText(text)
|
|
||||||
data = ""
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __applyPattern(self, pattern, data, patternIndex, startIndex=0):
|
|
||||||
"""
|
|
||||||
Check if the line fits the pattern, create the necessary
|
|
||||||
elements, add it to `stashed_nodes`.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `data`: the text to be processed
|
|
||||||
* `pattern`: the pattern to be checked
|
|
||||||
* `patternIndex`: index of current pattern
|
|
||||||
* `startIndex`: string index, from which we start searching
|
|
||||||
|
|
||||||
Returns: String with placeholders instead of `ElementTree` elements.
|
|
||||||
|
|
||||||
"""
|
|
||||||
new_style = isinstance(pattern, inlinepatterns.InlineProcessor)
|
|
||||||
|
|
||||||
for exclude in pattern.ANCESTOR_EXCLUDES:
|
|
||||||
if exclude.lower() in self.ancestors:
|
|
||||||
return data, False, 0
|
|
||||||
|
|
||||||
if new_style:
|
|
||||||
match = None
|
|
||||||
# Since `handleMatch` may reject our first match,
|
|
||||||
# we iterate over the buffer looking for matches
|
|
||||||
# until we can't find any more.
|
|
||||||
for match in pattern.getCompiledRegExp().finditer(data, startIndex):
|
|
||||||
node, start, end = pattern.handleMatch(match, data)
|
|
||||||
if start is None or end is None:
|
|
||||||
startIndex += match.end(0)
|
|
||||||
match = None
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
else: # pragma: no cover
|
|
||||||
match = pattern.getCompiledRegExp().match(data[startIndex:])
|
|
||||||
leftData = data[:startIndex]
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
return data, False, 0
|
|
||||||
|
|
||||||
if not new_style: # pragma: no cover
|
|
||||||
node = pattern.handleMatch(match)
|
|
||||||
start = match.start(0)
|
|
||||||
end = match.end(0)
|
|
||||||
|
|
||||||
if node is None:
|
|
||||||
return data, True, end
|
|
||||||
|
|
||||||
if not isString(node):
|
|
||||||
if not isinstance(node.text, util.AtomicString):
|
|
||||||
# We need to process current node too
|
|
||||||
for child in [node] + list(node):
|
|
||||||
if not isString(node):
|
|
||||||
if child.text:
|
|
||||||
self.ancestors.append(child.tag.lower())
|
|
||||||
child.text = self.__handleInline(
|
|
||||||
child.text, patternIndex + 1
|
|
||||||
)
|
|
||||||
self.ancestors.pop()
|
|
||||||
if child.tail:
|
|
||||||
child.tail = self.__handleInline(
|
|
||||||
child.tail, patternIndex
|
|
||||||
)
|
|
||||||
|
|
||||||
placeholder = self.__stashNode(node, pattern.type())
|
|
||||||
|
|
||||||
if new_style:
|
|
||||||
return "{}{}{}".format(data[:start],
|
|
||||||
placeholder, data[end:]), True, 0
|
|
||||||
else: # pragma: no cover
|
|
||||||
return "{}{}{}{}".format(leftData,
|
|
||||||
match.group(1),
|
|
||||||
placeholder, match.groups()[-1]), True, 0
|
|
||||||
|
|
||||||
def __build_ancestors(self, parent, parents):
|
|
||||||
"""Build the ancestor list."""
|
|
||||||
ancestors = []
|
|
||||||
while parent is not None:
|
|
||||||
if parent is not None:
|
|
||||||
ancestors.append(parent.tag.lower())
|
|
||||||
parent = self.parent_map.get(parent)
|
|
||||||
ancestors.reverse()
|
|
||||||
parents.extend(ancestors)
|
|
||||||
|
|
||||||
def run(self, tree, ancestors=None):
|
|
||||||
"""Apply inline patterns to a parsed Markdown tree.
|
|
||||||
|
|
||||||
Iterate over `ElementTree`, find elements with inline tag, apply inline
|
|
||||||
patterns and append newly created Elements to tree. If you don't
|
|
||||||
want to process your data with inline patterns, instead of normal
|
|
||||||
string, use subclass `AtomicString`:
|
|
||||||
|
|
||||||
node.text = markdown.AtomicString("This will not be processed.")
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
|
|
||||||
* `tree`: `ElementTree` object, representing Markdown tree.
|
|
||||||
* `ancestors`: List of parent tag names that precede the tree node (if needed).
|
|
||||||
|
|
||||||
Returns: `ElementTree` object with applied inline patterns.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.stashed_nodes = {}
|
|
||||||
|
|
||||||
# Ensure a valid parent list, but copy passed in lists
|
|
||||||
# to ensure we don't have the user accidentally change it on us.
|
|
||||||
tree_parents = [] if ancestors is None else ancestors[:]
|
|
||||||
|
|
||||||
self.parent_map = {c: p for p in tree.iter() for c in p}
|
|
||||||
stack = [(tree, tree_parents)]
|
|
||||||
|
|
||||||
while stack:
|
|
||||||
currElement, parents = stack.pop()
|
|
||||||
|
|
||||||
self.ancestors = parents
|
|
||||||
self.__build_ancestors(currElement, self.ancestors)
|
|
||||||
|
|
||||||
insertQueue = []
|
|
||||||
for child in currElement:
|
|
||||||
if child.text and not isinstance(
|
|
||||||
child.text, util.AtomicString
|
|
||||||
):
|
|
||||||
self.ancestors.append(child.tag.lower())
|
|
||||||
text = child.text
|
|
||||||
child.text = None
|
|
||||||
lst = self.__processPlaceholders(
|
|
||||||
self.__handleInline(text), child
|
|
||||||
)
|
|
||||||
for item in lst:
|
|
||||||
self.parent_map[item[0]] = child
|
|
||||||
stack += lst
|
|
||||||
insertQueue.append((child, lst))
|
|
||||||
self.ancestors.pop()
|
|
||||||
if child.tail:
|
|
||||||
tail = self.__handleInline(child.tail)
|
|
||||||
dumby = etree.Element('d')
|
|
||||||
child.tail = None
|
|
||||||
tailResult = self.__processPlaceholders(tail, dumby, False)
|
|
||||||
if dumby.tail:
|
|
||||||
child.tail = dumby.tail
|
|
||||||
pos = list(currElement).index(child) + 1
|
|
||||||
tailResult.reverse()
|
|
||||||
for newChild in tailResult:
|
|
||||||
self.parent_map[newChild[0]] = currElement
|
|
||||||
currElement.insert(pos, newChild[0])
|
|
||||||
if len(child):
|
|
||||||
self.parent_map[child] = currElement
|
|
||||||
stack.append((child, self.ancestors[:]))
|
|
||||||
|
|
||||||
for element, lst in insertQueue:
|
|
||||||
for i, obj in enumerate(lst):
|
|
||||||
newChild = obj[0]
|
|
||||||
element.insert(i, newChild)
|
|
||||||
return tree
|
|
||||||
|
|
||||||
|
|
||||||
class PrettifyTreeprocessor(Treeprocessor):
|
|
||||||
""" Add line breaks to the html document. """
|
|
||||||
|
|
||||||
def _prettifyETree(self, elem):
|
|
||||||
""" Recursively add line breaks to `ElementTree` children. """
|
|
||||||
|
|
||||||
i = "\n"
|
|
||||||
if self.md.is_block_level(elem.tag) and elem.tag not in ['code', 'pre']:
|
|
||||||
if (not elem.text or not elem.text.strip()) \
|
|
||||||
and len(elem) and self.md.is_block_level(elem[0].tag):
|
|
||||||
elem.text = i
|
|
||||||
for e in elem:
|
|
||||||
if self.md.is_block_level(e.tag):
|
|
||||||
self._prettifyETree(e)
|
|
||||||
if not elem.tail or not elem.tail.strip():
|
|
||||||
elem.tail = i
|
|
||||||
|
|
||||||
def run(self, root):
|
|
||||||
""" Add line breaks to `ElementTree` root object. """
|
|
||||||
|
|
||||||
self._prettifyETree(root)
|
|
||||||
# Do `<br />`'s separately as they are often in the middle of
|
|
||||||
# inline content and missed by `_prettifyETree`.
|
|
||||||
brs = root.iter('br')
|
|
||||||
for br in brs:
|
|
||||||
if not br.tail or not br.tail.strip():
|
|
||||||
br.tail = '\n'
|
|
||||||
else:
|
|
||||||
br.tail = '\n%s' % br.tail
|
|
||||||
# Clean up extra empty lines at end of code blocks.
|
|
||||||
pres = root.iter('pre')
|
|
||||||
for pre in pres:
|
|
||||||
if len(pre) and pre[0].tag == 'code':
|
|
||||||
code = pre[0]
|
|
||||||
# Only prettify code containing text only
|
|
||||||
if not len(code) and code.text is not None:
|
|
||||||
code.text = util.AtomicString(code.text.rstrip() + '\n')
|
|
||||||
|
|
||||||
|
|
||||||
class UnescapeTreeprocessor(Treeprocessor):
|
|
||||||
""" Restore escaped chars """
|
|
||||||
|
|
||||||
RE = re.compile(r'{}(\d+){}'.format(util.STX, util.ETX))
|
|
||||||
|
|
||||||
def _unescape(self, m):
|
|
||||||
return chr(int(m.group(1)))
|
|
||||||
|
|
||||||
def unescape(self, text):
|
|
||||||
return self.RE.sub(self._unescape, text)
|
|
||||||
|
|
||||||
def run(self, root):
|
|
||||||
""" Loop over all elements and unescape all text. """
|
|
||||||
for elem in root.iter():
|
|
||||||
# Unescape text content
|
|
||||||
if elem.text and not elem.tag == 'code':
|
|
||||||
elem.text = self.unescape(elem.text)
|
|
||||||
# Unescape tail content
|
|
||||||
if elem.tail:
|
|
||||||
elem.tail = self.unescape(elem.tail)
|
|
||||||
# Unescape attribute values
|
|
||||||
for key, value in elem.items():
|
|
||||||
elem.set(key, self.unescape(value))
|
|
|
@ -1,358 +0,0 @@
|
||||||
"""
|
|
||||||
Python Markdown
|
|
||||||
|
|
||||||
A Python implementation of John Gruber's Markdown.
|
|
||||||
|
|
||||||
Documentation: https://python-markdown.github.io/
|
|
||||||
GitHub: https://github.com/Python-Markdown/markdown/
|
|
||||||
PyPI: https://pypi.org/project/Markdown/
|
|
||||||
|
|
||||||
Started by Manfred Stienstra (http://www.dwerg.net/).
|
|
||||||
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
|
||||||
Currently maintained by Waylan Limberg (https://github.com/waylan),
|
|
||||||
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
|
||||||
|
|
||||||
Copyright 2007-2018 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)
|
|
||||||
|
|
||||||
License: BSD (see LICENSE.md for details).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import warnings
|
|
||||||
from collections import namedtuple
|
|
||||||
from functools import wraps, lru_cache
|
|
||||||
from itertools import count
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Constants you might want to modify
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
BLOCK_LEVEL_ELEMENTS = [
|
|
||||||
# Elements which are invalid to wrap in a `<p>` tag.
|
|
||||||
# See https://w3c.github.io/html/grouping-content.html#the-p-element
|
|
||||||
'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl',
|
|
||||||
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
|
|
||||||
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol',
|
|
||||||
'p', 'pre', 'section', 'table', 'ul',
|
|
||||||
# Other elements which Markdown should not be mucking up the contents of.
|
|
||||||
'canvas', 'colgroup', 'dd', 'body', 'dt', 'group', 'html', 'iframe', 'li', 'legend',
|
|
||||||
'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script',
|
|
||||||
'style', 'summary', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video'
|
|
||||||
]
|
|
||||||
|
|
||||||
# Placeholders
|
|
||||||
STX = '\u0002' # Use `STX` ("Start of text") for start-of-placeholder
|
|
||||||
ETX = '\u0003' # Use `ETX` ("End of text") for end-of-placeholder
|
|
||||||
INLINE_PLACEHOLDER_PREFIX = STX+"klzzwxh:"
|
|
||||||
INLINE_PLACEHOLDER = INLINE_PLACEHOLDER_PREFIX + "%s" + ETX
|
|
||||||
INLINE_PLACEHOLDER_RE = re.compile(INLINE_PLACEHOLDER % r'([0-9]+)')
|
|
||||||
AMP_SUBSTITUTE = STX+"amp"+ETX
|
|
||||||
HTML_PLACEHOLDER = STX + "wzxhzdk:%s" + ETX
|
|
||||||
HTML_PLACEHOLDER_RE = re.compile(HTML_PLACEHOLDER % r'([0-9]+)')
|
|
||||||
TAG_PLACEHOLDER = STX + "hzzhzkh:%s" + ETX
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Constants you probably do not need to change
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
"""
|
|
||||||
|
|
||||||
RTL_BIDI_RANGES = (
|
|
||||||
('\u0590', '\u07FF'),
|
|
||||||
# Hebrew (0590-05FF), Arabic (0600-06FF),
|
|
||||||
# Syriac (0700-074F), Arabic supplement (0750-077F),
|
|
||||||
# Thaana (0780-07BF), Nko (07C0-07FF).
|
|
||||||
('\u2D30', '\u2D7F') # Tifinagh
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
AUXILIARY GLOBAL FUNCTIONS
|
|
||||||
=============================================================================
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=None)
|
|
||||||
def get_installed_extensions():
|
|
||||||
if sys.version_info >= (3, 10):
|
|
||||||
from importlib import metadata
|
|
||||||
else: # `<PY310` use backport
|
|
||||||
import importlib_metadata as metadata
|
|
||||||
# Only load extension entry_points once.
|
|
||||||
return metadata.entry_points(group='markdown.extensions')
|
|
||||||
|
|
||||||
|
|
||||||
def deprecated(message, stacklevel=2):
|
|
||||||
"""
|
|
||||||
Raise a `DeprecationWarning` when wrapped function/method is called.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
@deprecated("This method will be removed in version X; use Y instead.")
|
|
||||||
def some_method()"
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
def wrapper(func):
|
|
||||||
@wraps(func)
|
|
||||||
def deprecated_func(*args, **kwargs):
|
|
||||||
warnings.warn(
|
|
||||||
f"'{func.__name__}' is deprecated. {message}",
|
|
||||||
category=DeprecationWarning,
|
|
||||||
stacklevel=stacklevel
|
|
||||||
)
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
return deprecated_func
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def parseBoolValue(value, fail_on_errors=True, preserve_none=False):
|
|
||||||
"""Parses a string representing boolean value. If parsing was successful,
|
|
||||||
returns True or False. If `preserve_none=True`, returns `True`, `False`,
|
|
||||||
or `None`. If parsing was not successful, raises `ValueError`, or, if
|
|
||||||
`fail_on_errors=False`, returns `None`."""
|
|
||||||
if not isinstance(value, str):
|
|
||||||
if preserve_none and value is None:
|
|
||||||
return value
|
|
||||||
return bool(value)
|
|
||||||
elif preserve_none and value.lower() == 'none':
|
|
||||||
return None
|
|
||||||
elif value.lower() in ('true', 'yes', 'y', 'on', '1'):
|
|
||||||
return True
|
|
||||||
elif value.lower() in ('false', 'no', 'n', 'off', '0', 'none'):
|
|
||||||
return False
|
|
||||||
elif fail_on_errors:
|
|
||||||
raise ValueError('Cannot parse bool value: %r' % value)
|
|
||||||
|
|
||||||
|
|
||||||
def code_escape(text):
|
|
||||||
"""Escape code."""
|
|
||||||
if "&" in text:
|
|
||||||
text = text.replace("&", "&")
|
|
||||||
if "<" in text:
|
|
||||||
text = text.replace("<", "<")
|
|
||||||
if ">" in text:
|
|
||||||
text = text.replace(">", ">")
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def _get_stack_depth(size=2):
|
|
||||||
"""Get current stack depth, performantly.
|
|
||||||
"""
|
|
||||||
frame = sys._getframe(size)
|
|
||||||
|
|
||||||
for size in count(size):
|
|
||||||
frame = frame.f_back
|
|
||||||
if not frame:
|
|
||||||
return size
|
|
||||||
|
|
||||||
|
|
||||||
def nearing_recursion_limit():
|
|
||||||
"""Return true if current stack depth is within 100 of maximum limit."""
|
|
||||||
return sys.getrecursionlimit() - _get_stack_depth() < 100
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
MISC AUXILIARY CLASSES
|
|
||||||
=============================================================================
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class AtomicString(str):
|
|
||||||
"""A string which should not be further processed."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Processor:
|
|
||||||
def __init__(self, md=None):
|
|
||||||
self.md = md
|
|
||||||
|
|
||||||
|
|
||||||
class HtmlStash:
|
|
||||||
"""
|
|
||||||
This class is used for stashing HTML objects that we extract
|
|
||||||
in the beginning and replace with place-holders.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
""" Create an `HtmlStash`. """
|
|
||||||
self.html_counter = 0 # for counting inline html segments
|
|
||||||
self.rawHtmlBlocks = []
|
|
||||||
self.tag_counter = 0
|
|
||||||
self.tag_data = [] # list of dictionaries in the order tags appear
|
|
||||||
|
|
||||||
def store(self, html):
|
|
||||||
"""
|
|
||||||
Saves an HTML segment for later reinsertion. Returns a
|
|
||||||
placeholder string that needs to be inserted into the
|
|
||||||
document.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
|
|
||||||
* `html`: an html segment
|
|
||||||
|
|
||||||
Returns : a placeholder string
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.rawHtmlBlocks.append(html)
|
|
||||||
placeholder = self.get_placeholder(self.html_counter)
|
|
||||||
self.html_counter += 1
|
|
||||||
return placeholder
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.html_counter = 0
|
|
||||||
self.rawHtmlBlocks = []
|
|
||||||
|
|
||||||
def get_placeholder(self, key):
|
|
||||||
return HTML_PLACEHOLDER % key
|
|
||||||
|
|
||||||
def store_tag(self, tag, attrs, left_index, right_index):
|
|
||||||
"""Store tag data and return a placeholder."""
|
|
||||||
self.tag_data.append({'tag': tag, 'attrs': attrs,
|
|
||||||
'left_index': left_index,
|
|
||||||
'right_index': right_index})
|
|
||||||
placeholder = TAG_PLACEHOLDER % str(self.tag_counter)
|
|
||||||
self.tag_counter += 1 # equal to the tag's index in `self.tag_data`
|
|
||||||
return placeholder
|
|
||||||
|
|
||||||
|
|
||||||
# Used internally by `Registry` for each item in its sorted list.
|
|
||||||
# Provides an easier to read API when editing the code later.
|
|
||||||
# For example, `item.name` is more clear than `item[0]`.
|
|
||||||
_PriorityItem = namedtuple('PriorityItem', ['name', 'priority'])
|
|
||||||
|
|
||||||
|
|
||||||
class Registry:
|
|
||||||
"""
|
|
||||||
A priority sorted registry.
|
|
||||||
|
|
||||||
A `Registry` instance provides two public methods to alter the data of the
|
|
||||||
registry: `register` and `deregister`. Use `register` to add items and
|
|
||||||
`deregister` to remove items. See each method for specifics.
|
|
||||||
|
|
||||||
When registering an item, a "name" and a "priority" must be provided. All
|
|
||||||
items are automatically sorted by "priority" from highest to lowest. The
|
|
||||||
"name" is used to remove ("deregister") and get items.
|
|
||||||
|
|
||||||
A `Registry` instance it like a list (which maintains order) when reading
|
|
||||||
data. You may iterate over the items, get an item and get a count (length)
|
|
||||||
of all items. You may also check that the registry contains an item.
|
|
||||||
|
|
||||||
When getting an item you may use either the index of the item or the
|
|
||||||
string-based "name". For example:
|
|
||||||
|
|
||||||
registry = Registry()
|
|
||||||
registry.register(SomeItem(), 'itemname', 20)
|
|
||||||
# Get the item by index
|
|
||||||
item = registry[0]
|
|
||||||
# Get the item by name
|
|
||||||
item = registry['itemname']
|
|
||||||
|
|
||||||
When checking that the registry contains an item, you may use either the
|
|
||||||
string-based "name", or a reference to the actual item. For example:
|
|
||||||
|
|
||||||
someitem = SomeItem()
|
|
||||||
registry.register(someitem, 'itemname', 20)
|
|
||||||
# Contains the name
|
|
||||||
assert 'itemname' in registry
|
|
||||||
# Contains the item instance
|
|
||||||
assert someitem in registry
|
|
||||||
|
|
||||||
The method `get_index_for_name` is also available to obtain the index of
|
|
||||||
an item using that item's assigned "name".
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._data = {}
|
|
||||||
self._priority = []
|
|
||||||
self._is_sorted = False
|
|
||||||
|
|
||||||
def __contains__(self, item):
|
|
||||||
if isinstance(item, str):
|
|
||||||
# Check if an item exists by this name.
|
|
||||||
return item in self._data.keys()
|
|
||||||
# Check if this instance exists.
|
|
||||||
return item in self._data.values()
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
self._sort()
|
|
||||||
return iter([self._data[k] for k, p in self._priority])
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
self._sort()
|
|
||||||
if isinstance(key, slice):
|
|
||||||
data = Registry()
|
|
||||||
for k, p in self._priority[key]:
|
|
||||||
data.register(self._data[k], k, p)
|
|
||||||
return data
|
|
||||||
if isinstance(key, int):
|
|
||||||
return self._data[self._priority[key].name]
|
|
||||||
return self._data[key]
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._priority)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<{}({})>'.format(self.__class__.__name__, list(self))
|
|
||||||
|
|
||||||
def get_index_for_name(self, name):
|
|
||||||
"""
|
|
||||||
Return the index of the given name.
|
|
||||||
"""
|
|
||||||
if name in self:
|
|
||||||
self._sort()
|
|
||||||
return self._priority.index(
|
|
||||||
[x for x in self._priority if x.name == name][0]
|
|
||||||
)
|
|
||||||
raise ValueError('No item named "{}" exists.'.format(name))
|
|
||||||
|
|
||||||
def register(self, item, name, priority):
|
|
||||||
"""
|
|
||||||
Add an item to the registry with the given name and priority.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
|
|
||||||
* `item`: The item being registered.
|
|
||||||
* `name`: A string used to reference the item.
|
|
||||||
* `priority`: An integer or float used to sort against all items.
|
|
||||||
|
|
||||||
If an item is registered with a "name" which already exists, the
|
|
||||||
existing item is replaced with the new item. Treat carefully as the
|
|
||||||
old item is lost with no way to recover it. The new item will be
|
|
||||||
sorted according to its priority and will **not** retain the position
|
|
||||||
of the old item.
|
|
||||||
"""
|
|
||||||
if name in self:
|
|
||||||
# Remove existing item of same name first
|
|
||||||
self.deregister(name)
|
|
||||||
self._is_sorted = False
|
|
||||||
self._data[name] = item
|
|
||||||
self._priority.append(_PriorityItem(name, priority))
|
|
||||||
|
|
||||||
def deregister(self, name, strict=True):
|
|
||||||
"""
|
|
||||||
Remove an item from the registry.
|
|
||||||
|
|
||||||
Set `strict=False` to fail silently.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
index = self.get_index_for_name(name)
|
|
||||||
del self._priority[index]
|
|
||||||
del self._data[name]
|
|
||||||
except ValueError:
|
|
||||||
if strict:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _sort(self):
|
|
||||||
"""
|
|
||||||
Sort the registry by priority from highest to lowest.
|
|
||||||
|
|
||||||
This method is called internally and should never be explicitly called.
|
|
||||||
"""
|
|
||||||
if not self._is_sorted:
|
|
||||||
self._priority.sort(key=lambda item: item.priority, reverse=True)
|
|
||||||
self._is_sorted = True
|
|
|
@ -1,738 +0,0 @@
|
||||||
@Switch01
|
|
||||||
A_Rog
|
|
||||||
Aakanksha Agrawal
|
|
||||||
Abhinav Sagar
|
|
||||||
ABHYUDAY PRATAP SINGH
|
|
||||||
abs51295
|
|
||||||
AceGentile
|
|
||||||
Adam Chainz
|
|
||||||
Adam Tse
|
|
||||||
Adam Wentz
|
|
||||||
admin
|
|
||||||
Adrien Morison
|
|
||||||
ahayrapetyan
|
|
||||||
Ahilya
|
|
||||||
AinsworthK
|
|
||||||
Akash Srivastava
|
|
||||||
Alan Yee
|
|
||||||
Albert Tugushev
|
|
||||||
Albert-Guan
|
|
||||||
albertg
|
|
||||||
Alberto Sottile
|
|
||||||
Aleks Bunin
|
|
||||||
Alethea Flowers
|
|
||||||
Alex Gaynor
|
|
||||||
Alex Grönholm
|
|
||||||
Alex Hedges
|
|
||||||
Alex Loosley
|
|
||||||
Alex Morega
|
|
||||||
Alex Stachowiak
|
|
||||||
Alexander Shtyrov
|
|
||||||
Alexandre Conrad
|
|
||||||
Alexey Popravka
|
|
||||||
Alli
|
|
||||||
Ami Fischman
|
|
||||||
Ananya Maiti
|
|
||||||
Anatoly Techtonik
|
|
||||||
Anders Kaseorg
|
|
||||||
Andre Aguiar
|
|
||||||
Andreas Lutro
|
|
||||||
Andrei Geacar
|
|
||||||
Andrew Gaul
|
|
||||||
Andrew Shymanel
|
|
||||||
Andrey Bienkowski
|
|
||||||
Andrey Bulgakov
|
|
||||||
Andrés Delfino
|
|
||||||
Andy Freeland
|
|
||||||
Andy Kluger
|
|
||||||
Ani Hayrapetyan
|
|
||||||
Aniruddha Basak
|
|
||||||
Anish Tambe
|
|
||||||
Anrs Hu
|
|
||||||
Anthony Sottile
|
|
||||||
Antoine Musso
|
|
||||||
Anton Ovchinnikov
|
|
||||||
Anton Patrushev
|
|
||||||
Antonio Alvarado Hernandez
|
|
||||||
Antony Lee
|
|
||||||
Antti Kaihola
|
|
||||||
Anubhav Patel
|
|
||||||
Anudit Nagar
|
|
||||||
Anuj Godase
|
|
||||||
AQNOUCH Mohammed
|
|
||||||
AraHaan
|
|
||||||
Arindam Choudhury
|
|
||||||
Armin Ronacher
|
|
||||||
Artem
|
|
||||||
Arun Babu Neelicattu
|
|
||||||
Ashley Manton
|
|
||||||
Ashwin Ramaswami
|
|
||||||
atse
|
|
||||||
Atsushi Odagiri
|
|
||||||
Avinash Karhana
|
|
||||||
Avner Cohen
|
|
||||||
Awit (Ah-Wit) Ghirmai
|
|
||||||
Baptiste Mispelon
|
|
||||||
Barney Gale
|
|
||||||
barneygale
|
|
||||||
Bartek Ogryczak
|
|
||||||
Bastian Venthur
|
|
||||||
Ben Bodenmiller
|
|
||||||
Ben Darnell
|
|
||||||
Ben Hoyt
|
|
||||||
Ben Mares
|
|
||||||
Ben Rosser
|
|
||||||
Bence Nagy
|
|
||||||
Benjamin Peterson
|
|
||||||
Benjamin VanEvery
|
|
||||||
Benoit Pierre
|
|
||||||
Berker Peksag
|
|
||||||
Bernard
|
|
||||||
Bernard Tyers
|
|
||||||
Bernardo B. Marques
|
|
||||||
Bernhard M. Wiedemann
|
|
||||||
Bertil Hatt
|
|
||||||
Bhavam Vidyarthi
|
|
||||||
Blazej Michalik
|
|
||||||
Bogdan Opanchuk
|
|
||||||
BorisZZZ
|
|
||||||
Brad Erickson
|
|
||||||
Bradley Ayers
|
|
||||||
Brandon L. Reiss
|
|
||||||
Brandt Bucher
|
|
||||||
Brett Randall
|
|
||||||
Brett Rosen
|
|
||||||
Brian Cristante
|
|
||||||
Brian Rosner
|
|
||||||
briantracy
|
|
||||||
BrownTruck
|
|
||||||
Bruno Oliveira
|
|
||||||
Bruno Renié
|
|
||||||
Bruno S
|
|
||||||
Bstrdsmkr
|
|
||||||
Buck Golemon
|
|
||||||
burrows
|
|
||||||
Bussonnier Matthias
|
|
||||||
bwoodsend
|
|
||||||
c22
|
|
||||||
Caleb Martinez
|
|
||||||
Calvin Smith
|
|
||||||
Carl Meyer
|
|
||||||
Carlos Liam
|
|
||||||
Carol Willing
|
|
||||||
Carter Thayer
|
|
||||||
Cass
|
|
||||||
Chandrasekhar Atina
|
|
||||||
Chih-Hsuan Yen
|
|
||||||
Chris Brinker
|
|
||||||
Chris Hunt
|
|
||||||
Chris Jerdonek
|
|
||||||
Chris Kuehl
|
|
||||||
Chris McDonough
|
|
||||||
Chris Pawley
|
|
||||||
Chris Pryer
|
|
||||||
Chris Wolfe
|
|
||||||
Christian Clauss
|
|
||||||
Christian Heimes
|
|
||||||
Christian Oudard
|
|
||||||
Christoph Reiter
|
|
||||||
Christopher Hunt
|
|
||||||
Christopher Snyder
|
|
||||||
cjc7373
|
|
||||||
Clark Boylan
|
|
||||||
Claudio Jolowicz
|
|
||||||
Clay McClure
|
|
||||||
Cody
|
|
||||||
Cody Soyland
|
|
||||||
Colin Watson
|
|
||||||
Collin Anderson
|
|
||||||
Connor Osborn
|
|
||||||
Cooper Lees
|
|
||||||
Cooper Ry Lees
|
|
||||||
Cory Benfield
|
|
||||||
Cory Wright
|
|
||||||
Craig Kerstiens
|
|
||||||
Cristian Sorinel
|
|
||||||
Cristina
|
|
||||||
Cristina Muñoz
|
|
||||||
Curtis Doty
|
|
||||||
cytolentino
|
|
||||||
Daan De Meyer
|
|
||||||
Damian
|
|
||||||
Damian Quiroga
|
|
||||||
Damian Shaw
|
|
||||||
Dan Black
|
|
||||||
Dan Savilonis
|
|
||||||
Dan Sully
|
|
||||||
Dane Hillard
|
|
||||||
daniel
|
|
||||||
Daniel Collins
|
|
||||||
Daniel Hahler
|
|
||||||
Daniel Holth
|
|
||||||
Daniel Jost
|
|
||||||
Daniel Katz
|
|
||||||
Daniel Shaulov
|
|
||||||
Daniele Esposti
|
|
||||||
Daniele Nicolodi
|
|
||||||
Daniele Procida
|
|
||||||
Daniil Konovalenko
|
|
||||||
Danny Hermes
|
|
||||||
Danny McClanahan
|
|
||||||
Darren Kavanagh
|
|
||||||
Dav Clark
|
|
||||||
Dave Abrahams
|
|
||||||
Dave Jones
|
|
||||||
David Aguilar
|
|
||||||
David Black
|
|
||||||
David Bordeynik
|
|
||||||
David Caro
|
|
||||||
David D Lowe
|
|
||||||
David Evans
|
|
||||||
David Hewitt
|
|
||||||
David Linke
|
|
||||||
David Poggi
|
|
||||||
David Pursehouse
|
|
||||||
David Runge
|
|
||||||
David Tucker
|
|
||||||
David Wales
|
|
||||||
Davidovich
|
|
||||||
Deepak Sharma
|
|
||||||
Deepyaman Datta
|
|
||||||
Denise Yu
|
|
||||||
derwolfe
|
|
||||||
Desetude
|
|
||||||
Devesh Kumar Singh
|
|
||||||
Diego Caraballo
|
|
||||||
Diego Ramirez
|
|
||||||
DiegoCaraballo
|
|
||||||
Dimitri Merejkowsky
|
|
||||||
Dimitri Papadopoulos
|
|
||||||
Dirk Stolle
|
|
||||||
Dmitry Gladkov
|
|
||||||
Dmitry Volodin
|
|
||||||
Domen Kožar
|
|
||||||
Dominic Davis-Foster
|
|
||||||
Donald Stufft
|
|
||||||
Dongweiming
|
|
||||||
doron zarhi
|
|
||||||
Dos Moonen
|
|
||||||
Douglas Thor
|
|
||||||
DrFeathers
|
|
||||||
Dustin Ingram
|
|
||||||
Dwayne Bailey
|
|
||||||
Ed Morley
|
|
||||||
Edgar Ramírez
|
|
||||||
Ee Durbin
|
|
||||||
Eitan Adler
|
|
||||||
ekristina
|
|
||||||
elainechan
|
|
||||||
Eli Schwartz
|
|
||||||
Elisha Hollander
|
|
||||||
Ellen Marie Dash
|
|
||||||
Emil Burzo
|
|
||||||
Emil Styrke
|
|
||||||
Emmanuel Arias
|
|
||||||
Endoh Takanao
|
|
||||||
enoch
|
|
||||||
Erdinc Mutlu
|
|
||||||
Eric Cousineau
|
|
||||||
Eric Gillingham
|
|
||||||
Eric Hanchrow
|
|
||||||
Eric Hopper
|
|
||||||
Erik M. Bray
|
|
||||||
Erik Rose
|
|
||||||
Erwin Janssen
|
|
||||||
Eugene Vereshchagin
|
|
||||||
everdimension
|
|
||||||
Federico
|
|
||||||
Felipe Peter
|
|
||||||
Felix Yan
|
|
||||||
fiber-space
|
|
||||||
Filip Kokosiński
|
|
||||||
Filipe Laíns
|
|
||||||
Finn Womack
|
|
||||||
finnagin
|
|
||||||
Florian Briand
|
|
||||||
Florian Rathgeber
|
|
||||||
Francesco
|
|
||||||
Francesco Montesano
|
|
||||||
Frost Ming
|
|
||||||
Gabriel Curio
|
|
||||||
Gabriel de Perthuis
|
|
||||||
Garry Polley
|
|
||||||
gavin
|
|
||||||
gdanielson
|
|
||||||
Geoffrey Sneddon
|
|
||||||
George Song
|
|
||||||
Georgi Valkov
|
|
||||||
Georgy Pchelkin
|
|
||||||
ghost
|
|
||||||
Giftlin Rajaiah
|
|
||||||
gizmoguy1
|
|
||||||
gkdoc
|
|
||||||
Godefroid Chapelle
|
|
||||||
Gopinath M
|
|
||||||
GOTO Hayato
|
|
||||||
gousaiyang
|
|
||||||
gpiks
|
|
||||||
Greg Roodt
|
|
||||||
Greg Ward
|
|
||||||
Guilherme Espada
|
|
||||||
Guillaume Seguin
|
|
||||||
gutsytechster
|
|
||||||
Guy Rozendorn
|
|
||||||
Guy Tuval
|
|
||||||
gzpan123
|
|
||||||
Hanjun Kim
|
|
||||||
Hari Charan
|
|
||||||
Harsh Vardhan
|
|
||||||
harupy
|
|
||||||
Harutaka Kawamura
|
|
||||||
hauntsaninja
|
|
||||||
Henrich Hartzer
|
|
||||||
Henry Schreiner
|
|
||||||
Herbert Pfennig
|
|
||||||
Holly Stotelmyer
|
|
||||||
Honnix
|
|
||||||
Hsiaoming Yang
|
|
||||||
Hugo Lopes Tavares
|
|
||||||
Hugo van Kemenade
|
|
||||||
Hugues Bruant
|
|
||||||
Hynek Schlawack
|
|
||||||
Ian Bicking
|
|
||||||
Ian Cordasco
|
|
||||||
Ian Lee
|
|
||||||
Ian Stapleton Cordasco
|
|
||||||
Ian Wienand
|
|
||||||
Igor Kuzmitshov
|
|
||||||
Igor Sobreira
|
|
||||||
Ilan Schnell
|
|
||||||
Illia Volochii
|
|
||||||
Ilya Baryshev
|
|
||||||
Inada Naoki
|
|
||||||
Ionel Cristian Mărieș
|
|
||||||
Ionel Maries Cristian
|
|
||||||
Ivan Pozdeev
|
|
||||||
Jacob Kim
|
|
||||||
Jacob Walls
|
|
||||||
Jaime Sanz
|
|
||||||
jakirkham
|
|
||||||
Jakub Kuczys
|
|
||||||
Jakub Stasiak
|
|
||||||
Jakub Vysoky
|
|
||||||
Jakub Wilk
|
|
||||||
James Cleveland
|
|
||||||
James Curtin
|
|
||||||
James Firth
|
|
||||||
James Gerity
|
|
||||||
James Polley
|
|
||||||
Jan Pokorný
|
|
||||||
Jannis Leidel
|
|
||||||
Jarek Potiuk
|
|
||||||
jarondl
|
|
||||||
Jason Curtis
|
|
||||||
Jason R. Coombs
|
|
||||||
JasonMo
|
|
||||||
JasonMo1
|
|
||||||
Jay Graves
|
|
||||||
Jean-Christophe Fillion-Robin
|
|
||||||
Jeff Barber
|
|
||||||
Jeff Dairiki
|
|
||||||
Jelmer Vernooij
|
|
||||||
jenix21
|
|
||||||
Jeremy Stanley
|
|
||||||
Jeremy Zafran
|
|
||||||
Jesse Rittner
|
|
||||||
Jiashuo Li
|
|
||||||
Jim Fisher
|
|
||||||
Jim Garrison
|
|
||||||
Jiun Bae
|
|
||||||
Jivan Amara
|
|
||||||
Joe Bylund
|
|
||||||
Joe Michelini
|
|
||||||
John Paton
|
|
||||||
John T. Wodder II
|
|
||||||
John-Scott Atlakson
|
|
||||||
johnthagen
|
|
||||||
Jon Banafato
|
|
||||||
Jon Dufresne
|
|
||||||
Jon Parise
|
|
||||||
Jonas Nockert
|
|
||||||
Jonathan Herbert
|
|
||||||
Joonatan Partanen
|
|
||||||
Joost Molenaar
|
|
||||||
Jorge Niedbalski
|
|
||||||
Joseph Bylund
|
|
||||||
Joseph Long
|
|
||||||
Josh Bronson
|
|
||||||
Josh Hansen
|
|
||||||
Josh Schneier
|
|
||||||
Juan Luis Cano Rodríguez
|
|
||||||
Juanjo Bazán
|
|
||||||
Judah Rand
|
|
||||||
Julian Berman
|
|
||||||
Julian Gethmann
|
|
||||||
Julien Demoor
|
|
||||||
Jussi Kukkonen
|
|
||||||
jwg4
|
|
||||||
Jyrki Pulliainen
|
|
||||||
Kai Chen
|
|
||||||
Kai Mueller
|
|
||||||
Kamal Bin Mustafa
|
|
||||||
kasium
|
|
||||||
kaustav haldar
|
|
||||||
keanemind
|
|
||||||
Keith Maxwell
|
|
||||||
Kelsey Hightower
|
|
||||||
Kenneth Belitzky
|
|
||||||
Kenneth Reitz
|
|
||||||
Kevin Burke
|
|
||||||
Kevin Carter
|
|
||||||
Kevin Frommelt
|
|
||||||
Kevin R Patterson
|
|
||||||
Kexuan Sun
|
|
||||||
Kit Randel
|
|
||||||
Klaas van Schelven
|
|
||||||
KOLANICH
|
|
||||||
kpinc
|
|
||||||
Krishna Oza
|
|
||||||
Kumar McMillan
|
|
||||||
Kyle Persohn
|
|
||||||
lakshmanaram
|
|
||||||
Laszlo Kiss-Kollar
|
|
||||||
Laurent Bristiel
|
|
||||||
Laurent LAPORTE
|
|
||||||
Laurie O
|
|
||||||
Laurie Opperman
|
|
||||||
layday
|
|
||||||
Leon Sasson
|
|
||||||
Lev Givon
|
|
||||||
Lincoln de Sousa
|
|
||||||
Lipis
|
|
||||||
lorddavidiii
|
|
||||||
Loren Carvalho
|
|
||||||
Lucas Cimon
|
|
||||||
Ludovic Gasc
|
|
||||||
Lukas Juhrich
|
|
||||||
Luke Macken
|
|
||||||
Luo Jiebin
|
|
||||||
luojiebin
|
|
||||||
luz.paz
|
|
||||||
László Kiss Kollár
|
|
||||||
M00nL1ght
|
|
||||||
Marc Abramowitz
|
|
||||||
Marc Tamlyn
|
|
||||||
Marcus Smith
|
|
||||||
Mariatta
|
|
||||||
Mark Kohler
|
|
||||||
Mark Williams
|
|
||||||
Markus Hametner
|
|
||||||
Martey Dodoo
|
|
||||||
Martin Fischer
|
|
||||||
Martin Häcker
|
|
||||||
Martin Pavlasek
|
|
||||||
Masaki
|
|
||||||
Masklinn
|
|
||||||
Matej Stuchlik
|
|
||||||
Mathew Jennings
|
|
||||||
Mathieu Bridon
|
|
||||||
Mathieu Kniewallner
|
|
||||||
Matt Bacchi
|
|
||||||
Matt Good
|
|
||||||
Matt Maker
|
|
||||||
Matt Robenolt
|
|
||||||
matthew
|
|
||||||
Matthew Einhorn
|
|
||||||
Matthew Feickert
|
|
||||||
Matthew Gilliard
|
|
||||||
Matthew Iversen
|
|
||||||
Matthew Treinish
|
|
||||||
Matthew Trumbell
|
|
||||||
Matthew Willson
|
|
||||||
Matthias Bussonnier
|
|
||||||
mattip
|
|
||||||
Maurits van Rees
|
|
||||||
Max W Chase
|
|
||||||
Maxim Kurnikov
|
|
||||||
Maxime Rouyrre
|
|
||||||
mayeut
|
|
||||||
mbaluna
|
|
||||||
mdebi
|
|
||||||
memoselyk
|
|
||||||
meowmeowcat
|
|
||||||
Michael
|
|
||||||
Michael Aquilina
|
|
||||||
Michael E. Karpeles
|
|
||||||
Michael Klich
|
|
||||||
Michael Mintz
|
|
||||||
Michael Williamson
|
|
||||||
michaelpacer
|
|
||||||
Michał Górny
|
|
||||||
Mickaël Schoentgen
|
|
||||||
Miguel Araujo Perez
|
|
||||||
Mihir Singh
|
|
||||||
Mike
|
|
||||||
Mike Hendricks
|
|
||||||
Min RK
|
|
||||||
MinRK
|
|
||||||
Miro Hrončok
|
|
||||||
Monica Baluna
|
|
||||||
montefra
|
|
||||||
Monty Taylor
|
|
||||||
Muha Ajjan
|
|
||||||
Nadav Wexler
|
|
||||||
Nahuel Ambrosini
|
|
||||||
Nate Coraor
|
|
||||||
Nate Prewitt
|
|
||||||
Nathan Houghton
|
|
||||||
Nathaniel J. Smith
|
|
||||||
Nehal J Wani
|
|
||||||
Neil Botelho
|
|
||||||
Nguyễn Gia Phong
|
|
||||||
Nicholas Serra
|
|
||||||
Nick Coghlan
|
|
||||||
Nick Stenning
|
|
||||||
Nick Timkovich
|
|
||||||
Nicolas Bock
|
|
||||||
Nicole Harris
|
|
||||||
Nikhil Benesch
|
|
||||||
Nikhil Ladha
|
|
||||||
Nikita Chepanov
|
|
||||||
Nikolay Korolev
|
|
||||||
Nipunn Koorapati
|
|
||||||
Nitesh Sharma
|
|
||||||
Niyas Sait
|
|
||||||
Noah
|
|
||||||
Noah Gorny
|
|
||||||
Nowell Strite
|
|
||||||
NtaleGrey
|
|
||||||
nvdv
|
|
||||||
OBITORASU
|
|
||||||
Ofek Lev
|
|
||||||
ofrinevo
|
|
||||||
Oliver Freund
|
|
||||||
Oliver Jeeves
|
|
||||||
Oliver Mannion
|
|
||||||
Oliver Tonnhofer
|
|
||||||
Olivier Girardot
|
|
||||||
Olivier Grisel
|
|
||||||
Ollie Rutherfurd
|
|
||||||
OMOTO Kenji
|
|
||||||
Omry Yadan
|
|
||||||
onlinejudge95
|
|
||||||
Oren Held
|
|
||||||
Oscar Benjamin
|
|
||||||
Oz N Tiram
|
|
||||||
Pachwenko
|
|
||||||
Patrick Dubroy
|
|
||||||
Patrick Jenkins
|
|
||||||
Patrick Lawson
|
|
||||||
patricktokeeffe
|
|
||||||
Patrik Kopkan
|
|
||||||
Paul Kehrer
|
|
||||||
Paul Moore
|
|
||||||
Paul Nasrat
|
|
||||||
Paul Oswald
|
|
||||||
Paul van der Linden
|
|
||||||
Paulus Schoutsen
|
|
||||||
Pavel Safronov
|
|
||||||
Pavithra Eswaramoorthy
|
|
||||||
Pawel Jasinski
|
|
||||||
Paweł Szramowski
|
|
||||||
Pekka Klärck
|
|
||||||
Peter Gessler
|
|
||||||
Peter Lisák
|
|
||||||
Peter Waller
|
|
||||||
petr-tik
|
|
||||||
Phaneendra Chiruvella
|
|
||||||
Phil Elson
|
|
||||||
Phil Freo
|
|
||||||
Phil Pennock
|
|
||||||
Phil Whelan
|
|
||||||
Philip Jägenstedt
|
|
||||||
Philip Molloy
|
|
||||||
Philippe Ombredanne
|
|
||||||
Pi Delport
|
|
||||||
Pierre-Yves Rofes
|
|
||||||
Pieter Degroote
|
|
||||||
pip
|
|
||||||
Prabakaran Kumaresshan
|
|
||||||
Prabhjyotsing Surjit Singh Sodhi
|
|
||||||
Prabhu Marappan
|
|
||||||
Pradyun Gedam
|
|
||||||
Prashant Sharma
|
|
||||||
Pratik Mallya
|
|
||||||
pre-commit-ci[bot]
|
|
||||||
Preet Thakkar
|
|
||||||
Preston Holmes
|
|
||||||
Przemek Wrzos
|
|
||||||
Pulkit Goyal
|
|
||||||
q0w
|
|
||||||
Qiangning Hong
|
|
||||||
Quentin Lee
|
|
||||||
Quentin Pradet
|
|
||||||
R. David Murray
|
|
||||||
Rafael Caricio
|
|
||||||
Ralf Schmitt
|
|
||||||
Razzi Abuissa
|
|
||||||
rdb
|
|
||||||
Reece Dunham
|
|
||||||
Remi Rampin
|
|
||||||
Rene Dudfield
|
|
||||||
Riccardo Magliocchetti
|
|
||||||
Riccardo Schirone
|
|
||||||
Richard Jones
|
|
||||||
Richard Si
|
|
||||||
Ricky Ng-Adam
|
|
||||||
Rishi
|
|
||||||
RobberPhex
|
|
||||||
Robert Collins
|
|
||||||
Robert McGibbon
|
|
||||||
Robert Pollak
|
|
||||||
Robert T. McGibbon
|
|
||||||
robin elisha robinson
|
|
||||||
Roey Berman
|
|
||||||
Rohan Jain
|
|
||||||
Roman Bogorodskiy
|
|
||||||
Roman Donchenko
|
|
||||||
Romuald Brunet
|
|
||||||
ronaudinho
|
|
||||||
Ronny Pfannschmidt
|
|
||||||
Rory McCann
|
|
||||||
Ross Brattain
|
|
||||||
Roy Wellington Ⅳ
|
|
||||||
Ruairidh MacLeod
|
|
||||||
Russell Keith-Magee
|
|
||||||
Ryan Shepherd
|
|
||||||
Ryan Wooden
|
|
||||||
ryneeverett
|
|
||||||
Sachi King
|
|
||||||
Salvatore Rinchiera
|
|
||||||
sandeepkiran-js
|
|
||||||
Savio Jomton
|
|
||||||
schlamar
|
|
||||||
Scott Kitterman
|
|
||||||
Sean
|
|
||||||
seanj
|
|
||||||
Sebastian Jordan
|
|
||||||
Sebastian Schaetz
|
|
||||||
Segev Finer
|
|
||||||
SeongSoo Cho
|
|
||||||
Sergey Vasilyev
|
|
||||||
Seth Michael Larson
|
|
||||||
Seth Woodworth
|
|
||||||
Shantanu
|
|
||||||
shireenrao
|
|
||||||
Shivansh-007
|
|
||||||
Shlomi Fish
|
|
||||||
Shovan Maity
|
|
||||||
Simeon Visser
|
|
||||||
Simon Cross
|
|
||||||
Simon Pichugin
|
|
||||||
sinoroc
|
|
||||||
sinscary
|
|
||||||
snook92
|
|
||||||
socketubs
|
|
||||||
Sorin Sbarnea
|
|
||||||
Srinivas Nyayapati
|
|
||||||
Stavros Korokithakis
|
|
||||||
Stefan Scherfke
|
|
||||||
Stefano Rivera
|
|
||||||
Stephan Erb
|
|
||||||
Stephen Rosen
|
|
||||||
stepshal
|
|
||||||
Steve (Gadget) Barnes
|
|
||||||
Steve Barnes
|
|
||||||
Steve Dower
|
|
||||||
Steve Kowalik
|
|
||||||
Steven Myint
|
|
||||||
Steven Silvester
|
|
||||||
stonebig
|
|
||||||
Stéphane Bidoul
|
|
||||||
Stéphane Bidoul (ACSONE)
|
|
||||||
Stéphane Klein
|
|
||||||
Sumana Harihareswara
|
|
||||||
Surbhi Sharma
|
|
||||||
Sviatoslav Sydorenko
|
|
||||||
Swat009
|
|
||||||
Sylvain
|
|
||||||
Takayuki SHIMIZUKAWA
|
|
||||||
Taneli Hukkinen
|
|
||||||
tbeswick
|
|
||||||
Thiago
|
|
||||||
Thijs Triemstra
|
|
||||||
Thomas Fenzl
|
|
||||||
Thomas Grainger
|
|
||||||
Thomas Guettler
|
|
||||||
Thomas Johansson
|
|
||||||
Thomas Kluyver
|
|
||||||
Thomas Smith
|
|
||||||
Thomas VINCENT
|
|
||||||
Tim D. Smith
|
|
||||||
Tim Gates
|
|
||||||
Tim Harder
|
|
||||||
Tim Heap
|
|
||||||
tim smith
|
|
||||||
tinruufu
|
|
||||||
Tobias Hermann
|
|
||||||
Tom Forbes
|
|
||||||
Tom Freudenheim
|
|
||||||
Tom V
|
|
||||||
Tomas Hrnciar
|
|
||||||
Tomas Orsava
|
|
||||||
Tomer Chachamu
|
|
||||||
Tommi Enenkel | AnB
|
|
||||||
Tomáš Hrnčiar
|
|
||||||
Tony Beswick
|
|
||||||
Tony Narlock
|
|
||||||
Tony Zhaocheng Tan
|
|
||||||
TonyBeswick
|
|
||||||
toonarmycaptain
|
|
||||||
Toshio Kuratomi
|
|
||||||
toxinu
|
|
||||||
Travis Swicegood
|
|
||||||
Tushar Sadhwani
|
|
||||||
Tzu-ping Chung
|
|
||||||
Valentin Haenel
|
|
||||||
Victor Stinner
|
|
||||||
victorvpaulo
|
|
||||||
Vikram - Google
|
|
||||||
Viktor Szépe
|
|
||||||
Ville Skyttä
|
|
||||||
Vinay Sajip
|
|
||||||
Vincent Philippon
|
|
||||||
Vinicyus Macedo
|
|
||||||
Vipul Kumar
|
|
||||||
Vitaly Babiy
|
|
||||||
Vladimir Rutsky
|
|
||||||
W. Trevor King
|
|
||||||
Wil Tan
|
|
||||||
Wilfred Hughes
|
|
||||||
William Edwards
|
|
||||||
William ML Leslie
|
|
||||||
William T Olson
|
|
||||||
William Woodruff
|
|
||||||
Wilson Mo
|
|
||||||
wim glenn
|
|
||||||
Winson Luk
|
|
||||||
Wolfgang Maier
|
|
||||||
Wu Zhenyu
|
|
||||||
XAMES3
|
|
||||||
Xavier Fernandez
|
|
||||||
xoviat
|
|
||||||
xtreak
|
|
||||||
YAMAMOTO Takashi
|
|
||||||
Yen Chi Hsuan
|
|
||||||
Yeray Diaz Diaz
|
|
||||||
Yoval P
|
|
||||||
Yu Jian
|
|
||||||
Yuan Jing Vincent Yan
|
|
||||||
Yusuke Hayashi
|
|
||||||
Zearin
|
|
||||||
Zhiping Deng
|
|
||||||
ziebam
|
|
||||||
Zvezdan Petkovic
|
|
||||||
Łukasz Langa
|
|
||||||
Роман Донченко
|
|
||||||
Семён Марьясин
|
|
||||||
rekcäH nitraM
|
|
|
@ -1 +0,0 @@
|
||||||
pip
|
|
|
@ -1,20 +0,0 @@
|
||||||
Copyright (c) 2008-present The pip developers (see AUTHORS.txt file)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
@ -1,90 +0,0 @@
|
||||||
Metadata-Version: 2.1
|
|
||||||
Name: pip
|
|
||||||
Version: 23.2.1
|
|
||||||
Summary: The PyPA recommended tool for installing Python packages.
|
|
||||||
Home-page: https://pip.pypa.io/
|
|
||||||
Author: The pip developers
|
|
||||||
Author-email: distutils-sig@python.org
|
|
||||||
License: MIT
|
|
||||||
Project-URL: Documentation, https://pip.pypa.io
|
|
||||||
Project-URL: Source, https://github.com/pypa/pip
|
|
||||||
Project-URL: Changelog, https://pip.pypa.io/en/stable/news/
|
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
|
||||||
Classifier: Intended Audience :: Developers
|
|
||||||
Classifier: License :: OSI Approved :: MIT License
|
|
||||||
Classifier: Topic :: Software Development :: Build Tools
|
|
||||||
Classifier: Programming Language :: Python
|
|
||||||
Classifier: Programming Language :: Python :: 3
|
|
||||||
Classifier: Programming Language :: Python :: 3 :: Only
|
|
||||||
Classifier: Programming Language :: Python :: 3.7
|
|
||||||
Classifier: Programming Language :: Python :: 3.8
|
|
||||||
Classifier: Programming Language :: Python :: 3.9
|
|
||||||
Classifier: Programming Language :: Python :: 3.10
|
|
||||||
Classifier: Programming Language :: Python :: 3.11
|
|
||||||
Classifier: Programming Language :: Python :: 3.12
|
|
||||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
||||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
||||||
Requires-Python: >=3.7
|
|
||||||
License-File: LICENSE.txt
|
|
||||||
License-File: AUTHORS.txt
|
|
||||||
|
|
||||||
pip - The Python Package Installer
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/v/pip.svg
|
|
||||||
:target: https://pypi.org/project/pip/
|
|
||||||
|
|
||||||
.. image:: https://readthedocs.org/projects/pip/badge/?version=latest
|
|
||||||
:target: https://pip.pypa.io/en/latest
|
|
||||||
|
|
||||||
pip is the `package installer`_ for Python. You can use pip to install packages from the `Python Package Index`_ and other indexes.
|
|
||||||
|
|
||||||
Please take a look at our documentation for how to install and use pip:
|
|
||||||
|
|
||||||
* `Installation`_
|
|
||||||
* `Usage`_
|
|
||||||
|
|
||||||
We release updates regularly, with a new version every 3 months. Find more details in our documentation:
|
|
||||||
|
|
||||||
* `Release notes`_
|
|
||||||
* `Release process`_
|
|
||||||
|
|
||||||
In pip 20.3, we've `made a big improvement to the heart of pip`_; `learn more`_. We want your input, so `sign up for our user experience research studies`_ to help us do it right.
|
|
||||||
|
|
||||||
**Note**: pip 21.0, in January 2021, removed Python 2 support, per pip's `Python 2 support policy`_. Please migrate to Python 3.
|
|
||||||
|
|
||||||
If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms:
|
|
||||||
|
|
||||||
* `Issue tracking`_
|
|
||||||
* `Discourse channel`_
|
|
||||||
* `User IRC`_
|
|
||||||
|
|
||||||
If you want to get involved head over to GitHub to get the source code, look at our development documentation and feel free to jump on the developer mailing lists and chat rooms:
|
|
||||||
|
|
||||||
* `GitHub page`_
|
|
||||||
* `Development documentation`_
|
|
||||||
* `Development IRC`_
|
|
||||||
|
|
||||||
Code of Conduct
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Everyone interacting in the pip project's codebases, issue trackers, chat
|
|
||||||
rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
|
|
||||||
|
|
||||||
.. _package installer: https://packaging.python.org/guides/tool-recommendations/
|
|
||||||
.. _Python Package Index: https://pypi.org
|
|
||||||
.. _Installation: https://pip.pypa.io/en/stable/installation/
|
|
||||||
.. _Usage: https://pip.pypa.io/en/stable/
|
|
||||||
.. _Release notes: https://pip.pypa.io/en/stable/news.html
|
|
||||||
.. _Release process: https://pip.pypa.io/en/latest/development/release-process/
|
|
||||||
.. _GitHub page: https://github.com/pypa/pip
|
|
||||||
.. _Development documentation: https://pip.pypa.io/en/latest/development
|
|
||||||
.. _made a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/11/pip-20-3-new-resolver.html
|
|
||||||
.. _learn more: https://pip.pypa.io/en/latest/user_guide/#changes-to-the-pip-dependency-resolver-in-20-3-2020
|
|
||||||
.. _sign up for our user experience research studies: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html
|
|
||||||
.. _Python 2 support policy: https://pip.pypa.io/en/latest/development/release-process/#python-2-support
|
|
||||||
.. _Issue tracking: https://github.com/pypa/pip/issues
|
|
||||||
.. _Discourse channel: https://discuss.python.org/c/packaging
|
|
||||||
.. _User IRC: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa
|
|
||||||
.. _Development IRC: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa-dev
|
|
||||||
.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +0,0 @@
|
||||||
Wheel-Version: 1.0
|
|
||||||
Generator: bdist_wheel (0.40.0)
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
[console_scripts]
|
|
||||||
pip = pip._internal.cli.main:main
|
|
||||||
pip3 = pip._internal.cli.main:main
|
|
||||||
pip3.11 = pip._internal.cli.main:main
|
|
|
@ -1 +0,0 @@
|
||||||
pip
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue