markdown-gost 0.2.0__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_gost/__init__.py +5 -0
- markdown_gost/cli/__init__.py +0 -0
- markdown_gost/cli/__main__.py +191 -0
- markdown_gost/cli/commands/__init__.py +0 -0
- markdown_gost/cli/commands/import_cmd.py +134 -0
- markdown_gost/config/__init__.py +0 -0
- markdown_gost/config/errors.py +8 -0
- markdown_gost/config/loader.py +59 -0
- markdown_gost/config/presets/__init__.py +0 -0
- markdown_gost/config/presets/default.yaml +94 -0
- markdown_gost/config/presets/gost-7-32-2017.yaml +124 -0
- markdown_gost/config/presets/mirea-practice.yaml +124 -0
- markdown_gost/config/schema.py +688 -0
- markdown_gost/config/units.py +61 -0
- markdown_gost/convert.py +84 -0
- markdown_gost/core/__init__.py +0 -0
- markdown_gost/core/ast/__init__.py +77 -0
- markdown_gost/core/ast/nodes.py +253 -0
- markdown_gost/core/parser/__init__.py +358 -0
- markdown_gost/core/parser/_marko_ext.py +155 -0
- markdown_gost/core/parser/attrs.py +191 -0
- markdown_gost/image_placeholder.py +78 -0
- markdown_gost/import_/__init__.py +21 -0
- markdown_gost/import_/image_handler.py +82 -0
- markdown_gost/import_/listing_detector.py +92 -0
- markdown_gost/import_/metrics.py +36 -0
- markdown_gost/import_/pandoc_runner.py +97 -0
- markdown_gost/import_/pdf_prepass.py +164 -0
- markdown_gost/import_/pipeline.py +345 -0
- markdown_gost/import_/postprocessor/README.md +56 -0
- markdown_gost/import_/postprocessor/__init__.py +55 -0
- markdown_gost/import_/postprocessor/_common.py +18 -0
- markdown_gost/import_/postprocessor/caption_folder.py +212 -0
- markdown_gost/import_/postprocessor/heading_normalizer.py +39 -0
- markdown_gost/import_/postprocessor/html_table_converter.py +348 -0
- markdown_gost/import_/postprocessor/inline_normalizer.py +50 -0
- markdown_gost/import_/postprocessor/listing_wrapper.py +130 -0
- markdown_gost/import_/postprocessor/unnumbered_heading_detector.py +57 -0
- markdown_gost/import_/result.py +61 -0
- markdown_gost/import_/semantic_docx.py +72 -0
- markdown_gost/metrics_compat.py +38 -0
- markdown_gost/output/__init__.py +0 -0
- markdown_gost/output/pdf_writer.py +183 -0
- markdown_gost/preview/__init__.py +34 -0
- markdown_gost/preview/builder.py +2316 -0
- markdown_gost/preview/html.py +1350 -0
- markdown_gost/preview/model.py +298 -0
- markdown_gost/preview_json.py +59 -0
- markdown_gost/render/__init__.py +14 -0
- markdown_gost/render/_assets/__init__.py +0 -0
- markdown_gost/render/_assets/mml2omml.xsl +3822 -0
- markdown_gost/render/bibliography.py +202 -0
- markdown_gost/render/document_factory.py +140 -0
- markdown_gost/render/latex_math.py +235 -0
- markdown_gost/render/layout_tracker.py +65 -0
- markdown_gost/render/numberer.py +102 -0
- markdown_gost/render/paragraph_sizer.py +395 -0
- markdown_gost/render/references.py +91 -0
- markdown_gost/render/render_index.py +126 -0
- markdown_gost/render/renderer.py +358 -0
- markdown_gost/renderable/__init__.py +25 -0
- markdown_gost/renderable/_oxml.py +40 -0
- markdown_gost/renderable/appendix.py +109 -0
- markdown_gost/renderable/base.py +84 -0
- markdown_gost/renderable/bibliography.py +58 -0
- markdown_gost/renderable/caption.py +150 -0
- markdown_gost/renderable/equation.py +358 -0
- markdown_gost/renderable/factory.py +238 -0
- markdown_gost/renderable/heading.py +247 -0
- markdown_gost/renderable/image.py +241 -0
- markdown_gost/renderable/list.py +306 -0
- markdown_gost/renderable/listing.py +514 -0
- markdown_gost/renderable/page_break.py +47 -0
- markdown_gost/renderable/paragraph.py +543 -0
- markdown_gost/renderable/table.py +797 -0
- markdown_gost/renderable/thematic_break.py +18 -0
- markdown_gost/storage/__init__.py +46 -0
- markdown_gost/storage/base.py +68 -0
- markdown_gost/storage/fs.py +96 -0
- markdown_gost/templates/__init__.py +202 -0
- markdown_gost/templates/_placeholder.py +41 -0
- markdown_gost/templates/_preview.py +93 -0
- markdown_gost/templates/_registry.py +128 -0
- markdown_gost/templates/_schema.py +184 -0
- markdown_gost/templates/base.docx +0 -0
- markdown_gost/templates/content/__init__.py +19 -0
- markdown_gost/templates/content/preview.py +77 -0
- markdown_gost/templates/content/render.py +241 -0
- markdown_gost/templates/content/schema.yaml +30 -0
- markdown_gost/templates/titlepage_university/__init__.py +28 -0
- markdown_gost/templates/titlepage_university/preview.py +108 -0
- markdown_gost/templates/titlepage_university/render.py +407 -0
- markdown_gost/templates/titlepage_university/schema.yaml +131 -0
- markdown_gost-0.2.0.dist-info/METADATA +104 -0
- markdown_gost-0.2.0.dist-info/RECORD +98 -0
- markdown_gost-0.2.0.dist-info/WHEEL +4 -0
- markdown_gost-0.2.0.dist-info/entry_points.txt +3 -0
- markdown_gost-0.2.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""markdown-gost CLI (T020).
|
|
2
|
+
|
|
3
|
+
Commands: ``convert``, ``validate``. Exit codes: ``0`` ok, ``1`` user error
|
|
4
|
+
(config/parse), ``2`` system error / usage error.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, NoReturn, cast
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from markdown_gost import __version__
|
|
19
|
+
from markdown_gost.cli.commands.import_cmd import import_command
|
|
20
|
+
from markdown_gost.config.errors import ConfigError
|
|
21
|
+
from markdown_gost.config.loader import load_config_from_path, load_config_from_string
|
|
22
|
+
from markdown_gost.config.schema import Config
|
|
23
|
+
from markdown_gost.convert import Format
|
|
24
|
+
from markdown_gost.convert import convert as convert_pipeline
|
|
25
|
+
from markdown_gost.core.parser import parse as parse_markdown
|
|
26
|
+
from markdown_gost.storage import get_storage
|
|
27
|
+
|
|
28
|
+
_DEFAULT_CONFIG = "preset: default\n"
|
|
29
|
+
_VALID_FORMATS: tuple[str, ...] = ("docx", "pdf")
|
|
30
|
+
_LOGGER = logging.getLogger("markdown_gost.cli")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_format(explicit: str | None, output: Path | None) -> Format:
|
|
34
|
+
if explicit is not None:
|
|
35
|
+
return cast(Format, explicit.lower())
|
|
36
|
+
if output is not None:
|
|
37
|
+
suffix = output.suffix.lstrip(".").lower()
|
|
38
|
+
if suffix in _VALID_FORMATS:
|
|
39
|
+
return cast(Format, suffix)
|
|
40
|
+
return "docx"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_config(config_path: Path | None) -> Config:
|
|
44
|
+
if config_path is None:
|
|
45
|
+
return load_config_from_string(_DEFAULT_CONFIG)
|
|
46
|
+
return load_config_from_path(config_path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _user_error(message: str) -> NoReturn:
|
|
50
|
+
raise click.ClickException(message)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _system_error(message: str) -> NoReturn:
|
|
54
|
+
click.echo(f"Error: {message}", err=True)
|
|
55
|
+
sys.exit(2)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _execute(action: Callable[[], Any]) -> Any:
|
|
59
|
+
"""Run *action* and translate exceptions to documented exit codes.
|
|
60
|
+
|
|
61
|
+
``ConfigError`` / ``ValueError`` (incl. pydantic ``ValidationError``) /
|
|
62
|
+
``yaml.YAMLError`` → exit 1. Anything else → exit 2.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
return action()
|
|
67
|
+
except click.ClickException:
|
|
68
|
+
raise
|
|
69
|
+
except (ConfigError, ValueError, yaml.YAMLError) as exc:
|
|
70
|
+
_user_error(str(exc))
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
_LOGGER.debug("system error", exc_info=True)
|
|
73
|
+
_system_error(str(exc))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
77
|
+
@click.version_option(__version__, prog_name="markdown-gost")
|
|
78
|
+
@click.option(
|
|
79
|
+
"--verbose",
|
|
80
|
+
"-v",
|
|
81
|
+
is_flag=True,
|
|
82
|
+
help="Enable DEBUG-level logging.",
|
|
83
|
+
)
|
|
84
|
+
def cli(verbose: bool) -> None:
|
|
85
|
+
"""markdown-gost — Markdown → DOCX/PDF по ГОСТ.
|
|
86
|
+
|
|
87
|
+
Пресет ГОСТа задаётся в YAML-конфиге, не флагом CLI (см. ADR-0004).
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
91
|
+
logging.basicConfig(
|
|
92
|
+
level=level,
|
|
93
|
+
format="%(levelname)s %(name)s: %(message)s",
|
|
94
|
+
force=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cli.command("convert")
|
|
99
|
+
@click.argument(
|
|
100
|
+
"input_path",
|
|
101
|
+
metavar="INPUT",
|
|
102
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"-o",
|
|
106
|
+
"--output",
|
|
107
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
108
|
+
default=None,
|
|
109
|
+
help="Output file. Defaults to <input>.<format>.",
|
|
110
|
+
)
|
|
111
|
+
@click.option(
|
|
112
|
+
"--config",
|
|
113
|
+
"config_path",
|
|
114
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
115
|
+
default=None,
|
|
116
|
+
help="Path to YAML config (preset + overrides). Defaults to built-in default preset.",
|
|
117
|
+
)
|
|
118
|
+
@click.option(
|
|
119
|
+
"--format",
|
|
120
|
+
"fmt",
|
|
121
|
+
type=click.Choice(list(_VALID_FORMATS), case_sensitive=False),
|
|
122
|
+
default=None,
|
|
123
|
+
help="Output format. Inferred from --output extension if omitted.",
|
|
124
|
+
)
|
|
125
|
+
def convert_command(
|
|
126
|
+
input_path: Path,
|
|
127
|
+
output: Path | None,
|
|
128
|
+
config_path: Path | None,
|
|
129
|
+
fmt: str | None,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Convert INPUT markdown into the chosen format."""
|
|
132
|
+
|
|
133
|
+
resolved_fmt = _resolve_format(fmt, output)
|
|
134
|
+
if output is None:
|
|
135
|
+
output = input_path.with_suffix(f".{resolved_fmt}")
|
|
136
|
+
|
|
137
|
+
def _do() -> None:
|
|
138
|
+
config = _load_config(config_path)
|
|
139
|
+
markdown = input_path.read_text(encoding="utf-8")
|
|
140
|
+
storage = get_storage(default_base_dir=input_path.parent)
|
|
141
|
+
_LOGGER.debug(
|
|
142
|
+
"convert input=%s output=%s format=%s preset=%s",
|
|
143
|
+
input_path,
|
|
144
|
+
output,
|
|
145
|
+
resolved_fmt,
|
|
146
|
+
config.preset,
|
|
147
|
+
)
|
|
148
|
+
data = convert_pipeline(
|
|
149
|
+
markdown, config, format=resolved_fmt, storage=storage
|
|
150
|
+
)
|
|
151
|
+
assert output is not None
|
|
152
|
+
output.write_bytes(data)
|
|
153
|
+
click.echo(f"Wrote {output}")
|
|
154
|
+
|
|
155
|
+
_execute(_do)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@cli.command("validate")
|
|
159
|
+
@click.argument(
|
|
160
|
+
"input_path",
|
|
161
|
+
metavar="INPUT",
|
|
162
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
163
|
+
)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--config",
|
|
166
|
+
"config_path",
|
|
167
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
168
|
+
default=None,
|
|
169
|
+
help="Path to YAML config (preset + overrides). Defaults to built-in default preset.",
|
|
170
|
+
)
|
|
171
|
+
def validate_command(input_path: Path, config_path: Path | None) -> None:
|
|
172
|
+
"""Validate INPUT markdown + config without producing output."""
|
|
173
|
+
|
|
174
|
+
def _do() -> None:
|
|
175
|
+
config = _load_config(config_path)
|
|
176
|
+
markdown = input_path.read_text(encoding="utf-8")
|
|
177
|
+
from markdown_gost.render.bibliography import BibliographyIndex
|
|
178
|
+
from markdown_gost.render.references import prepare_document
|
|
179
|
+
|
|
180
|
+
document = prepare_document(parse_markdown(markdown), config)
|
|
181
|
+
BibliographyIndex.from_document(document, config)
|
|
182
|
+
click.echo(f"OK (preset={config.preset})")
|
|
183
|
+
|
|
184
|
+
_execute(_do)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
cli.add_command(import_command)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
cli()
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""``markdown-gost import`` — конвертация DOCX/PDF → расширенный markdown.
|
|
2
|
+
|
|
3
|
+
T036 ввёл docx-импорт; T041 добавил PDF через unoserver-prepass.
|
|
4
|
+
|
|
5
|
+
Exit codes:
|
|
6
|
+
|
|
7
|
+
* ``0`` — успех (даже если были fallbacks).
|
|
8
|
+
* ``1`` — невалидный вход: файл не найден или расширение не ``.docx`` / ``.pdf``.
|
|
9
|
+
* ``2`` — pandoc/unoserver упал на верхнем уровне.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from markdown_gost.import_ import ImportContext, ImportResult, import_docx, import_pdf
|
|
22
|
+
from markdown_gost.import_.metrics import IMPORT_TOTAL
|
|
23
|
+
from markdown_gost.import_.pandoc_runner import PandocError
|
|
24
|
+
from markdown_gost.output.pdf_writer import UnoserverError
|
|
25
|
+
from markdown_gost.storage import get_storage
|
|
26
|
+
|
|
27
|
+
_LOGGER = logging.getLogger("markdown_gost.cli.import")
|
|
28
|
+
|
|
29
|
+
_HEADING_RE = re.compile(r"^#{1,6}\s")
|
|
30
|
+
_TABLE_SEP_RE = re.compile(r"^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _count_headings(markdown: str) -> int:
|
|
34
|
+
return sum(1 for line in markdown.splitlines() if _HEADING_RE.match(line))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _count_tables(markdown: str) -> int:
|
|
38
|
+
return sum(1 for line in markdown.splitlines() if _TABLE_SEP_RE.match(line))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _total_fallbacks(result: ImportResult) -> int:
|
|
42
|
+
return sum(result.fallbacks.values())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.command("import")
|
|
46
|
+
@click.argument("input_path", metavar="INPUT", type=click.Path(path_type=Path))
|
|
47
|
+
@click.option(
|
|
48
|
+
"-o",
|
|
49
|
+
"--output",
|
|
50
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
51
|
+
default=None,
|
|
52
|
+
help="Output markdown path. Defaults to <input-stem>.md рядом с input.",
|
|
53
|
+
)
|
|
54
|
+
@click.option(
|
|
55
|
+
"--images-dir",
|
|
56
|
+
"images_dir",
|
|
57
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
58
|
+
default=None,
|
|
59
|
+
help="Куда класть извлечённые картинки. По умолчанию <output-stem>_files/.",
|
|
60
|
+
)
|
|
61
|
+
def import_command(
|
|
62
|
+
input_path: Path,
|
|
63
|
+
output: Path | None,
|
|
64
|
+
images_dir: Path | None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Импорт DOCX/PDF в расширенный markdown markdown_gost.
|
|
67
|
+
|
|
68
|
+
PDF-input идёт через unoserver-prepass (PDF → DOCX → md). Сканированные
|
|
69
|
+
PDF без текстового слоя возвращаются как пустой md без падения.
|
|
70
|
+
"""
|
|
71
|
+
if not input_path.exists():
|
|
72
|
+
click.echo(f"Error: input not found: {input_path}", err=True)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
suffix = input_path.suffix.lower()
|
|
76
|
+
if suffix not in (".docx", ".pdf"):
|
|
77
|
+
click.echo(
|
|
78
|
+
f"Error: unsupported input extension {suffix!r}; expected .docx or .pdf",
|
|
79
|
+
err=True,
|
|
80
|
+
)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
if output is None:
|
|
84
|
+
output = input_path.with_suffix(".md")
|
|
85
|
+
if images_dir is None:
|
|
86
|
+
images_dir = output.with_name(f"{output.stem}_files")
|
|
87
|
+
|
|
88
|
+
storage = get_storage(default_base_dir=output.parent)
|
|
89
|
+
ctx = ImportContext(
|
|
90
|
+
storage=storage,
|
|
91
|
+
images_prefix=None,
|
|
92
|
+
images_dir=images_dir,
|
|
93
|
+
)
|
|
94
|
+
fmt_label = "pdf" if suffix == ".pdf" else "docx"
|
|
95
|
+
|
|
96
|
+
_LOGGER.info(
|
|
97
|
+
"import input=%s output=%s images_dir=%s format=%s",
|
|
98
|
+
input_path,
|
|
99
|
+
output,
|
|
100
|
+
images_dir,
|
|
101
|
+
fmt_label,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
importer = import_pdf if suffix == ".pdf" else import_docx
|
|
105
|
+
try:
|
|
106
|
+
result = importer(input_path, ctx)
|
|
107
|
+
except (PandocError, UnoserverError) as exc:
|
|
108
|
+
IMPORT_TOTAL.labels(format=fmt_label, result="error").inc()
|
|
109
|
+
click.echo(f"Error: {exc}", err=True)
|
|
110
|
+
sys.exit(2)
|
|
111
|
+
|
|
112
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
output.write_text(result.markdown, encoding="utf-8")
|
|
114
|
+
|
|
115
|
+
if logging.getLogger().level <= logging.DEBUG and result.warnings:
|
|
116
|
+
for warning in result.warnings:
|
|
117
|
+
click.echo(f"warning: {warning}", err=True)
|
|
118
|
+
|
|
119
|
+
fallback_total = _total_fallbacks(result)
|
|
120
|
+
IMPORT_TOTAL.labels(format=fmt_label, result="success").inc()
|
|
121
|
+
if fallback_total > 0:
|
|
122
|
+
IMPORT_TOTAL.labels(format=fmt_label, result="fallback").inc()
|
|
123
|
+
|
|
124
|
+
click.echo(
|
|
125
|
+
"Imported: "
|
|
126
|
+
f"{_count_headings(result.markdown)} headings, "
|
|
127
|
+
f"{_count_tables(result.markdown)} tables, "
|
|
128
|
+
f"{len(result.images)} images, "
|
|
129
|
+
f"{fallback_total} fallbacks",
|
|
130
|
+
err=True,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__ = ["import_command"]
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from importlib import resources
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from markdown_gost.config.errors import UnknownPresetError
|
|
8
|
+
from markdown_gost.config.schema import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _deep_merge(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
|
|
12
|
+
result: dict[str, Any] = dict(base)
|
|
13
|
+
for key, value in overrides.items():
|
|
14
|
+
existing = result.get(key)
|
|
15
|
+
if isinstance(existing, dict) and isinstance(value, dict):
|
|
16
|
+
result[key] = _deep_merge(existing, value)
|
|
17
|
+
else:
|
|
18
|
+
result[key] = value
|
|
19
|
+
return result
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_preset(name: str) -> dict[str, Any]:
|
|
23
|
+
try:
|
|
24
|
+
text = (
|
|
25
|
+
resources.files("markdown_gost.config.presets")
|
|
26
|
+
.joinpath(f"{name}.yaml")
|
|
27
|
+
.read_text(encoding="utf-8")
|
|
28
|
+
)
|
|
29
|
+
except (FileNotFoundError, IsADirectoryError, ModuleNotFoundError) as e:
|
|
30
|
+
raise UnknownPresetError(name) from e
|
|
31
|
+
parsed = yaml.safe_load(text)
|
|
32
|
+
if parsed is None:
|
|
33
|
+
return {}
|
|
34
|
+
if not isinstance(parsed, dict):
|
|
35
|
+
raise UnknownPresetError(name)
|
|
36
|
+
return parsed
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_config_from_string(content: str) -> Config:
|
|
40
|
+
user = yaml.safe_load(content)
|
|
41
|
+
if not isinstance(user, dict):
|
|
42
|
+
return Config.model_validate(user if user is not None else {})
|
|
43
|
+
|
|
44
|
+
preset_name = user.get("preset")
|
|
45
|
+
if not isinstance(preset_name, str) or not preset_name:
|
|
46
|
+
return Config.model_validate(user)
|
|
47
|
+
|
|
48
|
+
preset_data = _load_preset(preset_name)
|
|
49
|
+
overrides = user.get("overrides") or {}
|
|
50
|
+
if not isinstance(overrides, dict):
|
|
51
|
+
return Config.model_validate({"preset": preset_name, "overrides": overrides})
|
|
52
|
+
|
|
53
|
+
merged = _deep_merge(preset_data, overrides)
|
|
54
|
+
merged["preset"] = preset_name
|
|
55
|
+
return Config.model_validate(merged)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_config_from_path(path: Path) -> Config:
|
|
59
|
+
return load_config_from_string(path.read_text(encoding="utf-8"))
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
page:
|
|
2
|
+
size: A4
|
|
3
|
+
orientation: portrait
|
|
4
|
+
margins:
|
|
5
|
+
top: 2cm
|
|
6
|
+
right: 1cm
|
|
7
|
+
bottom: 1.25cm
|
|
8
|
+
left: 2.5cm
|
|
9
|
+
|
|
10
|
+
font:
|
|
11
|
+
family: Times New Roman
|
|
12
|
+
size: 14pt
|
|
13
|
+
line_spacing: 1.5
|
|
14
|
+
|
|
15
|
+
paragraph:
|
|
16
|
+
alignment: justify
|
|
17
|
+
indent_first_line: 1.25cm
|
|
18
|
+
|
|
19
|
+
headings:
|
|
20
|
+
numbering: continuous
|
|
21
|
+
leading_space_in_numbered: true
|
|
22
|
+
levels:
|
|
23
|
+
1:
|
|
24
|
+
size: 14pt
|
|
25
|
+
bold: true
|
|
26
|
+
uppercase: true
|
|
27
|
+
alignment: left
|
|
28
|
+
space_before: 0pt
|
|
29
|
+
space_after: 0pt
|
|
30
|
+
page_break_before: true
|
|
31
|
+
keep_with_next: true
|
|
32
|
+
indent_first_line: 1.25cm
|
|
33
|
+
2:
|
|
34
|
+
size: 14pt
|
|
35
|
+
bold: true
|
|
36
|
+
alignment: left
|
|
37
|
+
space_before: 12pt
|
|
38
|
+
space_after: 0pt
|
|
39
|
+
keep_with_next: true
|
|
40
|
+
indent_first_line: 1.25cm
|
|
41
|
+
3:
|
|
42
|
+
size: 14pt
|
|
43
|
+
bold: true
|
|
44
|
+
alignment: left
|
|
45
|
+
space_before: 12pt
|
|
46
|
+
space_after: 0pt
|
|
47
|
+
keep_with_next: true
|
|
48
|
+
indent_first_line: 1.25cm
|
|
49
|
+
|
|
50
|
+
captions:
|
|
51
|
+
image:
|
|
52
|
+
italic: false
|
|
53
|
+
bold: false
|
|
54
|
+
alignment: center
|
|
55
|
+
format: "{category} {number} — {text}"
|
|
56
|
+
space_before: 0pt
|
|
57
|
+
space_after: 0pt
|
|
58
|
+
table:
|
|
59
|
+
italic: false
|
|
60
|
+
bold: false
|
|
61
|
+
alignment: left
|
|
62
|
+
format: "{category} {number} — {text}"
|
|
63
|
+
space_before: 0pt
|
|
64
|
+
space_after: 0pt
|
|
65
|
+
listing:
|
|
66
|
+
italic: false
|
|
67
|
+
bold: false
|
|
68
|
+
alignment: left
|
|
69
|
+
format: "{category} {number} — {text}"
|
|
70
|
+
space_before: 0pt
|
|
71
|
+
space_after: 0pt
|
|
72
|
+
|
|
73
|
+
table:
|
|
74
|
+
repeat_header_on_break: true
|
|
75
|
+
space_before: 0pt
|
|
76
|
+
space_after: 12pt
|
|
77
|
+
|
|
78
|
+
listing:
|
|
79
|
+
syntax_highlighting: false
|
|
80
|
+
font:
|
|
81
|
+
family: Consolas
|
|
82
|
+
size: 12pt
|
|
83
|
+
line_spacing: 1.0
|
|
84
|
+
space_before: 0pt
|
|
85
|
+
space_after: 12pt
|
|
86
|
+
|
|
87
|
+
lists:
|
|
88
|
+
bullet_marker: "—"
|
|
89
|
+
indent_left: 1.25cm
|
|
90
|
+
indent_first_line: 0cm
|
|
91
|
+
|
|
92
|
+
equation:
|
|
93
|
+
numbering_alignment: right
|
|
94
|
+
parentheses: true
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
page:
|
|
2
|
+
size: A4
|
|
3
|
+
orientation: portrait
|
|
4
|
+
margins:
|
|
5
|
+
top: 20mm
|
|
6
|
+
right: 15mm
|
|
7
|
+
bottom: 20mm
|
|
8
|
+
left: 30mm
|
|
9
|
+
|
|
10
|
+
font:
|
|
11
|
+
family: Times New Roman
|
|
12
|
+
size: 14pt
|
|
13
|
+
line_spacing: 1.5
|
|
14
|
+
|
|
15
|
+
paragraph:
|
|
16
|
+
alignment: justify
|
|
17
|
+
indent_first_line: 1.25cm
|
|
18
|
+
|
|
19
|
+
headings:
|
|
20
|
+
numbering: continuous
|
|
21
|
+
leading_space_in_numbered: true
|
|
22
|
+
levels:
|
|
23
|
+
1:
|
|
24
|
+
size: 14pt
|
|
25
|
+
bold: true
|
|
26
|
+
italic: false
|
|
27
|
+
uppercase: false
|
|
28
|
+
alignment: left
|
|
29
|
+
space_before: 0pt
|
|
30
|
+
space_after: 0pt
|
|
31
|
+
page_break_before: true
|
|
32
|
+
keep_with_next: true
|
|
33
|
+
indent_first_line: 1.25cm
|
|
34
|
+
2:
|
|
35
|
+
size: 14pt
|
|
36
|
+
bold: true
|
|
37
|
+
italic: false
|
|
38
|
+
uppercase: false
|
|
39
|
+
alignment: left
|
|
40
|
+
space_before: 0pt
|
|
41
|
+
space_after: 0pt
|
|
42
|
+
page_break_before: false
|
|
43
|
+
keep_with_next: true
|
|
44
|
+
indent_first_line: 1.25cm
|
|
45
|
+
3:
|
|
46
|
+
size: 14pt
|
|
47
|
+
bold: true
|
|
48
|
+
italic: false
|
|
49
|
+
uppercase: false
|
|
50
|
+
alignment: left
|
|
51
|
+
space_before: 0pt
|
|
52
|
+
space_after: 0pt
|
|
53
|
+
page_break_before: false
|
|
54
|
+
keep_with_next: true
|
|
55
|
+
indent_first_line: 1.25cm
|
|
56
|
+
structural:
|
|
57
|
+
size: 14pt
|
|
58
|
+
bold: true
|
|
59
|
+
italic: false
|
|
60
|
+
uppercase: true
|
|
61
|
+
alignment: center
|
|
62
|
+
space_before: 0pt
|
|
63
|
+
space_after: 0pt
|
|
64
|
+
page_break_before: true
|
|
65
|
+
keep_with_next: true
|
|
66
|
+
indent_first_line: 0cm
|
|
67
|
+
structural_titles:
|
|
68
|
+
- СОДЕРЖАНИЕ
|
|
69
|
+
- ВВЕДЕНИЕ
|
|
70
|
+
- ЗАКЛЮЧЕНИЕ
|
|
71
|
+
- СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
|
72
|
+
|
|
73
|
+
captions:
|
|
74
|
+
image:
|
|
75
|
+
italic: false
|
|
76
|
+
bold: false
|
|
77
|
+
alignment: center
|
|
78
|
+
format: "{category} {number} — {text}"
|
|
79
|
+
space_before: 0pt
|
|
80
|
+
space_after: 0pt
|
|
81
|
+
line_spacing: 1.0
|
|
82
|
+
table:
|
|
83
|
+
italic: false
|
|
84
|
+
bold: false
|
|
85
|
+
alignment: left
|
|
86
|
+
format: "{category} {number} — {text}"
|
|
87
|
+
space_before: 0pt
|
|
88
|
+
space_after: 0pt
|
|
89
|
+
line_spacing: 1.0
|
|
90
|
+
listing:
|
|
91
|
+
italic: false
|
|
92
|
+
bold: false
|
|
93
|
+
alignment: left
|
|
94
|
+
format: "{category} {number} — {text}"
|
|
95
|
+
space_before: 0pt
|
|
96
|
+
space_after: 0pt
|
|
97
|
+
line_spacing: 1.0
|
|
98
|
+
continuation_break: false
|
|
99
|
+
|
|
100
|
+
table:
|
|
101
|
+
repeat_header_on_break: true
|
|
102
|
+
header_bold: true
|
|
103
|
+
space_before: 0pt
|
|
104
|
+
space_after: 0pt
|
|
105
|
+
|
|
106
|
+
listing:
|
|
107
|
+
syntax_highlighting: false
|
|
108
|
+
font:
|
|
109
|
+
family: Consolas
|
|
110
|
+
size: 14pt
|
|
111
|
+
line_spacing: 1.0
|
|
112
|
+
space_before: 0pt
|
|
113
|
+
space_after: 0pt
|
|
114
|
+
|
|
115
|
+
lists:
|
|
116
|
+
bullet_marker: "—"
|
|
117
|
+
indent_left: 1.25cm
|
|
118
|
+
indent_first_line: 0cm
|
|
119
|
+
|
|
120
|
+
equation:
|
|
121
|
+
space_before: 21pt
|
|
122
|
+
space_after: 21pt
|
|
123
|
+
numbering_alignment: right
|
|
124
|
+
parentheses: true
|