We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[v29.1] Le temps de lecture ne fonctionne pas sur les gros contenus
Sur les gros contenus, le temps de lecture affiché est "moins d'une minute" au lieu du temps calculé.

Si ma mémooire est bonne, la solution trouvée avec artragis, firm1 et TAlone sur Discord est de modifier la fonction `render_markdown_stats` du fichier `zds/utils/templatetags/emarkdown.py` pour qu'elle communique avec zmarkdown sur l'*endpoint* `tex` et non pas `html` (celui par défaut).

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `zds/tutorialv2/management/commands/adjust_char_count.py`
Content:
```
1 import logging
2 
3 from django.core.management.base import BaseCommand
4 
5 from zds.tutorialv2.models.database import PublishedContent
6 
7 
8 class Command(BaseCommand):
9     """
10     `python manage.py adjust_char_count`; set the number of characters for every published content.
11 
12     """
13 
14     help = 'Set the number of characters for every published content'
15 
16     def add_arguments(self, parser):
17         parser.add_argument('--id', dest='id', type=str)
18 
19     def handle(self, *args, **options):
20         opt = options.get('id')
21         if opt:
22             ids = list(set(opt.split(',')))
23             query = PublishedContent.objects.filter(content_pk__in=ids, must_redirect=False)
24         else:
25             query = PublishedContent.objects.filter(must_redirect=False)
26 
27         for content in query:
28             logging.error('ids = ' + str(content))
29             content.char_count = content.get_char_count()
30             content.save()
31             logging.info('content %s got %d letters', content.title(), content.char_count)
32
```

