mkdocs-entangled-plugin 0.3.0__tar.gz → 0.5.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,21 +1,22 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: mkdocs-entangled-plugin
3
- Version: 0.3.0
3
+ Version: 0.5.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[rich] (>=2.0.0a3,<3.0.0)
13
- 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.2
10
+ Requires-Dist: mawk<0.2,>=0.1.7
11
+ Requires-Dist: mkdocs<2,>=1.4.2
14
12
  Description-Content-Type: text/markdown
15
13
 
16
14
  # Welcome to MkDocs Entangled Plugin
17
15
  Using this plugin, you can make your Entangled documents look better.
18
16
 
17
+ ## Examples
18
+ Detailed examples are found in the [entangled/mkdocs-examples repository](https://entangled.github.io/mkdocs-examples).
19
+
19
20
  ## Install
20
21
 
21
22
  Install this with `pip install mkdocs-entangled-plugin`. To use the entangled plugin, add the following lines to your `mkdocs.yml`:
@@ -30,6 +31,8 @@ markdown_extensions:
30
31
  alternate_style: true
31
32
  ```
32
33
 
34
+ For more information, see [the documentation](https://entangled.github.io/mkdocs-plugin).
35
+
33
36
  ## Components
34
37
  This plugin bundles functionality for literate programming with Entangled.
35
38
 
@@ -119,4 +122,3 @@ Build artifacts by specifying a Makefile.
119
122
 
120
123
  ## License
121
124
  Licensed under the Apache-2 license agreement: see LICENSE
122
-
@@ -1,6 +1,9 @@
1
1
  # Welcome to MkDocs Entangled Plugin
2
2
  Using this plugin, you can make your Entangled documents look better.
3
3
 
4
+ ## Examples
5
+ Detailed examples are found in the [entangled/mkdocs-examples repository](https://entangled.github.io/mkdocs-examples).
6
+
4
7
  ## Install
5
8
 
6
9
  Install this with `pip install mkdocs-entangled-plugin`. To use the entangled plugin, add the following lines to your `mkdocs.yml`:
@@ -15,6 +18,8 @@ markdown_extensions:
15
18
  alternate_style: true
16
19
  ```
17
20
 
21
+ For more information, see [the documentation](https://entangled.github.io/mkdocs-plugin).
22
+
18
23
  ## Components
19
24
  This plugin bundles functionality for literate programming with Entangled.
20
25
 
@@ -0,0 +1,137 @@
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.document import ReferenceMap, Content, PlainText, ReferenceId, CodeBlock, document_to_text
12
+ from entangled.properties import get_attribute, Id, Attribute, Class
13
+ from entangled.hooks import get_hooks
14
+ from entangled.config import config
15
+ from repl_session import read_session
16
+
17
+ config.read()
18
+
19
+ # The Entangled config needs to be read before importing the Markdown reader
20
+ # This is very bad I know, and should be fixed in some future version.
21
+ from entangled.markdown_reader import read_markdown_string
22
+
23
+
24
+ type ContentFilter = Callable[[ReferenceMap, Content], Iterable[Content]]
25
+ type CodeBlockFilter = Callable[[ReferenceMap, ReferenceId], Iterable[Content]]
26
+
27
+
28
+ def codeblock_filter(f: CodeBlockFilter) -> ContentFilter:
29
+ def _foo(rm: ReferenceMap, c: Content) -> Iterable[Content]:
30
+ match c:
31
+ case PlainText():
32
+ return [c]
33
+ case ReferenceId():
34
+ return f(rm, c)
35
+ return _foo
36
+
37
+
38
+ def iter_bind[T, U](lst: Iterable[T], f: Callable[[T], Iterable[U]]) -> Iterable[U]:
39
+ return chain(*map(f, lst))
40
+
41
+
42
+ def compose_filter(a: ContentFilter, b: ContentFilter) -> ContentFilter:
43
+ def _joined(rm: ReferenceMap, c: Content) -> Iterable[Content]:
44
+ return iter_bind(partial(a, rm)(c), partial(b, rm))
45
+ return _joined
46
+
47
+
48
+ def compose_filters(*args: ContentFilter) -> ContentFilter:
49
+ return reduce(compose_filter, args, lambda _, c: [c])
50
+
51
+
52
+ def read_markdown(text: str) -> tuple[ReferenceMap, list[Content]]:
53
+ hooks = get_hooks()
54
+ return read_markdown_string(text, hooks=hooks)
55
+
56
+
57
+ @codeblock_filter
58
+ def add_title(reference_map: ReferenceMap, r: ReferenceId) -> list[Content]:
59
+ """
60
+ Changes the `open_line` member of a `CodeBlock` to reflect accepted
61
+ MkDocs syntax, adding a `title` attribute.
62
+ """
63
+ codeblock: CodeBlock = reference_map.get_codeblock(r)
64
+
65
+ ids = [p.value for p in codeblock.properties if isinstance(p, Id)]
66
+ classes = [p.value for p in codeblock.properties if isinstance(p, Class)]
67
+ filenames = [p.value for p in codeblock.properties if isinstance(p, Attribute) and p.key == "file"]
68
+
69
+ title = None
70
+ if len(ids) == 1 and len(filenames) == 1:
71
+ title = f"#{ids[0]} / file: {filenames[0]}"
72
+ elif len(ids) == 1:
73
+ title = f"#{ids[0]}"
74
+ elif len(filenames) == 1:
75
+ title = f"file: {filenames[0]}"
76
+ elif len(ids) > 1 or len(filenames) > 1:
77
+ title = "error: ambiguous code block title"
78
+
79
+ open_line = "```"
80
+ if classes:
81
+ language_id = classes[0]
82
+ open_line += language_id
83
+
84
+ if ids or classes or title:
85
+ open_line += " {"
86
+
87
+ if ids:
88
+ open_line += str(Id(ids[0]))
89
+
90
+ for c in classes[1:]:
91
+ open_line += " " + str(Class(c))
92
+
93
+ if title:
94
+ open_line += " " + str(Attribute("title", title))
95
+
96
+ open_line += "}"
97
+
98
+ codeblock.open_line = open_line
99
+ return [r]
100
+
101
+
102
+ @codeblock_filter
103
+ def include_repl_output(reference_map: ReferenceMap, r: ReferenceId) -> list[Content]:
104
+ """
105
+ Takes any codeblock that has the `repl` class and append its pre-computed output.
106
+ """
107
+ codeblock: CodeBlock = reference_map.get_codeblock(r)
108
+ if Class("repl") not in codeblock.properties:
109
+ return [r]
110
+
111
+ first: CodeBlock = next(iter(reference_map.by_name(r.name)))
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
+ reference_map, content = read_markdown(markdown)
136
+ filtered_content = iter_bind(content, partial(compose_filters(add_title, include_repl_output), reference_map))
137
+ return document_to_text(reference_map, filtered_content)
@@ -1,20 +1,14 @@
1
- import logging
2
- import mkdocs
1
+ from typing import override
2
+ from mkdocs.plugins import BasePlugin
3
3
  from mkdocs.config.defaults import MkDocsConfig
4
4
  from mkdocs.structure.pages import Page
5
5
  from mkdocs.structure.files import Files
6
6
 
7
- from entangled.commands import sync
8
- from entangled.main import configure
9
-
10
7
  from .on_page_markdown import on_page_markdown
11
8
  from .config import EntangledConfig
12
9
 
13
- class EntangledPlugin(mkdocs.plugins.BasePlugin[EntangledConfig]):
14
- def on_pre_build(self, config: MkDocsConfig):
15
- configure()
16
- logging.info("Running `entangled sync`")
17
- sync()
18
-
10
+
11
+ class EntangledPlugin(BasePlugin[EntangledConfig]):
12
+ @override
19
13
  def on_page_markdown(self, markdown: str, *, page: Page, config: MkDocsConfig, files: Files):
20
14
  return on_page_markdown(markdown, page=page, config=config, files=files)
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "mkdocs-entangled-plugin"
3
+ version = "0.5.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.2,<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"]
@@ -1,100 +0,0 @@
1
- """
2
- μAwk, a tiny awk-like interface in Python
3
- """
4
-
5
- from typing import Callable, Optional
6
- from functools import wraps, partial
7
- import re
8
-
9
-
10
- """Global counter to sort filters by the order in which they appear
11
- in a class."""
12
- _filter_creation_order = 0
13
-
14
-
15
- def match_filter(expr: str, handler: Callable[[re.Match], Optional[list[str]]], inp: str) -> Optional[list[str]]:
16
- if m := re.match(expr, inp):
17
- return handler(m)
18
- return None
19
-
20
-
21
- def on_match(expr: str):
22
- """Decorator for doing search-and-replace based on a given regex.
23
-
24
- The inner function should take as argument a `re.Match` object
25
- and return a list of strings. The resulting decorated function
26
- then becomes a function from string to list of strings.
27
-
28
- If the input doesn't match the given regex, the original string
29
- is returned.
30
-
31
- The inner function may still decide to do nothing by returning None.
32
-
33
- To erase the matched input return the empty list.
34
-
35
- This decorator also works on class methods. It is then assumed that
36
- the method has signature `(self, m: re.Match)`.
37
- """
38
- def _decorator(f):
39
- global _filter_creation_order
40
- method = len(f.__qualname__.split('.')) > 1
41
- if method:
42
- @wraps(f)
43
- def _impl(self, inp):
44
- return match_filter(expr, partial(f, self), inp)
45
- _impl._is_rule = _filter_creation_order # add tag to method
46
- _filter_creation_order += 1
47
- return _impl
48
- else:
49
- return wraps(f)(partial(match_filter, expr, f))
50
- return _decorator
51
-
52
-
53
- def always(f):
54
- """Suppose you have a rule with `@on_match(r".*")`, then it is better
55
- not to run the regex machinery at all and just pass on the string.
56
- In that case it is better to use `@always`."""
57
- global _filter_creation_order
58
- method = len(f.__qualname__.split('.')) > 1
59
- if method:
60
- f._is_rule = _filter_creation_order # add tag to method
61
- _filter_creation_order += 1
62
- return f
63
- else:
64
- return f
65
-
66
-
67
- Rule = Callable[[str], list[str]]
68
-
69
-
70
- def run(rules: list[Rule], inp: str, exclusive=True) -> str:
71
- """Takes a list of rules, being function from `str` to `list[str]`.
72
- The input string is split into lines, after which each line is fed
73
- through the list of rules. All the results are colected into a list
74
- of strings and then joined by newline.
75
- """
76
- lines = inp.splitlines()
77
- result = []
78
- for l in lines:
79
- for r in rules:
80
- v = r(l)
81
- if v is not None:
82
- result.extend(v)
83
- if exclusive:
84
- break
85
- else:
86
- result.append(l)
87
- return "\n".join(result)
88
-
89
-
90
- class RuleSet:
91
- """To be used as a base class for classes that contain `on_match`
92
- decorated methods."""
93
- def list_rules(self):
94
- members = (getattr(self, m) for m in dir(self) if m[0] != '_')
95
- rules = [m for m in members if hasattr(m, '_is_rule')]
96
- return sorted(rules, key=lambda r: r._is_rule)
97
-
98
- def run(self, inp: str, exclusive=True) -> str:
99
- """Runs all rules in the class on input."""
100
- return run(self.list_rules(), inp, exclusive)
@@ -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
- from . 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.3.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 = {path = "../entangled.py", extras = ["rich"], develop = true}
19
- entangled-cli = {extras = ["rich"], version = "^2.0.0a3"}
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"]