Compare commits
55 Commits
chrispy/su
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
241ed02bc1 | ||
|
|
add391a623 | ||
|
|
e89cc2a1f8 | ||
|
|
aafa4c3b16 | ||
|
|
c47709c21c | ||
|
|
fbc1353593 | ||
|
|
85ef82e083 | ||
|
|
f7053e46ab | ||
|
|
7edbc5a22b | ||
|
|
76e5edb357 | ||
|
|
48724e7002 | ||
|
|
9b1412aa5b | ||
|
|
75ab3064dd | ||
|
|
26566891a7 | ||
|
|
47856cd429 | ||
|
|
8f70e3952f | ||
|
|
e935ce819e | ||
|
|
b5c724ab33 | ||
|
|
8c810eb8a8 | ||
|
|
383847ee86 | ||
|
|
be3a7f4672 | ||
|
|
8219d2a673 | ||
|
|
0c8ac578c9 | ||
|
|
8f047753ae | ||
|
|
194c646a20 | ||
|
|
2c533339cf | ||
|
|
2b8cf444f1 | ||
|
|
d375116807 | ||
|
|
eb0330bfc6 | ||
|
|
28793ac0b3 | ||
|
|
9231704988 | ||
|
|
1613c302bc | ||
|
|
55c9e84f38 | ||
|
|
99875683ac | ||
|
|
eaeb0603eb | ||
|
|
cb73590623 | ||
|
|
59417ab115 | ||
|
|
917b01e548 | ||
|
|
652714859d | ||
|
|
ea5b22824b | ||
|
|
ec5858e42f | ||
|
|
02bb914ef3 | ||
|
|
21c0d034d0 | ||
|
|
e3ddc789a2 | ||
|
|
2d0cd97323 | ||
|
|
ec185e2e9c | ||
|
|
079d1721aa | ||
|
|
bf24df3e2e | ||
|
|
15329588b1 | ||
|
|
34ad8485fa | ||
|
|
f0ce934bf8 | ||
|
|
99cd237f27 | ||
|
|
2bde8d3e8e | ||
|
|
8c9b029756 | ||
|
|
ae50065872 |
21
.github/workflows/python-app.yml
vendored
21
.github/workflows/python-app.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
@@ -30,3 +30,22 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
python -m build -nwsx .
|
||||
|
||||
types:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade setuptools setuptools_scm wheel build tox mypy types-beautifulsoup4
|
||||
- name: Check types
|
||||
run: |
|
||||
mypy .
|
||||
mypy --strict tests/types.py
|
||||
|
||||
2
.github/workflows/python-publish.yml
vendored
2
.github/workflows/python-publish.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
|
||||
22
README.rst
22
README.rst
@@ -110,7 +110,7 @@ code_language_callback
|
||||
When the HTML code contains ``pre`` tags that in some way provide the code
|
||||
language, for example as class, this callback can be used to extract the
|
||||
language from the tag and prefix it to the converted ``pre`` tag.
|
||||
The callback gets one single argument, an BeautifylSoup object, and returns
|
||||
The callback gets one single argument, a BeautifulSoup object, and returns
|
||||
a string containing the code language, or ``None``.
|
||||
An example to use the class name as code language could be::
|
||||
|
||||
@@ -157,12 +157,22 @@ strip_document
|
||||
within the document are unaffected.
|
||||
Defaults to ``STRIP``.
|
||||
|
||||
beautiful_soup_parser
|
||||
Specify the Beautiful Soup parser to be used for interpreting HTML markup. Parsers such
|
||||
as `html5lib`, `lxml` or even a custom parser as long as it is installed on the execution
|
||||
environment. Defaults to ``html.parser``.
|
||||
strip_pre
|
||||
Controls whether leading/trailing blank lines are removed from ``<pre>``
|
||||
tags. Supported values are ``STRIP`` (all leading/trailing blank lines),
|
||||
``STRIP_ONE`` (one leading/trailing blank line), and ``None`` (neither).
|
||||
Defaults to ``STRIP``.
|
||||
|
||||
.. _BeautifulSoup: https://www.crummy.com/software/BeautifulSoup/
|
||||
bs4_options
|
||||
Specify additional configuration options for the ``BeautifulSoup`` object
|
||||
used to interpret the HTML markup. String and list values (such as ``lxml``
|
||||
or ``html5lib``) are treated as ``features`` arguments to control parser
|
||||
selection. Dictionary values (such as ``{"from_encoding": "iso-8859-8"}``)
|
||||
are treated as full kwargs to be used for the BeautifulSoup constructor,
|
||||
allowing specification of any parameter. For parameter details, see the
|
||||
Beautiful Soup documentation at:
|
||||
|
||||
.. _BeautifulSoup: https://www.crummy.com/software/BeautifulSoup/bs4/doc/
|
||||
|
||||
Options may be specified as kwargs to the ``markdownify`` function, or as a
|
||||
nested ``Options`` class in ``MarkdownConverter`` subclasses.
|
||||
|
||||
@@ -11,6 +11,10 @@ re_whitespace = re.compile(r'[\t ]+')
|
||||
re_all_whitespace = re.compile(r'[\t \r\n]+')
|
||||
re_newline_whitespace = re.compile(r'[\t \r\n]*[\r\n][\t \r\n]*')
|
||||
re_html_heading = re.compile(r'h(\d+)')
|
||||
re_pre_lstrip1 = re.compile(r'^ *\n')
|
||||
re_pre_rstrip1 = re.compile(r'\n *$')
|
||||
re_pre_lstrip = re.compile(r'^[ \n]*\n')
|
||||
re_pre_rstrip = re.compile(r'[ \n]*$')
|
||||
|
||||
# Pattern for creating convert_<tag> function names from tag names
|
||||
re_make_convert_fn_name = re.compile(r'[\[\]:-]')
|
||||
@@ -37,6 +41,9 @@ re_escape_misc_hashes = re.compile(r'(\s|^)(#{1,6}(?:\s|$))')
|
||||
# confused with a list item
|
||||
re_escape_misc_list_items = re.compile(r'((?:\s|^)[0-9]{1,9})([.)](?:\s|$))')
|
||||
|
||||
# Find consecutive backtick sequences in a string
|
||||
re_backtick_runs = re.compile(r'`+')
|
||||
|
||||
# Heading styles
|
||||
ATX = 'atx'
|
||||
ATX_CLOSED = 'atx_closed'
|
||||
@@ -51,10 +58,25 @@ BACKSLASH = 'backslash'
|
||||
ASTERISK = '*'
|
||||
UNDERSCORE = '_'
|
||||
|
||||
# Document strip styles
|
||||
# Document/pre strip styles
|
||||
LSTRIP = 'lstrip'
|
||||
RSTRIP = 'rstrip'
|
||||
STRIP = 'strip'
|
||||
STRIP_ONE = 'strip_one'
|
||||
|
||||
|
||||
def strip1_pre(text):
|
||||
"""Strip one leading and trailing newline from a <pre> string."""
|
||||
text = re_pre_lstrip1.sub('', text)
|
||||
text = re_pre_rstrip1.sub('', text)
|
||||
return text
|
||||
|
||||
|
||||
def strip_pre(text):
|
||||
"""Strip all leading and trailing newlines from a <pre> string."""
|
||||
text = re_pre_lstrip.sub('', text)
|
||||
text = re_pre_rstrip.sub('', text)
|
||||
return text
|
||||
|
||||
|
||||
def chomp(text):
|
||||
@@ -154,7 +176,7 @@ def _next_block_content_sibling(el):
|
||||
class MarkdownConverter(object):
|
||||
class DefaultOptions:
|
||||
autolinks = True
|
||||
beautiful_soup_parser = 'html.parser'
|
||||
bs4_options = 'html.parser'
|
||||
bullets = '*+-' # An iterable of bullet types.
|
||||
code_language = ''
|
||||
code_language_callback = None
|
||||
@@ -168,6 +190,7 @@ class MarkdownConverter(object):
|
||||
newline_style = SPACES
|
||||
strip = None
|
||||
strip_document = STRIP
|
||||
strip_pre = STRIP
|
||||
strong_em_symbol = ASTERISK
|
||||
sub_symbol = ''
|
||||
sup_symbol = ''
|
||||
@@ -188,11 +211,15 @@ class MarkdownConverter(object):
|
||||
raise ValueError('You may specify either tags to strip or tags to'
|
||||
' convert, but not both.')
|
||||
|
||||
# If a string or list is passed to bs4_options, assume it is a 'features' specification
|
||||
if not isinstance(self.options['bs4_options'], dict):
|
||||
self.options['bs4_options'] = {'features': self.options['bs4_options']}
|
||||
|
||||
# Initialize the conversion function cache
|
||||
self.convert_fn_cache = {}
|
||||
|
||||
def convert(self, html):
|
||||
soup = BeautifulSoup(html, self.options['beautiful_soup_parser'])
|
||||
soup = BeautifulSoup(html, **self.options['bs4_options'])
|
||||
return self.convert_soup(soup)
|
||||
|
||||
def convert_soup(self, soup):
|
||||
@@ -456,10 +483,24 @@ class MarkdownConverter(object):
|
||||
return ' \n'
|
||||
|
||||
def convert_code(self, el, text, parent_tags):
|
||||
if 'pre' in parent_tags:
|
||||
if '_noformat' in parent_tags:
|
||||
return text
|
||||
converter = abstract_inline_conversion(lambda self: '`')
|
||||
return converter(self, el, text, parent_tags)
|
||||
|
||||
prefix, suffix, text = chomp(text)
|
||||
if not text:
|
||||
return ''
|
||||
|
||||
# Find the maximum number of consecutive backticks in the text, then
|
||||
# delimit the code span with one more backtick than that
|
||||
max_backticks = max((len(match) for match in re.findall(re_backtick_runs, text)), default=0)
|
||||
markup_delimiter = '`' * (max_backticks + 1)
|
||||
|
||||
# If the maximum number of backticks is greater than zero, add a space
|
||||
# to avoid interpretation of inside backticks as literals
|
||||
if max_backticks > 0:
|
||||
text = " " + text + " "
|
||||
|
||||
return '%s%s%s%s%s' % (prefix, markup_delimiter, text, markup_delimiter, suffix)
|
||||
|
||||
convert_del = abstract_inline_conversion(lambda self: '~~')
|
||||
|
||||
@@ -652,6 +693,15 @@ class MarkdownConverter(object):
|
||||
if self.options['code_language_callback']:
|
||||
code_language = self.options['code_language_callback'](el) or code_language
|
||||
|
||||
if self.options['strip_pre'] == STRIP:
|
||||
text = strip_pre(text) # remove all leading/trailing newlines
|
||||
elif self.options['strip_pre'] == STRIP_ONE:
|
||||
text = strip1_pre(text) # remove one leading/trailing newline
|
||||
elif self.options['strip_pre'] is None:
|
||||
pass # leave leading and trailing newlines as-is
|
||||
else:
|
||||
raise ValueError('Invalid value for strip_pre: %s' % self.options['strip_pre'])
|
||||
|
||||
return '\n\n```%s\n%s\n```\n\n' % (code_language, text)
|
||||
|
||||
def convert_q(self, el, text, parent_tags):
|
||||
@@ -685,13 +735,13 @@ class MarkdownConverter(object):
|
||||
def convert_td(self, el, text, parent_tags):
|
||||
colspan = 1
|
||||
if 'colspan' in el.attrs and el['colspan'].isdigit():
|
||||
colspan = int(el['colspan'])
|
||||
colspan = max(1, min(1000, int(el['colspan'])))
|
||||
return ' ' + text.strip().replace("\n", " ") + ' |' * colspan
|
||||
|
||||
def convert_th(self, el, text, parent_tags):
|
||||
colspan = 1
|
||||
if 'colspan' in el.attrs and el['colspan'].isdigit():
|
||||
colspan = int(el['colspan'])
|
||||
colspan = max(1, min(1000, int(el['colspan'])))
|
||||
return ' ' + text.strip().replace("\n", " ") + ' |' * colspan
|
||||
|
||||
def convert_tr(self, el, text, parent_tags):
|
||||
@@ -712,7 +762,7 @@ class MarkdownConverter(object):
|
||||
full_colspan = 0
|
||||
for cell in cells:
|
||||
if 'colspan' in cell.attrs and cell['colspan'].isdigit():
|
||||
full_colspan += int(cell["colspan"])
|
||||
full_colspan += max(1, min(1000, int(cell['colspan'])))
|
||||
else:
|
||||
full_colspan += 1
|
||||
if ((is_headrow
|
||||
|
||||
77
markdownify/__init__.pyi
Normal file
77
markdownify/__init__.pyi
Normal file
@@ -0,0 +1,77 @@
|
||||
from _typeshed import Incomplete
|
||||
from typing import Callable, Union
|
||||
|
||||
ATX: str
|
||||
ATX_CLOSED: str
|
||||
UNDERLINED: str
|
||||
SETEXT = UNDERLINED
|
||||
SPACES: str
|
||||
BACKSLASH: str
|
||||
ASTERISK: str
|
||||
UNDERSCORE: str
|
||||
LSTRIP: str
|
||||
RSTRIP: str
|
||||
STRIP: str
|
||||
STRIP_ONE: str
|
||||
|
||||
|
||||
def markdownify(
|
||||
html: str,
|
||||
autolinks: bool = ...,
|
||||
bs4_options: str = ...,
|
||||
bullets: str = ...,
|
||||
code_language: str = ...,
|
||||
code_language_callback: Union[Callable[[Incomplete], Union[str, None]], None] = ...,
|
||||
convert: Union[list[str], None] = ...,
|
||||
default_title: bool = ...,
|
||||
escape_asterisks: bool = ...,
|
||||
escape_underscores: bool = ...,
|
||||
escape_misc: bool = ...,
|
||||
heading_style: str = ...,
|
||||
keep_inline_images_in: list[str] = ...,
|
||||
newline_style: str = ...,
|
||||
strip: Union[list[str], None] = ...,
|
||||
strip_document: Union[str, None] = ...,
|
||||
strip_pre: str = ...,
|
||||
strong_em_symbol: str = ...,
|
||||
sub_symbol: str = ...,
|
||||
sup_symbol: str = ...,
|
||||
table_infer_header: bool = ...,
|
||||
wrap: bool = ...,
|
||||
wrap_width: int = ...,
|
||||
) -> str: ...
|
||||
|
||||
|
||||
class MarkdownConverter:
|
||||
def __init__(
|
||||
self,
|
||||
autolinks: bool = ...,
|
||||
bs4_options: str = ...,
|
||||
bullets: str = ...,
|
||||
code_language: str = ...,
|
||||
code_language_callback: Union[Callable[[Incomplete], Union[str, None]], None] = ...,
|
||||
convert: Union[list[str], None] = ...,
|
||||
default_title: bool = ...,
|
||||
escape_asterisks: bool = ...,
|
||||
escape_underscores: bool = ...,
|
||||
escape_misc: bool = ...,
|
||||
heading_style: str = ...,
|
||||
keep_inline_images_in: list[str] = ...,
|
||||
newline_style: str = ...,
|
||||
strip: Union[list[str], None] = ...,
|
||||
strip_document: Union[str, None] = ...,
|
||||
strip_pre: str = ...,
|
||||
strong_em_symbol: str = ...,
|
||||
sub_symbol: str = ...,
|
||||
sup_symbol: str = ...,
|
||||
table_infer_header: bool = ...,
|
||||
wrap: bool = ...,
|
||||
wrap_width: int = ...,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def convert(self, html: str) -> str:
|
||||
...
|
||||
|
||||
def convert_soup(self, soup: Incomplete) -> str:
|
||||
...
|
||||
9
markdownify/main.py
Normal file → Executable file
9
markdownify/main.py
Normal file → Executable file
@@ -70,12 +70,11 @@ def main(argv=sys.argv[1:]):
|
||||
parser.add_argument('-w', '--wrap', action='store_true',
|
||||
help="Wrap all text paragraphs at --wrap-width characters.")
|
||||
parser.add_argument('--wrap-width', type=int, default=80)
|
||||
parser.add_argument('-p', '--beautiful-soup-parser',
|
||||
dest='beautiful_soup_parser',
|
||||
parser.add_argument('--bs4-options',
|
||||
default='html.parser',
|
||||
help="Specify the Beautiful Soup parser to be used for interpreting HTML markup. Parsers such "
|
||||
"as html5lib, lxml or even a custom parser as long as it is installed on the execution "
|
||||
"environment.")
|
||||
help="Specifies the parser that BeautifulSoup should use to parse "
|
||||
"the HTML markup. Examples include 'html5.parser', 'lxml', and "
|
||||
"'html5lib'.")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
print(markdownify(**vars(args)))
|
||||
|
||||
1
markdownify/py.typed
Normal file
1
markdownify/py.typed
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "markdownify"
|
||||
version = "1.1.0"
|
||||
version = "1.2.2"
|
||||
authors = [{name = "Matthew Tretter", email = "m@tthewwithanm.com"}]
|
||||
description = "Convert HTML to markdown."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Test whitelisting/blacklisting of specific tags.
|
||||
|
||||
"""
|
||||
from markdownify import markdownify, LSTRIP, RSTRIP, STRIP
|
||||
from markdownify import markdownify, LSTRIP, RSTRIP, STRIP, STRIP_ONE
|
||||
from .utils import md
|
||||
|
||||
|
||||
@@ -32,3 +32,16 @@ def test_strip_document():
|
||||
assert markdownify("<p>Hello</p>", strip_document=RSTRIP) == "\n\nHello"
|
||||
assert markdownify("<p>Hello</p>", strip_document=STRIP) == "Hello"
|
||||
assert markdownify("<p>Hello</p>", strip_document=None) == "\n\nHello\n\n"
|
||||
|
||||
|
||||
def test_strip_pre():
|
||||
assert markdownify("<pre> \n \n Hello \n \n </pre>") == "```\n Hello\n```"
|
||||
assert markdownify("<pre> \n \n Hello \n \n </pre>", strip_pre=STRIP) == "```\n Hello\n```"
|
||||
assert markdownify("<pre> \n \n Hello \n \n </pre>", strip_pre=STRIP_ONE) == "```\n \n Hello \n \n```"
|
||||
assert markdownify("<pre> \n \n Hello \n \n </pre>", strip_pre=None) == "```\n \n \n Hello \n \n \n```"
|
||||
|
||||
|
||||
def bs4_options():
|
||||
assert markdownify("<p>Hello</p>", bs4_options="html.parser") == "Hello"
|
||||
assert markdownify("<p>Hello</p>", bs4_options=["html.parser"]) == "Hello"
|
||||
assert markdownify("<p>Hello</p>", bs4_options={"features": "html.parser"}) == "Hello"
|
||||
|
||||
@@ -101,6 +101,9 @@ def test_code():
|
||||
assert md('<code>foo<s> bar </s>baz</code>') == '`foo bar baz`'
|
||||
assert md('<code>foo<sup>bar</sup>baz</code>', sup_symbol='^') == '`foobarbaz`'
|
||||
assert md('<code>foo<sub>bar</sub>baz</code>', sub_symbol='^') == '`foobarbaz`'
|
||||
assert md('foo<code>`bar`</code>baz') == 'foo`` `bar` ``baz'
|
||||
assert md('foo<code>``bar``</code>baz') == 'foo``` ``bar`` ```baz'
|
||||
assert md('foo<code> `bar` </code>baz') == 'foo `` `bar` `` baz'
|
||||
|
||||
|
||||
def test_dl():
|
||||
@@ -370,4 +373,4 @@ def test_spaces():
|
||||
assert md('test <blockquote> text </blockquote> after') == 'test\n> text\n\nafter'
|
||||
assert md(' <ol> <li> x </li> <li> y </li> </ol> ') == '\n\n1. x\n2. y\n'
|
||||
assert md(' <ul> <li> x </li> <li> y </li> </ol> ') == '\n\n* x\n* y\n'
|
||||
assert md('test <pre> foo </pre> bar') == 'test\n\n```\n foo \n```\n\nbar'
|
||||
assert md('test <pre> foo </pre> bar') == 'test\n\n```\n foo\n```\n\nbar'
|
||||
|
||||
70
tests/types.py
Normal file
70
tests/types.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from markdownify import markdownify, ASTERISK, BACKSLASH, LSTRIP, RSTRIP, SPACES, STRIP, UNDERLINED, UNDERSCORE, MarkdownConverter
|
||||
from bs4 import BeautifulSoup
|
||||
from typing import Union
|
||||
|
||||
markdownify("<p>Hello</p>") == "Hello" # test default of STRIP
|
||||
markdownify("<p>Hello</p>", strip_document=LSTRIP) == "Hello\n\n"
|
||||
markdownify("<p>Hello</p>", strip_document=RSTRIP) == "\n\nHello"
|
||||
markdownify("<p>Hello</p>", strip_document=STRIP) == "Hello"
|
||||
markdownify("<p>Hello</p>", strip_document=None) == "\n\nHello\n\n"
|
||||
|
||||
# default options
|
||||
MarkdownConverter(
|
||||
autolinks=True,
|
||||
bs4_options='html.parser',
|
||||
bullets='*+-',
|
||||
code_language='',
|
||||
code_language_callback=None,
|
||||
convert=None,
|
||||
default_title=False,
|
||||
escape_asterisks=True,
|
||||
escape_underscores=True,
|
||||
escape_misc=False,
|
||||
heading_style=UNDERLINED,
|
||||
keep_inline_images_in=[],
|
||||
newline_style=SPACES,
|
||||
strip=None,
|
||||
strip_document=STRIP,
|
||||
strip_pre=STRIP,
|
||||
strong_em_symbol=ASTERISK,
|
||||
sub_symbol='',
|
||||
sup_symbol='',
|
||||
table_infer_header=False,
|
||||
wrap=False,
|
||||
wrap_width=80,
|
||||
).convert("")
|
||||
|
||||
# custom options
|
||||
MarkdownConverter(
|
||||
strip_document=None,
|
||||
bullets="-",
|
||||
escape_asterisks=True,
|
||||
escape_underscores=True,
|
||||
escape_misc=True,
|
||||
autolinks=True,
|
||||
default_title=True,
|
||||
newline_style=BACKSLASH,
|
||||
sup_symbol='^',
|
||||
sub_symbol='^',
|
||||
keep_inline_images_in=['h3'],
|
||||
wrap=True,
|
||||
wrap_width=80,
|
||||
strong_em_symbol=UNDERSCORE,
|
||||
code_language='python',
|
||||
code_language_callback=None
|
||||
).convert("")
|
||||
|
||||
html = '<b>test</b>'
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
MarkdownConverter().convert_soup(soup) == '**test**'
|
||||
|
||||
|
||||
def callback(el: BeautifulSoup) -> Union[str, None]:
|
||||
return el['class'][0] if el.has_attr('class') else None
|
||||
|
||||
|
||||
MarkdownConverter(code_language_callback=callback).convert("")
|
||||
MarkdownConverter(code_language_callback=lambda el: None).convert("")
|
||||
|
||||
markdownify('<pre class="python">test\n foo\nbar</pre>', code_language_callback=callback)
|
||||
markdownify('<pre class="python">test\n foo\nbar</pre>', code_language_callback=lambda el: None)
|
||||
Reference in New Issue
Block a user