markdown-to-confluence 0.5.3__py3-none-any.whl → 0.5.5__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.
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/METADATA +275 -208
- markdown_to_confluence-0.5.5.dist-info/RECORD +57 -0
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +61 -189
- md2conf/api.py +35 -69
- md2conf/attachment.py +4 -3
- md2conf/clio.py +226 -0
- md2conf/compatibility.py +5 -0
- md2conf/converter.py +239 -147
- md2conf/csf.py +89 -9
- md2conf/drawio/extension.py +3 -3
- md2conf/drawio/render.py +2 -0
- md2conf/extension.py +4 -0
- md2conf/external.py +25 -8
- md2conf/frontmatter.py +18 -6
- md2conf/image.py +17 -14
- md2conf/latex.py +8 -1
- md2conf/markdown.py +68 -1
- md2conf/mermaid/render.py +1 -1
- md2conf/options.py +95 -24
- md2conf/plantuml/extension.py +7 -7
- md2conf/plantuml/render.py +6 -7
- md2conf/png.py +10 -6
- md2conf/processor.py +24 -3
- md2conf/publisher.py +193 -36
- md2conf/reflection.py +74 -0
- md2conf/scanner.py +16 -6
- md2conf/serializer.py +12 -1
- md2conf/svg.py +131 -109
- md2conf/toc.py +72 -0
- md2conf/xml.py +45 -0
- markdown_to_confluence-0.5.3.dist-info/RECORD +0 -55
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/zip-safe +0 -0
- /md2conf/{puppeteer-config.json → mermaid/puppeteer-config.json} +0 -0
md2conf/clio.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Publish Markdown files to Confluence wiki.
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/md2conf
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from argparse import ArgumentParser, Namespace
|
|
10
|
+
from dataclasses import MISSING, Field, dataclass, fields, is_dataclass
|
|
11
|
+
from types import NoneType, UnionType
|
|
12
|
+
from typing import Any, Literal, TypeVar, cast, get_args, get_origin
|
|
13
|
+
|
|
14
|
+
from .compatibility import LiteralString
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseOption:
|
|
18
|
+
@staticmethod
|
|
19
|
+
def field_name() -> str:
|
|
20
|
+
return "argument"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class BooleanOption(BaseOption):
|
|
25
|
+
true_text: LiteralString
|
|
26
|
+
false_text: LiteralString
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def boolean_option(true_text: LiteralString, false_text: LiteralString) -> dict[str, Any]:
|
|
30
|
+
"Identifies a command-line argument as a boolean (on/off) flag."
|
|
31
|
+
|
|
32
|
+
return {BaseOption.field_name(): BooleanOption(true_text, false_text)}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ValueOption(BaseOption):
|
|
37
|
+
text: LiteralString
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def value_option(text: LiteralString) -> dict[str, Any]:
|
|
41
|
+
"Identifies a command-line argument as an option that assigns a value."
|
|
42
|
+
|
|
43
|
+
return {BaseOption.field_name(): ValueOption(text)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CompositeOption(BaseOption):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def composite_option() -> dict[str, Any]:
|
|
51
|
+
"Identifies a command-line argument as a data-class that needs to be unnested."
|
|
52
|
+
|
|
53
|
+
return {BaseOption.field_name(): CompositeOption()}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
T = TypeVar("T")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _has_metadata(field: Field[Any]) -> bool:
|
|
60
|
+
return field.metadata.get(BaseOption.field_name()) is not None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_metadata(field: Field[Any], tp: type[T]) -> T | None:
|
|
64
|
+
attrs = field.metadata.get(BaseOption.field_name())
|
|
65
|
+
if attrs is None:
|
|
66
|
+
return None
|
|
67
|
+
elif isinstance(attrs, tp):
|
|
68
|
+
return attrs
|
|
69
|
+
else:
|
|
70
|
+
raise TypeError(f"expected: {tp.__name__}; got: {type(attrs).__name__}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _OptionTreeVisitor:
|
|
74
|
+
"Adds arguments to a command-line argument parser by recursively visiting fields of a composite type."
|
|
75
|
+
|
|
76
|
+
parser: ArgumentParser
|
|
77
|
+
prefixes: list[str]
|
|
78
|
+
|
|
79
|
+
def __init__(self, parser: ArgumentParser) -> None:
|
|
80
|
+
self.parser = parser
|
|
81
|
+
self.prefixes = []
|
|
82
|
+
|
|
83
|
+
def _get_arg_name(self, arg_name: str) -> str:
|
|
84
|
+
return f"--{'-'.join([*self.prefixes, arg_name])}"
|
|
85
|
+
|
|
86
|
+
def _get_field_name(self, field_name: str) -> str:
|
|
87
|
+
return "_".join([*self.prefixes, field_name])
|
|
88
|
+
|
|
89
|
+
def _add_value_field(self, field: Field[Any], field_type: Any) -> None:
|
|
90
|
+
arg_name = field.name.replace("_", "-")
|
|
91
|
+
value_opt = _get_metadata(field, ValueOption)
|
|
92
|
+
if value_opt is None:
|
|
93
|
+
return
|
|
94
|
+
help_text = value_opt.text
|
|
95
|
+
if field.default is not MISSING and field.default is not None:
|
|
96
|
+
help_text += f" (default: {field.default!s})"
|
|
97
|
+
if isinstance(field_type, type):
|
|
98
|
+
metavar = field_type.__name__.upper()
|
|
99
|
+
else:
|
|
100
|
+
metavar = None
|
|
101
|
+
self.parser.add_argument(
|
|
102
|
+
self._get_arg_name(arg_name),
|
|
103
|
+
dest=self._get_field_name(field.name),
|
|
104
|
+
default=field.default,
|
|
105
|
+
type=field_type,
|
|
106
|
+
help=help_text,
|
|
107
|
+
metavar=metavar,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _add_field_as_argument(self, field: Field[Any]) -> None:
|
|
111
|
+
arg_name = field.name.replace("_", "-")
|
|
112
|
+
if field.type is bool:
|
|
113
|
+
bool_opt = _get_metadata(field, BooleanOption)
|
|
114
|
+
if bool_opt is None:
|
|
115
|
+
return
|
|
116
|
+
true_text = bool_opt.true_text
|
|
117
|
+
if field.default is True:
|
|
118
|
+
true_text += " (default)"
|
|
119
|
+
self.parser.add_argument(
|
|
120
|
+
self._get_arg_name(arg_name),
|
|
121
|
+
dest=self._get_field_name(field.name),
|
|
122
|
+
action="store_true",
|
|
123
|
+
default=field.default,
|
|
124
|
+
help=true_text,
|
|
125
|
+
)
|
|
126
|
+
if arg_name.startswith("skip-"):
|
|
127
|
+
inverse_arg_name = "keep-" + arg_name.removeprefix("skip-")
|
|
128
|
+
elif arg_name.startswith("keep-"):
|
|
129
|
+
inverse_arg_name = "skip-" + arg_name.removeprefix("keep-")
|
|
130
|
+
else:
|
|
131
|
+
inverse_arg_name = f"no-{arg_name}"
|
|
132
|
+
false_text = bool_opt.false_text
|
|
133
|
+
if field.default is False:
|
|
134
|
+
false_text += " (default)"
|
|
135
|
+
self.parser.add_argument(
|
|
136
|
+
self._get_arg_name(inverse_arg_name),
|
|
137
|
+
dest=self._get_field_name(field.name),
|
|
138
|
+
action="store_false",
|
|
139
|
+
help=false_text,
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
origin = get_origin(field.type)
|
|
144
|
+
if origin is Literal:
|
|
145
|
+
value_opt = _get_metadata(field, ValueOption)
|
|
146
|
+
if value_opt is None:
|
|
147
|
+
return
|
|
148
|
+
value_text = value_opt.text
|
|
149
|
+
if field.default is not MISSING and field.default is not None:
|
|
150
|
+
value_text += f" (default: {field.default!s})"
|
|
151
|
+
self.parser.add_argument(
|
|
152
|
+
self._get_arg_name(arg_name),
|
|
153
|
+
dest=self._get_field_name(field.name),
|
|
154
|
+
choices=get_args(field.type),
|
|
155
|
+
default=field.default,
|
|
156
|
+
help=value_text,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
elif origin is UnionType:
|
|
160
|
+
union_types = list(get_args(field.type))
|
|
161
|
+
if len(union_types) != 2 or NoneType not in union_types:
|
|
162
|
+
raise TypeError(f"expected: `T` or `T | None` as argument type; got: {field.type}")
|
|
163
|
+
union_types.remove(NoneType)
|
|
164
|
+
required_type = union_types.pop()
|
|
165
|
+
|
|
166
|
+
self._add_value_field(field, required_type)
|
|
167
|
+
|
|
168
|
+
elif isinstance(field.type, type):
|
|
169
|
+
if hasattr(field.type, "__dataclass_fields__"):
|
|
170
|
+
composite_opt = _get_metadata(field, CompositeOption)
|
|
171
|
+
if composite_opt is None:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
self.prefixes.append(arg_name)
|
|
175
|
+
self.add_arguments(field.type)
|
|
176
|
+
self.prefixes.pop()
|
|
177
|
+
|
|
178
|
+
else:
|
|
179
|
+
self._add_value_field(field, field.type)
|
|
180
|
+
|
|
181
|
+
elif _has_metadata(field):
|
|
182
|
+
raise TypeError(f"expected: known argument type; got: {field.type}")
|
|
183
|
+
|
|
184
|
+
def add_arguments(self, options_type: type[Any]) -> None:
|
|
185
|
+
if is_dataclass(options_type):
|
|
186
|
+
for field in fields(options_type):
|
|
187
|
+
self._add_field_as_argument(field)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def add_arguments(parser: ArgumentParser, options_type: type[Any]) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Adds arguments to a command-line argument parser.
|
|
193
|
+
|
|
194
|
+
:param parser: A command-line argument parser.
|
|
195
|
+
:param options_type: A data-class type that encapsulates configuration options.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
_OptionTreeVisitor(parser).add_arguments(options_type)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _get_options(args: Namespace, options_type: type[T], prefixes: tuple[str, ...]) -> T:
|
|
202
|
+
params: dict[str, Any] = {}
|
|
203
|
+
if is_dataclass(options_type): # always true, condition included for type checkers
|
|
204
|
+
for field in fields(options_type):
|
|
205
|
+
field_prefixes = (*prefixes, field.name)
|
|
206
|
+
if isinstance(field.type, type) and is_dataclass(field.type):
|
|
207
|
+
params[field.name] = _get_options(args, field.type, field_prefixes)
|
|
208
|
+
else:
|
|
209
|
+
field_param = getattr(args, "_".join(field_prefixes), MISSING)
|
|
210
|
+
if field_param is not MISSING:
|
|
211
|
+
params[field.name] = field_param
|
|
212
|
+
return options_type(**params)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_options(args: Namespace, options_type: type[T]) -> T:
|
|
216
|
+
"""
|
|
217
|
+
Extracts configuration options from command-line arguments acquired by an argument parser.
|
|
218
|
+
|
|
219
|
+
:param args: Arguments acquired by a command-line argument parser.
|
|
220
|
+
:param options_type: A data-class type that encapsulates configuration options.
|
|
221
|
+
:returns: Configuration options as a data-class instance.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
if not is_dataclass(options_type):
|
|
225
|
+
raise TypeError(f"expected: data-class as argument target; got: {type(options_type).__name__}")
|
|
226
|
+
return _get_options(args, cast(type[T], options_type), ())
|
md2conf/compatibility.py
CHANGED
|
@@ -8,6 +8,11 @@ Copyright 2022-2026, Levente Hunyadi
|
|
|
8
8
|
|
|
9
9
|
import sys
|
|
10
10
|
|
|
11
|
+
if sys.version_info >= (3, 11):
|
|
12
|
+
from typing import LiteralString as LiteralString # noqa: F401
|
|
13
|
+
else:
|
|
14
|
+
from typing_extensions import LiteralString as LiteralString # noqa: F401
|
|
15
|
+
|
|
11
16
|
if sys.version_info >= (3, 12):
|
|
12
17
|
from typing import override as override # noqa: F401
|
|
13
18
|
else:
|