pySvdGenerator 1.0.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.
@@ -0,0 +1,37 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """SVD generation tools."""
5
+
6
+ from .chapter_splitter import DEFAULT_WORKSPACE, Chapter, list_chapters, split_chapters
7
+ from .register_extractor import (
8
+ ChapterRegisters,
9
+ Field,
10
+ PeripheralRegisters,
11
+ Register,
12
+ extract_peripherals,
13
+ extract_registers,
14
+ )
15
+ from .svd import Device, Interrupt, Peripheral, build_device, generate_svd
16
+ from .svd_enricher import EnrichmentReport, PeripheralMatch, enrich_svd
17
+
18
+ __all__ = [
19
+ "DEFAULT_WORKSPACE",
20
+ "Chapter",
21
+ "ChapterRegisters",
22
+ "Device",
23
+ "EnrichmentReport",
24
+ "Field",
25
+ "Interrupt",
26
+ "Peripheral",
27
+ "PeripheralMatch",
28
+ "PeripheralRegisters",
29
+ "Register",
30
+ "build_device",
31
+ "enrich_svd",
32
+ "extract_peripherals",
33
+ "extract_registers",
34
+ "generate_svd",
35
+ "list_chapters",
36
+ "split_chapters",
37
+ ]
@@ -0,0 +1,78 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Lookup of device tree binding documentation shipped with the kernel."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Iterable
12
+
13
+ __all__ = ["BindingIndex"]
14
+
15
+ BINDINGS_DIR = Path("Documentation/devicetree/bindings")
16
+
17
+ _TITLE_RE = re.compile(r"^title:\s*(?:[|>][-+]?\s*)?(.*)$", re.M)
18
+ _TXT_TITLE_RE = re.compile(r"^[*#=\s]*(.+?)\s*$", re.M)
19
+
20
+
21
+ @dataclass
22
+ class BindingIndex:
23
+ """Map ``compatible`` strings to a human readable description.
24
+
25
+ :param titles: title of the binding documenting each compatible.
26
+ :param sources: binding file each compatible was found in.
27
+ """
28
+
29
+ titles: dict[str, str] = field(default_factory=dict)
30
+ sources: dict[str, Path] = field(default_factory=dict)
31
+
32
+ @classmethod
33
+ def build(cls, kernel_path: Path | str, compatibles: Iterable[str]) -> "BindingIndex":
34
+ """Scan the kernel bindings for the given ``compatible`` strings."""
35
+ index = cls()
36
+ wanted = {compatible for compatible in compatibles if compatible}
37
+ root = Path(kernel_path) / BINDINGS_DIR
38
+ if not wanted or not root.is_dir():
39
+ return index
40
+
41
+ for path in sorted(root.rglob("*")):
42
+ if not wanted:
43
+ break
44
+ if path.suffix not in (".yaml", ".txt") or not path.is_file():
45
+ continue
46
+ text = path.read_text(encoding="utf-8", errors="replace")
47
+ found = [compatible for compatible in wanted if compatible in text]
48
+ if not found:
49
+ continue
50
+ title = _extract_title(path, text)
51
+ for compatible in found:
52
+ if title:
53
+ index.titles[compatible] = title
54
+ index.sources[compatible] = path
55
+ wanted.difference_update(found)
56
+ return index
57
+
58
+ def describe(self, compatibles: Iterable[str]) -> str | None:
59
+ """Return the binding title of the first documented compatible."""
60
+ for compatible in compatibles:
61
+ title = self.titles.get(compatible)
62
+ if title:
63
+ return title
64
+ return None
65
+
66
+
67
+ def _extract_title(path: Path, text: str) -> str | None:
68
+ """Return the title of a binding, YAML front matter or first text line."""
69
+ if path.suffix == ".yaml":
70
+ match = _TITLE_RE.search(text)
71
+ if match:
72
+ return match.group(1).strip().strip("\"'") or None
73
+ return None
74
+ for line in text.splitlines():
75
+ cleaned = _TXT_TITLE_RE.match(line)
76
+ if cleaned and cleaned.group(1).strip():
77
+ return cleaned.group(1).strip()
78
+ return None
@@ -0,0 +1,147 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Split a reference manual PDF into one document per chapter.
5
+
6
+ Chapters are discovered from the top level entries of the PDF outline
7
+ (bookmarks). Each chapter is written as a standalone PDF named after the
8
+ chapter title, inside a given workspace directory.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import unicodedata
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Iterator, Sequence
18
+
19
+ from pypdf import PdfReader, PdfWriter
20
+ from pypdf.generic import Destination
21
+
22
+ __all__ = ["DEFAULT_WORKSPACE", "Chapter", "list_chapters", "matches", "split_chapters"]
23
+
24
+ DEFAULT_WORKSPACE = Path("workspace")
25
+
26
+ _INVALID_NAME_CHARS = re.compile(r"[^A-Za-z0-9._-]+")
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class Chapter:
31
+ """A chapter of a document, as a page range and a title.
32
+
33
+ :param title: chapter title, as written in the PDF outline.
34
+ :param first_page: 0 based index of the first page.
35
+ :param last_page: 0 based index of the last page, included.
36
+ """
37
+
38
+ title: str
39
+ first_page: int
40
+ last_page: int
41
+
42
+ @property
43
+ def filename(self) -> str:
44
+ """Return the sanitized file name (with extension) of the chapter."""
45
+ return f"{sanitize_name(self.title)}.pdf"
46
+
47
+
48
+ def sanitize_name(title: str) -> str:
49
+ """Turn a chapter title into a safe, portable file name stem."""
50
+ # Drop zero-width and other formatting code points kept in PDF bookmarks.
51
+ cleaned = "".join(char for char in title if unicodedata.category(char) != "Cf")
52
+ cleaned = unicodedata.normalize("NFKD", cleaned)
53
+ cleaned = cleaned.encode("ascii", "ignore").decode("ascii")
54
+ cleaned = _INVALID_NAME_CHARS.sub("_", cleaned).strip("._-")
55
+ return cleaned or "chapter"
56
+
57
+
58
+ def _top_level_outline(reader: PdfReader) -> Iterator[Destination]:
59
+ """Iterate over the first level entries of the document outline."""
60
+ for item in reader.outline:
61
+ if isinstance(item, Destination):
62
+ yield item
63
+
64
+
65
+ def list_chapters(document: Path | str) -> list[Chapter]:
66
+ """Return the chapters of ``document`` without writing anything."""
67
+ reader = PdfReader(str(document))
68
+ page_count = len(reader.pages)
69
+
70
+ starts: list[tuple[str, int]] = []
71
+ for entry in _top_level_outline(reader):
72
+ page = reader.get_destination_page_number(entry)
73
+ if page is None:
74
+ continue
75
+ starts.append((str(entry.title), page))
76
+
77
+ chapters: list[Chapter] = []
78
+ for index, (title, first_page) in enumerate(starts):
79
+ next_start = starts[index + 1][1] if index + 1 < len(starts) else page_count
80
+ last_page = max(first_page, next_start - 1)
81
+ chapters.append(Chapter(title=title, first_page=first_page, last_page=last_page))
82
+ return chapters
83
+
84
+
85
+ def matches(chapter: Chapter, select: Sequence[str]) -> bool:
86
+ """Tell whether a chapter title or file name contains one of ``select``."""
87
+ if not select:
88
+ return True
89
+ haystack = f"{chapter.title} {chapter.filename}".lower()
90
+ return any(needle.lower() in haystack for needle in select)
91
+
92
+
93
+ def split_chapters(
94
+ document: Path | str,
95
+ workspace: Path | str = DEFAULT_WORKSPACE,
96
+ *,
97
+ overwrite: bool = True,
98
+ select: Sequence[str] | None = None,
99
+ ) -> list[Path]:
100
+ """Split ``document`` into one PDF per chapter inside ``workspace``.
101
+
102
+ :param document: path to the source PDF document.
103
+ :param workspace: directory where the chapter documents are written.
104
+ :param overwrite: when False, already existing chapter files are kept.
105
+ :param select: only split the chapters whose title or file name contains
106
+ one of these substrings.
107
+ :return: the list of written (or already present) chapter file paths.
108
+ :raises FileNotFoundError: if ``document`` does not exist.
109
+ :raises ValueError: if no chapter could be found in the document outline.
110
+ """
111
+ source = Path(document)
112
+ if not source.is_file():
113
+ raise FileNotFoundError(f"No such document: {source}")
114
+
115
+ chapters = list_chapters(source)
116
+ if not chapters:
117
+ raise ValueError(f"No chapter outline found in {source}")
118
+
119
+ target_dir = Path(workspace)
120
+ target_dir.mkdir(parents=True, exist_ok=True)
121
+
122
+ reader = PdfReader(str(source))
123
+ written: list[Path] = []
124
+ used: dict[str, int] = {}
125
+
126
+ for chapter in chapters:
127
+ name = chapter.filename
128
+ seen = used.get(name, 0)
129
+ used[name] = seen + 1
130
+ if seen:
131
+ name = f"{Path(name).stem}_{seen}.pdf"
132
+ if not matches(chapter, select or ()):
133
+ continue
134
+
135
+ output = target_dir / name
136
+ written.append(output)
137
+ if output.exists() and not overwrite:
138
+ continue
139
+
140
+ writer = PdfWriter()
141
+ for page in range(chapter.first_page, chapter.last_page + 1):
142
+ writer.add_page(reader.pages[page])
143
+ with output.open("wb") as stream:
144
+ writer.write(stream)
145
+ writer.close()
146
+
147
+ return written