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,371 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-markdown-view
3
+ Version: 0.0.6
4
+ Summary: Serve .md pages as Django views.
5
+ Author-email: "Ryan J. Sullivan" <ryanj@ryanjsullivan.com>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, http://github.com/rgs258/django-markdown-view
8
+ Keywords: django,markdown,markdown view,md
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: Django :: 5.2
11
+ Classifier: Framework :: Django :: 6.0
12
+ Classifier: Framework :: Django
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/x-rst
24
+ License-File: LICENSE
25
+ Requires-Dist: django>=5.2
26
+ Requires-Dist: markdown>=3.2
27
+ Dynamic: license-file
28
+
29
+ Django Markdown View
30
+ ====================
31
+
32
+ .. image:: https://github.com/rgs258/django-markdown-view/actions/workflows/test.yml/badge.svg
33
+ :target: https://github.com/rgs258/django-markdown-view/actions/workflows/test.yml
34
+
35
+ .. image:: https://coveralls.io/repos/github/rgs258/django-markdown-view/badge.svg?branch=master
36
+ :target: https://coveralls.io/github/rgs258/django-markdown-view?branch=master
37
+
38
+
39
+ **Serve .md pages as Django views.**
40
+
41
+ This package aims to make it easy to serve .md files on Django sites.
42
+
43
+
44
+ .. contents:: Contents
45
+ :depth: 5
46
+
47
+ Installation
48
+ ------------
49
+
50
+ #. Install with ``pip install django-markdown-view``.
51
+
52
+ #. Add ``'markdown_view'`` to your ``INSTALLED_APPS`` settings.
53
+
54
+ .. code-block:: python
55
+
56
+ INSTALLED_APPS = [
57
+ ...,
58
+ 'markdown_view',
59
+ ...
60
+ ]
61
+
62
+ #. (OPTIONAL) Add ``MARKDOWN_VIEW_BASE_DIR`` or ``BASE_DIR`` to settings
63
+ The dictionary of the application's base. See Settings_ below
64
+
65
+ For example, if settings are in config/settings/base.py, then:
66
+
67
+ .. code-block:: python
68
+
69
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
70
+
71
+
72
+ Usage
73
+ -----
74
+
75
+ Views
76
+ ~~~~~
77
+
78
+ Use one of ``MarkdownView``, ``LoggedInMarkdownView``, or ``StaffMarkdownView``
79
+ from ``markdown_view.views`` to serve a .md file
80
+
81
+ .. code-block:: python
82
+
83
+ from markdown_view.views import StaffMarkdownView
84
+
85
+ path('readme/',
86
+ StaffMarkdownView.as_view(file_name='my_app/README.md'),
87
+ name="readme"),
88
+
89
+ Settings
90
+ ~~~~~~~~
91
+
92
+ All settings are optional. See `<markdown_view/constants.py>`_ for the defaults.
93
+
94
+ * `MARKDOWN_VIEW_BASE_DIR` and `BASE_DIR`
95
+
96
+ When present, the value is taken as a location to append to the list of dirs that
97
+ Django's `django.template.utils.get_app_template_dirs` will return when passed
98
+ `dirname=""`. This is used to locate .md files in the root of the project, e.g.,
99
+ a README.md file. Looks for `BASE_DIR` if `MARKDOWN_VIEW_BASE_DIR` is not found.
100
+
101
+ * `MARKDOWN_VIEW_LOADERS`
102
+
103
+ A list of loaders that locate .md files. The default list includes only
104
+ `markdown_view.loaders.MarkdownLoader` which will by default try to load .md files
105
+ from root directories in INSTALLED_APPS packages much the same as Django's
106
+ `django.template.loaders.app_directories.Loader` looks to load from "templates".
107
+
108
+ * `MARKDOWN_VIEW_LOADER_TEMPLATES_DIR`
109
+
110
+ The name of the directories in INSTALLED_APPS packages in which to locate .md
111
+ files. Defaults to "" in order to locate .md filed in the root directories.
112
+
113
+ * `MARKDOWN_VIEW_EXTENSIONS`
114
+
115
+ The extensions to enable. These extensions are enabled be default:
116
+
117
+ * `toc`:
118
+ generates a Table of Contents. If `toc` is removed from extensions, then
119
+ `MARKDOWN_VIEW_TEMPLATE_USE_TOC` must be set to False.
120
+
121
+ * `tables`:
122
+ enables tables.
123
+
124
+ * `fenced_code`:
125
+ enables code blocks. If `fenced_code` is removed from extensions, then
126
+ `MARKDOWN_VIEW_TEMPLATE_USE_HIGHLIGHT_JS`, which provides the highlighting for
127
+ code blocks, can be disabled.
128
+
129
+ * `markdown_view.markdown_extensions.ImageExtension`:
130
+ makes images responsive in bootstrap4.
131
+
132
+ See https://python-markdown.github.io/extensions/ and
133
+ https://github.com/Python-Markdown/markdown/wiki/Third-Party-Extensions for more
134
+ extensions.
135
+
136
+ You can create your own extensions by following
137
+ https://github.com/Python-Markdown/markdown/wiki/Tutorial-1---Writing-Extensions-for-Python-Markdown
138
+
139
+ * `MARKDOWN_VIEW_TEMPLATE`
140
+
141
+ The Django template that'll be used to render the HTML that is generated from the
142
+ Markdown. Set your own template to style your pages. Context includes:
143
+
144
+ * `markdown_content`:
145
+ The HTML produced from the Markdown.
146
+
147
+ * `use_highlight_js`:
148
+ If highlight.js is enabled.
149
+
150
+ * `use_toc`:
151
+ If the table of contents should be rendered.
152
+
153
+ * `markdown_toc`:
154
+ A table of contents from the headers of the Markdown. Not set when `use_toc`
155
+ is False.
156
+
157
+ * `page_title`:
158
+ A guess at a page title, for now it's the first row of the TOC. Not set when
159
+ `use_toc` is False, or when the Markdown has no headers for the TOC to be
160
+ built from.
161
+
162
+ * `MARKDOWN_VIEW_TEMPLATE_USE_TOC`
163
+
164
+ Whether to render the TOC. If false, in the template context, `use_toc` is False
165
+ and `markdown_toc` and `page_title` are not present.
166
+
167
+ * `MARKDOWN_VIEW_TEMPLATE_USE_HIGHLIGHT_JS`
168
+
169
+ Whether to load and activate the highlight.js library in the template.
170
+
171
+ * `MARKDOWN_VIEW_REWRITE_INTERNAL_LINKS`
172
+
173
+ Defaults to `True`. Markdown source files commonly link to other `.md`
174
+ files using plain, filesystem-relative links (e.g.
175
+ ``[See also](../README-cron.md)``), exactly as they'd resolve on GitHub
176
+ or in a local editor. Since a rendered page is served at its *view's*
177
+ URL rather than at the source file's location on disk, such a link
178
+ would otherwise 404 in the browser.
179
+
180
+ When enabled, `MarkdownView` looks up whether any URL pattern in the
181
+ project serves the linked `.md` file via a `MarkdownView` (or
182
+ subclass), and if so, rewrites the link to that view's actual URL
183
+ instead of leaving the raw relative file path in place. See
184
+ `markdown_view/registry.py <markdown_view/registry.py>`_ for the
185
+ resolution logic. This is best-effort: links to files that aren't
186
+ routed anywhere, or whose route is behind a required URL argument, are
187
+ left exactly as Markdown would otherwise have rendered them -- nothing
188
+ breaks if a linked file simply isn't routed, unless
189
+ `MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT` is also configured (see below).
190
+ Set to `False` to disable this rewriting entirely, including the
191
+ `MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT` fallback.
192
+
193
+ * `MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT`
194
+
195
+ Defaults to `None` (disabled). Relative links that `MARKDOWN_VIEW_REWRITE_INTERNAL_LINKS`
196
+ can't resolve to a registered `MarkdownView` route -- including links to
197
+ files other than `.md`, such as repository-relative links to source
198
+ code (e.g. ``[`example.module.helper`](src/example/module.py)``) --
199
+ are left unchanged by default. Setting this to a root URL instead
200
+ resolves such links against it, e.g.:
201
+
202
+ .. code-block:: python
203
+
204
+ MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT = (
205
+ "https://git.example.com/example/project/-/blob/main/"
206
+ )
207
+
208
+ With the setting above, ``[`example.module.helper`](src/example/module.py)``
209
+ would render as a link to
210
+ ``https://git.example.com/example/project/-/blob/main/src/example/module.py``.
211
+ Registered-route rewriting always takes precedence over this fallback;
212
+ it only applies once a link has already failed to resolve to a route.
213
+ `./` and `../` path segments, and any query string or `#fragment`, are
214
+ resolved/preserved correctly. Absolute URLs, root-relative URLs,
215
+ fragment-only links, and URLs with an explicit scheme are never
216
+ affected by this setting.
217
+
218
+ * `MARKDOWN_VIEW_ESCAPE_UNSAFE_TEMPLATE_SYNTAX`
219
+
220
+ Defaults to `True`. `MarkdownView` re-parses the HTML converted from
221
+ Markdown as a Django template, so that the `{% static %}` tags
222
+ `ImageExtension` injects for image sources (and, when
223
+ `MARKDOWN_VIEW_USE_REQUEST_CONTEXT`/`MARKDOWN_VIEW_EXTRA_CONTEXT` are
224
+ used, any `{{ variable }}` reference to a real context key) actually
225
+ resolve. Markdown source files are prose, though, and prose about
226
+ Django itself routinely contains literal `{% ... %}`/`{{ ... }}` text
227
+ that was never meant to be executed as a template -- left unescaped,
228
+ an unrecognized `{% ... %}` block raises `TemplateSyntaxError` (a hard
229
+ 500 for the whole page), and an unrecognized `{{ ... }}` variable is
230
+ silently rendered as an empty string (silent content loss, with no
231
+ error at all).
232
+
233
+ When enabled (the default), every `{% ... %}`/`{{ ... }}` span in the
234
+ converted HTML is escaped to inert literal text *except* the exact
235
+ `{% static '...' %}` tags `ImageExtension` injects, and bare
236
+ `{{ identifier }}` references to a name actually present in the
237
+ context the page renders with. See
238
+ `markdown_view/escaping.py <markdown_view/escaping.py>`_ for the exact
239
+ rules. Set to `False` to restore the previous, fully-permissive
240
+ behavior (the whole converted document is parsed as a real Django
241
+ template, with no escaping at all).
242
+
243
+ Experimental Settings
244
+ ~~~~~~~~~~~~~~~~~~~~~
245
+
246
+ * `MARKDOWN_VIEW_USE_REQUEST_CONTEXT`
247
+
248
+ If the request context should be used as a base when creating the context with
249
+ which to render the Markdown internally. This is because the Markdown is rendered
250
+ once first in order to prepend it with `{% load static %}`.
251
+ This is not well tested; YMMV. Note that a `{{ name }}` reference only
252
+ resolves against this context if `MARKDOWN_VIEW_ESCAPE_UNSAFE_TEMPLATE_SYNTAX`
253
+ (see above) recognizes `name` as a safe context key -- otherwise it's
254
+ escaped to literal text rather than looked up.
255
+
256
+ * `MARKDOWN_VIEW_EXTRA_CONTEXT`
257
+
258
+ Any extra context to send to the internal render of the Markdown. Can be used
259
+ to expose context to template tags embedded in the Markdown.
260
+ This is not well tested; YMMV. As with `MARKDOWN_VIEW_USE_REQUEST_CONTEXT`
261
+ above, a `{{ name }}` reference to one of these keys is only left live
262
+ when `MARKDOWN_VIEW_ESCAPE_UNSAFE_TEMPLATE_SYNTAX` recognizes it as safe.
263
+
264
+
265
+ Implementation
266
+ --------------
267
+
268
+ At a high level, `MarkdownView` will:
269
+
270
+ #. Use a template loader to locate `.md` given as `file_name`
271
+
272
+ #. Convert the Markdown to HTML
273
+
274
+ #. If `MARKDOWN_VIEW_REWRITE_INTERNAL_LINKS` is enabled, rewrite any
275
+ relative links to other `.md` files that resolve to a file served by a
276
+ registered `MarkdownView` route, to that route's actual URL; any
277
+ relative link left unresolved by that step is instead resolved against
278
+ `MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT`, if configured
279
+
280
+ #. Render as a template, the resulting HTML prepended with
281
+ `{% load static %}`, into several context variables
282
+
283
+ #. Serve the `MARKDOWN_VIEW_TEMPLATE` with the context variables
284
+
285
+ Release Notes and Contributors
286
+ ------------------------------
287
+
288
+ * `Release notes <https://github.com/rgs258/django-markdown-view/releases>`_
289
+ * `Our wonderful contributors <https://github.com/rgs258/django-markdown-view/graphs/contributors>`_
290
+
291
+ Releasing
292
+ ---------
293
+
294
+ Versions are derived from git tags via `setuptools_scm`_, so there is no
295
+ hardcoded ``version`` to bump.
296
+
297
+ To publish a new release to PyPI:
298
+
299
+ #. Merge the changes for the release into ``master``.
300
+
301
+ #. `Create a GitHub Release`_ with a tag matching the desired version
302
+ (e.g. ``0.0.6``).
303
+
304
+ #. Publishing the release triggers the `Publish to PyPI`_ GitHub Actions
305
+ workflow, which builds the sdist/wheel and uploads them to PyPI using
306
+ `PyPI Trusted Publishing`_ (no stored token required).
307
+
308
+ Publishing beta / pre-releases
309
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
310
+
311
+ Beta and other pre-releases can be published to PyPI the same way as a
312
+ regular release, and this is standard practice: PyPI understands
313
+ `PEP 440`_ pre-release version identifiers and will not let a plain
314
+ ``pip install django-markdown-view`` pick them up by default, so betas are
315
+ safe to publish without risking accidental adoption.
316
+
317
+ #. Tag the release commit with an explicit PEP 440 pre-release suffix
318
+ instead of a plain version, e.g. ``0.0.6b1`` for a beta, or ``0.0.6rc1``
319
+ for a release candidate. ``setuptools_scm`` will pick up the tag as-is.
320
+
321
+ #. `Create a GitHub Release`_ for that tag and check the **"Set as a
322
+ pre-release"** box. This only affects how the release is labeled on
323
+ GitHub; PyPI determines pre-release status purely from the version
324
+ string.
325
+
326
+ #. Publishing the release triggers the same `Publish to PyPI`_ workflow,
327
+ which uploads the pre-release to PyPI. It will appear in the release
328
+ history but will not be installed by ``pip install
329
+ django-markdown-view``.
330
+
331
+ #. Users who want to try it must opt in, e.g. ``pip install --pre
332
+ django-markdown-view`` or by pinning the exact version, such as
333
+ ``pip install django-markdown-view==0.0.6b1``.
334
+
335
+ #. If further fixes are needed, bump the pre-release number (``0.0.6b2``,
336
+ ``0.0.6b3``, ...) rather than reusing one, since PyPI does not allow
337
+ re-uploading files for a version that has already been published.
338
+ Once the release is stable, tag and publish the final version without
339
+ a suffix (e.g. ``0.0.6``), which then becomes installable by default.
340
+
341
+ .. _setuptools_scm: https://github.com/pypa/setuptools_scm
342
+ .. _`Create a GitHub Release`: https://github.com/rgs258/django-markdown-view/releases/new
343
+ .. _`Publish to PyPI`: https://github.com/rgs258/django-markdown-view/actions/workflows/publish.yml
344
+ .. _`PyPI Trusted Publishing`: https://docs.pypi.org/trusted-publishers/
345
+ .. _`PEP 440`: https://peps.python.org/pep-0440/#pre-releases
346
+
347
+ Contributing
348
+ ------------
349
+
350
+ All contributions are very welcomed. Propositions, problems, bugs, and
351
+ enhancement are tracked with `GitHub issues`_ and patches are submitted
352
+ via `pull requests`_.
353
+
354
+ We use `GitHub Actions`_ coupled with `Coveralls`_ as continuous integration tools.
355
+
356
+ .. _`GitHub issues`: https://github.com/rgs258/django-markdown-view/issues
357
+ .. _`pull requests`: https://github.com/rgs258/django-markdown-view/pulls
358
+ .. _`GitHub Actions`: https://github.com/rgs258/django-markdown-view/actions
359
+ .. _Coveralls: https://coveralls.io/github/rgs258/django-markdown-view
360
+
361
+ Requirements
362
+ ------------
363
+
364
+ We aspire to support the currently supported versions of Django.
365
+
366
+ **The Tested With section describes aspirational goals.**
367
+
368
+ Tested with:
369
+
370
+ * Python: 3.10, 3.11, 3.12, 3.13
371
+ * Django: 5.2, 6.0
@@ -0,0 +1,42 @@
1
+ django_markdown_view-0.0.6.dist-info/licenses/LICENSE,sha256=Xun9tu6GoUnr8qy5C3MBQeqJZeuzkw67SQiFzexknls,1542
2
+ markdown_view/__init__.py,sha256=EXOCt-R6NOvU0yLhQcmwQT_U-DVK0TPZYpErkJ6tbko,61
3
+ markdown_view/apps.py,sha256=QwgUCdY8AupkXpT7AOqqigSaPY45bpp7KHeAYvDlMds,332
4
+ markdown_view/checks.py,sha256=z5YHRcwFF-_1vS6citYfLwQA4wFoXp_d7TQg8S_YtiI,1790
5
+ markdown_view/constants.py,sha256=ao2aD9_rgQrCWJRn3sQ7VMHUHOcGcAktW60bmfZJY_k,689
6
+ markdown_view/escaping.py,sha256=zofhvJzLc5qTII_9_IGh5731AWyQ3fCg8CbP-J8tTms,4097
7
+ markdown_view/loaders.py,sha256=dtZ2X01MGfW5cnGMH78jSQ08SATqsv_amPmBM9jOA14,2221
8
+ markdown_view/loading.py,sha256=I-H_d2pwHCmot5Ol9YZ9uDvcMm7qIx6R3yGd74wain0,2749
9
+ markdown_view/markdown_extensions.py,sha256=T_9cRg99vnXyhBA-evqVRRE0kHITx1YitRGJV9IghX8,635
10
+ markdown_view/models.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ markdown_view/registry.py,sha256=tHOKuCX3fYLauRShxK21hibLLleUVRotlFWUXgJCGqg,18244
12
+ markdown_view/views.py,sha256=qNQlhihNz-bs46aicoApDkn8R6Uowy99WiOu8jMvKvQ,6197
13
+ markdown_view/templates/markdown_view/markdown.html,sha256=QyddBcicEe0CnGQFN66BRCNcUr3SUVXBEqwzLm9NCWQ,1232
14
+ markdown_view/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ markdown_view/tests/test_checks.py,sha256=dh4LYFj6kD_r7rEoPtsqfbqpJ5sOQmpnoQEi2-zkaq4,1584
16
+ markdown_view/tests/test_escaping.py,sha256=JGYO6TG5_-GTSCwUlbqDB5DsmOFgiaq1XRkSCkOKzhU,3282
17
+ markdown_view/tests/test_loaders.py,sha256=w1-WBFcRjTnYjZWFRFcJVWqbT5B8CYvU0oCwJiz7sZY,2583
18
+ markdown_view/tests/test_loading.py,sha256=or4xpgWRo5NztC1GKk4Dgix2DTq5mlsEWFFZNoKDf4Q,1534
19
+ markdown_view/tests/test_markdown_extensions.py,sha256=GHgcI3zeUFi-BisdrHJ8gx_NOXOv10q-vAKjuDuQ_kk,1271
20
+ markdown_view/tests/test_registry.py,sha256=5eRczxgaOEzkkBEA7DDKNUGcSj6A7tp4up7lJVAlL4M,20015
21
+ markdown_view/tests/test_settings_validation.py,sha256=dbelvQ2Z5j5Xu0GrOSQB3bsPI_fyVcpn8LyaNHRFM5w,3645
22
+ markdown_view/tests/test_views.py,sha256=lMWQgwHKAO7U2Hqnv6FZacII_cyjdV-Yf_95xxbdPwg,12058
23
+ markdown_view/tests/urls.py,sha256=Sgidy3YVxHGOsvfIjdB0lrRZBAmBVDgn-7m1TLaqcGA,4040
24
+ markdown_view/tests/fixtures/loaders_a/readme.md,sha256=3ZLpASU8bfOp6GY2ExZPrzlnHfCb3LR8qGNpVC_9iLU,227
25
+ markdown_view/tests/fixtures/loaders_b/readme.md,sha256=NwuNR4ZsdXlmp2wE5GvfVNrkYoRzXcHwtqdvU_t72sA,227
26
+ markdown_view/tests/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ markdown_view/tests/settings/coveralls_settings.py,sha256=JG-heiLhtoiGwxKL0sb77yIFqPeZGMAKaM4FcDxMgTs,2263
28
+ markdown_view/tests/testapp/CONTEXT_TEST.md,sha256=25kLvqZC8pHZ2DUuEUVABV28to3aMzUKsN-e5rDZPGY,72
29
+ markdown_view/tests/testapp/ESCAPING_TEST.md,sha256=rH2gp79OE-7CTtH5eGr77Sjc_NbTAcyaYxaKhi74B9w,418
30
+ markdown_view/tests/testapp/LOGGED_IN.md,sha256=_ycP4KN2-LCpdapSnskA-6VLs6f7nG3WTdmAGn3VDsw,73
31
+ markdown_view/tests/testapp/NESTED.md,sha256=hXAcDFiEwtG0aZatjSKRzoDiZIOOH3voYMZqfFGBYjc,192
32
+ markdown_view/tests/testapp/NO_HEADERS.md,sha256=YbL0KxChybEQauZ46zltWsQbzKRcvfGxTHzIZ174vkg,237
33
+ markdown_view/tests/testapp/README.md,sha256=xk_cwqo8kS6qNHCMTmmFsgohoMvzrs_tgIpC8e010S4,896
34
+ markdown_view/tests/testapp/STAFF.md,sha256=kJyaIF4JFSjc4QBcX_jAmlJ32_HHzpmWaygPbLeYqGw,68
35
+ markdown_view/tests/testapp/UNROUTED.md,sha256=6xbM35PNQ2e1FJWmihJ8_aTXSN96vYG_6RUrQIibl0A,157
36
+ markdown_view/tests/testapp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ markdown_view/tests/testapp/apps.py,sha256=my2snU00q5vjhr0BK_fs9tPVnDTLnk6PM8ezS3lV-ZY,173
38
+ markdown_view/tests/testapp/docs/OTHER.md,sha256=xRf-5jaJCnJiaz_bQaVMI5OVpVcuhWlNVejSedyY57k,231
39
+ django_markdown_view-0.0.6.dist-info/METADATA,sha256=5eaR6Bl6OcZEY6JprrDDRA6FS0lKJ7NDu8bYMDM9yYM,14716
40
+ django_markdown_view-0.0.6.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
41
+ django_markdown_view-0.0.6.dist-info/top_level.txt,sha256=MqRRE0ldzEe17rDjvoESYz22dyeBcPAG9ELXUR5FD7w,14
42
+ django_markdown_view-0.0.6.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,27 @@
1
+ Copyright (c) Ryan J. Sullivan and individual contributors.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of Django nor the names of its contributors may be used
15
+ to endorse or promote products derived from this software without
16
+ specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ markdown_view
@@ -0,0 +1 @@
1
+ default_app_config = "markdown_view.apps.MarkdownViewConfig"
markdown_view/apps.py ADDED
@@ -0,0 +1,12 @@
1
+ from django.apps import AppConfig
2
+ from django.core.checks import register, Tags
3
+
4
+ from markdown_view.checks import markdown_view_check
5
+
6
+
7
+ class MarkdownViewConfig(AppConfig):
8
+ name = "markdown_view"
9
+ verbose_name = "Serve .md pages as Django templates"
10
+
11
+ def ready(self):
12
+ register(markdown_view_check, Tags.security)
@@ -0,0 +1,46 @@
1
+ from django.conf import settings
2
+ from django.core import checks
3
+
4
+ # Expected type for each optional markdown_view setting. Validated by
5
+ # `markdown_view_check()` below (registered as a Django system check in
6
+ # `apps.py`), rather than at import time, so `manage.py check` -- and
7
+ # `override_settings` in tests -- can exercise this validation directly.
8
+ SETTINGS_TYPES = {
9
+ "MARKDOWN_VIEW_LOADERS": list,
10
+ "MARKDOWN_VIEW_LOADER_TEMPLATES_DIR": str,
11
+ "MARKDOWN_VIEW_EXTENSIONS": list,
12
+ "MARKDOWN_VIEW_BASE_DIR": str,
13
+ "MARKDOWN_VIEW_TEMPLATE": str,
14
+ "MARKDOWN_VIEW_TEMPLATE_USE_TOC": bool,
15
+ "MARKDOWN_VIEW_TEMPLATE_USE_HIGHLIGHT_JS": bool,
16
+ "MARKDOWN_VIEW_USE_REQUEST_CONTEXT": bool,
17
+ "MARKDOWN_VIEW_EXTRA_CONTEXT": dict,
18
+ "MARKDOWN_VIEW_REWRITE_INTERNAL_LINKS": bool,
19
+ # `None` is an explicitly valid value here (meaning "no fallback root
20
+ # configured"), unlike every other setting in this dict.
21
+ "MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT": (str, type(None)),
22
+ "MARKDOWN_VIEW_ESCAPE_UNSAFE_TEMPLATE_SYNTAX": bool,
23
+ }
24
+
25
+
26
+ def markdown_view_check(app_configs, **kwargs):
27
+ """
28
+ Validate that any of markdown_view's optional settings the project has
29
+ defined are of the expected type.
30
+ """
31
+ errors = []
32
+
33
+ for variable, instance_type in SETTINGS_TYPES.items():
34
+ if hasattr(settings, variable) and not isinstance(
35
+ getattr(settings, variable), instance_type
36
+ ):
37
+ errors.append(
38
+ checks.Error(
39
+ "Setting %s is not of type %s" % (variable, instance_type),
40
+ hint="Update %s in your settings module to be of type %s."
41
+ % (variable, instance_type),
42
+ id="markdown_view.E001",
43
+ )
44
+ )
45
+
46
+ return errors
@@ -0,0 +1,13 @@
1
+ from markdown_view.markdown_extensions import ImageExtension
2
+
3
+ DEFAULT_MARKDOWN_VIEW_LOADERS = ["markdown_view.loaders.MarkdownLoader", ]
4
+ DEFAULT_MARKDOWN_VIEW_LOADER_TEMPLATES_DIR = ""
5
+ DEFAULT_MARKDOWN_VIEW_EXTENSIONS = ["tables", "fenced_code", "toc", ImageExtension(), ]
6
+ DEFAULT_MARKDOWN_VIEW_TEMPLATE = "markdown_view/markdown.html"
7
+ DEFAULT_MARKDOWN_VIEW_TEMPLATE_USE_TOC = True
8
+ DEFAULT_MARKDOWN_VIEW_TEMPLATE_USE_HIGHLIGHT_JS = True
9
+ DEFAULT_MARKDOWN_VIEW_USE_REQUEST_CONTEXT = False
10
+ DEFAULT_MARKDOWN_VIEW_EXTRA_CONTEXT = {}
11
+ DEFAULT_MARKDOWN_VIEW_REWRITE_INTERNAL_LINKS = True
12
+ DEFAULT_MARKDOWN_VIEW_UNRESOLVED_LINK_ROOT = None
13
+ DEFAULT_MARKDOWN_VIEW_ESCAPE_UNSAFE_TEMPLATE_SYNTAX = True
@@ -0,0 +1,82 @@
1
+ """
2
+ Escaping for literal Django template syntax found inside rendered Markdown.
3
+
4
+ `MarkdownView` re-parses the HTML produced from a `.md` file as a Django
5
+ template, so that the `{% static %}` tags `ImageExtension` injects for
6
+ image sources (and, when a project opts in via
7
+ `MARKDOWN_VIEW_EXTRA_CONTEXT`/`MARKDOWN_VIEW_USE_REQUEST_CONTEXT`, any
8
+ `{{ variable }}` references to those context values) actually resolve.
9
+
10
+ Markdown source files are prose, though, and prose about Django itself
11
+ routinely contains literal `{% ... %}`/`{{ ... }}` text that was never
12
+ meant to be executed as a template -- e.g. a paragraph explaining how a
13
+ template tag works. Left unescaped, that literal text is a genuine
14
+ production hazard: an unrecognized `{% ... %}` block raises
15
+ `TemplateSyntaxError` (a hard 500 for the whole page), and an unrecognized
16
+ `{{ ... }}` variable is silently rendered as an empty string by Django's
17
+ default `Context` behavior (silent content loss, with no error at all).
18
+
19
+ `escape_unsafe_template_syntax()` walks the converted HTML and leaves only
20
+ the two genuinely-intended constructs alone -- the exact `{% static '...'
21
+ %}` tags `ImageExtension` injects, and `{{ identifier }}` references to a
22
+ name actually present in the context the page will render with -- while
23
+ converting every other `{% ... %}`/`{{ ... }}` span into inert literal
24
+ text, so a documentation author can safely write about template syntax
25
+ without needing to know anything about this rendering pipeline.
26
+ """
27
+ import re
28
+
29
+ # The exact shape `ImageExtension.InlineImageProcessor` emits: always
30
+ # single-quoted, no filters, no whitespace variation beyond a single space
31
+ # after `static`.
32
+ _STATIC_TAG_RE = re.compile(r"^\{%\s*static\s+'[^']*'\s*%\}$")
33
+
34
+ # A bare `{{ identifier }}` reference -- no dotted lookups, no filters. This
35
+ # intentionally covers only the simple case `MarkdownView`'s own context-
36
+ # variable support is documented/tested against; anything more elaborate
37
+ # appearing in a `.md` file is far more likely to be prose describing
38
+ # template syntax than a real, intended lookup.
39
+ _SIMPLE_VARIABLE_RE = re.compile(r"^\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}$")
40
+
41
+ # Matches one `{% ... %}` or `{{ ... }}` span at a time, restricted to a
42
+ # single line (`.` does not match `\n`) since genuine template tags and
43
+ # variable references are not expected to span multiple lines, and
44
+ # allowing them to would risk one stray, unmatched delimiter swallowing an
45
+ # entire unrelated section of a document into a single "match".
46
+ _TEMPLATE_SYNTAX_RE = re.compile(r"(\{%.*?%\})|(\{\{.*?\}\})")
47
+
48
+
49
+ def _escape_braces(text):
50
+ """Render literal ``{``/``}`` characters inert to Django's template lexer."""
51
+ return text.replace("{", "&#123;").replace("}", "&#125;")
52
+
53
+
54
+ def escape_unsafe_template_syntax(html, safe_context_keys=()):
55
+ """
56
+ Escape any ``{% ... %}``/``{{ ... }}`` span in `html` that is not one
57
+ of the two constructs `MarkdownView` genuinely intends to execute,
58
+ rendering it as inert literal text instead.
59
+
60
+ :param html: Rendered HTML markup, already converted from Markdown.
61
+ :param safe_context_keys: Top-level variable names that will actually
62
+ be present in the context `html` renders with (e.g. from
63
+ `MARKDOWN_VIEW_EXTRA_CONTEXT` or, when
64
+ `MARKDOWN_VIEW_USE_REQUEST_CONTEXT` is enabled, the view's own
65
+ context). A `{{ name }}` reference to one of these is left alone;
66
+ anything else is treated as literal text.
67
+ :return: `html` with every unsafe template-syntax-shaped span escaped.
68
+ """
69
+ safe_context_keys = set(safe_context_keys)
70
+
71
+ def _replace(match):
72
+ block_tag, variable_tag = match.group(1), match.group(2)
73
+ if block_tag is not None:
74
+ if _STATIC_TAG_RE.match(block_tag):
75
+ return block_tag
76
+ return _escape_braces(block_tag)
77
+ variable_match = _SIMPLE_VARIABLE_RE.match(variable_tag)
78
+ if variable_match and variable_match.group(1) in safe_context_keys:
79
+ return variable_tag
80
+ return _escape_braces(variable_tag)
81
+
82
+ return _TEMPLATE_SYNTAX_RE.sub(_replace, html)
@@ -0,0 +1,61 @@
1
+ from django.conf import settings
2
+ from django.core.exceptions import SuspiciousFileOperation
3
+ from django.template import Origin
4
+ from django.template.loaders.filesystem import Loader as FilesystemLoader
5
+ from django.template.utils import get_app_template_dirs
6
+ from django.utils._os import safe_join
7
+
8
+ from markdown_view.constants import DEFAULT_MARKDOWN_VIEW_LOADER_TEMPLATES_DIR
9
+
10
+
11
+ class MarkdownLoader(FilesystemLoader):
12
+ """
13
+ A loader that will find a .md file in the context of any installed app or
14
+ application root.
15
+ """
16
+
17
+ def get_dirs(self):
18
+ base_dir = getattr(
19
+ settings,
20
+ "MARKDOWN_VIEW_BASE_DIR",
21
+ getattr(
22
+ settings,
23
+ "BASE_DIR",
24
+ None)
25
+ )
26
+ dirs = [*get_app_template_dirs(
27
+ getattr(
28
+ settings,
29
+ "MARKDOWN_VIEW_LOADER_TEMPLATES_DIR",
30
+ DEFAULT_MARKDOWN_VIEW_LOADER_TEMPLATES_DIR
31
+ )
32
+ )]
33
+ if base_dir:
34
+ dirs.extend([base_dir])
35
+ return dirs
36
+
37
+ def get_template_sources(self, template_name):
38
+ """
39
+ Return an Origin object pointing to an absolute path in each directory
40
+ in template_dirs. For security reasons, if a path doesn't lie inside
41
+ one of the template_dirs it is excluded from the result set.
42
+ """
43
+ if template_name.endswith('.md'):
44
+ template_split = template_name.split("/")
45
+ template_split.reverse()
46
+ template_app_dir = template_split.pop()
47
+ template_split.reverse()
48
+ for template_dir in self.get_dirs():
49
+ if str(template_dir).endswith(template_app_dir):
50
+ try:
51
+ name = safe_join(template_dir, *template_split)
52
+ except SuspiciousFileOperation:
53
+ # The joined path was located outside of this template_dir
54
+ # (it might be inside another one, so this isn't fatal).
55
+ continue
56
+
57
+ yield Origin(
58
+ name=name,
59
+ template_name=template_name,
60
+ loader=self,
61
+ )