mkdocs-entangled-plugin 0.4.0__tar.gz → 0.6.0__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.
@@ -0,0 +1,6 @@
1
+ site
2
+ .mypy_cache
3
+ .pytest_cache
4
+ __pycache__
5
+ dist
6
+ poetry.lock
@@ -1,17 +1,14 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: mkdocs-entangled-plugin
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Plugin for MkDocs helping with rendering Entangled (entangled.github.io) projects.
5
- License: Apache 2.0
6
- Author: Johan Hidding
7
- Author-email: j.hidding@esciencecenter.nl
8
- Requires-Python: >=3.11,<4.0
9
- Classifier: License :: Other/Proprietary License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.11
12
- Requires-Dist: entangled-cli (>=2.0.0a6,<3.0.0)
13
- Requires-Dist: mawk (>=0.1.4,<0.2.0)
14
- Requires-Dist: mkdocs (>=1.4.2,<2.0.0)
5
+ Author-email: Johan Hidding <j.hidding@esciencecenter.nl>
6
+ License-Expression: Apache-2.0
7
+ License-File: LICENSE
8
+ Requires-Python: <4,>=3.12
9
+ Requires-Dist: entangled-cli<3,>=2.4
10
+ Requires-Dist: mawk<0.2,>=0.1.7
11
+ Requires-Dist: mkdocs<2,>=1.4.2
15
12
  Description-Content-Type: text/markdown
16
13
 
17
14
  # Welcome to MkDocs Entangled Plugin
@@ -125,4 +122,3 @@ Build artifacts by specifying a Makefile.
125
122
 
126
123
  ## License
127
124
  Licensed under the Apache-2 license agreement: see LICENSE
