uEdition 1.3.2__py3-none-any.whl → 2.0.0a1__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.

Potentially problematic release.


This version of uEdition might be problematic. Click here for more details.

uedition/__about__.py CHANGED
@@ -2,4 +2,5 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
  """About this package."""
5
- __version__ = "1.3.2"
5
+
6
+ __version__ = "2.0.0a1"
uedition/cli/__init__.py CHANGED
@@ -7,7 +7,6 @@ from rich import print as print_cli
7
7
 
8
8
  from uedition.__about__ import __version__
9
9
  from uedition.cli import build as build_module
10
- from uedition.cli import check as check_module
11
10
  from uedition.cli import create as create_module
12
11
  from uedition.cli import language as language_module
13
12
  from uedition.cli import serve as serve_module
@@ -37,12 +36,6 @@ def serve() -> None:
37
36
  serve_module.run()
38
37
 
39
38
 
40
- @app.command()
41
- def check() -> None:
42
- """Check that the μEdition is set up correctly."""
43
- check_module.run()
44
-
45
-
46
39
  @app.command()
47
40
  def update() -> None:
48
41
  """Update the μEdition."""
uedition/cli/build.py CHANGED
@@ -2,15 +2,15 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
  """Build functionality."""
5
+
5
6
  import json
6
7
  import subprocess
7
- from copy import deepcopy
8
8
  from os import makedirs, path
9
9
  from shutil import copytree, ignore_patterns, rmtree
10
10
 
11
11
  from yaml import safe_dump, safe_load
12
12
 
13
- from uedition.settings import reload_settings, settings
13
+ from uedition.settings import NoConfigError, reload_settings, settings
14
14
 
15
15
  LANDING_PAGE_TEMPLATE = """\
16
16
  <!DOCTYPE html>
@@ -103,41 +103,50 @@ def toc_build(lang: dict) -> None:
103
103
 
104
104
 
105
105
  def config_build(lang: dict) -> None:
106
- """Build the language-specific JupyterBook config based on the main config."""
107
- config = deepcopy(settings["jb_config"])
108
- config["author"] = settings["author"]["name"]
109
- # Set the language-specific title
106
+ """Build the language-specific Sphinx config based on the main config."""
107
+ with open("toc.yml") as in_f:
108
+ toc = safe_load(in_f)
109
+ # Build the default configuration
110
+ config = {
111
+ "needs_sphinx": "8",
112
+ "language": lang["code"],
113
+ "root_doc": toc["root"],
114
+ "html_theme": "sphinx_book_theme",
115
+ "html_theme_options": {},
116
+ "extensions": ["myst_parser", "sphinx_external_toc", "uedition"],
117
+ "myst_enable_extensions": [
118
+ "amsmath",
119
+ "attrs_inline",
120
+ "colon_fence",
121
+ "deflist",
122
+ "dollarmath",
123
+ "fieldlist",
124
+ "html_admonition",
125
+ "html_image",
126
+ "replacements",
127
+ "smartquotes",
128
+ "strikethrough",
129
+ "substitution",
130
+ "tasklist",
131
+ ],
132
+ }
133
+ # Load in any sphinx configuration
134
+ config.update(settings["sphinx_config"])
135
+ # Set settings-based
110
136
  if lang["code"] in settings["title"]:
111
- config["title"] = settings["title"][lang["code"]]
137
+ config["project"] = settings["title"][lang["code"]]
112
138
  elif len(settings["languages"]) > 0 and settings["languages"][0]["code"] in settings["title"]:
113
- config["title"] = settings["title"][settings["languages"][0]["code"]]
139
+ config["project"] = settings["title"][settings["languages"][0]["code"]]
114
140
  else:
115
- config["title"] = f"Missing title for {lang['label']}"
116
- # Set the repository information
117
- if settings["repository"]["url"] and "repository" not in config:
118
- config["repository"] = {
119
- "url": settings["repository"]["url"],
120
- "path": lang["path"],
121
- "branch": settings["repository"]["branch"],
122
- }
123
- if "html" not in config:
124
- config["html"] = {}
125
- if "use_repository_button" not in config["html"]:
126
- config["html"]["use_repository_button"] = True
127
- # Set the Sphinx language
128
- if "sphinx" not in config:
129
- config["sphinx"] = {}
130
- if "config" not in config["sphinx"]:
131
- config["sphinx"]["config"] = {}
132
- config["sphinx"]["config"]["language"] = lang["code"]
133
- # Add the uEdition extension
134
- if "extra_extensions" not in config["sphinx"]:
135
- config["sphinx"]["extra_extensions"] = []
136
- if "uedition" not in config["sphinx"]["extra_extensions"]:
137
- config["sphinx"]["extra_extensions"].append("uedition")
138
-
139
- with open(path.join(lang["path"], "_config.yml"), "w") as out_f:
140
- safe_dump(config, out_f, encoding="utf-8")
141
+ config["project"] = f"Missing title for {lang['label']}"
142
+ config["author"] = settings["author"]["name"]
143
+ if settings["repository"]["url"]:
144
+ config["html_theme_options"]["repository_url"] = f"{settings['repository']['url']}"
145
+ config["html_theme_options"]["use_repository_button"] = True
146
+
147
+ with open(path.join(lang["path"], "conf.py"), "w") as out_f:
148
+ for name, value in config.items():
149
+ out_f.write(f"{name} = {value!a}\n")
141
150
 
142
151
 
143
152
  def static_build(lang: dict) -> None:
@@ -153,41 +162,39 @@ def full_build(lang: dict) -> None:
153
162
  toc_build(lang)
154
163
  config_build(lang)
155
164
  static_build(lang)
156
- subprocess.run(
157
- [ # noqa: S603, S607
158
- "jupyter-book",
159
- "build",
160
- "--all",
161
- "--path-output",
162
- path.join("_build", lang["path"]),
165
+ subprocess.run( # noqa:S603
166
+ [ # noqa: S607
167
+ "sphinx-build",
168
+ "--builder",
169
+ "html",
170
+ "--fresh-env",
163
171
  lang["path"],
172
+ path.join("_build", lang["path"], "html"),
164
173
  ],
165
174
  check=False,
175
+ shell=False,
166
176
  )
167
177
  if settings["output"]["tei"]:
168
- subprocess.run(
169
- [ # noqa: S603, S607
170
- "jupyter-book",
171
- "build",
172
- "--all",
173
- "--path-output",
174
- path.join("_build", lang["path"]),
178
+ subprocess.run( # noqa: S603
179
+ [ # noqa:S607
180
+ "sphinx-build",
175
181
  "--builder",
176
- "custom",
177
- "--custom-builder",
178
182
  "tei",
183
+ "--fresh-env",
179
184
  lang["path"],
185
+ path.join("_build", lang["path"], "tei"),
180
186
  ],
181
187
  check=False,
188
+ shell=False,
182
189
  )
183
190
  copytree(
184
- path.join("_build", lang["path"], "_build", "html"),
191
+ path.join("_build", lang["path"], "html"),
185
192
  path.join(settings["output"]["path"], lang["path"]),
186
193
  dirs_exist_ok=True,
187
194
  )
188
195
  if settings["output"]["tei"]:
189
196
  copytree(
190
- path.join("_build", lang["path"], "_build", "tei"),
197
+ path.join("_build", lang["path"], "tei"),
191
198
  path.join(settings["output"]["path"], lang["path"]),
192
199
  ignore=ignore_patterns("_sphinx_design_static"),
193
200
  dirs_exist_ok=True,
@@ -197,39 +204,37 @@ def full_build(lang: dict) -> None:
197
204
  def partial_build(lang: dict) -> None:
198
205
  """Run the as-needed build process for a single language."""
199
206
  landing_build()
200
- subprocess.run(
201
- [ # noqa: S603, S607
202
- "jupyter-book",
203
- "build",
204
- "--path-output",
205
- path.join("_build", lang["path"]),
207
+ subprocess.run( # noqa: S603
208
+ [ # noqa: S607
209
+ "sphinx-build",
210
+ "--builder",
211
+ "html",
206
212
  lang["path"],
213
+ path.join("_build", lang["path"], "html"),
207
214
  ],
208
215
  check=False,
216
+ shell=False,
209
217
  )
210
218
  if settings["output"]["tei"]:
211
- subprocess.run(
212
- [ # noqa: S603, S607
213
- "jupyter-book",
214
- "build",
215
- "--path-output",
216
- path.join("_build", lang["path"]),
219
+ subprocess.run( # noqa:S603
220
+ [ # noqa: S607
221
+ "sphinx-build",
217
222
  "--builder",
218
- "custom",
219
- "--custom-builder",
220
223
  "tei",
221
224
  lang["path"],
225
+ path.join("_build", lang["path"], "tei"),
222
226
  ],
223
227
  check=False,
228
+ shell=False,
224
229
  )
225
230
  copytree(
226
- path.join("_build", lang["path"], "_build", "html"),
231
+ path.join("_build", lang["path"], "html"),
227
232
  path.join(settings["output"]["path"], lang["path"]),
228
233
  dirs_exist_ok=True,
229
234
  )
230
235
  if settings["output"]["tei"]:
231
236
  copytree(
232
- path.join("_build", lang["path"], "_build", "tei"),
237
+ path.join("_build", lang["path"], "tei"),
233
238
  path.join(settings["output"]["path"], lang["path"]),
234
239
  ignore=ignore_patterns("_sphinx_design_static"),
235
240
  dirs_exist_ok=True,
@@ -238,6 +243,8 @@ def partial_build(lang: dict) -> None:
238
243
 
239
244
  def run() -> None:
240
245
  """Build the full uEdition."""
246
+ if not path.exists("uEdition.yml") and not path.exists("uEdition.yaml"):
247
+ raise NoConfigError()
241
248
  if path.exists(settings["output"]["path"]):
242
249
  rmtree(settings["output"]["path"])
243
250
  for lang in settings["languages"]:
uedition/cli/language.py CHANGED
@@ -7,9 +7,13 @@ import os
7
7
  from copier import run_copy, run_update
8
8
  from yaml import dump, safe_load
9
9
 
10
+ from uedition.settings import NoConfigError
11
+
10
12
 
11
13
  def add(path: str) -> None:
12
14
  """Add a language to the μEdition using Copier."""
15
+ if not os.path.exists("uEdition.yml") and not os.path.exists("uEdition.yaml"):
16
+ raise NoConfigError()
13
17
  run_copy("gh:uEdition/uEdition-language-template", path, data={"path": path})
14
18
  with open(os.path.join(path, ".uEdition.answers")) as in_f:
15
19
  answers = safe_load(in_f)
@@ -32,6 +36,8 @@ def add(path: str) -> None:
32
36
 
33
37
  def update(path: str) -> None:
34
38
  """Update a language to the latest template."""
39
+ if not os.path.exists("uEdition.yml") and not os.path.exists("uEdition.yaml"):
40
+ raise NoConfigError()
35
41
  run_update(path, answers_file=".uEdition.answers", overwrite=True, data={"path": path})
36
42
  with open(os.path.join(path, ".uEdition.answers")) as in_f:
37
43
  answers = safe_load(in_f)
uedition/cli/serve.py CHANGED
@@ -8,7 +8,7 @@ from typing import Callable
8
8
  from livereload import Server
9
9
 
10
10
  from uedition.cli.build import full_build, partial_build
11
- from uedition.settings import settings
11
+ from uedition.settings import NoConfigError, settings
12
12
 
13
13
 
14
14
  def build_cmd(lang: dict, full: bool = True) -> Callable[[], None]: # noqa: FBT001, FBT002
@@ -29,6 +29,8 @@ def build_cmd(lang: dict, full: bool = True) -> Callable[[], None]: # noqa: FBT
29
29
 
30
30
  def run() -> None:
31
31
  """Run the development server."""
32
+ if not path.exists("uEdition.yml") and not path.exists("uEdition.yaml"):
33
+ raise NoConfigError()
32
34
  full_rebuilds = [build_cmd(lang, full=True) for lang in settings["languages"]]
33
35
  partial_rebuilds = [build_cmd(lang, full=False) for lang in settings["languages"]]
34
36
  for cmd in full_rebuilds:
uedition/cli/update.py CHANGED
@@ -2,9 +2,15 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
  """The μEdition check functionality for validating a μEdition and its files."""
5
+ from os import path
6
+
5
7
  from copier import run_update
6
8
 
9
+ from uedition.settings import NoConfigError
10
+
7
11
 
8
12
  def run() -> None:
9
13
  """Update the μEdition using Copier."""
14
+ if not path.exists("uEdition.yml") and not path.exists("uEdition.yaml"):
15
+ raise NoConfigError()
10
16
  run_update(".", answers_file=".uEdition.answers", overwrite=True, skip_answered=True)
uedition/ext/config.py CHANGED
@@ -1,197 +1,31 @@
1
1
  # SPDX-FileCopyrightText: 2023-present Mark Hall <mark.hall@work.room3b.eu>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- """uEdition configuration handling.