Path: `zds/utils/templatetags/emarkdown.py`
Content:
```
1 import re
2 import json
3 import logging
4 from requests import post, HTTPError
5 
6 from django import template
7 from django.conf import settings
8 from django.template.defaultfilters import stringfilter
9 from django.utils.safestring import mark_safe
10 from django.utils.translation import ugettext_lazy as _
11 
12 logger = logging.getLogger(__name__)
13 register = template.Library()
14 """
15 Markdown related filters.
16 """
17 
18 # Constants
19 MAX_ATTEMPTS = 3
20 MD_PARSING_ERROR = _('Une erreur est survenue dans la génération de texte Markdown. Veuillez rapporter le bug.')
21 
22 FORMAT_ENDPOINTS = {
23     'html': '/html',
24     'texfile': '/latex-document',
25     'epub': '/epub',
26     'tex': '/latex',
27 }
28 
29 
30 def _render_markdown_once(md_input, *, output_format='html', **kwargs):
31     """
32     Returns None on error (error details are logged). No retry mechanism.
33     """
34     def log_args():
35         logger.error('md_input: {!r}'.format(md_input))
36         logger.error('kwargs: {!r}'.format(kwargs))
37 
38     inline = kwargs.get('inline', False) is True
39 
40     if settings.ZDS_APP['zmd']['disable_pings'] is True:
41         kwargs['disable_ping'] = True
42 
43     endpoint = FORMAT_ENDPOINTS[output_format]
44 
45     try:
46         timeout = 10
47         if output_format.startswith('tex'):
48             # latex may be really long to generate but it is also restrained by server configuration
49             timeout = 120
50         response = post('{}{}'.format(settings.ZDS_APP['zmd']['server'], endpoint), json={
51             'opts': kwargs,
52             'md': str(md_input),
53         }, timeout=timeout)
54     except HTTPError:
55         logger.exception('An HTTP error happened, markdown rendering failed')
56         log_args()
57         return '', {}, []
58 
59     if response.status_code == 413:
60         return '', {}, [{'message': str(_('Texte trop volumineux.'))}]
61 
62     if response.status_code != 200:
63         logger.error('The markdown server replied with status {} (expected 200)'.format(response.status_code))
64         log_args()
65         return '', {}, []
66 
67     try:
68         content, metadata, messages = response.json()
69         logger.debug('Result %s, %s, %s', content, metadata, messages)
70         if messages:
71             logger.error('Markdown errors %s', json.dumps(messages))
72         content = content.strip()
73         if inline:
74             content = content.replace('</p>\n', '\n\n').replace('\n<p>', '\n')
75         return mark_safe(content), metadata, messages
76     except:  # noqa
77         logger.exception('Unexpected exception raised')
78         log_args()
79         return '', {}, []
80 
81 
82 def render_markdown(md_input, *, on_error=None, **kwargs):
83     """Render a markdown string.
84 
85     Returns a tuple ``(rendered_content, metadata)``, where
86     ``rendered_content`` is a string and ``metadata`` is a dict.
87 
88     Handles errors gracefully by returning an user-friendly HTML
89     string which explains that the Markdown rendering has failed
90     (without any technical details).
91 
92     """
93     content, metadata, messages = _render_markdown_once(md_input, **kwargs)
94     if messages and on_error:
95         on_error([m['message'] for m in messages])
96     if content is not None:
97         # Success!
98         return content, metadata, messages
99 
100     # Oops, something went wrong
101 
102     attempts = kwargs.get('attempts', 0)
103     inline = kwargs.get('inline', False) is True
104 
105     if attempts < MAX_ATTEMPTS:
106         if not kwargs:
107             kwargs = dict()
108         return render_markdown(md_input, **dict(kwargs, attempts=attempts + 1))
109 
110     logger.error('Max attempt count reached, giving up')
111     logger.error('md_input: {!r}'.format(md_input))
112     logger.error('kwargs: {!r}'.format(kwargs))
113 
114     # FIXME: This cannot work with LaTeX.
115     if inline:
116         return mark_safe('<p>{}</p>'.format(json.dumps(messages))), metadata, []
117     else:
118         return mark_safe('<div class="error ico-after"><p>{}</p></div>'.format(json.dumps(messages))), metadata, []
119 
120 
121 def render_markdown_stats(md_input, **kwargs):
122     """
123     Returns contents statistics (words and chars)
124     """
125     kwargs['stats'] = True
126     content, metadata, messages = _render_markdown_once(md_input, **kwargs)
127     if metadata:
128         return metadata.get('stats', {}).get('signs', {})
129     return None
130 
131 
132 @register.filter(name='epub_markdown', needs_autoescape=False)
133 def epub_markdown(md_input, image_directory):
134     return emarkdown(md_input, output_format='epub', images_download_dir=image_directory.absolute,
135                      local_url_to_local_path=[settings.MEDIA_URL + 'galleries/[0-9]+', image_directory.relative])
136 
137 
138 @register.filter(needs_autoescape=False)
139 @stringfilter
140 def emarkdown(md_input, use_jsfiddle='', **kwargs):
141     """
142     :param str md_input: Markdown string.
143     :return: HTML string.
144     :rtype: str
145     """
146     disable_jsfiddle = (use_jsfiddle != 'js')
147 
148     content, metadata, messages = render_markdown(
149         md_input,
150         on_error=lambda m: logger.error('Markdown errors %s', str(m)),
151         **dict(kwargs, disable_jsfiddle=disable_jsfiddle))
152 
153     return content or ''
154 
155 
156 @register.filter(needs_autoescape=False)
157 @stringfilter
158 def emarkdown_preview(md_input, use_jsfiddle='', **kwargs):
159     """
160     Filter markdown string and render it to html.
161 
162     :param str md_input: Markdown string.
163     :return: HTML string.
164     :rtype: str
165     """
166     disable_jsfiddle = (use_jsfiddle != 'js')
167 
168     content, metadata, messages = render_markdown(
169         md_input,
170         **dict(kwargs, disable_jsfiddle=disable_jsfiddle))
171 
172     if messages:
173         content = _('</div><div class="preview-error"><strong>Erreur du serveur Markdown:</strong>\n{}'
174                     .format('<br>- '.join([m['message'] for m in messages])))
175         content = mark_safe(content)
176 
177     return content
178 
179 
180 @register.filter(needs_autoescape=False)
181 @stringfilter
182 def emarkdown_inline(text):
183     """
184     Parses inline elements only and renders HTML. Mainly for member signatures.
185     Although they are inline elements, pings are disabled.
186 
187     :param str text: Markdown string.
188     :return: HTML string.
189     :rtype: str
190     """
191     rendered = emarkdown(text, inline=True)
192     return mark_safe(rendered.replace('<a href=', '<a rel="nofollow" href='))
193 
194 
195 def sub_hd(match, count):
196     """Replace header shifted."""
197     subt = match.group(1)
198     lvl = match.group('level')
199     header = match.group('header')
200     end = match.group(4)
201 
202     new_content = subt + '#' * count + lvl + header + end
203 
204     return new_content
205 
206 
207 def shift_heading(text, count):
208     """
209     Shift header in markdown document.
210 
211     :param str text: Text to filter.
212     :param int count:
213     :return: Filtered text.
214     :rtype: str
215     """
216     text_by_code = re.split('(```|~~~)', text)
217     starting_code = None
218     for i, element in enumerate(text_by_code):
219         if element in ['```', '~~~'] and not starting_code:
220             starting_code = element
221         elif element == starting_code:
222             starting_code = None
223         elif starting_code is None:
224             text_by_code[i] = re.sub(r'(^|\n)(?P<level>#{1,4})(?P<header>.*?)#*(\n|$)',
225                                      lambda t: sub_hd(t, count), text_by_code[i])
226 
227     return ''.join(text_by_code)
228 
229 
230 @register.filter('shift_heading_1')
231 def shift_heading_1(text):
232     return shift_heading(text, 1)
233 
234 
235 @register.filter('shift_heading_2')
236 def shift_heading_2(text):
237     return shift_heading(text, 2)
238 
239 
240 @register.filter('shift_heading_3')
241 def shift_heading_3(text):
242     return shift_heading(text, 3)
243
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/zds/tutorialv2/management/commands/adjust_char_count.py b/zds/tutorialv2/management/commands/adjust_char_count.py
--- a/zds/tutorialv2/management/commands/adjust_char_count.py
+++ b/zds/tutorialv2/management/commands/adjust_char_count.py
@@ -1,5 +1,3 @@
-import logging
-
 from django.core.management.base import BaseCommand
 
 from zds.tutorialv2.models.database import PublishedContent