128
-
@@ -0,0 +1,138 @@
1
+ from collections.abc import Iterable, Callable
2
+ from functools import partial, reduce
3
+ from pathlib import Path
4
+ from textwrap import indent
5
+ from itertools import chain
6
+
7
+ from mkdocs.config.defaults import MkDocsConfig
8
+ from mkdocs.structure.pages import Page
9
+ from mkdocs.structure.files import Files
10
+
11
+ from entangled.model import ReferenceMap, Content, PlainText, ReferenceId, CodeBlock, content_to_text
12
+ from entangled.model.properties import get_attribute, Id, Attribute, Class
13
+ from entangled.config import ConfigUpdate, read_config
14
+ from entangled.interface import Context, read_markdown
15
+ from repl_session import read_session
16
+
17
+
18
+ type ContentFilter = Callable[[ReferenceMap, Content], Iterable[Content]]
19
+ type CodeBlockFilter = Callable[[ReferenceMap, ReferenceId], Iterable[Content]]
20
+
21
+
22
+ def codeblock_filter(f: CodeBlockFilter) -> ContentFilter:
23
+ def _foo(rm: ReferenceMap, c: Content) -> Iterable[Content]:
24
+ match c:
25
+ case PlainText():
26
+ return [c]
27
+ case ReferenceId():
28
+ return f(rm, c)
29
+ return _foo
30
+
31
+
32
+ def document_to_text(refs: ReferenceMap, content: Iterable[Content]) -> str:
33
+ return "".join(map(lambda c: content_to_text(refs, c)[0], content))
34
+
35
+
36
+ def iter_bind[T, U](lst: Iterable[T], f: Callable[[T], Iterable[U]]) -> Iterable[U]:
37
+ return chain(*map(f, lst))
38
+
39
+
40
+ def compose_filter(a: ContentFilter, b: ContentFilter) -> ContentFilter:
41
+ def _joined(rm: ReferenceMap, c: Content) -> Iterable[Content]:
42
+ return iter_bind(partial(a, rm)(c), partial(b, rm))
43
+ return _joined
44
+
45
+
46
+ def compose_filters(*args: ContentFilter) -> ContentFilter:
47
+ return reduce(compose_filter, args, lambda _, c: [c])
48
+
49
+
50
+ def read_single_markdown(ctx: Context, text: str) -> tuple[ReferenceMap, list[Content]]:
51
+ refs = ReferenceMap()
52
+ content, _ = read_markdown(ctx, refs, text)
53
+ return refs, content
54
+
55
+
56
+ @codeblock_filter
57
+ def add_title(reference_map: ReferenceMap, r: ReferenceId) -> list[Content]:
58
+ """
59
+ Changes the `open_line` member of a `CodeBlock` to reflect accepted
60
+ MkDocs syntax, adding a `title` attribute.
61
+ """
62
+ codeblock: CodeBlock = reference_map[r]
63
+
64
+ ids = [p.value for p in codeblock.properties if isinstance(p, Id)]
65
+ classes = [p.value for p in codeblock.properties if isinstance(p, Class)]
66
+ filenames = [p.value for p in codeblock.properties if isinstance(p, Attribute) and p.key == "file"]
67
+
68
+ title = None
69
+ if len(ids) == 1 and len(filenames) == 1:
70
+ title = f"#{ids[0]} / file: {filenames[0]}"
71
+ elif len(ids) == 1:
72
+ title = f"#{ids[0]}"
73
+ elif len(filenames) == 1:
74
+ title = f"file: {filenames[0]}"
75
+ elif len(ids) > 1 or len(filenames) > 1:
76
+ title = "error: ambiguous code block title"
77
+
78
+ open_line = "```"
79
+ if classes:
80
+ language_id = classes[0]
81
+ open_line += language_id
82
+
83
+ if ids or classes or title:
84
+ open_line += " {"
85
+
86
+ if ids:
87
+ open_line += str(Id(ids[0]))
88
+
89
+ for c in classes[1:]:
90
+ open_line += " " + str(Class(c))
91
+
92
+ if title:
93
+ open_line += " " + str(Attribute("title", title))
94
+
95
+ open_line += "}\n"
96
+
97
+ codeblock.open_line = open_line
98
+ return [r]
99
+
100
+
101
+ @codeblock_filter
102
+ def include_repl_output(reference_map: ReferenceMap, r: ReferenceId) -> list[Content]:
103
+ """
104
+ Takes any codeblock that has the `repl` class and append its pre-computed output.
105
+ """
106
+ codeblock: CodeBlock = reference_map[r]
107
+ if Class("repl") not in codeblock.properties:
108
+ return [r]
109
+
110
+ ref = next(iter(reference_map.select_by_name(r.name)))
111
+ first: CodeBlock = reference_map[ref]
112
+ session_filename = get_attribute(first.properties, "session")
113
+ assert session_filename is not None
114
+ session_path: Path = Path(session_filename)
115
+ assert session_path.exists()
116
+ session_output_path: Path = session_path.with_suffix(".out.json")
117
+ assert session_output_path.exists()
118
+
119
+ session = read_session(session_output_path.open("r"))
120
+ command = session.commands[r.ref_count]
121
+
122
+ if not command.output:
123
+ return [r]
124
+
125
+ output: str
126
+ if command.output_type == "text/plain":
127
+ output = indent(f"\n``` {{.text .output}}\n{command.output}\n```", codeblock.indent)
128
+ else:
129
+ output = indent(f"\n**unknown MIME type: {command.output_type}**", codeblock.indent)
130
+
131
+ return [r, PlainText(output)]
132
+
133
+
134
+ def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files: Files) -> str:
135
+ context = Context() | read_config()
136
+ reference_map, content = read_single_markdown(context, markdown)
137
+ filtered_content = iter_bind(content, partial(compose_filters(add_title, include_repl_output), reference_map))
138
+ return document_to_text(reference_map, filtered_content)
@@ -1,4 +1,5 @@
1
- import mkdocs
1
+ from typing import override
2
+ from mkdocs.plugins import BasePlugin
2
3
  from mkdocs.config.defaults import MkDocsConfig
3
4
  from mkdocs.structure.pages import Page
4
5
  from mkdocs.structure.files import Files
@@ -6,6 +7,8 @@ from mkdocs.structure.files import Files
6
7
  from .on_page_markdown import on_page_markdown
7
8
  from .config import EntangledConfig
8
9
 
9
- class EntangledPlugin(mkdocs.plugins.BasePlugin[EntangledConfig]):
10
+
11
+ class EntangledPlugin(BasePlugin[EntangledConfig]):
12
+ @override
10
13
  def on_page_markdown(self, markdown: str, *, page: Page, config: MkDocsConfig, files: Files):