4
+ """
5
+ uEdition configuration handling.
5
6
 
6
7
  This module handles reading the uEdition-specific configuration settings, validating them and
7
8
  adding any required default values.
8
9
  """
9
- from typing import Annotated, Any, Literal, Optional, Union
10
10
 
11
- from pydantic import BaseModel, ValidationError
12
- from pydantic.functional_validators import BeforeValidator
13
11
  from sphinx.application import Sphinx
12
+ from sphinx.config import Config
14
13
  from sphinx.util import logging
15
14
 
16
- logger = logging.getLogger(__name__)
17
-
18
-
19
- class RuleSelectorAttribute(BaseModel):
20
- """Validation rule for selecting based on an attribute with a given value."""
21
-
22
- attr: str
23
- value: str
24
-
25
-
26
- def expand_tei_namespace(value: str) -> str:
27
- """Expand any ```tei:``` namespace prefixes."""
28
- return value.replace("tei:", "{http://www.tei-c.org/ns/1.0}")
29
-
30
-
31
- def force_list(value: Any) -> list: # noqa: ANN401
32
- """Force the value into a list form."""
33
- if isinstance(value, list):
34
- return value
35
- return [value]
36
-
37
-
38
- class RuleSelector(BaseModel):
39
- """Validation rule for the selector for matching a TEI tag."""
40
-
41
- tag: Annotated[str, BeforeValidator(expand_tei_namespace)]
42
- attributes: Annotated[list[RuleSelectorAttribute], BeforeValidator(force_list)] = []
43
-
44
-
45
- class RuleText(BaseModel):
46
- """Validation rule for retrieving the text content from an attribute."""
47
-
48
- action: Literal["from-attribute"]
49
- attr: str
50
-
51
-
52
- class RuleAttributeCopy(BaseModel):
53
- """Validation rule for copying and attribute."""
54
-
55
- action: Literal["copy"] = "copy"
56
- attr: str
57
- source: str
58
-
59
-
60
- class RuleAttributeSet(BaseModel):
61
- """Validation rule for settings an attribute to a specific value."""
62
-
63
- action: Literal["set"]
64
- attr: str
65
- value: str
66
-
67
-
68
- class RuleAttributeDelete(BaseModel):
69
- """Validation rule for deleting an attribute."""
70
-
71
- action: Literal["delete"]
72
- attr: str
73
-
74
-
75
- def convert_string_to_selector_dict(value: str | dict) -> dict:
76
- """Convert a simple string selector into the dictionary representation."""
77
- if isinstance(value, str):
78
- return {"tag": value}
79
- return value
15
+ from uedition.ext.settings import TEISettings
80
16
 
