django-markdown-view 0.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. django_markdown_view-0.0.6.dist-info/METADATA +371 -0
  2. django_markdown_view-0.0.6.dist-info/RECORD +42 -0
  3. django_markdown_view-0.0.6.dist-info/WHEEL +5 -0
  4. django_markdown_view-0.0.6.dist-info/licenses/LICENSE +27 -0
  5. django_markdown_view-0.0.6.dist-info/top_level.txt +1 -0
  6. markdown_view/__init__.py +1 -0
  7. markdown_view/apps.py +12 -0
  8. markdown_view/checks.py +46 -0
  9. markdown_view/constants.py +13 -0
  10. markdown_view/escaping.py +82 -0
  11. markdown_view/loaders.py +61 -0
  12. markdown_view/loading.py +57 -0
  13. markdown_view/markdown_extensions.py +19 -0
  14. markdown_view/models.py +0 -0
  15. markdown_view/registry.py +387 -0
  16. markdown_view/templates/markdown_view/markdown.html +28 -0
  17. markdown_view/tests/__init__.py +0 -0
  18. markdown_view/tests/fixtures/loaders_a/readme.md +4 -0
  19. markdown_view/tests/fixtures/loaders_b/readme.md +4 -0
  20. markdown_view/tests/settings/__init__.py +0 -0
  21. markdown_view/tests/settings/coveralls_settings.py +74 -0
  22. markdown_view/tests/test_checks.py +38 -0
  23. markdown_view/tests/test_escaping.py +75 -0
  24. markdown_view/tests/test_loaders.py +68 -0
  25. markdown_view/tests/test_loading.py +37 -0
  26. markdown_view/tests/test_markdown_extensions.py +36 -0
  27. markdown_view/tests/test_registry.py +467 -0
  28. markdown_view/tests/test_settings_validation.py +76 -0
  29. markdown_view/tests/test_views.py +280 -0
  30. markdown_view/tests/testapp/CONTEXT_TEST.md +3 -0
  31. markdown_view/tests/testapp/ESCAPING_TEST.md +14 -0
  32. markdown_view/tests/testapp/LOGGED_IN.md +3 -0
  33. markdown_view/tests/testapp/NESTED.md +5 -0
  34. markdown_view/tests/testapp/NO_HEADERS.md +4 -0
  35. markdown_view/tests/testapp/README.md +35 -0
  36. markdown_view/tests/testapp/STAFF.md +3 -0
  37. markdown_view/tests/testapp/UNROUTED.md +4 -0
  38. markdown_view/tests/testapp/__init__.py +0 -0
  39. markdown_view/tests/testapp/apps.py +6 -0
  40. markdown_view/tests/testapp/docs/OTHER.md +8 -0
  41. markdown_view/tests/urls.py +118 -0
  42. markdown_view/views.py +145 -0