@@ -25,7 +23,7 @@
             query = PublishedContent.objects.filter(must_redirect=False)
 
         for content in query:
-            logging.error('ids = ' + str(content))
+            self.stdout.write('Processing « {} »...'.format(content.title()))
             content.char_count = content.get_char_count()
             content.save()
-            logging.info('content %s got %d letters', content.title(), content.char_count)
+            self.stdout.write('  It got {} letters.'.format(content.char_count))
diff --git a/zds/utils/templatetags/emarkdown.py b/zds/utils/templatetags/emarkdown.py
--- a/zds/utils/templatetags/emarkdown.py
+++ b/zds/utils/templatetags/emarkdown.py
@@ -123,7 +123,9 @@
     Returns contents statistics (words and chars)
     """
     kwargs['stats'] = True
-    content, metadata, messages = _render_markdown_once(md_input, **kwargs)
+    kwargs['disable_images_download'] = True
+    logger.setLevel(logging.INFO)
+    content, metadata, messages = _render_markdown_once(md_input, output_format='tex', **kwargs)
     if metadata:
         return metadata.get('stats', {}).get('signs', {})
     return None

{"golden_diff": "diff --git a/zds/tutorialv2/management/commands/adjust_char_count.py b/zds/tutorialv2/management/commands/adjust_char_count.py\n--- a/zds/tutorialv2/management/commands/adjust_char_count.py\n+++ b/zds/tutorialv2/management/commands/adjust_char_count.py\n@@ -1,5 +1,3 @@\n-import logging\n-\n from django.core.management.base import BaseCommand\n \n from zds.tutorialv2.models.database import PublishedContent\n@@ -25,7 +23,7 @@\n             query = PublishedContent.objects.filter(must_redirect=False)\n \n         for content in query:\n-            logging.error('ids = ' + str(content))\n+            self.stdout.write('Processing \u00ab {} \u00bb...'.format(content.title()))\n             content.char_count = content.get_char_count()\n             content.save()\n-            logging.info('content %s got %d letters', content.title(), content.char_count)\n+            self.stdout.write('  It got {} letters.'.format(content.char_count))\ndiff --git a/zds/utils/templatetags/emarkdown.py b/zds/utils/templatetags/emarkdown.py\n--- a/zds/utils/templatetags/emarkdown.py\n+++ b/zds/utils/templatetags/emarkdown.py\n@@ -123,7 +123,9 @@\n     Returns contents statistics (words and chars)\n     \"\"\"\n     kwargs['stats'] = True\n-    content, metadata, messages = _render_markdown_once(md_input, **kwargs)\n+    kwargs['disable_images_download'] = True\n+    logger.setLevel(logging.INFO)\n+    content, metadata, messages = _render_markdown_once(md_input, output_format='tex', **kwargs)\n     if metadata:\n         return metadata.get('stats', {}).get('signs', {})\n     return None\n", "issue": "[v29.1] Le temps de lecture ne fonctionne pas sur les gros contenus\nSur les gros contenus, le temps de lecture affich\u00e9 est \"moins d'une minute\" au lieu du temps calcul\u00e9.\r\n\r\nSi ma m\u00e9mooire est bonne, la solution trouv\u00e9e avec artragis, firm1 et TAlone sur Discord est de modifier la fonction `render_markdown_stats` du fichier `zds/utils/templatetags/emarkdown.py` pour qu'elle communique avec zmarkdown sur l'*endpoint* `tex` et non pas `html` (celui par d\u00e9faut).\n", "before_files": [{"content": "import logging\n\nfrom django.core.management.base import BaseCommand\n\nfrom zds.tutorialv2.models.database import PublishedContent\n\n\nclass Command(BaseCommand):\n    \"\"\"\n    `python manage.py adjust_char_count`; set the number of characters for every published content.\n\n    \"\"\"\n\n    help = 'Set the number of characters for every published content'\n\n    def add_arguments(self, parser):\n        parser.add_argument('--id', dest='id', type=str)\n\n    def handle(self, *args, **options):\n        opt = options.get('id')\n        if opt:\n            ids = list(set(opt.split(',')))\n            query = PublishedContent.objects.filter(content_pk__in=ids, must_redirect=False)\n        else:\n            query = PublishedContent.objects.filter(must_redirect=False)\n\n        for content in query:\n            logging.error('ids = ' + str(content))\n            content.char_count = content.get_char_count()\n            content.save()\n            logging.info('content %s got %d letters', content.title(), content.char_count)\n", "path": "zds/tutorialv2/management/commands/adjust_char_count.py"}, {"content": "import re\nimport json\nimport logging\nfrom requests import post, HTTPError\n\nfrom django import template\nfrom django.conf import settings\nfrom django.template.defaultfilters import stringfilter\nfrom django.utils.safestring import mark_safe\nfrom django.utils.translation import ugettext_lazy as _\n\nlogger = logging.getLogger(__name__)\nregister = template.Library()\n\"\"\"\nMarkdown related filters.\n\"\"\"\n\n# Constants\nMAX_ATTEMPTS = 3\nMD_PARSING_ERROR = _('Une erreur est survenue dans la g\u00e9n\u00e9ration de texte Markdown. Veuillez rapporter le bug.')\n\nFORMAT_ENDPOINTS = {\n    'html': '/html',\n    'texfile': '/latex-document',\n    'epub': '/epub',\n    'tex': '/latex',\n}\n\n\ndef _render_markdown_once(md_input, *, output_format='html', **kwargs):\n    \"\"\"\n    Returns None on error (error details are logged). No retry mechanism.\n    \"\"\"\n    def log_args():\n        logger.error('md_input: {!r}'.format(md_input))\n        logger.error('kwargs: {!r}'.format(kwargs))\n\n    inline = kwargs.get('inline', False) is True\n\n    if settings.ZDS_APP['zmd']['disable_pings'] is True:\n        kwargs['disable_ping'] = True\n\n    endpoint = FORMAT_ENDPOINTS[output_format]\n\n    try:\n        timeout = 10\n        if output_format.startswith('tex'):\n            # latex may be really long to generate but it is also restrained by server configuration\n            timeout = 120\n        response = post('{}{}'.format(settings.ZDS_APP['zmd']['server'], endpoint), json={\n            'opts': kwargs,\n            'md': str(md_input),\n        }, timeout=timeout)\n    except HTTPError:\n        logger.exception('An HTTP error happened, markdown rendering failed')\n        log_args()\n        return '', {}, []\n\n    if response.status_code == 413:\n        return '', {}, [{'message': str(_('Texte trop volumineux.'))}]\n\n    if response.status_code != 200:\n        logger.error('The markdown server replied with status {} (expected 200)'.format(response.status_code))\n        log_args()\n        return '', {}, []\n\n    try:\n        content, metadata, messages = response.json()\n        logger.debug('Result %s, %s, %s', content, metadata, messages)\n        if messages:\n            logger.error('Markdown errors %s', json.dumps(messages))\n        content = content.strip()\n        if inline:\n            content = content.replace('</p>\\n', '\\n\\n').replace('\\n<p>', '\\n')\n        return mark_safe(content), metadata, messages\n    except:  # noqa\n        logger.exception('Unexpected exception raised')\n        log_args()\n        return '', {}, []\n\n\ndef render_markdown(md_input, *, on_error=None, **kwargs):\n    \"\"\"Render a markdown string.\n\n    Returns a tuple ``(rendered_content, metadata)``, where\n    ``rendered_content`` is a string and ``metadata`` is a dict.\n\n    Handles errors gracefully by returning an user-friendly HTML\n    string which explains that the Markdown rendering has failed\n    (without any technical details).\n\n    \"\"\"\n    content, metadata, messages = _render_markdown_once(md_input, **kwargs)\n    if messages and on_error:\n        on_error([m['message'] for m in messages])\n    if content is not None:\n        # Success!\n        return content, metadata, messages\n\n    # Oops, something went wrong\n\n    attempts = kwargs.get('attempts', 0)\n    inline = kwargs.get('inline', False) is True\n\n    if attempts < MAX_ATTEMPTS:\n        if not kwargs:\n            kwargs = dict()\n        return render_markdown(md_input, **dict(kwargs, attempts=attempts + 1))\n\n    logger.error('Max attempt count reached, giving up')\n    logger.error('md_input: {!r}'.format(md_input))\n    logger.error('kwargs: {!r}'.format(kwargs))\n\n    # FIXME: This cannot work with LaTeX.\n    if inline:\n        return mark_safe('<p>{}</p>'.format(json.dumps(messages))), metadata, []\n    else:\n        return mark_safe('<div class=\"error ico-after\"><p>{}</p></div>'.format(json.dumps(messages))), metadata, []\n\n\ndef render_markdown_stats(md_input, **kwargs):\n    \"\"\"\n    Returns contents statistics (words and chars)\n    \"\"\"\n    kwargs['stats'] = True\n    content, metadata, messages = _render_markdown_once(md_input, **kwargs)\n    if metadata:\n        return metadata.get('stats', {}).get('signs', {})\n    return None\n\n\n@register.filter(name='epub_markdown', needs_autoescape=False)\ndef epub_markdown(md_input, image_directory):\n    return emarkdown(md_input, output_format='epub', images_download_dir=image_directory.absolute,\n                     local_url_to_local_path=[settings.MEDIA_URL + 'galleries/[0-9]+', image_directory.relative])\n\n\n@register.filter(needs_autoescape=False)\n@stringfilter\ndef emarkdown(md_input, use_jsfiddle='', **kwargs):\n    \"\"\"\n    :param str md_input: Markdown string.\n    :return: HTML string.\n    :rtype: str\n    \"\"\"\n    disable_jsfiddle = (use_jsfiddle != 'js')\n\n    content, metadata, messages = render_markdown(\n        md_input,\n        on_error=lambda m: logger.error('Markdown errors %s', str(m)),\n        **dict(kwargs, disable_jsfiddle=disable_jsfiddle))\n\n    return content or ''\n\n\n@register.filter(needs_autoescape=False)\n@stringfilter\ndef emarkdown_preview(md_input, use_jsfiddle='', **kwargs):\n    \"\"\"\n    Filter markdown string and render it to html.\n\n    :param str md_input: Markdown string.\n    :return: HTML string.\n    :rtype: str\n    \"\"\"\n    disable_jsfiddle = (use_jsfiddle != 'js')\n\n    content, metadata, messages = render_markdown(\n        md_input,\n        **dict(kwargs, disable_jsfiddle=disable_jsfiddle))\n\n    if messages:\n        content = _('</div><div class=\"preview-error\"><strong>Erreur du serveur Markdown:</strong>\\n{}'\n                    .format('<br>- '.join([m['message'] for m in messages])))\n        content = mark_safe(content)\n\n    return content\n\n\n@register.filter(needs_autoescape=False)\n@stringfilter\ndef emarkdown_inline(text):\n    \"\"\"\n    Parses inline elements only and renders HTML. Mainly for member signatures.\n    Although they are inline elements, pings are disabled.\n\n    :param str text: Markdown string.\n    :return: HTML string.\n    :rtype: str\n    \"\"\"\n    rendered = emarkdown(text, inline=True)\n    return mark_safe(rendered.replace('<a href=', '<a rel=\"nofollow\" href='))\n\n\ndef sub_hd(match, count):\n    \"\"\"Replace header shifted.\"\"\"\n    subt = match.group(1)\n    lvl = match.group('level')\n    header = match.group('header')\n    end = match.group(4)\n\n    new_content = subt + '#' * count + lvl + header + end\n\n    return new_content\n\n\ndef shift_heading(text, count):\n    \"\"\"\n    Shift header in markdown document.\n\n    :param str text: Text to filter.\n    :param int count:\n    :return: Filtered text.\n    :rtype: str\n    \"\"\"\n    text_by_code = re.split('(```|~~~)', text)\n    starting_code = None\n    for i, element in enumerate(text_by_code):\n        if element in ['```', '~~~'] and not starting_code:\n            starting_code = element\n        elif element == starting_code:\n            starting_code = None\n        elif starting_code is None:\n            text_by_code[i] = re.sub(r'(^|\\n)(?P<level>#{1,4})(?P<header>.*?)#*(\\n|$)',\n                                     lambda t: sub_hd(t, count), text_by_code[i])\n\n    return ''.join(text_by_code)\n\n\n@register.filter('shift_heading_1')\ndef shift_heading_1(text):\n    return shift_heading(text, 1)\n\n\n@register.filter('shift_heading_2')\ndef shift_heading_2(text):\n    return shift_heading(text, 2)\n\n\n@register.filter('shift_heading_3')\ndef shift_heading_3(text):\n    return shift_heading(text, 3)\n", "path": "zds/utils/templatetags/emarkdown.py"}], "after_files": [{"content": "from django.core.management.base import BaseCommand\n\nfrom zds.tutorialv2.models.database import PublishedContent\n\n\nclass Command(BaseCommand):\n    \"\"\"\n    `python manage.py adjust_char_count`; set the number of characters for every published content.\n\n    \"\"\"\n\n    help = 'Set the number of characters for every published content'\n\n    def add_arguments(self, parser):\n        parser.add_argument('--id', dest='id', type=str)\n\n    def handle(self, *args, **options):\n        opt = options.get('id')\n        if opt:\n            ids = list(set(opt.split(',')))\n            query = PublishedContent.objects.filter(content_pk__in=ids, must_redirect=False)\n        else:\n            query = PublishedContent.objects.filter(must_redirect=False)\n\n        for content in query:\n            self.stdout.write('Processing \u00ab {} \u00bb...'.format(content.title()))\n            content.char_count = content.get_char_count()\n            content.save()\n            self.stdout.write('  It got {} letters.'.format(content.char_count))\n", "path": "zds/tutorialv2/management/commands/adjust_char_count.py"}, {"content": "import re\nimport json\nimport logging\nfrom requests import post, HTTPError\n\nfrom django import template\nfrom django.conf import settings\nfrom django.template.defaultfilters import stringfilter\nfrom django.utils.safestring import mark_safe\nfrom django.utils.translation import ugettext_lazy as _\n\nlogger = logging.getLogger(__name__)\nregister = template.Library()\n\"\"\"\nMarkdown related filters.\n\"\"\"\n\n# Constants\nMAX_ATTEMPTS = 3\nMD_PARSING_ERROR = _('Une erreur est survenue dans la g\u00e9n\u00e9ration de texte Markdown. Veuillez rapporter le bug.')\n\nFORMAT_ENDPOINTS = {\n    'html': '/html',\n    'texfile': '/latex-document',\n    'epub': '/epub',\n    'tex': '/latex',\n}\n\n\ndef _render_markdown_once(md_input, *, output_format='html', **kwargs):\n    \"\"\"\n    Returns None on error (error details are logged). No retry mechanism.\n    \"\"\"\n    def log_args():\n        logger.error('md_input: {!r}'.format(md_input))\n        logger.error('kwargs: {!r}'.format(kwargs))\n\n    inline = kwargs.get('inline', False) is True\n\n    if settings.ZDS_APP['zmd']['disable_pings'] is True:\n        kwargs['disable_ping'] = True\n\n    endpoint = FORMAT_ENDPOINTS[output_format]\n\n    try:\n        timeout = 10\n        if output_format.startswith('tex'):\n            # latex may be really long to generate but it is also restrained by server configuration\n            timeout = 120\n        response = post('{}{}'.format(settings.ZDS_APP['zmd']['server'], endpoint), json={\n            'opts': kwargs,\n            'md': str(md_input),\n        }, timeout=timeout)\n    except HTTPError:\n        logger.exception('An HTTP error happened, markdown rendering failed')\n        log_args()\n        return '', {}, []\n\n    if response.status_code == 413:\n        return '', {}, [{'message': str(_('Texte trop volumineux.'))}]\n\n    if response.status_code != 200:\n        logger.error('The markdown server replied with status {} (expected 200)'.format(response.status_code))\n        log_args()\n        return '', {}, []\n\n    try:\n        content, metadata, messages = response.json()\n        logger.debug('Result %s, %s, %s', content, metadata, messages)\n        if messages:\n            logger.error('Markdown errors %s', json.dumps(messages))\n        content = content.strip()\n        if inline:\n            content = content.replace('</p>\\n', '\\n\\n').replace('\\n<p>', '\\n')\n        return mark_safe(content), metadata, messages\n    except:  # noqa\n        logger.exception('Unexpected exception raised')\n        log_args()\n        return '', {}, []\n\n\ndef render_markdown(md_input, *, on_error=None, **kwargs):\n    \"\"\"Render a markdown string.\n\n    Returns a tuple ``(rendered_content, metadata)``, where\n    ``rendered_content`` is a string and ``metadata`` is a dict.\n\n    Handles errors gracefully by returning an user-friendly HTML\n    string which explains that the Markdown rendering has failed\n    (without any technical details).\n\n    \"\"\"\n    content, metadata, messages = _render_markdown_once(md_input, **kwargs)\n    if messages and on_error:\n        on_error([m['message'] for m in messages])\n    if content is not None:\n        # Success!\n        return content, metadata, messages\n\n    # Oops, something went wrong\n\n    attempts = kwargs.get('attempts', 0)\n    inline = kwargs.get('inline', False) is True\n\n    if attempts < MAX_ATTEMPTS:\n        if not kwargs:\n            kwargs = dict()\n        return render_markdown(md_input, **dict(kwargs, attempts=attempts + 1))\n\n    logger.error('Max attempt count reached, giving up')\n    logger.error('md_input: {!r}'.format(md_input))\n    logger.error('kwargs: {!r}'.format(kwargs))\n\n    # FIXME: This cannot work with LaTeX.\n    if inline:\n        return mark_safe('<p>{}</p>'.format(json.dumps(messages))), metadata, []\n    else:\n        return mark_safe('<div class=\"error ico-after\"><p>{}</p></div>'.format(json.dumps(messages))), metadata, []\n\n\ndef render_markdown_stats(md_input, **kwargs):\n    \"\"\"\n    Returns contents statistics (words and chars)\n    \"\"\"\n    kwargs['stats'] = True\n    kwargs['disable_images_download'] = True\n    logger.setLevel(logging.INFO)\n    content, metadata, messages = _render_markdown_once(md_input, output_format='tex', **kwargs)\n    if metadata:\n        return metadata.get('stats', {}).get('signs', {})\n    return None\n\n\n@register.filter(name='epub_markdown', needs_autoescape=False)\ndef epub_markdown(md_input, image_directory):\n    return emarkdown(md_input, output_format='epub', images_download_dir=image_directory.absolute,\n                     local_url_to_local_path=[settings.MEDIA_URL + 'galleries/[0-9]+', image_directory.relative])\n\n\n@register.filter(needs_autoescape=False)\n@stringfilter\ndef emarkdown(md_input, use_jsfiddle='', **kwargs):\n    \"\"\"\n    :param str md_input: Markdown string.\n    :return: HTML string.\n    :rtype: str\n    \"\"\"\n    disable_jsfiddle = (use_jsfiddle != 'js')\n\n    content, metadata, messages = render_markdown(\n        md_input,\n        on_error=lambda m: logger.error('Markdown errors %s', str(m)),\n        **dict(kwargs, disable_jsfiddle=disable_jsfiddle))\n\n    return content or ''\n\n\n@register.filter(needs_autoescape=False)\n@stringfilter\ndef emarkdown_preview(md_input, use_jsfiddle='', **kwargs):\n    \"\"\"\n    Filter markdown string and render it to html.\n\n    :param str md_input: Markdown string.\n    :return: HTML string.\n    :rtype: str\n    \"\"\"\n    disable_jsfiddle = (use_jsfiddle != 'js')\n\n    content, metadata, messages = render_markdown(\n        md_input,\n        **dict(kwargs, disable_jsfiddle=disable_jsfiddle))\n\n    if messages:\n        content = _('</div><div class=\"preview-error\"><strong>Erreur du serveur Markdown:</strong>\\n{}'\n                    .format('<br>- '.join([m['message'] for m in messages])))\n        content = mark_safe(content)\n\n    return content\n\n\n@register.filter(needs_autoescape=False)\n@stringfilter\ndef emarkdown_inline(text):\n    \"\"\"\n    Parses inline elements only and renders HTML. Mainly for member signatures.\n    Although they are inline elements, pings are disabled.\n\n    :param str text: Markdown string.\n    :return: HTML string.\n    :rtype: str\n    \"\"\"\n    rendered = emarkdown(text, inline=True)\n    return mark_safe(rendered.replace('<a href=', '<a rel=\"nofollow\" href='))\n\n\ndef sub_hd(match, count):\n    \"\"\"Replace header shifted.\"\"\"\n    subt = match.group(1)\n    lvl = match.group('level')\n    header = match.group('header')\n    end = match.group(4)\n\n    new_content = subt + '#' * count + lvl + header + end\n\n    return new_content\n\n\ndef shift_heading(text, count):\n    \"\"\"\n    Shift header in markdown document.\n\n    :param str text: Text to filter.\n    :param int count:\n    :return: Filtered text.\n    :rtype: str\n    \"\"\"\n    text_by_code = re.split('(```|~~~)', text)\n    starting_code = None\n    for i, element in enumerate(text_by_code):\n        if element in ['```', '~~~'] and not starting_code:\n            starting_code = element\n        elif element == starting_code:\n            starting_code = None\n        elif starting_code is None:\n            text_by_code[i] = re.sub(r'(^|\\n)(?P<level>#{1,4})(?P<header>.*?)#*(\\n|$)',\n                                     lambda t: sub_hd(t, count), text_by_code[i])\n\n    return ''.join(text_by_code)\n\n\n@register.filter('shift_heading_1')\ndef shift_heading_1(text):\n    return shift_heading(text, 1)\n\n\n@register.filter('shift_heading_2')\ndef shift_heading_2(text):\n    return shift_heading(text, 2)\n\n\n@register.filter('shift_heading_3')\ndef shift_heading_3(text):\n    return shift_heading(text, 3)\n", "path": "zds/utils/templatetags/emarkdown.py"}]}