81
-
82
- class Rule(BaseModel):
83
- """Validation model for a rule transforming a TEI tag into a HTML tag."""
84
-
85
- selector: Annotated[RuleSelector, BeforeValidator(convert_string_to_selector_dict)]
86
- tag: Union[str, None] = "div"
87
- text: Union[RuleText, None] = None
88
- attributes: list[Union[RuleAttributeCopy, RuleAttributeSet, RuleAttributeDelete]] = []
89
-
90
-
91
- class TextSection(BaseModel):
92
- """Validation model for a TEI text section."""
93
-
94
- title: str
95
- type: Literal["text"] = "text"
96
- content: str
97
- sort: str | None = None
98
- mappings: list[Rule] = []
99
-
100
-
101
- class SingleFieldRule(BaseModel):
102
- """Validation model for a TEI field rule with a single value."""
103
-
104
- title: str
105
- type: Literal["single"] = "single"
106
- content: str
107
-
108
-
109
- class ListFieldRule(BaseModel):
110
- """Validation model for a TEI field rule with a list of values."""
111
-
112
- title: str
113
- type: Literal["list"]
114
- content: str
115
-
116
-
117
- class DownloadFieldRule(BaseModel):
118
- """Validation model for a TEI field rule that creates a download link."""
119
-
120
- title: str
121
- type: Literal["download"]
122
- content: str
123
- target: str
124
-
125
-
126
- class FieldsSection(BaseModel):
127
- """Validation model for a TEI fields section."""
128
-
129
- title: str
130
- type: Literal["fields"]
131
- fields: list[SingleFieldRule | ListFieldRule | DownloadFieldRule]
132
-
133
-
134
- class TEIConfig(BaseModel):
135
- """Validation model for the TEI-specific settings."""
136
-
137
- text_only_in_leaf_nodes: bool = False
138
- mappings: list[Rule] = []
139
- sections: list[TextSection | FieldsSection] = []
140
-
141
-
142
- class Config(BaseModel):
143
- """Configuration validation model."""
144
-
145
- tei: Optional[TEIConfig]
146
-
147
-
148
- BASE_RULES = [
149
- {"selector": "tei:body", "tag": "section"},
150
- {"selector": "tei:head", "tag": "h1"},
151
- {"selector": "tei:p", "tag": "p"},
152
- {"selector": "tei:seg", "tag": "span"},
153
- {"selector": "tei:pb", "tag": "span"},
154
- {"selector": "tei:hi", "tag": "span"},
155
- {
156
- "selector": "tei:ref",
157
- "tag": "a",
158
- "attributes": [{"attr": "href", "source": "target"}],
159
- },
160
- {"selector": "tei:citedRange", "tag": "span"},
161
- {"selector": "tei:q", "tag": "span"},
162
- {"selector": "tei:hi", "tag": "span"},
163
- {"selector": "tei:foreign", "tag": "span"},
164
- {"selector": "tei:speaker", "tag": "span"},
165
- {"selector": "tei:stage", "tag": "span"},
166
- {"selector": "tei:lem", "tag": "span"},
167
- {"selector": "tei:sic", "tag": "span"},
168
- ]
169
- """Base mapping rules for mapping TEI tags to default HTML elements."""
17
+ logger = logging.getLogger(__name__)
170
18
 
171
19
 
172
20
  def validate_config(app: Sphinx, config: Config) -> None: # noqa: ARG001
173
21
  """Validate the configuration and add any default values."""
174
- if config.uEdition:
175
- if "tei" in config.uEdition:
176
- if "sections" in config.uEdition["tei"] and isinstance(config.uEdition["tei"]["sections"], list):
177
- if "mappings" in config.uEdition["tei"] and isinstance(config.uEdition["tei"]["mappings"], list):
178
- for section in config.uEdition["tei"]["sections"]:
179
- if "mappings" in section and isinstance(section["mappings"], list):
180
- section["mappings"] = section["mappings"] + config.uEdition["tei"]["mappings"] + BASE_RULES
181
- else:
182
- section["mappings"] = config.uEdition["tei"]["mappings"] + BASE_RULES
183
- try:
184
- config.uEdition = Config(**config.uEdition).dict()
185
- except ValidationError as e:
186
- for error in e.errors():
187
- logger.error(" -> ".join([str(loc) for loc in error["loc"]]))
188
- logger.error(f' {error["msg"]}')
189
- config.uEdition = {}
22
+ if config.tei:
23
+ config.tei = TEISettings(**config.tei).model_dump()
190
24
  else:
191
- config.uEdition = {}
25
+ config.tei = {"blocks": [], "marks": "", "sections": []}
192
26
 
193
27
 
194
28
  def setup(app: Sphinx) -> None:
195
29
  """Set up the Sphinx configuration handling for the uEdition."""
196
- app.add_config_value("uEdition", default=None, rebuild="html", types=[dict])
30
+ app.add_config_value("tei", default=None, rebuild="html", types=[dict])
197
31
  app.connect("config-inited", validate_config)
