importpython-markdown 0.1.0__tar.gz → 0.1.2__tar.gz
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.
- {importpython_markdown-0.1.0/src/importpython_markdown.egg-info → importpython_markdown-0.1.2}/PKG-INFO +6 -4
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/README.md +5 -3
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/pyproject.toml +1 -1
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/src/importpython_markdown/__init__.py +69 -16
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2/src/importpython_markdown.egg-info}/PKG-INFO +6 -4
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/tests/test_extension.py +91 -10
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/LICENSE +0 -0
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/setup.cfg +0 -0
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/src/importpython_markdown.egg-info/SOURCES.txt +0 -0
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/src/importpython_markdown.egg-info/dependency_links.txt +0 -0
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/src/importpython_markdown.egg-info/entry_points.txt +0 -0
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/src/importpython_markdown.egg-info/requires.txt +0 -0
- {importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/src/importpython_markdown.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: importpython-markdown
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Runnable Python examples in Markdown, via importpython.org.
|
|
5
5
|
Author-email: "Nicholas H.Tollervey" <ntoll@ntoll.org>
|
|
6
6
|
License: MIT
|
|
@@ -43,9 +43,11 @@ reader presses Run. Plain fences are the program; a fence with
|
|
|
43
43
|
`title="name.py"` is a file delivered beside it; `config` and
|
|
44
44
|
`body` fences carry BFP settings and page markup (escaped for
|
|
45
45
|
you). The `py` and `mpy` types choose Pyodide or MicroPython,
|
|
46
|
-
and `terminal`, `debug`, `
|
|
47
|
-
`stop-label="..."`, `theme="..."`,
|
|
48
|
-
|
|
46
|
+
and `terminal`, `debug`, `send-embedder-url`, `title="..."`,
|
|
47
|
+
`run-label="..."`, `stop-label="..."`, `theme="..."`,
|
|
48
|
+
`terminal-rows="..."`, `terminal-cols="..."`, `class` and `id`
|
|
49
|
+
pass through to the element - each documented at
|
|
50
|
+
[importpython.org](https://importpython.org/docs/writing-markdown/).
|
|
49
51
|
|
|
50
52
|
Install and enable:
|
|
51
53
|
|
|
@@ -22,9 +22,11 @@ reader presses Run. Plain fences are the program; a fence with
|
|
|
22
22
|
`title="name.py"` is a file delivered beside it; `config` and
|
|
23
23
|
`body` fences carry BFP settings and page markup (escaped for
|
|
24
24
|
you). The `py` and `mpy` types choose Pyodide or MicroPython,
|
|
25
|
-
and `terminal`, `debug`, `
|
|
26
|
-
`stop-label="..."`, `theme="..."`,
|
|
27
|
-
|
|
25
|
+
and `terminal`, `debug`, `send-embedder-url`, `title="..."`,
|
|
26
|
+
`run-label="..."`, `stop-label="..."`, `theme="..."`,
|
|
27
|
+
`terminal-rows="..."`, `terminal-cols="..."`, `class` and `id`
|
|
28
|
+
pass through to the element - each documented at
|
|
29
|
+
[importpython.org](https://importpython.org/docs/writing-markdown/).
|
|
28
30
|
|
|
29
31
|
Install and enable:
|
|
30
32
|
|
{importpython_markdown-0.1.0 → importpython_markdown-0.1.2}/src/importpython_markdown/__init__.py
RENAMED
|
@@ -14,8 +14,11 @@ time in the element's own vocabulary, so an authoring mistake is
|
|
|
14
14
|
a loud build failure rather than a silent page.
|
|
15
15
|
|
|
16
16
|
The emitted markup follows the element's contract exactly: each
|
|
17
|
-
part is a
|
|
18
|
-
order the author wrote it.
|
|
17
|
+
part is a ``<pre>`` holding one ``<code>``, the pre a direct
|
|
18
|
+
child of the element, in the order the author wrote it. The
|
|
19
|
+
element reads a ``<pre><code>`` pair and a bare ``<pre>``
|
|
20
|
+
identically; the inner ``<code>`` is for the surrounding theme,
|
|
21
|
+
whose block styling and copy button expect it.
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
import html
|
|
@@ -49,8 +52,9 @@ ATTRIBUTE = re.compile(r'([\w-]+)(?:="([^"]*)")?')
|
|
|
49
52
|
# fail the build, not be silently ignored in every reader's
|
|
50
53
|
# browser.
|
|
51
54
|
TYPES = ("py", "mpy")
|
|
52
|
-
FLAGS = ("terminal", "debug")
|
|
53
|
-
VALUED = ("title", "run-label", "stop-label", "theme", "class",
|
|
55
|
+
FLAGS = ("terminal", "debug", "send-embedder-url")
|
|
56
|
+
VALUED = ("title", "run-label", "stop-label", "theme", "class",
|
|
57
|
+
"id", "terminal-rows", "terminal-cols")
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
class ExampleError(ValueError):
|
|
@@ -106,6 +110,23 @@ def parse_gate(header, line):
|
|
|
106
110
|
raise ExampleError(
|
|
107
111
|
line, f'cannot parse attributes at "{rest[span:]}"'
|
|
108
112
|
)
|
|
113
|
+
# The terminal's explicit size: each dimension needs the
|
|
114
|
+
# terminal it sizes, and only a positive whole number will do
|
|
115
|
+
# - the same rules the element applies, failing the build
|
|
116
|
+
# here instead of a reader's browser there.
|
|
117
|
+
for name in ("terminal-rows", "terminal-cols"):
|
|
118
|
+
if name in attributes:
|
|
119
|
+
if "terminal" not in attributes:
|
|
120
|
+
raise ExampleError(
|
|
121
|
+
line, f"{name} requires the terminal flag"
|
|
122
|
+
)
|
|
123
|
+
value = attributes[name]
|
|
124
|
+
if not (value.isdigit() and int(value) >= 1):
|
|
125
|
+
raise ExampleError(
|
|
126
|
+
line,
|
|
127
|
+
f'{name} is "{value}"; use a positive whole '
|
|
128
|
+
"number",
|
|
129
|
+
)
|
|
109
130
|
return attributes
|
|
110
131
|
|
|
111
132
|
|
|
@@ -173,7 +194,7 @@ def split_example(lines, first_line):
|
|
|
173
194
|
return pieces
|
|
174
195
|
|
|
175
196
|
|
|
176
|
-
def validate(pieces, line):
|
|
197
|
+
def validate(pieces, attributes, line):
|
|
177
198
|
"""
|
|
178
199
|
The element's own rules, applied at build time: exactly one
|
|
179
200
|
program, no duplicate filenames, at most one config (as valid
|
|
@@ -201,12 +222,37 @@ def validate(pieces, line):
|
|
|
201
222
|
)
|
|
202
223
|
for piece in [p for p in pieces if p["part"] == "config"]:
|
|
203
224
|
try:
|
|
204
|
-
json.loads("\n".join(piece["text"]))
|
|
225
|
+
settings = json.loads("\n".join(piece["text"]))
|
|
205
226
|
except json.JSONDecodeError as error:
|
|
206
227
|
raise ExampleError(
|
|
207
228
|
piece["line"],
|
|
208
229
|
f"the config block is not valid JSON: {error}",
|
|
209
230
|
) from None
|
|
231
|
+
# send-embedder-url writes importpython.py, so while the
|
|
232
|
+
# flag is on the name is reserved - here in the author's
|
|
233
|
+
# own config files too, matching the element's rule.
|
|
234
|
+
if "send-embedder-url" in attributes:
|
|
235
|
+
names = (settings.get("files") or {}).values()
|
|
236
|
+
if any(
|
|
237
|
+
str(name).removeprefix("./") == "importpython.py"
|
|
238
|
+
for name in names
|
|
239
|
+
):
|
|
240
|
+
raise ExampleError(
|
|
241
|
+
piece["line"],
|
|
242
|
+
"the send-embedder-url flag reserves the "
|
|
243
|
+
'file "importpython.py"',
|
|
244
|
+
)
|
|
245
|
+
if "send-embedder-url" in attributes:
|
|
246
|
+
for piece in pieces:
|
|
247
|
+
if (
|
|
248
|
+
piece["part"] == "file"
|
|
249
|
+
and piece["title"] == "importpython.py"
|
|
250
|
+
):
|
|
251
|
+
raise ExampleError(
|
|
252
|
+
piece["line"],
|
|
253
|
+
"the send-embedder-url flag reserves the "
|
|
254
|
+
'file "importpython.py"',
|
|
255
|
+
)
|
|
210
256
|
|
|
211
257
|
|
|
212
258
|
class ImportPythonPreprocessor(Preprocessor):
|
|
@@ -301,7 +347,7 @@ class ImportPythonPreprocessor(Preprocessor):
|
|
|
301
347
|
"""
|
|
302
348
|
attributes = parse_gate(header, first_line)
|
|
303
349
|
pieces = split_example(lines, first_line + 1)
|
|
304
|
-
validate(pieces, first_line)
|
|
350
|
+
validate(pieces, attributes, first_line)
|
|
305
351
|
parts = ["<import-python"]
|
|
306
352
|
for name, value in attributes.items():
|
|
307
353
|
if value is None:
|
|
@@ -331,22 +377,29 @@ class ImportPythonPreprocessor(Preprocessor):
|
|
|
331
377
|
)
|
|
332
378
|
return inner.convert("\n".join(piece["text"])) + "\n"
|
|
333
379
|
text, lit = self.paint("\n".join(piece["text"]), piece)
|
|
380
|
+
# One code element inside every pre, highlighted or not:
|
|
381
|
+
# MkDocs-family themes hang block styling and the copy
|
|
382
|
+
# button on pre > code, and the element reads both shapes
|
|
383
|
+
# identically. No newline before the text - a pre swallows
|
|
384
|
+
# one after its own tag, a code does not - so textContent
|
|
385
|
+
# stays byte-identical to the bare-pre form.
|
|
386
|
+
text = f"<code>{text}\n</code>"
|
|
334
387
|
if piece["part"] == "file":
|
|
335
388
|
title = html.escape(piece["title"])
|
|
336
|
-
return f'<pre{lit} title="{title}"
|
|
389
|
+
return f'<pre{lit} title="{title}">{text}</pre>\n'
|
|
337
390
|
if piece["part"] in ("config", "body"):
|
|
338
391
|
role = piece["part"]
|
|
339
|
-
return f'<pre{lit} data-role="{role}"
|
|
340
|
-
return f"<pre{lit}
|
|
392
|
+
return f'<pre{lit} data-role="{role}">{text}</pre>\n'
|
|
393
|
+
return f"<pre{lit}>{text}</pre>\n"
|
|
341
394
|
|
|
342
395
|
def paint(self, code, piece):
|
|
343
396
|
"""
|
|
344
397
|
Highlight a part's code as Pygments spans with no wrapper
|
|
345
|
-
of any kind
|
|
346
|
-
|
|
347
|
-
the coloured path adds is what lets a
|
|
348
|
-
Zensical site's existing Pygments stylesheet
|
|
349
|
-
blocks, palettes and all, with no CSS of ours.
|
|
398
|
+
of any kind - render_piece adds the one code element both
|
|
399
|
+
paths share, and the pre stays the direct child. The
|
|
400
|
+
class="highlight" the coloured path adds is what lets a
|
|
401
|
+
Material or Zensical site's existing Pygments stylesheet
|
|
402
|
+
colour these blocks, palettes and all, with no CSS of ours.
|
|
350
403
|
|
|
351
404
|
The language comes from the fence, or a file's name, with
|
|
352
405
|
json and html assumed for config and body; an unknown or
|
|
@@ -385,7 +438,7 @@ class ImportPythonExtension(Extension):
|
|
|
385
438
|
def __init__(self, **kwargs):
|
|
386
439
|
self.config = {
|
|
387
440
|
"script_url": [
|
|
388
|
-
"https://importpython.org/
|
|
441
|
+
"https://importpython.org/v/test/embed.js",
|
|
389
442
|
"Where embed.js is served from; point this at "
|
|
390
443
|
"your own copy to use one.",
|
|
391
444
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: importpython-markdown
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Runnable Python examples in Markdown, via importpython.org.
|
|
5
5
|
Author-email: "Nicholas H.Tollervey" <ntoll@ntoll.org>
|
|
6
6
|
License: MIT
|
|
@@ -43,9 +43,11 @@ reader presses Run. Plain fences are the program; a fence with
|
|
|
43
43
|
`title="name.py"` is a file delivered beside it; `config` and
|
|
44
44
|
`body` fences carry BFP settings and page markup (escaped for
|
|
45
45
|
you). The `py` and `mpy` types choose Pyodide or MicroPython,
|
|
46
|
-
and `terminal`, `debug`, `
|
|
47
|
-
`stop-label="..."`, `theme="..."`,
|
|
48
|
-
|
|
46
|
+
and `terminal`, `debug`, `send-embedder-url`, `title="..."`,
|
|
47
|
+
`run-label="..."`, `stop-label="..."`, `theme="..."`,
|
|
48
|
+
`terminal-rows="..."`, `terminal-cols="..."`, `class` and `id`
|
|
49
|
+
pass through to the element - each documented at
|
|
50
|
+
[importpython.org](https://importpython.org/docs/writing-markdown/).
|
|
49
51
|
|
|
50
52
|
Install and enable:
|
|
51
53
|
|
|
@@ -91,7 +91,7 @@ def test_close_gate_inside_inner_fence_is_content():
|
|
|
91
91
|
"```\nprint('hi')\n```\n!#\n"
|
|
92
92
|
)
|
|
93
93
|
assert out.count("<import-python") == 1
|
|
94
|
-
assert "
|
|
94
|
+
assert "<code>!#\n</code></pre>" in out
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
def test_attributes_pass_through_escaped():
|
|
@@ -198,9 +198,9 @@ def test_script_tag_once_for_many_examples():
|
|
|
198
198
|
configured one.
|
|
199
199
|
"""
|
|
200
200
|
two = EXAMPLE + "\n" + EXAMPLE
|
|
201
|
-
out = render(two, script_url="/
|
|
201
|
+
out = render(two, script_url="/v/test/embed.js")
|
|
202
202
|
assert out.count("<import-python") == 2
|
|
203
|
-
assert out.count('src="/
|
|
203
|
+
assert out.count('src="/v/test/embed.js"') == 1
|
|
204
204
|
assert 'type="module"' in out
|
|
205
205
|
|
|
206
206
|
|
|
@@ -213,18 +213,28 @@ def test_script_injection_can_be_turned_off():
|
|
|
213
213
|
assert "embed.js" not in out
|
|
214
214
|
|
|
215
215
|
|
|
216
|
-
def
|
|
216
|
+
def test_highlighting_is_spans_in_a_pre_code_pair():
|
|
217
217
|
"""
|
|
218
|
-
Intent:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
Intent: every part is a pre holding one code element - the
|
|
219
|
+
shape MkDocs-family themes hang block styling and the copy
|
|
220
|
+
button on, and one the element reads identically to a bare
|
|
221
|
+
pre. Colour is Pygments spans inside the code, the pre
|
|
222
|
+
marked class="highlight" so a site's existing stylesheet
|
|
223
|
+
applies - never a wrapper outside the pre, which would hide
|
|
224
|
+
it from the element. With highlighting off the code element
|
|
225
|
+
remains, so theme styling never depends on Pygments.
|
|
222
226
|
"""
|
|
223
227
|
out = render("#!mpy\n\n```python\nprint(1)\n```\n!#\n")
|
|
224
|
-
assert '<pre class="highlight">' in out
|
|
228
|
+
assert '<pre class="highlight"><code>' in out
|
|
225
229
|
assert '<span class="nb">print</span>' in out
|
|
230
|
+
assert "</code></pre>" in out
|
|
226
231
|
assert "<div" not in out
|
|
227
|
-
|
|
232
|
+
plain = render(
|
|
233
|
+
"#!mpy\n\n```python\nprint(1)\n```\n!#\n",
|
|
234
|
+
highlight=False,
|
|
235
|
+
)
|
|
236
|
+
assert "<pre><code>print(1)\n</code></pre>" in plain
|
|
237
|
+
assert "<span" not in plain
|
|
228
238
|
|
|
229
239
|
|
|
230
240
|
def test_file_language_guessed_from_its_name():
|
|
@@ -272,3 +282,74 @@ def test_the_element_is_never_paragraph_wrapped():
|
|
|
272
282
|
out = render(EXAMPLE)
|
|
273
283
|
assert "<p><import-python" not in out
|
|
274
284
|
assert "<p><script" not in out
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_send_embedder_url_flag_passes_through():
|
|
288
|
+
"""
|
|
289
|
+
Intent: the flag is allow-listed and reaches the element as
|
|
290
|
+
its attribute, so the page's embed.js can act on it.
|
|
291
|
+
"""
|
|
292
|
+
out = render(
|
|
293
|
+
"#!mpy send-embedder-url\n\n```\nimport importpython\n```\n!#\n"
|
|
294
|
+
)
|
|
295
|
+
assert "send-embedder-url" in out.split(">")[0]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_send_embedder_url_reserves_the_filename():
|
|
299
|
+
"""
|
|
300
|
+
Intent: while the flag is on, importpython.py belongs to the
|
|
301
|
+
service - a titled block or a configured files entry by that
|
|
302
|
+
name is a named build failure, never a silent overwrite.
|
|
303
|
+
"""
|
|
304
|
+
with pytest.raises(ExampleError, match="reserves the file"):
|
|
305
|
+
render(
|
|
306
|
+
"#!mpy send-embedder-url\n\n"
|
|
307
|
+
'```title="importpython.py"\nx\n```\n\n'
|
|
308
|
+
"```\nimport importpython\n```\n!#\n"
|
|
309
|
+
)
|
|
310
|
+
with pytest.raises(ExampleError, match="reserves the file"):
|
|
311
|
+
render(
|
|
312
|
+
"#!mpy send-embedder-url\n\n"
|
|
313
|
+
"```config\n"
|
|
314
|
+
'{ "files": { "data:x": "./importpython.py" } }\n'
|
|
315
|
+
"```\n\n```\nimport importpython\n```\n!#\n"
|
|
316
|
+
)
|
|
317
|
+
# Without the flag, the name is the author's to use.
|
|
318
|
+
out = render(
|
|
319
|
+
'#!mpy\n\n```title="importpython.py"\nx = 1\n```\n\n'
|
|
320
|
+
"```\nimport importpython\n```\n!#\n"
|
|
321
|
+
)
|
|
322
|
+
assert 'title="importpython.py"' in out
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_terminal_size_passes_through():
|
|
326
|
+
"""
|
|
327
|
+
Intent: terminal-rows and terminal-cols are allow-listed valued
|
|
328
|
+
attributes that reach the element, sized and named as BFP
|
|
329
|
+
has them.
|
|
330
|
+
"""
|
|
331
|
+
out = render(
|
|
332
|
+
'#!mpy terminal terminal-rows="4" terminal-cols="60"\n\n'
|
|
333
|
+
"```\nprint(1)\n```\n!#\n"
|
|
334
|
+
)
|
|
335
|
+
assert 'terminal-rows="4"' in out
|
|
336
|
+
assert 'terminal-cols="60"' in out
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_terminal_size_is_validated_at_build_time():
|
|
340
|
+
"""
|
|
341
|
+
Intent: a dimension without the terminal flag, or a value
|
|
342
|
+
that is not a positive whole number, fails the build with
|
|
343
|
+
the element's own words - never a runtime error in a
|
|
344
|
+
reader's browser.
|
|
345
|
+
"""
|
|
346
|
+
with pytest.raises(ExampleError, match="requires the terminal"):
|
|
347
|
+
render('#!mpy terminal-rows="4"\n\n```\nx\n```\n!#\n')
|
|
348
|
+
with pytest.raises(ExampleError, match="positive whole"):
|
|
349
|
+
render(
|
|
350
|
+
'#!mpy terminal terminal-rows="0"\n\n```\nx\n```\n!#\n'
|
|
351
|
+
)
|
|
352
|
+
with pytest.raises(ExampleError, match="positive whole"):
|
|
353
|
+
render(
|
|
354
|
+
'#!mpy terminal terminal-cols="wide"\n\n```\nx\n```\n!#\n'
|
|
355
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|