@@ -0,0 +1,57 @@
1
+ """
2
+ Locate and read a ``markdown_view`` ``file_name`` value's underlying
3
+ source file without compiling its raw, pre-Markdown-conversion contents
4
+ as a Django template.
5
+
6
+ Both `MarkdownView` and `registry.py` need to resolve a `file_name` (e.g.
7
+ ``"users/README.md"``) to an actual file on disk, using whatever loaders
8
+ `MARKDOWN_VIEW_LOADERS` configures. The obvious way to do that with
9
+ Django's own `Engine`/`Loader` API is `engine.get_template(file_name)` --
10
+ but `Loader.get_template()` doesn't just locate the file: it also
11
+ constructs a `django.template.base.Template` from its *raw* contents,
12
+ which eagerly compiles and validates that source as Django template
13
+ syntax immediately, before any Markdown conversion has happened. A `.md`
14
+ file whose raw text contains literal template-tag-shaped syntax anywhere
15
+ at all -- inside a fenced code block documenting Django itself, for
16
+ instance, or in an inline-code span -- would then raise
17
+ `TemplateSyntaxError` merely from being *loaded*, well before
18
+ `MarkdownView` gets a chance to convert it to HTML, and therefore before
19
+ `escaping.escape_unsafe_template_syntax()` (which only ever sees the
20
+ already-converted HTML) gets any chance to make that literal syntax safe.
21
+
22
+ `load_markdown_source()` instead walks the engine's configured loaders and
23
+ origins directly, reading each candidate file's raw contents (via each
24
+ loader's own `get_contents()`, the same plain, uncompiled read
25
+ `Loader.get_template()` itself uses before wrapping the result in a
26
+ `Template`) without ever constructing a `Template` from them.
27
+ """
28
+ import os
29
+
30
+ from django.template import TemplateDoesNotExist
31
+
32
+
33
+ def load_markdown_source(file_name, engine):
34
+ """
35
+ Resolve `file_name` to its underlying source file using `engine`'s
36
+ configured template loaders, and return its raw text -- without
37
+ compiling that text as a Django template.
38
+
39
+ :param file_name: A `file_name` value, as passed to
40
+ `MarkdownView.as_view(file_name=...)`.
41
+ :param engine: The `django.template.Engine` whose loaders should be
42
+ used to resolve `file_name`.
43
+ :return: A `(source_text, source_path)` tuple, where `source_path` is
44
+ the absolute, normalized filesystem path the source was read from.
45
+ :raises TemplateDoesNotExist: If no configured loader can locate
46
+ `file_name`.
47
+ """
48
+ tried = []
49
+ for loader in engine.template_loaders:
50
+ for origin in loader.get_template_sources(file_name):
51
+ try:
52
+ contents = loader.get_contents(origin)
53
+ except TemplateDoesNotExist:
54
+ tried.append((origin, "Source does not exist"))
55
+ continue
56
+ return contents, os.path.normpath(origin.name)
57
+ raise TemplateDoesNotExist(file_name, tried=tried)
@@ -0,0 +1,19 @@
1
+ import re
2
+
3
+ from markdown.treeprocessors import Treeprocessor
4
+ from markdown.extensions import Extension
5
+
6
+
7
+ class InlineImageProcessor(Treeprocessor):
8
+ def run(self, root):
9
+ for element in root.iter("img"):
10
+ src_orig = element.attrib.get('src', '')
11
+ src_re = re.sub(r"[/\\]*static[/\\]*", "", src_orig)
12
+ src = "{{% static '{}' %}}".format(src_re)
13
+ element.attrib["src"] = src
14
+ element.attrib["class"] = "img-fluid"
15
+
16
+
17
+ class ImageExtension(Extension):
18
+ def extendMarkdown(self, md):
19
+ md.treeprocessors.register(InlineImageProcessor(md), 'inlineimageprocessor', 15)
File without changes
@@ -0,0 +1,387 @@
1
+ """
2
+ Registry mapping ``MarkdownView`` (and subclass) ``file_name`` values to the
3
+ concrete URLs that serve them.
4
+
5
+ Markdown source files commonly link to other markdown files using plain,
6
+ filesystem-relative links (e.g. ``[See also](../README-cron.md)``), exactly
7
+ as they would on GitHub or in a local editor. When such a file is rendered
8
+ as an HTML page instead of viewed as a raw file, that relative link no
9
+ longer resolves to anything meaningful in the browser -- the rendered page
10
+ lives at whatever URL its own view was routed to, not at the file's
11
+ location on disk.
12
+
13
+ This module lets ``MarkdownView`` automatically discover, for any given
14
+ target ``.md`` file, whether some URL pattern in the project serves that
15
+ file via a ``MarkdownView`` (or subclass), and if so, resolve to that URL
16
+ instead of leaving the raw relative path in place.
17
+
18
+ This is intentionally best-effort: routes behind a required URL argument,
19
+ or wrapped by a decorator that doesn't preserve the underlying view's
20
+ ``view_class``/``view_initkwargs`` attributes, are silently skipped. A link
21
+ that can't be resolved to a known route is left exactly as markdown would
22
+ otherwise have rendered it, unless a fallback root is configured via
23
+ ``MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT`` (see ``rewrite_markdown_links()``),
24
+ in which case such links -- including relative links to files other than
25
+ ``.md``, such as repository-relative source links -- are resolved against
26
+ that root instead.
27
+ """
28
+ import logging
29
+ import os
30
+ import re
31
+ from urllib.parse import urljoin, urlsplit, urlunsplit
32
+
33
+ from django.conf import settings
34
+ from django.core.signals import setting_changed
35
+ from django.template import Engine, TemplateDoesNotExist
36
+ from django.urls import NoReverseMatch, URLResolver, get_resolver, reverse
37
+
38
+ from markdown_view.constants import (
39
+ DEFAULT_MARKDOWN_VIEW_LOADERS, DEFAULT_MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT,
40
+ )
41
+ from markdown_view.loading import load_markdown_source
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # Cached {id(resolver): {absolute file path: full reverse name}} registries.
46
+ # Keyed by the resolver's identity (Django itself caches `get_resolver()` per
47
+ # urlconf, so this naturally rebuilds if `ROOT_URLCONF`/the active urlconf
48
+ # ever changes, e.g. under `override_settings` in tests). Note that the
49
+ # *reverse name* is cached here, not the result of calling `reverse()` on
50
+ # it -- `reverse()` can depend on request/runtime context (e.g. the active
51
+ # language under `i18n_patterns()`, or the active script prefix), so it must
52
+ # be called fresh every time a link is actually rewritten.
53
+ #
54
+ # This cache key does *not* otherwise account for `MARKDOWN_VIEW_LOADERS`,
55
+ # since a route's file resolution normally never changes while its
56
+ # resolver/urlconf stays the same -- so instead we listen for
57
+ # `MARKDOWN_VIEW_LOADERS` changing (e.g. via `override_settings` in tests)
58
+ # below and explicitly clear the cache when it does.
59
+ _registry_cache = {}
60
+
61
+
62
+ def _on_setting_changed(*, setting, **kwargs):
63
+ """
64
+ Invalidate the cached URL registry whenever `MARKDOWN_VIEW_LOADERS`
65
+ changes (e.g. via `override_settings` in tests), since the registry's
66
+ `source_path` keys depend on how that setting resolves `file_name`
67
+ values to files on disk, and merely rebuilding on urlconf/resolver
68
+ identity change (as `get_markdown_view_url_registry()` does) wouldn't
69
+ otherwise catch a loader-only change.
70
+ """
71
+ if setting == "MARKDOWN_VIEW_LOADERS":
72
+ clear_markdown_view_url_registry_cache()
73
+
74
+
75
+ setting_changed.connect(_on_setting_changed)
76
+
77
+
78
+ def _build_engine():
79
+ """
80
+ Return a new `Engine` configured with the same template loaders
81
+ `MarkdownView` itself uses, so relative `file_name` values resolve
82
+ identically here and at render time. Built fresh (not cached at module
83
+ scope) so it always reflects the current `MARKDOWN_VIEW_LOADERS` setting,
84
+ e.g. under `override_settings` in tests.
85
+ """
86
+ return Engine(
87
+ loaders=getattr(
88
+ settings, "MARKDOWN_VIEW_LOADERS", DEFAULT_MARKDOWN_VIEW_LOADERS
89
+ )
90
+ )
91
+
92
+
93
+ def resolve_markdown_source_path(file_name, engine=None):
94
+ """
95
+ Resolve a markdown_view `file_name` to the absolute filesystem path of
96
+ its underlying source file, using the same template-loader resolution
97
+ `MarkdownView` uses to actually load and render it.
98
+
99
+ Deliberately does not use `engine.get_template(...)`: that constructs
100
+ a `Template` from the file's raw, pre-Markdown-conversion contents,
101
+ which eagerly compiles/validates that raw text as Django template
102
+ syntax and can raise `TemplateSyntaxError` for a `.md` file whose raw
103
+ source happens to contain literal template-tag-shaped text anywhere.
104
+ `load_markdown_source()` resolves the same file without compiling it.
105
+
106
+ :param file_name: A `file_name` value, as passed to
107
+ `MarkdownView.as_view(file_name=...)`.
108
+ :param engine: An optional `django.template.Engine` to resolve
109
+ `file_name` with. Built via `_build_engine()` if not provided.
110
+ :return: An absolute, normalized filesystem path, or `None` if it
111
+ cannot be resolved (e.g. no matching file exists).
112
+ """
113
+ if engine is None:
114
+ engine = _build_engine()
115
+ try:
116
+ _, source_path = load_markdown_source(file_name, engine)
117
+ except TemplateDoesNotExist:
118
+ return None
119
+ return source_path
120
+
121
+
122
+ def _iter_markdown_view_routes(resolver, namespace_parts=()):
123
+ """
124
+ Recursively walk a URLResolver's patterns, yielding
125
+ `(file_name, full_reverse_name)` for every leaf pattern whose view is a
126
+ `MarkdownView` (or subclass) constructed with a `file_name` kwarg.
127
+
128
+ :param resolver: A `django.urls.URLResolver` (typically the project's
129
+ root resolver, from `django.urls.get_resolver()`).
130
+ :param namespace_parts: Namespace segments accumulated from ancestor
131
+ `include()` calls, used to build the fully-qualified reverse name.
132
+ """
133
+ # Local import to avoid a hard import-time dependency between this
134
+ # module and `views.py` (which doesn't need to import this module at
135
+ # module load time).
136
+ from markdown_view.views import MarkdownView
137
+
138
+ for pattern in resolver.url_patterns:
139
+ if isinstance(pattern, URLResolver):
140
+ # A nested `include()` -- recurse into it, extending the
141
+ # namespace path if this include() was itself namespaced.
142
+ child_namespace = namespace_parts
143
+ if pattern.namespace:
144
+ child_namespace = namespace_parts + (pattern.namespace,)
145
+ yield from _iter_markdown_view_routes(pattern, child_namespace)
146
+ continue
147
+
148
+ # Leaf pattern (an actual URL, not a nested urlconf). Only
149
+ # `as_view()`-based class-based views expose `view_class`/
150
+ # `view_initkwargs` on their callback -- function views and
151
+ # views wrapped by decorators that don't forward these
152
+ # attributes will simply have `view_class=None` and get skipped
153
+ # below.
154
+ callback = getattr(pattern, "callback", None)
155
+ view_class = getattr(callback, "view_class", None)
156
+ if not (isinstance(view_class, type) and issubclass(view_class, MarkdownView)):
157
+ continue
158
+
159
+ file_name = getattr(callback, "view_initkwargs", {}).get("file_name")
160
+ if not file_name or not pattern.name:
161
+ # No `file_name`, or an unnamed route we can't `reverse()`.
162
+ continue
163
+
164
+ # Build the fully-qualified name (e.g. "users:support:readme")
165
+ # needed to `reverse()` this route later, since `pattern.name`
166
+ # alone is only unique within its own urlconf.
167
+ full_name = ":".join((*namespace_parts, pattern.name))
168
+ yield file_name, full_name
169
+
170
+
171
+ def build_markdown_view_url_registry(resolver=None):
172
+ """
173
+ Build a `{absolute source file path: full reverse name}` mapping for
174
+ every registered `MarkdownView` (or subclass) route in the project, by
175
+ walking the resolved root URLconf.
176
+
177
+ Note that this stores the *reverse name* for each route, not the result
178
+ of calling `reverse()` on it -- `reverse()`'s result can depend on
179
+ request/runtime context (e.g. the active language under
180
+ `i18n_patterns()`, or the active script prefix), so it must be called
181
+ fresh at the point a link is actually rewritten, not cached here.
182
+
183
+ :param resolver: A `django.urls.URLResolver` to walk. Defaults to
184
+ `django.urls.get_resolver()` (the project's root resolver).
185
+ :return: dict mapping each route's absolute source file path to the
186
+ full reverse name (e.g. `"users:support:readme"`) that serves it.
187
+ """
188
+ if resolver is None:
189
+ resolver = get_resolver()
190
+
191
+ engine = _build_engine()
192
+ registry = {}
193
+ for file_name, full_name in _iter_markdown_view_routes(resolver):
194
+ # Map the route to the file it serves by resolving `file_name`
195
+ # (e.g. "users/README.md") through the same loader MarkdownView
196
+ # itself uses, so the key here matches the `source_path` that
197
+ # `rewrite_markdown_links()` will later look up against it.
198
+ source_path = resolve_markdown_source_path(file_name, engine=engine)
199
+ if source_path is None:
200
+ logger.debug(
201
+ "markdown_view: route %r references file_name %r, which "
202
+ "could not be resolved to a source file; skipping.",
203
+ full_name, file_name,
204
+ )
205
+ continue
206
+ try:
207
+ # Routes behind a required URL argument (e.g.
208
+ # "<int:pk>/readme/") can't be reversed without knowing that
209
+ # argument's value, so they're simply not linkable targets.
210
+ # The result is only used here to confirm reversibility --
211
+ # it's discarded rather than cached, since `reverse()` must be
212
+ # called fresh (with the active request's language/script
213
+ # prefix, etc.) whenever a link is actually rewritten.
214
+ reverse(full_name)
215
+ except NoReverseMatch:
216
+ logger.debug(
217
+ "markdown_view: could not reverse %r for file_name %r; "
218
+ "skipping (likely behind a required URL argument).",
219
+ full_name, file_name,
220
+ )
221
+ continue
222
+ if source_path in registry:
223
+ # The same source file is served by more than one route. There's
224
+ # no inherently correct answer here, so we make the choice
225
+ # explicit: the first route encountered wins, and later aliases
226
+ # are logged rather than silently overriding it -- adding a
227
+ # later alias shouldn't unexpectedly change all of a file's
228
+ # internal links to the new alias.
229
+ logger.debug(
230
+ "markdown_view: file_name %r is already routed to %r; "
231
+ "ignoring additional route %r for the same file.",
232
+ file_name, registry[source_path], full_name,
233
+ )
234
+ continue
235
+ registry[source_path] = full_name
236
+ return registry
237
+
238
+
239
+ def get_markdown_view_url_registry(force_refresh=False):
240
+ """
241
+ Return the (cached) `{absolute source file path: full reverse name}`
242
+ registry for the currently active URLconf, building it on first access.
243
+
244
+ :param force_refresh: Rebuild the registry even if a cached copy exists.
245
+ :return: dict mapping each route's absolute source file path to the
246
+ full reverse name that serves it.
247
+ """
248
+ resolver = get_resolver()
249
+ # `get_resolver()` is itself cached per-urlconf by Django, so keying
250
+ # on its identity means this registry naturally rebuilds if the
251
+ # active urlconf ever changes (e.g. `ROOT_URLCONF` swapped via
252
+ # `override_settings` in tests) without needing to track that
253
+ # explicitly.
254
+ cache_key = id(resolver)
255
+ if force_refresh or cache_key not in _registry_cache:
256
+ _registry_cache[cache_key] = build_markdown_view_url_registry(resolver)
257
+ return _registry_cache[cache_key]
258
+
259
+
260
+ def clear_markdown_view_url_registry_cache():
261
+ """Clear all cached URL registries. Primarily useful in tests."""
262
+ _registry_cache.clear()
263
+
264
+
265
+ # Matches an `<a href="...">` opening tag, capturing the text before the
266
+ # href value (group 1), the href value itself (group 2), and the closing
267
+ # quote (group 3), so `_replace()` below can substitute just the middle
268
+ # group back into the original tag without disturbing anything else on
269
+ # the `<a>` tag (other attributes, casing, etc.).
270
+ _HREF_RE = re.compile(r'(<a\b[^>]*\bhref=")([^"]*)(")', re.IGNORECASE)
271
+
272
+
273
+ def rewrite_markdown_links(html, source_path, registry=None):
274
+ """
275
+ Rewrite `<a href="...">` targets in rendered markdown HTML that point
276
+ at other markdown source files -- via a plain, filesystem-relative
277
+ link resolved relative to `source_path`'s directory -- to the actual
278
+ routed URL of the `MarkdownView` serving that file, if one is
279
+ registered.
280
+
281
+ When a relative link doesn't resolve to a registered route (e.g. it
282
+ targets a file that isn't served by any `MarkdownView`, or isn't a
283
+ `.md` file at all -- such as a repository-relative link to source
284
+ code), it's instead resolved against the optional
285
+ `MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT` setting, if configured. This lets
286
+ links like ``[helper](src/example/module.py)`` -- which resolve
287
+ correctly when browsing the source repository, but not when rendered
288
+ as a page served at its own unrelated URL -- point somewhere useful
289
+ (e.g. the same file, viewed on the project's SCM web interface).
290
+ Registered-route resolution always takes precedence over this
291
+ fallback. With no fallback root configured, such links are left
292
+ exactly as markdown would otherwise have rendered them.
293
+
294
+ Links that are absolute (have a scheme or network location, start
295
+ with `/`, or are fragment-only) are always left untouched. Any query
296
+ string or `#fragment` on the original link is preserved on the
297
+ rewritten URL, and `./`/`../` relative-path segments are resolved
298
+ correctly (rather than through simple string concatenation) when
299
+ applying the fallback root.
300
+
301
+ :param html: Rendered HTML markup, already converted from Markdown.
302
+ :param source_path: Absolute filesystem path of the `.md` file `html`
303
+ was rendered from; used to resolve relative link targets.
304
+ :param registry: An optional pre-built `{absolute path: full reverse
305
+ name}` registry. Built via `get_markdown_view_url_registry()` if
306
+ not provided.
307
+ :return: The HTML with resolvable internal links rewritten.
308
+ """
309
+ if registry is None:
310
+ registry = get_markdown_view_url_registry()
311
+ # Relative links in the source `.md` file are resolved the same way a
312
+ # browser or file manager would: relative to the directory the *source
313
+ # file* lives in, not relative to the URL the rendered page happens to
314
+ # be served at.
315
+ source_dir = os.path.dirname(source_path)
316
+ unresolved_link_root = getattr(
317
+ settings,
318
+ "MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT",
319
+ DEFAULT_MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT,
320
+ )
321
+
322
+ def _replace(match):
323
+ prefix, href, suffix = match.groups()
324
+ # Use real URL parsing rather than string tests, so query strings
325
+ # and fragments on otherwise-relative links (e.g.
326
+ # `guide.md?format=print#section`) are recognized and preserved,
327
+ # while links with a scheme (`mailto:`, `tel:`, `custom-scheme:`,
328
+ # `https://...`) or netloc, or that are site-root-relative, are
329
+ # correctly left alone.
330
+ parts = urlsplit(href)
331
+ if parts.scheme or parts.netloc or parts.path.startswith("/"):
332
+ # Already absolute -- nothing for us to resolve, leave as-is.
333
+ return match.group(0)
334
+ if not parts.path:
335
+ # Fragment-only (e.g. "#section") or query-only link -- no
336
+ # relative target to resolve at all.
337
+ return match.group(0)
338
+
339
+ resolved_url = None
340
+ if parts.path.lower().endswith(".md"):
341
+ # Resolve the relative path exactly as the browser would if
342
+ # this were a raw file link, then see if some route serves
343
+ # that exact file.
344
+ resolved_path = os.path.normpath(
345
+ os.path.join(source_dir, parts.path)
346
+ )
347
+ full_name = registry.get(resolved_path)
348
+ if full_name is None:
349
+ logger.debug(
350
+ "markdown_view: no registered route for linked file "
351
+ "%r (resolved from %r while rendering %r).",
352
+ resolved_path, href, source_path,
353
+ )
354
+ else:
355
+ try:
356
+ # Resolved fresh on every call (rather than cached),
357
+ # since `reverse()`'s result can depend on
358
+ # request/runtime context -- e.g. the currently
359
+ # active language under `i18n_patterns()`, or the
360
+ # active script prefix -- not just the URLconf.
361
+ resolved_url = reverse(full_name)
362
+ except NoReverseMatch:
363
+ # The registry is stale relative to the active
364
+ # URLconf (e.g. it changed since the registry was
365
+ # built).
366
+ logger.debug(
367
+ "markdown_view: could not reverse %r for linked "
368
+ "file %r while rendering %r; falling back.",
369
+ full_name, resolved_path, source_path,
370
+ )
371
+
372
+ if resolved_url is None:
373
+ if not unresolved_link_root:
374
+ # No registered route, and no fallback root configured --
375
+ # leave the link exactly as Markdown would otherwise have
376
+ # rendered it, rather than producing a broken URL of our
377
+ # own.
378
+ return match.group(0)
379
+ # `urljoin()` (rather than plain string concatenation)
380
+ # correctly resolves `./` and `../` path segments in the
381
+ # original relative link against the fallback root.
382
+ resolved_url = urljoin(unresolved_link_root, parts.path)
383
+
384
+ new_href = urlunsplit(("", "", resolved_url, parts.query, parts.fragment))
385
+ return f"{prefix}{new_href}{suffix}"
386
+
387
+ return _HREF_RE.sub(_replace, html)
@@ -0,0 +1,28 @@
1
+ {% if use_highlight_js %}
2
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/a11y-light.min.css" integrity="sha512-PW96n2amVglidqEDLPUdjJ0zByhT20poSqWJYZRutR6CP2QH58k96WmorqNnC4QXnosNeqMJM8FR/93isIifDQ==" crossorigin="anonymous" />
3
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha512-lnOllyZZlRk/gQIyjS3+h+LUy54uyM4aGq2zbGc82KTvBlp/fodSpdh/pywPztpU9zUqHcJr+jP+a1zLa9oCJw==" crossorigin="anonymous"></script>
4
+ {% endif %}
5
+
6
+ {% comment %}{{ page_title|safe }}{% endcomment %}
7
+
8
+ {% if use_toc %}
9
+ <section id="markdown_view_toc">
10
+ <h3>Table of Contents</h3>
11
+ {{ markdown_toc }}
12
+ </section>
13
+ {% endif %}
14
+
15
+ <section id="markdown_view_content">
16
+ {{ markdown_content }}
17
+ </section>
18
+
19
+ {% if use_highlight_js %}
20
+ <script>
21
+ hljs.initHighlightingOnLoad();
22
+ $(document).ready(function(){
23
+ $('.md-content div.toc ul').attr("class", "cta-links");
24
+ $('.md-content div.toc ul li a').attr("class", "default-fa");
25
+ $('.md-content table').attr("class", "table table-bordered table-hover table-striped table-mobile tablesorter tablesorter-default");
26
+ });
27
+ </script>
28
+ {% endif %}
File without changes
@@ -0,0 +1,4 @@
1
+ # Fixture used by markdown_view.tests.test_registry to prove that the
2
+ # markdown_view URL registry rebuilds when MARKDOWN_VIEW_LOADERS changes,
3
+ # even without ROOT_URLCONF/the resolver identity changing.
4
+ # "loaders_a" version.
@@ -0,0 +1,4 @@
1
+ # Fixture used by markdown_view.tests.test_registry to prove that the
2
+ # markdown_view URL registry rebuilds when MARKDOWN_VIEW_LOADERS changes,
3
+ # even without ROOT_URLCONF/the resolver identity changing.
4
+ # "loaders_b" version.
File without changes
@@ -0,0 +1,74 @@
1
+ """
2
+ Django settings used to run this project's test suite (see ``manage.py`` and
3
+ ``tox.ini``, which invoke ``manage.py test`` / ``coverage run manage.py
4
+ test`` against
5
+ ``DJANGO_SETTINGS_MODULE=markdown_view.tests.settings.coveralls_settings``).
6
+ """
7
+ import os
8
+
9
+ # The `markdown_view/tests/` directory, used below to locate fixtures.
10
+ TESTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
11
+
12
+ # The repository root. `markdown_view.loaders.MarkdownLoader` falls back to
13
+ # this (via `MarkdownLoader.get_dirs()`) when `MARKDOWN_VIEW_BASE_DIR` isn't
14
+ # set, so this is also exercised directly by `test_loaders.py`.
15
+ BASE_DIR = os.path.dirname(os.path.dirname(TESTS_DIR))
16
+
17
+ SECRET_KEY = "not-a-secret-only-used-to-run-tests"
18
+ DEBUG = True
19
+ ALLOWED_HOSTS = ["*"]
20
+
21
+ INSTALLED_APPS = [
22
+ "django.contrib.auth",
23
+ "django.contrib.contenttypes",
24
+ "django.contrib.sessions",
25
+ "django.contrib.staticfiles",
26
+ "markdown_view",
27
+ "markdown_view.tests.testapp",
28
+ ]
29
+
30
+ MIDDLEWARE = [
31
+ "django.contrib.sessions.middleware.SessionMiddleware",
32
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
33
+ "django.middleware.common.CommonMiddleware",
34
+ ]
35
+
36
+ DATABASES = {
37
+ "default": {
38
+ "ENGINE": "django.db.backends.sqlite3",
39
+ "NAME": ":memory:",
40
+ }
41
+ }
42
+
43
+ ROOT_URLCONF = "markdown_view.tests.urls"
44
+
45
+ TEMPLATES = [
46
+ {
47
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
48
+ "DIRS": [],
49
+ "APP_DIRS": True,
50
+ "OPTIONS": {
51
+ "context_processors": [
52
+ "django.template.context_processors.request",
53
+ "django.contrib.auth.context_processors.auth",
54
+ ],
55
+ },
56
+ },
57
+ ]
58
+
59
+ # The default `MARKDOWN_VIEW_LOADERS` (`markdown_view.loaders.MarkdownLoader`)
60
+ # is used for the vast majority of the suite -- it discovers fixtures under
61
+ # `markdown_view/tests/testapp/` via `INSTALLED_APPS`, exactly as it would in
62
+ # a real project. `markdown_view.tests.test_registry`'s loader-cache
63
+ # invalidation tests override this per-test (via `override_settings`) to
64
+ # point at `fixtures/loaders_a`/`fixtures/loaders_b` instead.
65
+
66
+ AUTH_PASSWORD_VALIDATORS = []
67
+
68
+ LOGIN_URL = "/accounts/login/"
69
+
70
+ STATIC_URL = "/static/"
71
+
72
+ USE_TZ = True
73
+
74
+ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
@@ -0,0 +1,38 @@
1
+ from django.core.checks import Tags, run_checks
2
+ from django.test import SimpleTestCase, override_settings
3
+
4
+ from markdown_view.checks import markdown_view_check
5
+
6
+
7
+ class MarkdownViewCheckTests(SimpleTestCase):
8
+ def test_returns_empty_list_for_valid_settings(self):
9
+ # markdown_view/tests/settings/coveralls_settings.py doesn't define
10
+ # any of the optional MARKDOWN_VIEW_* settings, so there's nothing
11
+ # to flag.
12
+ self.assertEqual(markdown_view_check(app_configs=[]), [])
13
+
14
+
15
+ class MarkdownViewCheckRegistrationTests(SimpleTestCase):
16
+ """
17
+ Confirms `markdown_view_check` is actually *wired up* through Django's
18
+ system-check framework (registered against `Tags.security` in
19
+ `MarkdownViewConfig.ready()`, see `markdown_view/apps.py`), not just
20
+ that the function itself behaves correctly in isolation.
21
+ """
22
+
23
+ def test_run_checks_surfaces_invalid_setting_via_registered_check(self):
24
+ with override_settings(MARKDOWN_VIEW_TEMPLATE_USE_TOC="not-a-bool"):
25
+ errors = run_checks(tags=[Tags.security])
26
+
27
+ self.assertTrue(
28
+ any(error.id == "markdown_view.E001" for error in errors),
29
+ "Expected django.core.checks.run_checks() to surface "
30
+ "markdown_view.E001 for an invalid MARKDOWN_VIEW_* setting; "
31
+ "got: %r" % (errors,),
32
+ )
33
+
34
+ def test_run_checks_reports_no_markdown_view_errors_for_valid_settings(self):
35
+ errors = run_checks(tags=[Tags.security])
36
+ self.assertFalse(
37
+ any(error.id == "markdown_view.E001" for error in errors)
38
+ )