@@ -0,0 +1,131 @@
1
+ # SPDX-FileCopyrightText: 2023-present Mark Hall <mark.hall@work.room3b.eu>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """Additional settings validation."""
5
+
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel
9
+
10
+
11
+ class ValueTitlePair(BaseModel):
12
+ """A simple pair of value and title."""
13
+
14
+ value: str
15
+ """The value for the pair."""
16
+ title: str
17
+ """The title to show for the pair."""
18
+
19
+
20
+ class TEINodeAttribute(BaseModel):
21
+ """Single attribute for a TEINode."""
22
+
23
+ name: str
24
+ """The name of the attribute."""
25
+ value: str | None = None
26
+ """A fixed value to use for the attribute."""
27
+ type: Literal["string"] | Literal["static"] | Literal["id-ref"] | Literal["text"] | Literal["html-attribute"] = (
28
+ "string"
29
+ )
30
+ """The type of attribute this is."""
31
+ default: str = ""
32
+ """The default value to use if none is set."""
33
+ target: str | None = None
34
+ """The target HTML attribute."""
35
+
36
+
37
+ class TEINode(BaseModel):
38
+ """A single node in a TEI document."""
39
+
40
+ name: str
41
+ """The name to use to address this node."""
42
+ selector: str
43
+ """The selector to identify this node."""
44
+ attributes: list[TEINodeAttribute] = []
45
+ """A list of attributes that are used on this node."""
46
+ tag: str | None = None
47
+ """The HTML tag to use to render the node."""
48
+ text: str | None = None
49
+ """Where to get the text from."""
50
+ content: str | None = None
51
+ """Allowed child nodes. Only relevant for block nodes."""
52
+
53
+
54
+ class TEIMetadataSectionSingleFieldRule(BaseModel):
55
+ """Validation model for a TEI field rule with a single value."""
56
+
57
+ title: str
58
+ type: Literal["single"] = "single"
59
+ selector: str
60
+
61
+
62
+ class TEIMetadataSectionListFieldRule(BaseModel):
63
+ """Validation model for a TEI field rule with a list of values."""
64
+
65
+ title: str
66
+ type: Literal["list"]
67
+ selector: str
68
+
69
+
70
+ class TEIMetadataSectionDownloadFieldRule(BaseModel):
71
+ """Validation model for a TEI field rule that creates a download link."""
72
+
73
+ title: str
74
+ type: Literal["download"]
75
+ selector: str
76
+ target: str
77
+
78
+
79
+ class TEIMetadataSection(BaseModel):
80
+ """A metadata section in the TEI document."""
81
+
82
+ name: str
83
+ """The name of the section."""
84
+ title: str | None = None
85
+ """The title to show in the UI."""
86
+ type: Literal["metadata"]
87
+ """The type must be set to metadata."""
88
+ selector: str
89
+ """The XPath selector to retrieve this section."""
90
+ fields: list[
91
+ TEIMetadataSectionSingleFieldRule | TEIMetadataSectionListFieldRule | TEIMetadataSectionDownloadFieldRule
92
+ ]
93
+ """Fields to display."""
94
+
95
+
96
+ class TEITextSection(BaseModel):
97
+ """A section in the TEI document containing a single text."""
98
+
99
+ name: str
100
+ """The name of the section."""
101
+ title: str | None = None
102
+ """The title to show in the UI."""
103
+ type: Literal["text"]
104
+ """The type must be set to text."""
105
+ selector: str
106
+ """The XPath selector to retrieve this section."""
107
+
108
+
109
+ class TEITextListSection(BaseModel):
110
+ """A section in the TEI document containing multiple texts."""
111
+
112
+ name: str
113
+ """The name of the section."""
114
+ title: str | None = None
115
+ """The title to show in the UI."""
116
+ type: Literal["textlist"]
117
+ """The type must be set to textlist."""
118
+ selector: str
119
+ """The XPath selector to retrieve the texts in this section."""
120
+ sort: str | None = None
121
+
122
+
123
+ class TEISettings(BaseModel):
124
+ """Settings for the TEI processing."""
125
+
126
+ blocks: list[TEINode] = []
127
+ """List of blocks supported in the TEI document."""
128
+ marks: list[TEINode] = []
129
+ """List of inline marks supported in the TEI document."""
130
+ sections: list[TEIMetadataSection | TEITextSection | TEITextListSection] = []
131
+ """List of sections within the TEI document."""
@@ -6,7 +6,6 @@ import xml.sax.saxutils
6
6
  from collections.abc import Iterator
7
7
 
8
8
  import sphinx
9
- import sphinx_jupyterbook_latex
10
9
  from docutils import nodes
11
10
  from docutils.io import StringOutput
12
11
  from sphinx.application import Sphinx
@@ -37,6 +36,7 @@ MAPPINGS = [
37
36
  "attrs": [{"target": "type", "value": "literal-block"}, {"source": "language", "target": "subtype"}],
38
37
  },
39
38
  {"cls": nodes.compound, "tagname": "div", "type": "block"},
39
+ {"cls": nodes.admonition, "tagname": "div", "type": "block"},
40
40
  {"cls": sphinx.addnodes.toctree},
41
41
  {
42
42
  "cls": nodes.footnote,
@@ -47,7 +47,6 @@ MAPPINGS = [
47
47
  {"target": "target", "source": "backrefs", "format": "#{value}", "join": " "},
48
48
  ],
49
49
  },
