diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 904c1e3..193ec92 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -13,12 +13,12 @@ jobs: - uses: actions/checkout@v2 - name: Run Tox tests id: test - uses: fedora-python/tox-github-action@master + uses: fedora-python/tox-github-action@main with: tox_env: ${{ matrix.tox_env }} strategy: matrix: - tox_env: [py37, py38, py39, py310, py311] + tox_env: [py38, py39, py310, py311, py312] # Use GitHub's Linux Docker host runs-on: ubuntu-latest diff --git a/README.md b/README.md index 5590e73..e4ac771 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,14 @@ licensed under the same license. ## Changelog +### naucse_render 2.0 + +* Update to mistune 3.x & nbconvert 7.x. This changes parsing & formatting + for Markdown, syntax highlighting, and Notebooks. In most cases the + differences should be superficial. + +* Tested with Python 3.8-3.12 + ### naucse_render 1.10 * `naucse_render compile` now checks for links to missing lessons and diff --git a/naucse_render/markdown.py b/naucse_render/markdown.py index 3cb9bfa..0abb534 100644 --- a/naucse_render/markdown.py +++ b/naucse_render/markdown.py @@ -12,57 +12,62 @@ import pygments.formatters.html -ansi_convertor = Ansi2HTMLConverter(inline=True) - -pygments_formatter = pygments.formatters.html.HtmlFormatter( - cssclass='highlight' -) - -_admonition_leading_pattern = re.compile(r'^ *> ?', flags=re.M) - - -class BlockGrammar(mistune.BlockGrammar): - admonition = re.compile(r'^> *\[(\S+)\]([^\n]*)\n((>[^\n]*[\n]{0,1})*)') - deflist = re.compile(r'^(([^\n: ][^\n]*\n)+)((:( {0,3})[^\n]*\n)( \5[^\n]*\n|\n)+)') - - -class BlockLexer(mistune.BlockLexer): - grammar_class = BlockGrammar +def naucse_admonition_plugin(md): + """Parse blockquote-based admonitions - default_rules = [ - 'admonition', - 'deflist', - ] + mistune.BlockLexer.default_rules + Like this: - def parse_admonition(self, m): - self.tokens.append({ - 'type': 'admonition_start', - 'name': m.group(1), - 'title': m.group(2).strip(), - }) + > [note] Note Title + > rest of note goes here + """ + # Based on Mistune's "spoiler" plugin (the documentation says: + # "take a look at the source code in mistune/plugins to find + # out how to write a plugin") + + ADMONITION_NAME_PATTERN = re.compile(r' *\[(\S+)\]([^\n]*)\n') + + def parse_naucse_admonition(block, m, state): + + text, end_pos = block.extract_block_quote(m, state) + name_match = ADMONITION_NAME_PATTERN.match(text) + if name_match: + # It's an admonition + token = { + 'type': 'naucse_admonition', + 'attrs': { + 'name': name_match[1].strip(), + 'title': name_match[2].strip(), + }, + } + text = text[name_match.end():] + else: + token = { + 'type': 'block_quote', + } + + child = state.child_state(text) + rules = block.block_quote_rules + block.parse(child, rules) + token['children'] = child.tokens + if end_pos: + state.prepend_token(token) + return end_pos + state.append_token(token) + return state.cursor + + md.block.register( + 'block_quote', + None, + parse_naucse_admonition, + before='block_quote', + ) - text = _admonition_leading_pattern.sub('', m.group(3)) - self.parse(dedent(text)) - self.tokens.append({ - 'type': 'admonition_end', - }) +ansi_convertor = Ansi2HTMLConverter(inline=True) - def parse_deflist(self, m): - self.tokens.append({ - 'type': 'deflist_term_start', - }) - self.parse(dedent(m.group(1))) - self.tokens.append({ - 'type': 'deflist_term_end', - }) - self.tokens.append({ - 'type': 'deflist_def_start', - }) - self.parse(dedent(' ' + m.group(3)[1:])) - self.tokens.append({ - 'type': 'deflist_def_end', - }) +pygments_formatter = pygments.formatters.html.HtmlFormatter( + cssclass='highlight' +) def ansi_convert(code): @@ -76,11 +81,6 @@ def style_space_after_prompt(html): html) -def matrix_multiplication_operator(html): - return html.replace('@', - '@') - - class MSDOSSessionVenvLexer(RegexLexer): """Lexer for simplistic MSDOS sessions with optional venvs. @@ -129,23 +129,26 @@ def text_to_id(text): return text -class Renderer(mistune.Renderer): +class NaucseRenderer(mistune.HTMLRenderer): code_tmpl = '
{}
{title}
\n{text}' + return '{}
\n' - body += template.format(self.token['title']) - while self.pop()['type'] != 'admonition_end': - body += self.tok() - return self.renderer.admonition(name, body) - - def output_deflist_term(self): - items = [['term', self.renderer.placeholder()]] - while True: - end_token = 'deflist_{}_end'.format(items[-1][0]) - while self.pop()['type'] not in (end_token, 'paragraph'): - items[-1][1] += self.tok() - if self.token['type'] == 'paragraph': - if items[-1][0] == 'term': - items.append(['term', self.renderer.placeholder()]) - items[-1][1] += self.token['text'] - else: - items[-1][1] += self.output_paragraph() - elif self.peek()['type'] == 'deflist_term_start': - self.pop() - items.append(['term', self.renderer.placeholder()]) - elif self.peek()['type'] == 'deflist_def_start': - self.pop() - items.append(['def', self.renderer.placeholder()]) - else: - break - return self.renderer.deflist(items) + def link(self, text, url, title=None): + return super().link(text, self._convert_url(url), title) + + def image(self, alt, url, title=None): + return super().image(alt, self._convert_url(url), title) def convert_markdown(text, convert_url=None, *, inline=False): @@ -214,10 +174,9 @@ def convert_markdown(text, convert_url=None, *, inline=False): text = dedent(text) - markdown = Markdown( - escape=False, - block=BlockLexer(), - renderer=Renderer(convert_url), + markdown = mistune.create_markdown( + plugins=['def_list', naucse_admonition_plugin], + renderer=NaucseRenderer(convert_url), ) result = markdown(text).strip() diff --git a/naucse_render/templates.py b/naucse_render/templates.py index ecb03a7..7b57da9 100644 --- a/naucse_render/templates.py +++ b/naucse_render/templates.py @@ -67,6 +67,9 @@ def solution(ctx, text): solution = ctx['$markdown'](text) solutions.append(solution) + # make sure there are no empty lines, which exit Markdown's raw-HTML mode + solution = solution.replace('\n', Markup('\n')) + return Markup(textwrap.dedent("""A task
Task with list:
Static files handled with a .
+Static files handled with a .
# Třikrát:\nfor i in range(3):\n\n # Nakresli čtverec (kód zkopírovaný z předchozí úlohy a odsazený)\n for j in range(4):\n forward(50)\n left(90)\n\n # Otoč se o 20°\n left(20)\n
Na Linuxu se Gedit instaluje jako ostatní programy:
$ sudo dnf install gedit
-
$ sudo apt-get install gedit
-
Používáš-li jiný Linux, předpokládám že programy instalovat umíš. :)
+$ sudo dnf install gedit
+
$ sudo apt-get install gedit
+
Používáš-li jiný Linux, předpokládám že programy instalovat umíš. :)
Pro Windows a macOS se Gedit dá stáhnout z domovské stránky.
...
V sekci Zobrazit/View vyber +
V sekci Zobrazit/View vyber Zobrazovat čísla řádků/Display Line Numbers.
-...
+ + +...
...
Větší projekty sestávají z více souborů, které můžeš mít v editoru -otevřené všechny najednou.
-Před každým řádkem se ukazuje číslo. -To se bude velice hodit, až Python bude nadávat, že chyba je na řádku 183.
-...
+...
Pro ilustraci, takhle může v editoru vypadat kousek kódu:
1 @app.route('/courses/<course:course>/')
2 def course_page(course):
@@ -28,4 +31,23 @@ Co programátorský editor umí
-
...
\ No newline at end of file
+...
+
+ Řešení
+
+ Ukázat řešení
+
+
+
\ No newline at end of file
diff --git a/test_naucse_render/fixtures/expected-compiled/lessons/course.json b/test_naucse_render/fixtures/expected-compiled/lessons/course.json
index 1c470a8..280e35c 100644
--- a/test_naucse_render/fixtures/expected-compiled/lessons/course.json
+++ b/test_naucse_render/fixtures/expected-compiled/lessons/course.json
@@ -70,6 +70,7 @@
},
"ids": [
"co_programatorsky_editor_umi",
+ "solution-0",
"volba_a_nastaveni_editoru"
],
"license": "cc-by-sa-40",
@@ -78,10 +79,15 @@
"#volba_a_nastaveni_editoru",
"naucse:page?lesson=beginners/install-editor&page=atom",
"naucse:page?lesson=beginners/install-editor&page=gedit",
- "naucse:page?lesson=beginners/install-editor&page=gedit#nacvik_odsazovani"
+ "naucse:page?lesson=beginners/install-editor&page=gedit#nacvik_odsazovani",
+ "naucse:solution?solution=0"
],
"slug": "index",
- "solutions": [],
+ "solutions": [
+ {
+ "content": "# Třikrát:\nfor i in range(3):\n\n # Nakresli čtverec (kód zkopírovaný z předchozí úlohy a odsazený)\n for j in range(4):\n forward(50)\n left(90)\n\n # Otoč se o 20°\n left(20)\n
"
+ }
+ ],
"source_file": "lessons/beginners/install-editor/index.md",
"title": "Instalace editoru",
"vars": {}
diff --git a/test_naucse_render/fixtures/expected-compiled/lessons/install-editor/gedit.html b/test_naucse_render/fixtures/expected-compiled/lessons/install-editor/gedit.html
index 50617e2..af68a81 100644
--- a/test_naucse_render/fixtures/expected-compiled/lessons/install-editor/gedit.html
+++ b/test_naucse_render/fixtures/expected-compiled/lessons/install-editor/gedit.html
@@ -3,19 +3,27 @@ Instalace Geditu
Na Linuxu se Gedit instaluje jako ostatní programy:
$ sudo dnf install gedit
-
$ sudo apt-get install gedit
-
Používáš-li jiný Linux, předpokládám že programy instalovat umíš. :)
+$ sudo dnf install gedit
+
$ sudo apt-get install gedit
+
Používáš-li jiný Linux, předpokládám že programy instalovat umíš. :)
Pro Windows a macOS se Gedit dá stáhnout z domovské stránky.
...
V sekci Zobrazit/View vyber +
V sekci Zobrazit/View vyber Zobrazovat čísla řádků/Display Line Numbers.
-...
+ + +...
...
Větší projekty sestávají z více souborů, které můžeš mít v editoru -otevřené všechny najednou.
-Před každým řádkem se ukazuje číslo. -To se bude velice hodit, až Python bude nadávat, že chyba je na řádku 183.
-...
+...
Pro ilustraci, takhle může v editoru vypadat kousek kódu:
1 @app.route('/courses/<course:course>/')
2 def course_page(course):
@@ -28,4 +31,23 @@ Co programátorský editor umí
-
...
\ No newline at end of file
+...
+
+ Řešení
+
+ Ukázat řešení
+
+
+
\ No newline at end of file
diff --git a/test_naucse_render/fixtures/expected-compiled/lessons/tasks/index.html b/test_naucse_render/fixtures/expected-compiled/lessons/tasks/index.html
index e447fc1..f8921ec 100644
--- a/test_naucse_render/fixtures/expected-compiled/lessons/tasks/index.html
+++ b/test_naucse_render/fixtures/expected-compiled/lessons/tasks/index.html
@@ -1,9 +1,7 @@
-
-
A task
-
-
Task with list:
@@ -11,9 +9,7 @@
- b
-
-
-
Static files handled with a .
+ Static files handled with a .
-
\ No newline at end of file
diff --git a/test_naucse_render/fixtures/expected-compiled/lessons/test_static_tree/index.html b/test_naucse_render/fixtures/expected-compiled/lessons/test_static_tree/index.html
index 3abd2a0..87f365f 100644
--- a/test_naucse_render/fixtures/expected-compiled/lessons/test_static_tree/index.html
+++ b/test_naucse_render/fixtures/expected-compiled/lessons/test_static_tree/index.html
@@ -1,2 +1,2 @@
-
+
Output should have '/' as the directory separator, regardless of OS.
\ No newline at end of file
diff --git a/test_naucse_render/fixtures/expected-compiled/lessons/test_subpages/index.html b/test_naucse_render/fixtures/expected-compiled/lessons/test_subpages/index.html
index 1c31e54..f305c88 100644
--- a/test_naucse_render/fixtures/expected-compiled/lessons/test_subpages/index.html
+++ b/test_naucse_render/fixtures/expected-compiled/lessons/test_subpages/index.html
@@ -1 +1 @@
-This is the main page with links to subpages one, two and three.
\ No newline at end of file
+This is the main page with links to subpages one, two and three.
\ No newline at end of file
diff --git a/test_naucse_render/fixtures/expected-dumps/beginners/install-editor.yaml b/test_naucse_render/fixtures/expected-dumps/beginners/install-editor.yaml
index d7e50f9..2a2be7c 100644
--- a/test_naucse_render/fixtures/expected-dumps/beginners/install-editor.yaml
+++ b/test_naucse_render/fixtures/expected-dumps/beginners/install-editor.yaml
@@ -42,24 +42,27 @@ data:
content: "Instalace Geditu\n#\n
\nNa\
\ Linuxu se Gedit instaluje jako ostatn\xED programy:
\n
$ sudo dnf install gedit\n
$ sudo\
- \ apt-get install gedit\n
Pou\u017E\xED\ +
$ sudo dnf install gedit\n\
+
$ sudo apt-get install gedit\n
Pou\u017E\xED\ v\xE1\u0161-li jin\xFD Linux, p\u0159edpokl\xE1d\xE1m \u017Ee\ \ programy instalovat um\xED\u0161. :)
\nPro Windows a macOS\ \ se Gedit d\xE1 st\xE1hnout z\_domovsk\xE9 str\xE1nky.
\n...
\nV\_sekci Zobrazit/View\ +
...
\nV\_sekci Zobrazit/View\ \ vyber\nZobrazovat \u010D\xEDsla \u0159\xE1dk\u016F/Display Line Numbers.
\n\n\ -...
\n...
" +...
\n...
" ids: - instalace_geditu - nacvik_odsazovani @@ -81,14 +84,14 @@ data: - Pro PyLadies Brno napsal Petr Viktorin, 2014-2017 content: "...
\n...
\nV\u011Bt\u0161\xED projekty\ - \ sest\xE1vaj\xED z\_v\xEDce soubor\u016F, kter\xE9 m\u016F\u017E\ - e\u0161 m\xEDt v\_editoru\notev\u0159en\xE9 v\u0161echny najednou.
\n\ -P\u0159\ + \ class=\"header-link\">#\n\n
...
\n...
\n...
\nPro ilustraci, takhle m\u016F\u017E\ e v\_editoru vypadat kousek k\xF3du:
\n 1 \n...
\n\n- Gedit \u2013 b\xFDv\xE1 na syst\xE9mech s\_prost\u0159ed\xED\
m GNOME.
\n- M\u016F\u017Eeme odk\xE1zat na N\xE1cvik odsazov\xE1n\xED.
\n
\n \n
\n...
"
+ >N\xE1cvik odsazov\xE1n\xED.\n\n\n\n...
\n\
+ \n \u0158e\u0161\
+ en\xED
\n \n \n # T\u0159ikr\xE1t:\n\
+ for i\
+ \ in range(3):\n\n # Nakresli \u010Dtverec (k\xF3d zkop\xEDrovan\xFD z p\u0159\
+ edchoz\xED \xFAlohy a odsazen\xFD)\n for j in range(4):\n\
+ forward(50)\n\
+ left(90)\n\
+ \n # Oto\u010D\
+ \ se o 20\xB0\n left(20)\n
\n \n"
ids:
- co_programatorsky_editor_umi
+ - solution-0
- volba_a_nastaveni_editoru
license: cc-by-sa-40
links:
@@ -117,8 +144,27 @@ data:
- naucse:page?lesson=beginners/install-editor&page=atom
- naucse:page?lesson=beginners/install-editor&page=gedit
- naucse:page?lesson=beginners/install-editor&page=gedit#nacvik_odsazovani
+ - naucse:solution?solution=0
slug: index
- solutions: []
+ solutions:
+ - content: "# T\u0159ikr\xE1t:\nfor\
+ \ i in\
+ \ range(3):\n\n \
+ \ # Nakresli \u010Dtverec (k\xF3d zkop\xED\
+ rovan\xFD z p\u0159edchoz\xED \xFAlohy a odsazen\xFD)\n\
+ \ for j\
+ \ in range(4):\n forward(50)\n left(90)\n\n # Oto\u010D se o 20\xB0\
+ \n left(20)\n\
+
"
source_file: lessons/beginners/install-editor/index.md
title: Instalace editoru
vars: {}
diff --git a/test_naucse_render/fixtures/expected-dumps/homework/tasks.yaml b/test_naucse_render/fixtures/expected-dumps/homework/tasks.yaml
index 40fe978..2b482f6 100644
--- a/test_naucse_render/fixtures/expected-dumps/homework/tasks.yaml
+++ b/test_naucse_render/fixtures/expected-dumps/homework/tasks.yaml
@@ -7,11 +7,11 @@ data:
index:
attribution:
- Pro PyLadies Brno napsal Petr Viktorin, 2014-2019
- content: "\n\n - \n
A task
\n \n\n \
- \ - \n
Task with list:
\n\n- a
\n- b
\n\
-
\n \n\n - \n
Static files handled\
- \ with a .
\n \n\n
"
+ content: "\n - \n
A task
\n \n - \n\
+ \
Task with list:
\n\n- a
\n- b
\n\
+
\n \n - \n
Static files handled with\
+ \ a .
\n \n
"
ids: []
license: cc-by-sa-40
links:
diff --git a/test_naucse_render/fixtures/expected-dumps/testcases/test_static_tree.yaml b/test_naucse_render/fixtures/expected-dumps/testcases/test_static_tree.yaml
index d6e204c..d9693f6 100644
--- a/test_naucse_render/fixtures/expected-dumps/testcases/test_static_tree.yaml
+++ b/test_naucse_render/fixtures/expected-dumps/testcases/test_static_tree.yaml
@@ -8,7 +8,7 @@ data:
attribution:
- Petr Viktorin
content: '
+ alt="smile" />
Output should have ''/'' as the directory separator, regardless
of OS.
'
diff --git a/test_naucse_render/fixtures/expected-dumps/testcases/test_subpages.yaml b/test_naucse_render/fixtures/expected-dumps/testcases/test_subpages.yaml
index 518aa88..337ab2e 100644
--- a/test_naucse_render/fixtures/expected-dumps/testcases/test_subpages.yaml
+++ b/test_naucse_render/fixtures/expected-dumps/testcases/test_subpages.yaml
@@ -20,9 +20,9 @@ data:
index:
attribution:
- Petr Viktorin
- content: This is the main page with links to subpages one,
- two
- and three.
+ content: This is the main page with links to subpages one,
+ two
+ and three.
ids: []
license: cc0
links:
diff --git a/test_naucse_render/fixtures/test_content/lessons/beginners/install-editor/index.md b/test_naucse_render/fixtures/test_content/lessons/beginners/install-editor/index.md
index d943d52..d0bac26 100644
--- a/test_naucse_render/fixtures/test_content/lessons/beginners/install-editor/index.md
+++ b/test_naucse_render/fixtures/test_content/lessons/beginners/install-editor/index.md
@@ -39,3 +39,19 @@ Podpora více souborů
* Můžeme odkázat na [Nácvik odsazování]({{ subpage_url('gedit') }}#nacvik_odsazovani).
...
+
+{% filter solution %}
+```python
+# Třikrát:
+for i in range(3):
+
+ # Nakresli čtverec (kód zkopírovaný z předchozí úlohy a odsazený)
+ for j in range(4):
+ forward(50)
+ left(90)
+
+ # Otoč se o 20°
+ left(20)
+
+```
+{% endfilter %}
diff --git a/test_naucse_render/fixtures/test_content/lessons/homework/tasks/index.md b/test_naucse_render/fixtures/test_content/lessons/homework/tasks/index.md
index f4fc44d..4c450cb 100644
--- a/test_naucse_render/fixtures/test_content/lessons/homework/tasks/index.md
+++ b/test_naucse_render/fixtures/test_content/lessons/homework/tasks/index.md
@@ -1,7 +1,7 @@
-{% for item in data.tasks %}
+{%- for item in data.tasks %}
-
{{ item.markdown | markdown }}
-{% endfor %}
+{%- endfor %}
diff --git a/test_naucse_render/test_markdown.py b/test_naucse_render/test_markdown.py
index 4bbfe87..787cf94 100644
--- a/test_naucse_render/test_markdown.py
+++ b/test_naucse_render/test_markdown.py
@@ -99,8 +99,10 @@ def test_markdown_definition_list():
expected = dedent("""
Bla Bla
- - The Term
Its Definition
-
More Text
+ - The Term
+ - Its Definition
+
+ More Text
""").strip()
assert convert_markdown(src).strip() == expected
@@ -123,11 +125,15 @@ def test_markdown_definition_list_advanced():
expected = dedent("""
Bla Bla
- - The Term
Its Definition
+
- The Term
+ Its Definition
More Definition
Even More
- - Another Term
Define this
-
More Text
+
+ - Another Term
+ - Define this
+
+ More Text
""").strip()
print(convert_markdown(src))
assert convert_markdown(src).strip() == expected
@@ -160,7 +166,8 @@ def test_markdown_ansi_colors():
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
- modified: test_naucse/test_markdown.py
> whoami
- helena
+ helena
> venv\Scripts\activate # activate virtualenv
(venv)> dir
Directory of C:\Users\helena
diff --git a/test_naucse_render/test_notebook.py b/test_naucse_render/test_notebook.py
index 208415f..5c85571 100644
--- a/test_naucse_render/test_notebook.py
+++ b/test_naucse_render/test_notebook.py
@@ -33,7 +33,7 @@ def notebook(_notebook):
def test_notebook_markdown_cell_conversion(notebook):
markdown = dedent(r"""
Markdown
- #
+ #
This is Markdown cell!
It even has some $\LaTeX$:
@@ -43,7 +43,7 @@ def test_notebook_markdown_cell_conversion(notebook):
def test_notebook_has_input_prompt(notebook):
- input_prompt = 'In [1]:'
+ input_prompt = 'In [1]:'
assert input_prompt in notebook
@@ -54,7 +54,7 @@ def test_notebook_has_output_prompt(notebook):
def test_notebook_has_highlighted_input_area(notebook):
input_area = dedent("""
-
+
print(
'foo')
diff --git a/tox.ini b/tox.ini
index b82ae6c..5b3e97d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,9 @@
[tox]
-envlist = py{37,38,39,310,311}
+envlist = py{38,39,310,311,312}
isolated_build = True
[testenv]
deps=
pytest
commands=
- python -m pytest
+ python -m pytest -vv