11
14
  return on_page_markdown(markdown, page=page, config=config, files=files)
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "mkdocs-entangled-plugin"
3
+ version = "0.6.0"
4
+ description = "Plugin for MkDocs helping with rendering Entangled (entangled.github.io) projects."
5
+ authors = [{ name = "Johan Hidding", email = "j.hidding@esciencecenter.nl" }]
6
+ requires-python = ">=3.12,<4"
7
+ readme = "README.md"
8
+ license = "Apache-2.0"
9
+ dependencies = [
10
+ "mkdocs>=1.4.2,<2",
11
+ "entangled-cli>=2.4,<3",
12
+ "mawk>=0.1.7,<0.2",
13
+ ]
14
+
15
+ [project.entry-points."mkdocs.plugins"]
16
+ entangled = "mkdocs_entangled:EntangledPlugin"
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "mypy>=1.2.0,<2",
21
+ "pytest>=7.3.0,<8",
22
+ "pytest-mypy>=0.10.3,<0.11",
23
+ "mkdocs-material>=9.1.6,<10",
24
+ "pyright>=1.1.406",
25
+ ]
26
+
27
+ [tool.hatch.build.targets.sdist]
28
+ include = ["mkdocs_entangled"]
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ include = ["mkdocs_entangled"]
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.pytest.ini_options]
38
+ addopts = ["--mypy"]
39
+ testpaths = [
40
+ "test"
41
+ ]
42
+
43
+ [tool.mypy]
44
+ packages = ["mkdocs_entangled"]
45
+
46
+ [tool.uv.sources]
47
+ entangled-cli = { git = "https://github.com/entangled/entangled.py" }
48
+
@@ -1,64 +0,0 @@
1
- from typing import Optional
2
- from dataclasses import dataclass
3
- import re
4
-
5
- from mkdocs.config.defaults import MkDocsConfig
6
- from mkdocs.structure.pages import Page
7
- from mkdocs.structure.files import Files
8
-
9
- from .properties import read_properties, Id, Attribute
10
- import mawk
11
-
12
-
13
- @dataclass
14
- class EntangledFilter(mawk.RuleSet):
15
- add_title: bool = True
16
- _ignore: bool = False
17
-
18
- @mawk.on_match(r"~~~markdown")
19
- def start_ignore(self, m):
20
- self._ignore = True
21
- return [m[0]]
22
-
23
- @mawk.on_match(r"~~~")
24
- def stop_ignore(self, m):
25
- self._ignore = False
26
- return [m[0]]
27
-
28
- @mawk.on_match(r"(\s*)``` *\{([^}]*)\}")
29
- def open_code_block(self, m: re.Match) -> Optional[list[str]]:
30
- if self._ignore:
31
- return None
32
-
33
- indent = m[1]
34
- props = list(read_properties(m[2]))
35
-
36
- ids = [p.value for p in props if isinstance(p, Id)]
37
- filenames = [p.value for p in props if isinstance(p, Attribute) and p.key == "file"]
38
-
39
- if self.add_title:
40
- title = None
41
- if len(ids) == 1 and len(filenames) == 1:
42
- title = f"#{ids[0]} / file: {filenames[0]}"
43
- elif len(ids) == 1:
44
- title = f"#{ids[0]}"
45
- elif len(filenames) == 1:
46
- title = f"file: {filenames[0]}"
47
- elif len(ids) > 1 or len(filenames) > 1:
48
- title = f"error: ambiguous code block title"
49
-
50
- if title is not None:
51
- props.append(Attribute("title", title))
52
-
53
- prop_str = " ".join(str(p) for p in props)
54
- return [f"{indent}``` {{{prop_str}}}"]
55
-
56
- @mawk.on_match(r"\s*```\s*$")
57
- def close_code_block(self, m: re.Match) -> Optional[list[str]]:
58
- return None
59
-
60
-
61
- def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files: Files) -> str:
62
- result = EntangledFilter().run(markdown)
63
- return result
64
-
@@ -1,124 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Optional, Protocol, TypeVar, Generic, Any, Sequence, Union
5
- import re
6
-
7
-
8
- T = TypeVar("T")
9
- T_co = TypeVar("T_co", covariant=True)
10
-
11
- class Parser(Protocol[T_co]):
12
- def read(self, inp: str) -> Optional[tuple[T_co, str]]:
13
- ...
14
-
15
- @dataclass
16
- class Id:
17
- value: str
18
-
19
- def __str__(self):
20
- return f"#{self.value}"
21
-
22
- @staticmethod
23
- def read(inp: str) -> Optional[tuple[Id, str]]:
24
- if m := re.match("#([^ {}]+)", inp):
25
- return Id(m[1]), inp[m.end():]
26
- return None
27
-
28
-
29
- @dataclass
30
- class Class:
31
- value: str
32
-
33
- def __str__(self):
34
- return f".{self.value}"
35
-
36
- @staticmethod
37
- def read(inp: str) -> Optional[tuple[Class, str]]:
38
- if m := re.match("\\.([^ {}]+)", inp):
39
- return Class(m[1]), inp[m.end():]
40
- return None
41
-
42
-
43
- @dataclass
44
- class Attribute:
45
- key: str
46
- value: str
47
-
48
- def __str__(self):
49
- return f"{self.key}=\"{self.value}\""
50
-
51
- @staticmethod
52
- def read(inp: str) -> Optional[tuple[Attribute, str]]:
53
- if m := re.match("([^ {}]+) *= *([^ {}\"]+)", inp):
54
- return Attribute(m[1], m[2]), inp[m.end():]
55
- if m := re.match("([^ {}]+) *= *\"([^\"]*)\"", inp):
56
- return Attribute(m[1], m[2]), inp[m.end():]
57
- return None
58
-
59
-
60
- Property = Union[Attribute, Class, Id]
61
-
62
- @dataclass
63
- class Choice(Generic[T]):
64
- options: Sequence[Parser[T]]
65
-
66
- def read(self, inp: str) -> Optional[tuple[T, str]]:
67
- for o in self.options:
68
- rv = o.read(inp)
69
- if rv is None:
70
- continue
71
- else:
72
- return rv
73
- return None
74
-
75
-
76
- @dataclass
77
- class Many(Generic[T]):
78
- parser: Parser[T]
79
-
80
- def read(self, inp: str) -> tuple[list[T], str]:
81
- result: list[T] = []
82
- while True:
83
- rv = self.parser.read(inp)
84
- if rv is None:
85
- return result, inp
86
- else:
87
- value, rest = rv
88
- result.append(value)
89
- inp = rest
90
-
91
-
92
- @dataclass
93
- class Skip:
94
- what: str
95
-
96
- def read(self, inp: str) -> Optional[tuple[None, str]]:
97
- if m := re.match(self.what, inp):
98
- return None, inp[m.end():]
99
- return None
100
-
101
-
102
- @dataclass
103
- class Last(Generic[T]):
104
- first: Parser[Any]
105
- second: Parser[T]
106
-
107
- def read(self, inp: str) -> Optional[tuple[T, str]]:
108
- rv = self.first.read(inp)
109
- if rv is None:
110
- return None
111
- else:
112
- _, rest = rv
113
- return self.second.read(rest)
114
-
115
-
116
- def tokenize(p: Parser[T]) -> Parser[T]:
117
- return Last(Skip(" *"), p)
118
-
119
-
120
- def read_properties(inp: str) -> list[Property]:
121
- # Explicit typing is needed to convince MyPy of correctness
122
- parsers: list[Parser[Property]] = [Id, Class, Attribute]
123
- result, _ = Many(tokenize(Choice(parsers))).read(inp)
124
- return result
@@ -1,39 +0,0 @@
1
- [tool.poetry]
2
- name = "mkdocs-entangled-plugin"
3
- version = "0.4.0"
4
- description = "Plugin for MkDocs helping with rendering Entangled (entangled.github.io) projects."
5
- readme = "README.md"
6
- authors = ["Johan Hidding <j.hidding@esciencecenter.nl>"]
7
- license = "Apache 2.0"
8
- packages = [
9
- { include = "mkdocs_entangled" }
10
- ]
11
-
12
- [tool.poetry.plugins."mkdocs.plugins"]
13
- entangled = "mkdocs_entangled:EntangledPlugin"
14
-
15
- [tool.poetry.dependencies]
16
- python = "^3.11"
17
- mkdocs = "^1.4.2"
18
- entangled-cli = "^2.0.0a6"
19
- mawk = "^0.1.4"
20
-
21
- [tool.poetry.dev-dependencies]
22
- mypy = "^1.2.0"
23
- pytest = "^7.3.0"
24
- pytest-mypy = "^0.10.3"
25
- mkdocs-material = "^9.1.6"
26
- twine = "^4.0.2"
27
-
28
- [build-system]
29
- requires = ["poetry-core>=1.0.0"]
30
- build-backend = "poetry.core.masonry.api"
31
-
32
- [tool.pytest.ini_options]
33
- addopts = ["--mypy"]
34
- testpaths = [
35
- "test"
36
- ]
37
-
38
- [tool.mypy]
39
- packages = ["mkdocs_entangled"]