libs: new markdown rendereres for python3

This commit is contained in:
RhodeCode Admin 2023-07-18 11:43:49 +02:00
parent a637b39cbf
commit 12e9cdc96a
2 changed files with 42 additions and 118 deletions

View file

@ -24,6 +24,8 @@ import xml.etree.ElementTree as etree
from markdown.extensions import Extension
from markdown.extensions.fenced_code import FencedCodeExtension
from markdown.extensions.tables import TableExtension
from markdown.extensions.nl2br import Nl2BrExtension as _Nl2BrExtension
from markdown.extensions.wikilinks import WikiLinkExtension
from markdown.inlinepatterns import Pattern
import gfm
@ -87,53 +89,8 @@ class SubstituteTagInlineProcessor(SimpleTagInlineProcessor):
return etree.Element(self.tag), m.start(0), m.end(0)
class Nl2BrExtension(Extension):
BR_RE = r'\n'
def extendMarkdown(self, md, md_globals):
br_tag = SubstituteTagInlineProcessor(self.BR_RE, 'br')
md.inlinePatterns.add('nl', br_tag, '_end')
class GithubFlavoredMarkdownExtension(Extension):
"""
An extension that is as compatible as possible with GitHub-flavored
Markdown (GFM).
This extension aims to be compatible with the variant of GFM that GitHub
uses for Markdown-formatted gists and files (including READMEs). This
variant seems to have all the extensions described in the `GFM
documentation`_, except:
- Newlines in paragraphs are not transformed into ``br`` tags.
- Intra-GitHub links to commits, repositories, and issues are not
supported.
If you need support for features specific to GitHub comments and issues,
please use :class:`mdx_gfm.GithubFlavoredMarkdownExtension`.
.. _GFM documentation: https://guides.github.com/features/mastering-markdown/
"""
def extendMarkdown(self, md, md_globals):
# Built-in extensions
Nl2BrExtension().extendMarkdown(md, md_globals)
FencedCodeExtension().extendMarkdown(md, md_globals)
TableExtension().extendMarkdown(md, md_globals)
# Custom extensions
gfm.AutolinkExtension().extendMarkdown(md, md_globals)
gfm.AutomailExtension().extendMarkdown(md, md_globals)
gfm.HiddenHiliteExtension([
('guess_lang', 'False'),
('css_class', 'highlight')
]).extendMarkdown(md, md_globals)
gfm.SemiSaneListExtension().extendMarkdown(md, md_globals)
gfm.SpacedLinkExtension().extendMarkdown(md, md_globals)
gfm.StrikethroughExtension().extendMarkdown(md, md_globals)
gfm.TaskListExtension([
('list_attrs', {'class': 'checkbox'})
]).extendMarkdown(md, md_globals)
class Nl2BrExtension(_Nl2BrExtension):
pass
# Global Vars
@ -167,9 +124,9 @@ class UrlizePattern(markdown.inlinepatterns.Pattern):
return el
class UrlizeExtension(markdown.Extension):
class UrlizeExtension(Extension):
""" Urlize Extension for Python-Markdown. """
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
""" Replace autolink with UrlizePattern """
md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md)

View file