50
- {"cls": sphinx_jupyterbook_latex.nodes.HiddenCellNode},
51
50
  {
52
51
  "cls": nodes.transition,
53
52
  "tagname": "div",
@@ -204,7 +203,7 @@ class TEITranslator(nodes.GenericNodeVisitor):
204
203
  else:
205
204
  self.output.append(text)
206
205
  else:
207
- self.output.append(f"{self.indent*self.level}<tei:span>{text}</tei:span>\n")
206
+ self.output.append(f"{self.indent * self.level}<tei:span>{text}</tei:span>\n")
208
207
 
209
208
  def depart_Text(self, node: nodes.TextElement) -> None: # noqa: N802
210
209
  """Unused."""
@@ -1,7 +1,9 @@
1
+ # noqa: A005
1
2
  # SPDX-FileCopyrightText: 2023-present Mark Hall <mark.hall@work.room3b.eu>
2
3
  #
3
4
  # SPDX-License-Identifier: MIT
4
5
  """TEI parsing extension for Sphinx."""
6
+
5
7
  import re
6
8
  from typing import Callable
7
9
 
@@ -10,8 +12,10 @@ from lxml import etree
10
12
  from sphinx import addnodes
11
13
  from sphinx.application import Sphinx
12
14
  from sphinx.parsers import Parser as SphinxParser
15
+ from sphinx.util import logging
13
16
  from sphinx.writers.html import HTMLWriter
14
17
 
18
+ logger = logging.getLogger(__name__)
15
19
  namespaces = {"tei": "http://www.tei-c.org/ns/1.0", "uedition": "https://uedition.readthedocs.org"}
16
20
 
17
21
 
@@ -33,18 +37,18 @@ class TeiElement(nodes.Element):
33
37
  def tei_element_html_enter(self: "HTMLWriter", node: TeiElement) -> None:
34
38
  """Visit a TeiElement and generate the correct HTML."""
35
39
  if node.get("html_tag") is not None:
36
- buffer = [f'<{node.get("html_tag")} data-tei-tag="{node.get("tei_tag")[29:]}"']
40
+ buffer = [f"<{node.get('html_tag')}"]
37
41
  if node.get("ids"):
38
42
  buffer.append(f' id="{node.get("ids")[0]}"')
39
43
  for key, value in node.get("tei_attributes").items():
40
44
  buffer.append(f' {key}="{value}"')
41
- self.body.append(f'{"".join(buffer)}>')
45
+ self.body.append(f"{''.join(buffer)}>")
42
46
 
43
47
 
44
48
  def tei_element_html_exit(self: "HTMLWriter", node: TeiElement) -> None:
45
49
  """Close the HTML tag."""
46
50
  if node.get("html_tag") is not None:
47
- self.body.append(f'</{node.get("html_tag")}>')
51
+ self.body.append(f"</{node.get('html_tag')}>")
48
52
 
49
53
 
50
54
  class TEIParser(SphinxParser):
@@ -54,7 +58,8 @@ class TEIParser(SphinxParser):
54
58
  """Specify that only .tei files are parsed"""
55
59
 
56
60
  def parse(self: "TEIParser", inputstring: str, document: nodes.document) -> None:
57
- """Parse source TEI text.
61
+ """
62
+ Parse source TEI text.
58
63
 
59
64
  This function creates the basic structure and then the :func:`~uEdition.extensions.tei.TEIParser._walk_tree`
60
65
  function is used to actually process the XML.
@@ -75,35 +80,49 @@ class TEIParser(SphinxParser):
75
80
  doc_title = nodes.title()
76
81
  doc_title.append(nodes.Text(title if title else "[Untitled]"))
77
82
  doc_section.append(doc_title)
78
- if "tei" in self.config.uEdition and "sections" in self.config.uEdition["tei"]:
79
- for conf_section in self.config.uEdition["tei"]["sections"]:
80
- section = nodes.section(ids=[nodes.make_id(conf_section["title"])])
83
+ for conf_section in self.config.tei["sections"]:
84
+ section = nodes.section(ids=[nodes.make_id(conf_section["name"])])
85
+ if conf_section["title"]:
81
86
  section_title = nodes.title()
82
87
  section_title.append(nodes.Text(conf_section["title"]))
83
88
  section.append(section_title)
84
- if conf_section["type"] == "text":
85
- # Process a text section
86
- source = root.xpath(conf_section["content"], namespaces=namespaces)
87
- if len(source) > 0:
88
- if conf_section["sort"]:
89
- source.sort(key=self._sort_key(conf_section["sort"]))
90
- doc_section.append(section)
91
- tmp = nodes.section()
89
+ if conf_section["type"] == "text":
90
+ # Process a text section
91
+ sources = root.xpath(conf_section["selector"], namespaces=namespaces)
92
+ if len(sources) > 0:
93
+ doc_section.append(section)
94
+ tmp = nodes.section()
95
+ for source in sources:
92
96
  for child in source:
93
- self._walk_tree(child, tmp, conf_section["mappings"])
94
- self._wrap_sections(section, tmp)
95
- elif conf_section["type"] == "fields":
96
- # Process a field or metadata section
97
+ self._walk_tree(child, tmp)
98
+ self._wrap_sections(section, tmp)
99
+ elif conf_section["type"] == "textlist":
100
+ # Process a text section
101
+ sources = root.xpath(conf_section["selector"], namespaces=namespaces)
102
+ if len(sources) > 0:
103
+ if conf_section["sort"]:
104
+ source.sort(key=self._sort_key(conf_section["sort"]))
105
+ doc_section.append(section)
106
+ for source in sources:
107
+ tmp = nodes.section(ids=[source.attrib["id"]])
108
+ for child in source:
109
+ self._walk_tree(child, tmp)
110
+ section.append(tmp)
111
+ # self._wrap_sections(section, tmp)
112
+ elif conf_section["type"] == "metadata":
113
+ # Process a field or metadata section
114
+ sources = root.xpath(conf_section["selector"], namespaces=namespaces)
115
+ if len(sources) > 0:
97
116
  doc_section.append(section)
98
117
  fields = nodes.definition_list()
99
118
  section.append(fields)
100
119
  for field in conf_section["fields"]:
101
120
  if field["type"] == "single":
102
- self._parse_single_field(fields, field, root)
121
+ self._parse_single_field(fields, field, sources[0])
103
122
  elif field["type"] == "list":
104
- self._parse_list_field(fields, field, root)
123
+ self._parse_list_field(fields, field, sources[0])
105
124
  elif field["type"] == "download":
106
- self._parse_download_field(fields, field, root)
125
+ self._parse_download_field(fields, field, sources[0])
107
126
  document.append(doc_section)
108
127
 
109
128
  def _sort_key(self: "TEIParser", xpath: str) -> Callable[[etree.Element], tuple[tuple[int, ...], ...]]:
@@ -130,70 +149,53 @@ class TEIParser(SphinxParser):
130
149
 
131
150
  return sorter
132
151
 
133
- def _walk_tree(self: "TEIParser", node: etree.Element, parent: nodes.Element, rules: list) -> None:
134
- """Walk the XML tree and create the appropriate AST nodes.
152
+ def _walk_tree(self: "TEIParser", node: etree.Element, parent: nodes.Element) -> None:
153
+ """Walk the XML tree and create the appropriate AST nodes."""
154
+ for conf in self.config.tei["blocks"]:
155
+ if len(node.xpath(f"self::{conf['selector']}", namespaces=namespaces)) > 0:
156
+ attrs = self._parse_attributes(node, conf["attributes"])
157
+ attrs.update({f"data-tei-block-{conf['name']}": ""})
158
+ element = TeiElement(
159
+ html_tag=conf["tag"] if conf["tag"] else "div", tei_tag=node.tag, tei_attributes=attrs
160
+ )
161
+ for child in node:
162
+ self._walk_tree(child, element)
163
+ parent.append(element)
164
+ return
165
+ for conf in self.config.tei["marks"]:
166
+ if len(node.xpath(f"self::{conf['selector']}", namespaces=namespaces)) > 0:
167
+ attrs = self._parse_attributes(node, conf["attributes"])
168
+ attrs.update({f"data-tei-mark-{conf['name']}": ""})
169
+ element = TeiElement(
170
+ html_tag=conf["tag"] if conf["tag"] else "span", tei_tag=node.tag, tei_attributes=attrs
171
+ )
172
+ if len(node) == 0 and node.text:
173
+ element.append(nodes.Text(node.text))
174
+ else:
175
+ for child in node:
176
+ self._walk_tree(child, element)
177
+ parent.append(element)
178
+ return
179
+ if len(node) == 0:
180
+ parent.append(nodes.Text(node.text))
181
+ else:
182
+ logger.warning(f"No block or mark configured for {node.tag}")
135
183
 
136
- Uses the mapping rules defined in :mod:`~uEdition.extensions.config` to determine what to
137
- map the XML to.
138
- """
139
- is_leaf = len(node) == 0
140
- text_only_in_leaf_nodes = (
141
- self.config.uEdition["tei"]["text_only_in_leaf_nodes"] if "tei" in self.config.uEdition else False
142
- )
143
- attributes = {}
144
- # Get the first matching rule for the current node
145
- rule = self._rule_for_node(node, rules)
146
- # Loop over the XML node attributes and apply any attribute transforms defined in the matching rule
147
- for key, value in node.attrib.items():
148
- # Always strip the namespace from the `id` attribute
149
- if key == "{http://www.w3.org/XML/1998/namespace}id":
150
- key = "id" # noqa: PLW2901
151
- if rule and "attributes" in rule:
152
- processed = False
153
- for attr_rule in rule["attributes"]:
154
- if attr_rule["action"] == "copy":
155
- if key == attr_rule["source"]:
156
- # Copied attributes are added without a `data-` prefix
157
- attributes[attr_rule["attr"]] = value
158
- elif attr_rule["action"] == "delete":
159
- if key == attr_rule["attr"]:
160
- processed = True
161
- elif attr_rule["action"] == "set":
162
- if key == attr_rule["attr"]:
163
- value = attr_rule["value"] # noqa: PLW2901
164
- # if the attribute did not match any attribute transform
165
- if not processed:
166
- # The id attribute is always output as is, all other attributes are prefixed with `data-`
167
- if key == "id":
168
- attributes["id"] = value
169
- else:
170
- attributes[f"data-{key}"] = value
171
- else: # noqa: PLR5501
172
- # The id attribute is always output as is, all other attributes are prefixed with `data-`
173
- if key == "id":
174
- attributes["id"] = value
184
+ def _parse_attributes(self, node: etree.Element, attribute_configs: list) -> dict:
185
+ attrs = {}
186
+ for conf in attribute_configs:
187
+ if conf["name"] in node.attrib:
188
+ if conf["type"] == "id-ref" and node.attrib[conf["name"]].startswith("#"):
189
+ attrs[f"data-tei-attribute-{conf['name']}"] = node.attrib[conf["name"]][1:]
190
+ elif conf["type"] == "static":
191
+ attrs[f"data-tei-attribute-{conf['name']}"] = conf["value"]
192
+ elif conf["type"] == "html-attribute":
193
+ attrs[conf["target"]] = node.attrib[conf["name"]]
175
194
  else:
176
- attributes[f"data-{key}"] = value
177
- # Create the docutils AST element
178
- new_element = TeiElement(
179
- html_tag=rule["tag"] if rule is not None and "tag" in rule else "div",
180
- tei_tag=node.tag,
181
- tei_attributes=attributes,
182
- )
183
- parent.append(new_element)
184
- if rule is not None and "text" in rule and rule["text"]:
185
- # If there is a `text` key in the rule, use that to set the text
186
- if rule["text"]["action"] == "from-attribute" and rule["text"]["attr"] in node.attrib:
187
- new_element.append(nodes.Text(node.attrib[rule["text"]["attr"]]))
188
- elif node.text and (is_leaf or not text_only_in_leaf_nodes):
189
- # Only create text content if there is text and we either are in a leaf node or are adding all text
190
- new_element.append(nodes.Text(node.text))
191
- # Process any children
192
- for child in node:
193
- self._walk_tree(child, new_element, rules)
194
- # If there is text after this XML node and we are adding all text, then add text content to the parent
195
- if node.tail and not text_only_in_leaf_nodes:
196
- parent.append(nodes.Text(node.tail))
195
+ attrs[f"data-tei-attribute-{conf['name']}"] = node.attrib[conf["name"]]
196
+ elif conf["default"]:
197
+ attrs[f"data-tei-attribute-{conf['name']}"] = conf["default"]
198
+ return attrs
197
199
 
198
200
  def _wrap_sections(self: "TEIParser", section: nodes.Element, tmp: nodes.Element) -> None:
199
201
  """Ensure that sections are correctly wrapped."""
@@ -211,7 +213,7 @@ class TEIParser(SphinxParser):
211
213
  while section_level <= section_stack[-1][0]:
212
214
  section_stack.pop()
213
215
  new_section = nodes.section(ids=[nodes.make_id(node.astext())])
214
- title = nodes.title()
216
+ title = nodes.title(attributes={"data-test": ""})
215
217
  title.children = node.children
216
218
  new_section.append(title)
217
219
  section_stack[-1][1].append(new_section)
@@ -237,25 +239,9 @@ class TEIParser(SphinxParser):
237
239
  if not in_heading:
238
240
  section_stack[-1][1].append(node)
239
241
 
240
- def _rule_for_node(self: "TEIParser", node: etree.Element, rules: list[dict]) -> dict:
241
- """Determine the first matching mapping rule for the node from the configured rules."""
242
- tei_tag = node.tag
243
- for rule in rules:
244
- if rule["selector"]["tag"] == tei_tag:
245
- if "attributes" in rule["selector"]:
246
- attr_match = True
247
- for attr_rule in rule["selector"]["attributes"]:
248
- if attr_rule["attr"] not in node.attrib or node.attrib[attr_rule["attr"]] != attr_rule["value"]:
249
- attr_match = False
250
- break
251
- if not attr_match:
252
- continue
253
- return rule
254
- return None
255
-
256
242
  def _parse_single_field(self: "TEIParser", parent: etree.Element, field: dict, root: etree.Element) -> None:
257
243
  """Parse a single metadata field."""
258
- content = root.xpath(field["content"], namespaces=namespaces)
244
+ content = root.xpath(field["selector"], namespaces=namespaces)
259
245
  if len(content) > 0:
260
246
  if isinstance(content, list):
261
247
  content = content[0]
uedition/settings.py CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  All application settings are accessed via the `settings` dictionary.
7
7
  """
8
+
8
9
  import os
9
10
  from typing import Annotated, Any, Dict, Tuple, Type
10
11
 
@@ -15,11 +16,21 @@ from pydantic_settings import BaseSettings, PydanticBaseSettingsSource
15
16
  from yaml import safe_load
16
17
 
17
18
 
19
+ class NoConfigError(Exception):
20
+ """Exception to signal that no configuration file was found in the current directory."""
21
+
22
+ def __init__(self) -> None:
23
+ """Initialise the Exception with the default error message."""
24
+ super().__init__("No uEdition.yml or uEdition.yaml was found in the current directory")
25
+
26
+
18
27
  class YAMLConfigSettingsSource(PydanticBaseSettingsSource):
19
28
  """Loads the configuration settings from a YAML file."""
20
29
 
21
30
  def get_field_value(
22
- self: "YAMLConfigSettingsSource", field: FieldInfo, field_name: str # noqa: ARG002
31
+ self: "YAMLConfigSettingsSource",
32
+ field: FieldInfo, # noqa: ARG002
33
+ field_name: str,
23
34
  ) -> Tuple[Any, str, bool]:
24
35
  """Get the value of a specific field."""
25
36
  encoding = self.config.get("env_file_encoding")
@@ -75,8 +86,6 @@ class RepositorySettings(BaseModel):
75
86
 
76
87
  url: str | None = None
77
88
  """The repository's URL."""
78
- branch: str | None = None
79
- """The repository's branch."""
80
89
 
81
90
 
82
91
  class AuthorSettings(BaseModel):
@@ -107,7 +116,7 @@ def convert_output_str_to_dict(value: str | dict) -> dict:
107
116
  class Settings(BaseSettings):
108
117
  """Application settings."""
109
118
 
110
- version: str = "1"
119
+ version: str = "2"
111
120
  """The configuration file version."""
112
121
  author: AuthorSettings = AuthorSettings()
113
122
  """The author settings."""
@@ -119,8 +128,8 @@ class Settings(BaseSettings):
119
128
  """The repository settings."""
120
129
  title: dict = {}
121
130
  """The titles for the individual languages."""
122
- jb_config: dict = {}
123
- """Additional JupyterBook configuration."""
131
+ sphinx_config: dict = {}
132
+ """Sphinx configuration."""
124
133
 
125
134
  @classmethod
126
135
  def settings_customise_sources(
@@ -147,4 +156,4 @@ settings = Settings().model_dump()
147
156
  def reload_settings() -> None:
148
157
  """Reload the settings."""
149
158
  settings.clear()
150
- settings.update(Settings().dict())
159
+ settings.update(Settings().model_dump())
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: uEdition
3
- Version: 1.3.2
3
+ Version: 2.0.0a1
4
4
  Project-URL: Documentation, https://github.com/uEdition/uEdition#readme
5
5
  Project-URL: Issues, https://github.com/uEdition/uEdition/issues
6
6
  Project-URL: Source, https://github.com/uEdition/uEdition
@@ -11,17 +11,22 @@ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
14
16
  Classifier: Programming Language :: Python :: Implementation :: CPython
15
17
  Classifier: Programming Language :: Python :: Implementation :: PyPy
16
18
  Requires-Python: >=3.10
17
19
  Requires-Dist: copier<10.0.0,>=9.0.0
18
- Requires-Dist: jupyter-book<2.0.0,>=1.0.0
19
20
  Requires-Dist: livereload
20
21
  Requires-Dist: lxml<6.0.0,>=4.9.2
22
+ Requires-Dist: myst-parser<5,>=4.0.1
21
23
  Requires-Dist: pydantic-settings<3.0.0,>=2.0.0
22
24
  Requires-Dist: pydantic<3.0.0,>=2.0.0
23
25
  Requires-Dist: pyyaml<7.0.0,>=6.0.0
24
- Requires-Dist: typer[all]<1.0.0
26
+ Requires-Dist: sphinx-book-theme<2,>=1.1.4
27
+ Requires-Dist: sphinx-external-toc<2,>=1.0.1
28
+ Requires-Dist: sphinx<9,>=8.2.3
29
+ Requires-Dist: typer<1.0.0
25
30
  Description-Content-Type: text/markdown
26
31
 
27
32
  # μEdition
@@ -0,0 +1,25 @@
1
+ uedition/__about__.py,sha256=kq7WGmRyG7G8lDeopl9H4oZ4X9xtex9Qs3BryTuumU0,160
2
+ uedition/__init__.py,sha256=xDDK2i5l1NLhxq6_LJH5g7UJDrFEjIJNmT2tcg7xNWI,301
3
+ uedition/__main__.py,sha256=Pg_yGV-ndR2iiImDC3f017llSX6pSYMslIwZlw8vEpQ,189
4
+ uedition/settings.py,sha256=G73r6muJmRzuENZ-2n51bBf88qS3tRv0kdxr2SRt1j4,5048
5
+ uedition/cli/__init__.py,sha256=gnpl_N8uaw-4uF5ByWV3jXveJBvjLb_uay5YkYCUQWw,1478
6
+ uedition/cli/build.py,sha256=L_-OsIEFBXQh-aTHLwLhOXJzXqRH61X2SgVIPxmUgQ0,8169
7
+ uedition/cli/create.py,sha256=Q-SvDq9VtmUP4DQhuuvt1eZ_72sX8_tcFOj2Bt_T6J8,371
8
+ uedition/cli/language.py,sha256=JAyUwNa4gwqMvrJDPPKGkMLm5Cx9sHJkU5r6xTAMb7M,2214
9
+ uedition/cli/serve.py,sha256=FcKp0IbjcyCgn1XjU8VdhI59pGRMNCSXa5jbAS23nxs,1513
10
+ uedition/cli/update.py,sha256=SmAcczHxj3j3X0QYplvXPoIHS8XEfXYVqgXBney0v9c,550
11
+ uedition/ext/__init__.py,sha256=hAK3MB5il4tAkfWnZNVxIJhfJ5vN0Fdmmtz0ZAYsvo4,406
12
+ uedition/ext/config.py,sha256=zu0XSH3Ca09n--umhUJ7k6611lVCecOTVZCWAFn4TRU,994
13
+ uedition/ext/language_switcher.css,sha256=y4LzkCgCm6E_nHt15I4Ku5QzBNpjwda9bt9FsqD1ybM,132
14
+ uedition/ext/language_switcher.js,sha256=HIgQiLg0WGS_G_VjpfEpTDLqb1HwHxcL3r6mdoSUix4,2697
15
+ uedition/ext/language_switcher.py,sha256=tHpf4HsvMtatVn5dQ3EFlrk5urFaMzsZZY755cvgCu8,1425
16
+ uedition/ext/settings.py,sha256=CCbvwlWjhikhoeRZ5E_SuA4zUIqDBMkRes_hjOUxjWk,3735
17
+ uedition/ext/tei/__init__.py,sha256=8KgVi31bz8nI65m6u4EdT_f1qNCP45HrU0V7MSGlZxA,1074
18
+ uedition/ext/tei/builder.py,sha256=LkLR3cu1M6lHHdcjhH5esZ9Qn6eFBXCZWkxVxdXv69E,11719
19
+ uedition/ext/tei/parser.py,sha256=ebeqEiji2RZ-Gfszf0Yaa-Lqww-EPqLmxXiMBq-gj90,13216
20
+ uedition/ext/tei/tei_download.js,sha256=5_IPCuamZGPXWriPPPz5wA-zo0Y0Oy1858S6ltxSdQ8,2151
21
+ uedition-2.0.0a1.dist-info/METADATA,sha256=YquOplbaOq4Z17fRpMEQMK2S1imGpd0HS3TWeCaWXEk,2572
22
+ uedition-2.0.0a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ uedition-2.0.0a1.dist-info/entry_points.txt,sha256=cDOOVBb1SZ072ZkY1hW4Y7I_WKKGCtCJtDY1XST1Hr4,96
24
+ uedition-2.0.0a1.dist-info/licenses/LICENSE.txt,sha256=MhLJl8GE8mnuO5i_pvKaobpIGnhiAEdkY-a6LKiuwCE,1101
25
+ uedition-2.0.0a1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.21.1
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
uedition/cli/check.py DELETED
@@ -1,183 +0,0 @@
1
- # SPDX-FileCopyrightText: 2023-present Mark Hall <mark.hall@work.room3b.eu>
2
- #
3
- # SPDX-License-Identifier: MIT
4
- """The uEdition check functionality for validating a uEdition and its files."""
5
- import os
6
- from threading import Thread
7
-
8
- import typer
9
- from rich import print as print_cli
10
- from rich.progress import Progress
11
- from yaml import safe_load
12
-
13
- from uedition.settings import settings
14
-
15
-
16
- def collect_files(toc: dict) -> list[str]:
17
- """Collect all files from a TOC."""
18
- files = []
19
- if "parts" in toc:
20
- for part in toc["parts"]:
21
- files.extend(collect_files(part))
22
- elif "chapters" in toc:
23
- for chapter in toc["chapters"]:
24
- files.append(chapter["file"])
25
- files.extend(collect_files(chapter))
26
- elif "sections" in toc:
27
- for sections in toc["sections"]:
28
- files.append(sections["file"])
29
- return files
30
-
31
-
32
- def compare_tocs(prefix: str, toc_a: dict, toc_b: dict) -> list[tuple[str | None, str | None]]:
33
- """Recursively compare the structure of two TOCs."""
34
- mismatches = []
35
- if "parts" in toc_a and "parts" in toc_b:
36
- pass
37
- elif "chapters" in toc_a and "chapters" in toc_b:
38
- pass
39
- elif "sections" in toc_a and "sections" in toc_b:
40
- pass
41
- elif "parts" in toc_a:
42
- mismatches.append(f"{prefix}has parts, which are missing from")
43
- elif "chapters" in toc_a:
44
- mismatches.append(f"{prefix}has chapters, which are missing from")
45
- elif "sections" in toc_b:
46
- mismatches.append(f"{prefix}has sections, which are missing from")
47
- return mismatches
48
-
49
-
50
- class ConfigurationFileChecks(Thread):
51
- """Basic configuration file checks."""
52
-
53
- def __init__(self: "ConfigurationFileChecks", progress: Progress, task: int) -> None:
54
- """Initialise the thread."""
55
- super().__init__(group=None)
56
- self._progress = progress
57
- self._task = task
58
- self.errors = []
59
-
60
- def run(self: "ConfigurationFileChecks") -> None:
61
- """Run the checks."""
62
- for lang in settings["languages"]:
63
- yaml_path = os.path.join(lang["path"], "_config.yml")
64
- if os.path.exists(yaml_path):
65
- with open(yaml_path) as in_f:
66
- try:
67
- safe_load(in_f)
68
- except Exception as e:
69
- self.errors.append(str(e))
70
- else:
71
- self.errors.append(f"Missing configuration file {yaml_path}")
72
- self._progress.update(self._task, advance=1)
73
-
74
-
75
- class TocFileChecks(Thread):
76
- """TOC file checks."""
77
-
78
- def __init__(self: "TocFileChecks", progress: Progress, task: int) -> None:
79
- """Initialise the thread."""
80
- super().__init__(group=None)
81
- self._progress = progress
82
- self._task = task
83
- self.errors = []
84
-
85
- def run(self: "TocFileChecks") -> None:
86
- """Run the checks."""
87
- for lang in settings["languages"]:
88
- yaml_path = os.path.join(lang["path"], "_toc.yml")
89
- if os.path.exists(yaml_path):
90
- with open(yaml_path) as in_f:
91
- try:
92
- toc = safe_load(in_f)
93
- if "root" in toc:
94
- root_path = os.path.join(lang["path"], f'{toc["root"]}.md')
95
- if not os.path.exists(root_path):
96
- self.errors.append(f'Root in {yaml_path} points to missing file {toc["root"]}.md')
97
- root_base = os.path.dirname(root_path)
98
- for filename in collect_files(toc):
99
- if not os.path.exists(os.path.join(root_base, f"{filename}.md")):
100
- self.errors.append(f"File {filename}.md missing in {yaml_path}")
101
- else:
102
- self.errors.append(f"No root in {yaml_path}")
103
- except Exception as e:
104
- self.errors.append(str(e))
105
- else:
106
- self.errors.append(f"Missing toc file {yaml_path}")
107
- self._progress.update(self._task, advance=1)
108
-
109
-
110
- class LanguageConsistencyChecks(Thread):
111
- """Multi-language consistency checks."""
112
-
113
- def __init__(self: "LanguageConsistencyChecks", progress: Progress, task: int) -> None:
114
- """Initialise the thread."""
115
- super().__init__(group=None)
116
- self._progress = progress
117
- self._task = task
118
- self.errors = []
119
-
120
- def run(self: "LanguageConsistencyChecks") -> None:
121
- """Run the checks."""
122
- if len(settings["languages"]) == 0:
123
- return
124
- base_toc_path = os.path.join(settings["languages"][0]["path"], "_toc.yml")
125
- if not os.path.exists(base_toc_path):
126
- return
127
- with open(base_toc_path) as in_f:
128
- try:
129
- base_toc = safe_load(in_f)
130
- except Exception:
131
- return
132
- for lang in settings["languages"][1:]:
133
- lang_toc_path = os.path.join(lang["path"], "_toc.yml")
134
- if not os.path.exists(lang_toc_path):
135
- continue
136
- try:
137
- with open(lang_toc_path) as in_f:
138
- lang_toc = safe_load(in_f)
139
- missmatches = compare_tocs("", base_toc, lang_toc)
140
- if len(missmatches) > 0:
141
- for mismatch in missmatches:
142
- if mismatch[0] is None:
143
- self.errors.append(f"{base_toc_path} {mismatch[1]} {lang_toc_path}")
144
- elif mismatch[0] is None:
145
- self.errors.append(f"{lang_toc_path} {mismatch[0]} {base_toc_path}")
146
- self._progress.update(self._task, advance=1)
147
- except Exception as e:
148
- self.errors.append(f"Fail to check langauge {lang}: {e!s}")
149
-
150
-
151
- def run() -> None:
152
- """Check that the μEdition is correctly set up."""
153
- errors = []
154
- with Progress() as progress:
155
- threads = [
156
- ConfigurationFileChecks(
157
- progress,
158
- progress.add_task("[green]Configuration file checks", total=len(settings["languages"])),
159
- ),
160
- TocFileChecks(
161
- progress,
162
- progress.add_task("[green]TOC file checks", total=len(settings["languages"])),
163
- ),
164
- LanguageConsistencyChecks(
165
- progress,
166
- progress.add_task(
167
- "[green]Language consistency checks",
168
- total=len(settings["languages"]) - 1,
169
- ),
170
- ),
171
- ]
172
- for thread in threads:
173
- thread.start()
174
- for thread in threads:
175
- thread.join()
176
- errors.extend(thread.errors)
177
- if len(errors) > 0:
178
- print_cli("[red]:bug: The following errors were found:")
179
- for error in errors:
180
- print_cli(f"[red]* {error}")
181
- raise typer.Exit(code=1)
182
- else:
183
- print_cli("[green]:+1: All checks successfully passed")
@@ -1,25 +0,0 @@
1
- uedition/__about__.py,sha256=qc1tq_AqKfnOrx88PfqDhfox8UD5GLclkXNzDTGwgaQ,157
2
- uedition/__init__.py,sha256=xDDK2i5l1NLhxq6_LJH5g7UJDrFEjIJNmT2tcg7xNWI,301
3
- uedition/__main__.py,sha256=Pg_yGV-ndR2iiImDC3f017llSX6pSYMslIwZlw8vEpQ,189
4
- uedition/settings.py,sha256=4jWfjD1KmYTwzeD6DhP9taWS-zFp9XHxo1QqUqsSlrA,4774
5
- uedition/cli/__init__.py,sha256=h3N4ZwgRC72IlkCsTbke4PLTFv-_pn1-npXXPC7S78U,1642
6
- uedition/cli/build.py,sha256=qrpNP227DlTqa6pHX3NrDw1cohc1dHPi5J6pMFgxKsk,8135
7
- uedition/cli/check.py,sha256=Jlkyw6mgcz3HM-wJpmzCtKv054MFolCe7m6bSUDtsZA,7005
8
- uedition/cli/create.py,sha256=Q-SvDq9VtmUP4DQhuuvt1eZ_72sX8_tcFOj2Bt_T6J8,371
9
- uedition/cli/language.py,sha256=IqSJrZbrQzU-7TJqnCBeC2HUs1N01EAa7jFMvfXTsoA,1943
10
- uedition/cli/serve.py,sha256=UfVsY26OW9yAz7rnjjatz1gEDGIi1kaTYlkUAiVCuRw,1391
11
- uedition/cli/update.py,sha256=XKHnvorHqizsB5zP-8ifMrgnQuq6zRk6Tb03dBz_MI4,377
12
- uedition/ext/__init__.py,sha256=hAK3MB5il4tAkfWnZNVxIJhfJ5vN0Fdmmtz0ZAYsvo4,406
13
- uedition/ext/config.py,sha256=eEkBkAOaDQPzO_2Uj5OJxyAZiCG4I-TcOEqUyIY8IFY,6023
14
- uedition/ext/language_switcher.css,sha256=y4LzkCgCm6E_nHt15I4Ku5QzBNpjwda9bt9FsqD1ybM,132
15
- uedition/ext/language_switcher.js,sha256=HIgQiLg0WGS_G_VjpfEpTDLqb1HwHxcL3r6mdoSUix4,2697
16
- uedition/ext/language_switcher.py,sha256=tHpf4HsvMtatVn5dQ3EFlrk5urFaMzsZZY755cvgCu8,1425
17
- uedition/ext/tei/__init__.py,sha256=8KgVi31bz8nI65m6u4EdT_f1qNCP45HrU0V7MSGlZxA,1074
18
- uedition/ext/tei/builder.py,sha256=XLFM11Gnt_4G4H-2gaJyjOgko_aMmvgZnKftMsSk8qw,11743
19
- uedition/ext/tei/parser.py,sha256=QpDZ0D9ai4JCvtpBzfimI83rCdDQsD2ey4-_cW9Llv0,14291
20
- uedition/ext/tei/tei_download.js,sha256=5_IPCuamZGPXWriPPPz5wA-zo0Y0Oy1858S6ltxSdQ8,2151
21
- uedition-1.3.2.dist-info/METADATA,sha256=WlCa6UH0dUY63Mwwk6HPUFr5ltvei2KwXmObxB883YM,2358
22
- uedition-1.3.2.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
23
- uedition-1.3.2.dist-info/entry_points.txt,sha256=cDOOVBb1SZ072ZkY1hW4Y7I_WKKGCtCJtDY1XST1Hr4,96
24
- uedition-1.3.2.dist-info/licenses/LICENSE.txt,sha256=MhLJl8GE8mnuO5i_pvKaobpIGnhiAEdkY-a6LKiuwCE,1101
25
- uedition-1.3.2.dist-info/RECORD,,