@ -29,6 +29,7 @@ import lxml
import logging
import urllib.parse
import bleach
import pycmarkgfm
from mako.lookup import TemplateLookup
from mako.template import Template as MakoTemplate
@ -39,8 +40,7 @@ from docutils import writers
from docutils.writers import html4css1
import markdown
from rhodecode.lib.markdown_ext import GithubFlavoredMarkdownExtension
from rhodecode.lib.utils2 import (safe_unicode, md5_safe, MENTIONS_REGEX)
from rhodecode.lib.utils2 import safe_str, md5_safe, MENTIONS_REGEX
log = logging.getLogger(__name__)
@ -81,7 +81,7 @@ class CustomHTMLTranslator(writers.html4css1.HTMLTranslator):
class RhodeCodeWriter(writers.html4css1.Writer):
def __init__(self):
writers.Writer.__init__(self)
super(RhodeCodeWriter, self).__init__()
self.translator_class = CustomHTMLTranslator
@ -111,7 +111,7 @@ def relative_links(html_source, server_paths):
else:
el.attrib['href'] = relative_path(src, server_paths['standard'])
return lxml.html.tostring(doc)
return lxml.html.tostring(doc, encoding='unicode')
def relative_path(path, request_path, is_repo_file=None):
@ -126,7 +126,7 @@ def relative_path(path, request_path, is_repo_file=None):
produces: '/repo/files/logo.png'
"""
# TODO(marcink): unicode/str support ?
# maybe=> safe_unicode(urllib.quote(safe_str(final_path), '/:'))
# maybe=> safe_str(urllib.quote(safe_str(final_path), '/:'))
def dummy_check(p):
return True # assume default is a valid file path
@ -135,8 +135,8 @@ def relative_path(path, request_path, is_repo_file=None):
if not path:
return request_path
path = safe_unicode(path)
request_path = safe_unicode(request_path)
path = safe_str(path)
request_path = safe_str(request_path)
if path.startswith(('data:', 'javascript:', '#', ':')):
# skip data, anchor, invalid links
@ -185,22 +185,25 @@ def get_markdown_renderer(extensions, output_format):
if _cached_markdown_renderer is None:
_cached_markdown_renderer = markdown.Markdown(
extensions=extensions,
enable_attributes=False, output_format=output_format)
extensions=extensions + ['legacy_attrs'],
output_format=output_format)
return _cached_markdown_renderer
_cached_markdown_renderer_flavored = None
def get_markdown_renderer_flavored(extensions, output_format):
global _cached_markdown_renderer_flavored
"""
Dummy wrapper to mimic markdown API and render github HTML rendered
if _cached_markdown_renderer_flavored is None:
_cached_markdown_renderer_flavored = markdown.Markdown(
extensions=extensions + [GithubFlavoredMarkdownExtension()],
enable_attributes=False, output_format=output_format)
return _cached_markdown_renderer_flavored
"""
md = get_markdown_renderer(extensions, output_format)
class GFM(object):
def convert(self, source):
with pycmarkgfm.parse_gfm(source) as document:
parsed_md = document.to_commonmark()
return md.convert(parsed_md)
return GFM()
class MarkupRenderer(object):
@ -267,7 +270,10 @@ class MarkupRenderer(object):
return getattr(MarkupRenderer, detected_renderer)
@classmethod
def bleach_clean(cls, text):
def sanitize_html(cls, text):
# TODO: replace this with https://nh3.readthedocs.io/en/latest
# bleach is abandoned and deprecated :/
from .bleach_whitelist import markdown_attrs, markdown_tags
allowed_tags = markdown_tags
allowed_attrs = markdown_attrs
@ -275,7 +281,7 @@ class MarkupRenderer(object):
try:
return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs)
except Exception:
return 'UNPARSEABLE TEXT'
return 'TEXT CANNOT BE PARSED USING SANITIZE'
@classmethod
def renderer_from_filename(cls, filename, exclude):
@ -302,54 +308,17 @@ class MarkupRenderer(object):
Renders a given filename using detected renderer
it detects renderers based on file extension or mimetype.
At last it will just do a simple html replacing new lines with <br/>
:param file_name:
:param source:
"""
renderer = self._detect_renderer(source, filename)
readme_data = renderer(source)
return readme_data
@classmethod
def _flavored_markdown(cls, text):
"""
Github style flavored markdown
:param text:
"""
# Extract pre blocks.
extractions = {}
def pre_extraction_callback(matchobj):
digest = md5_safe(matchobj.group(0))
extractions[digest] = matchobj.group(0)
return "{gfm-extraction-%s}" % digest
pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
text = re.sub(pattern, pre_extraction_callback, text)
# Prevent foo_bar_baz from ending up with an italic word in the middle.
def italic_callback(matchobj):
s = matchobj.group(0)
if list(s).count('_') >= 2:
return s.replace('_', r'\_')
return s
text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
# Insert pre block extractions.
def pre_insert_callback(matchobj):
return '\n\n' + extractions[matchobj.group(1)]
text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
pre_insert_callback, text)
return text
@classmethod
def urlify_text(cls, text):
def url_func(match_obj):
url_full = match_obj.groups()[0]
return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
return f'<a href="{url_full}">{url_full}</a>'
return cls.URL_PAT.sub(url_func, text)
@ -375,7 +344,7 @@ class MarkupRenderer(object):
@classmethod
def plain(cls, source, universal_newline=True, leading_newline=True):
source = safe_unicode(source)
source = safe_str(source)
if universal_newline:
newline = '\n'
source = newline.join(source.splitlines())
@ -386,7 +355,7 @@ class MarkupRenderer(object):
source += '<br />'
source += rendered_source.replace("\n", '<br />')
rendered = cls.bleach_clean(source)
rendered = cls.sanitize_html(source)
return rendered
@classmethod
@ -409,12 +378,9 @@ class MarkupRenderer(object):
return cls.markdown(mention_hl, safe=safe, flavored=flavored,
mentions=False)
source = safe_unicode(source)
try:
if flavored:
source = cls._flavored_markdown(source)
rendered = markdown_renderer.convert(source)
except Exception:
log.exception('Error when rendering Markdown')
if safe:
@ -424,17 +390,18 @@ class MarkupRenderer(object):
raise
if clean_html:
rendered = cls.bleach_clean(rendered)
rendered = cls.sanitize_html(rendered)
return rendered
@classmethod
def rst(cls, source, safe=True, mentions=False, clean_html=False):
if mentions:
mention_hl = cls.convert_mentions(source, mode='rst')
# we extracted mentions render with this using Mentions false
return cls.rst(mention_hl, safe=safe, mentions=False)
source = safe_unicode(source)
source = safe_str(source)
try:
docutils_settings = dict(
[(alias, None) for alias in
@ -446,7 +413,7 @@ class MarkupRenderer(object):
'syntax_highlight': 'short',
})
for k, v in docutils_settings.items():
for k, v in list(docutils_settings.items()):
directives.register_directive(k, v)
parts = publish_parts(source=source,
@ -454,7 +421,7 @@ class MarkupRenderer(object):
settings_overrides=docutils_settings)
rendered = parts["fragment"]
if clean_html:
rendered = cls.bleach_clean(rendered)
rendered = cls.sanitize_html(rendered)
return parts['html_title'] + rendered
except Exception:
log.exception('Error when rendering RST')
@ -494,7 +461,7 @@ class MarkupRenderer(object):
if 'source' in cell and cell['cell_type'] == 'markdown':
# sanitize similar like in markdown
cell['source'] = cls.bleach_clean(cell['source'])
cell['source'] = cls.sanitize_html(cell['source'])
return nb, resources