primarysource 0.1.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.
Files changed (303) hide show
  1. primarysource/__init__.py +5 -0
  2. primarysource/bbf/__init__.py +56 -0
  3. primarysource/bbf/_xml.py +63 -0
  4. primarysource/bbf/catalogue.json +777 -0
  5. primarysource/bbf/catalogue.py +137 -0
  6. primarysource/bbf/cli.py +79 -0
  7. primarysource/bbf/datamodel.py +281 -0
  8. primarysource/bbf/importer.py +151 -0
  9. primarysource/bbf/library.py +380 -0
  10. primarysource/bbf/server.py +72 -0
  11. primarysource/budget.py +63 -0
  12. primarysource/c2sp/__init__.py +53 -0
  13. primarysource/c2sp/_fetch.py +5 -0
  14. primarysource/c2sp/catalogue.json +231 -0
  15. primarysource/c2sp/catalogue.py +95 -0
  16. primarysource/c2sp/cli.py +62 -0
  17. primarysource/c2sp/importer.py +90 -0
  18. primarysource/c2sp/library.py +309 -0
  19. primarysource/c2sp/markdown.py +31 -0
  20. primarysource/c2sp/server.py +68 -0
  21. primarysource/cavp/__init__.py +42 -0
  22. primarysource/cavp/cli.py +84 -0
  23. primarysource/cavp/data/vectors.toml +47 -0
  24. primarysource/cavp/importer.py +156 -0
  25. primarysource/cavp/model.py +279 -0
  26. primarysource/cavp/parse.py +269 -0
  27. primarysource/cavp/paths.py +17 -0
  28. primarysource/cavp/server.py +67 -0
  29. primarysource/cavp/tools.py +169 -0
  30. primarysource/cli.py +107 -0
  31. primarysource/cursor.py +82 -0
  32. primarysource/cve/__init__.py +5 -0
  33. primarysource/cve/__main__.py +6 -0
  34. primarysource/cve/cli.py +144 -0
  35. primarysource/cve/collection.py +20 -0
  36. primarysource/cve/config.py +58 -0
  37. primarysource/cve/errors.py +16 -0
  38. primarysource/cve/kernel.py +232 -0
  39. primarysource/cve/mcp.py +139 -0
  40. primarysource/cve/parse.py +318 -0
  41. primarysource/cve/py.typed +0 -0
  42. primarysource/cve/service.py +760 -0
  43. primarysource/cve/store.py +214 -0
  44. primarysource/cve/sync.py +438 -0
  45. primarysource/cve/views.py +304 -0
  46. primarysource/debpolicy/__init__.py +16 -0
  47. primarysource/debpolicy/catalogue.py +263 -0
  48. primarysource/debpolicy/cli.py +12 -0
  49. primarysource/debpolicy/server.py +11 -0
  50. primarysource/draft/__init__.py +11 -0
  51. primarysource/draft/catalogue.py +117 -0
  52. primarysource/draft/cli.py +149 -0
  53. primarysource/draft/collection.py +21 -0
  54. primarysource/draft/datatracker.py +158 -0
  55. primarysource/draft/importer.py +163 -0
  56. primarysource/draft/licence.py +89 -0
  57. primarysource/draft/names.py +64 -0
  58. primarysource/draft/net.py +133 -0
  59. primarysource/draft/segment.py +192 -0
  60. primarysource/draft/server.py +91 -0
  61. primarysource/draft/status.py +146 -0
  62. primarysource/draft/store.py +199 -0
  63. primarysource/draft/tracked.json +104 -0
  64. primarysource/draft/views.py +430 -0
  65. primarysource/draft/xmlmeta.py +74 -0
  66. primarysource/ecma/__init__.py +52 -0
  67. primarysource/ecma/_pdf.py +143 -0
  68. primarysource/ecma/catalogue.json +22 -0
  69. primarysource/ecma/catalogue.py +100 -0
  70. primarysource/ecma/cli.py +67 -0
  71. primarysource/ecma/identify.py +25 -0
  72. primarysource/ecma/importer.py +108 -0
  73. primarysource/ecma/library.py +240 -0
  74. primarysource/ecma/server.py +67 -0
  75. primarysource/errors.py +29 -0
  76. primarysource/fetch.py +360 -0
  77. primarysource/fsutil.py +150 -0
  78. primarysource/gitdocs/__init__.py +15 -0
  79. primarysource/gitdocs/catalogue.py +116 -0
  80. primarysource/gitdocs/cli.py +12 -0
  81. primarysource/gitdocs/server.py +8 -0
  82. primarysource/home.py +65 -0
  83. primarysource/iana/__init__.py +32 -0
  84. primarysource/iana/catalogue.json +244 -0
  85. primarysource/iana/catalogue.py +69 -0
  86. primarysource/iana/cli.py +69 -0
  87. primarysource/iana/importer.py +157 -0
  88. primarysource/iana/model.py +325 -0
  89. primarysource/iana/reader.py +315 -0
  90. primarysource/iana/server.py +67 -0
  91. primarysource/iana/store.py +83 -0
  92. primarysource/iana/xmlsafe.py +47 -0
  93. primarysource/identify.py +186 -0
  94. primarysource/ieee/__init__.py +58 -0
  95. primarysource/ieee/_html.py +49 -0
  96. primarysource/ieee/catalogue.json +74 -0
  97. primarysource/ieee/catalogue.py +123 -0
  98. primarysource/ieee/cli.py +79 -0
  99. primarysource/ieee/identify.py +24 -0
  100. primarysource/ieee/ieee802.py +47 -0
  101. primarysource/ieee/importer.py +305 -0
  102. primarysource/ieee/posix.py +233 -0
  103. primarysource/ieee/server.py +79 -0
  104. primarysource/index.py +446 -0
  105. primarysource/iso/__init__.py +51 -0
  106. primarysource/iso/cli.py +98 -0
  107. primarysource/iso/designation.py +95 -0
  108. primarysource/iso/identify.py +100 -0
  109. primarysource/iso/importer.py +224 -0
  110. primarysource/iso/library.py +249 -0
  111. primarysource/iso/server.py +69 -0
  112. primarysource/itu/__init__.py +41 -0
  113. primarysource/itu/__main__.py +8 -0
  114. primarysource/itu/asn1mod.py +478 -0
  115. primarysource/itu/catalog.py +472 -0
  116. primarysource/itu/cli.py +445 -0
  117. primarysource/itu/collection.py +20 -0
  118. primarysource/itu/corpus.py +118 -0
  119. primarysource/itu/errata.py +16 -0
  120. primarysource/itu/formal.py +565 -0
  121. primarysource/itu/identify.py +26 -0
  122. primarysource/itu/index.py +490 -0
  123. primarysource/itu/mcp.py +474 -0
  124. primarysource/itu/py.typed +0 -0
  125. primarysource/itu/sources.py +600 -0
  126. primarysource/itu/text.py +609 -0
  127. primarysource/kconfig/__init__.py +16 -0
  128. primarysource/kconfig/catalogue.py +98 -0
  129. primarysource/kconfig/cli.py +12 -0
  130. primarysource/kconfig/server.py +8 -0
  131. primarysource/model.py +63 -0
  132. primarysource/nist/__init__.py +5 -0
  133. primarysource/nist/collection.py +46 -0
  134. primarysource/nist/data/acquired-nist.json +1657 -0
  135. primarysource/nist/data/nist-catalog.json +12090 -0
  136. primarysource/nist/data/sources.toml +1996 -0
  137. primarysource/nist/identify.py +41 -0
  138. primarysource/nist/ingest/__init__.py +1 -0
  139. primarysource/nist/ingest/acquire_nist.py +128 -0
  140. primarysource/nist/ingest/controls.py +82 -0
  141. primarysource/nist/ingest/discover_nist.py +218 -0
  142. primarysource/nist/ingest/extract.py +48 -0
  143. primarysource/nist/ingest/fetch.py +211 -0
  144. primarysource/nist/ingest/index.py +14 -0
  145. primarysource/nist/ingest/oscal.py +480 -0
  146. primarysource/nist/ingest/prose_layouts.py +236 -0
  147. primarysource/nist/ingest/sources.py +94 -0
  148. primarysource/nist/ingest/status.py +255 -0
  149. primarysource/nist/models.py +7 -0
  150. primarysource/nist/paths.py +33 -0
  151. primarysource/nist/server/__init__.py +3 -0
  152. primarysource/nist/server/corpus.py +38 -0
  153. primarysource/nist/server/mcp.py +19 -0
  154. primarysource/nist/server/tools.py +35 -0
  155. primarysource/nist/spec.py +38 -0
  156. primarysource/pdfcorpus/__init__.py +16 -0
  157. primarysource/pdfcorpus/corpus.py +159 -0
  158. primarysource/pdfcorpus/fts.py +175 -0
  159. primarysource/pdfcorpus/index.py +70 -0
  160. primarysource/pdfcorpus/models.py +30 -0
  161. primarysource/pdfcorpus/server.py +96 -0
  162. primarysource/pdfcorpus/spec.py +37 -0
  163. primarysource/pdfcorpus/split.py +391 -0
  164. primarysource/pdfcorpus/tools.py +244 -0
  165. primarysource/pep/__init__.py +15 -0
  166. primarysource/pep/catalogue.py +191 -0
  167. primarysource/pep/cli.py +12 -0
  168. primarysource/pep/server.py +8 -0
  169. primarysource/pkgdocs/__init__.py +19 -0
  170. primarysource/pkgdocs/fetch.py +356 -0
  171. primarysource/pkgdocs/index.py +89 -0
  172. primarysource/pkgdocs/model.py +76 -0
  173. primarysource/pkgdocs/readers/__init__.py +1 -0
  174. primarysource/pkgdocs/readers/_common.py +134 -0
  175. primarysource/pkgdocs/readers/asciidoc.py +160 -0
  176. primarysource/pkgdocs/readers/latex.py +315 -0
  177. primarysource/pkgdocs/readers/markdown.py +173 -0
  178. primarysource/pkgdocs/readers/pod.py +132 -0
  179. primarysource/pkgdocs/readers/rst.py +239 -0
  180. primarysource/pkgdocs/readers/scdoc.py +108 -0
  181. primarysource/pkgdocs/runtime.py +552 -0
  182. primarysource/pkgdocs/serving.py +158 -0
  183. primarysource/pkgdocs/store.py +167 -0
  184. primarysource/pms/__init__.py +16 -0
  185. primarysource/pms/catalogue.py +88 -0
  186. primarysource/pms/cli.py +12 -0
  187. primarysource/pms/server.py +8 -0
  188. primarysource/py.typed +0 -0
  189. primarysource/readers/__init__.py +10 -0
  190. primarysource/readers/bookmarks.py +192 -0
  191. primarysource/readers/html.py +1312 -0
  192. primarysource/readers/markdown.py +189 -0
  193. primarysource/readers/pdf.py +187 -0
  194. primarysource/readers/xml.py +230 -0
  195. primarysource/refs.py +87 -0
  196. primarysource/registry.py +161 -0
  197. primarysource/render.py +49 -0
  198. primarysource/rfc/__init__.py +3 -0
  199. primarysource/rfc/__main__.py +6 -0
  200. primarysource/rfc/cite.py +204 -0
  201. primarysource/rfc/cli.py +903 -0
  202. primarysource/rfc/collection.py +19 -0
  203. primarysource/rfc/config.py +138 -0
  204. primarysource/rfc/errata.py +203 -0
  205. primarysource/rfc/fsutil.py +82 -0
  206. primarysource/rfc/graph.py +213 -0
  207. primarysource/rfc/index/__init__.py +1 -0
  208. primarysource/rfc/index/chunker.py +281 -0
  209. primarysource/rfc/index/embed.py +166 -0
  210. primarysource/rfc/index/hybrid.py +305 -0
  211. primarysource/rfc/index/lexical.py +223 -0
  212. primarysource/rfc/index/rfcxml.py +673 -0
  213. primarysource/rfc/index/search.py +508 -0
  214. primarysource/rfc/index/txtseg.py +585 -0
  215. primarysource/rfc/library.py +294 -0
  216. primarysource/rfc/mcp/__init__.py +1 -0
  217. primarysource/rfc/mcp/server.py +381 -0
  218. primarysource/rfc/packs/__init__.py +1 -0
  219. primarysource/rfc/packs/builder.py +228 -0
  220. primarysource/rfc/packs/collections/all-current-txt.json +10 -0
  221. primarysource/rfc/packs/collections/essentials.json +19 -0
  222. primarysource/rfc/packs/collections/fyi-current.json +10 -0
  223. primarysource/rfc/packs/collections/standards-current.json +10 -0
  224. primarysource/rfc/packs/collections/year-2025.json +10 -0
  225. primarysource/rfc/packs/collections/year-2026.json +10 -0
  226. primarysource/rfc/packs/collections.py +163 -0
  227. primarysource/rfc/packs/docsetexport.py +163 -0
  228. primarysource/rfc/packs/epub.py +159 -0
  229. primarysource/rfc/packs/export.py +306 -0
  230. primarysource/rfc/packs/htmlexport.py +82 -0
  231. primarysource/rfc/packs/serve.py +143 -0
  232. primarysource/rfc/packs/zimexport.py +171 -0
  233. primarysource/rfc/query.py +466 -0
  234. primarysource/rfc/subjects.py +457 -0
  235. primarysource/rfc/sync/__init__.py +1 -0
  236. primarysource/rfc/sync/downloader.py +132 -0
  237. primarysource/rfc/sync/manifest.py +162 -0
  238. primarysource/rfc/sync/scanner.py +49 -0
  239. primarysource/rfc/sync/state.py +464 -0
  240. primarysource/rfc/views.py +1067 -0
  241. primarysource/rpmdocs/__init__.py +15 -0
  242. primarysource/rpmdocs/catalogue.py +117 -0
  243. primarysource/rpmdocs/cli.py +12 -0
  244. primarysource/rpmdocs/server.py +8 -0
  245. primarysource/scaffold.py +223 -0
  246. primarysource/serve.py +75 -0
  247. primarysource/settings.py +152 -0
  248. primarysource/usb/__init__.py +5 -0
  249. primarysource/usb/collection.py +37 -0
  250. primarysource/usb/data/acquired.json +2025 -0
  251. primarysource/usb/data/sources-usb.toml +852 -0
  252. primarysource/usb/data/sources.toml +10 -0
  253. primarysource/usb/data/usb-catalog.json +1663 -0
  254. primarysource/usb/data/usb-classification.json +1986 -0
  255. primarysource/usb/data/usb-corpus-build.json +608 -0
  256. primarysource/usb/data/usb-pdf-choice.json +839 -0
  257. primarysource/usb/data/usb-split-measure.json +1803 -0
  258. primarysource/usb/ingest/__init__.py +1 -0
  259. primarysource/usb/ingest/acquire.py +130 -0
  260. primarysource/usb/ingest/discover_usb.py +88 -0
  261. primarysource/usb/ingest/extract.py +48 -0
  262. primarysource/usb/ingest/fetch.py +197 -0
  263. primarysource/usb/ingest/index.py +14 -0
  264. primarysource/usb/ingest/sources.py +85 -0
  265. primarysource/usb/models.py +7 -0
  266. primarysource/usb/paths.py +33 -0
  267. primarysource/usb/server/__init__.py +3 -0
  268. primarysource/usb/server/corpus.py +38 -0
  269. primarysource/usb/server/mcp.py +19 -0
  270. primarysource/usb/server/tools.py +35 -0
  271. primarysource/usb/spec.py +20 -0
  272. primarysource/usbids/__init__.py +56 -0
  273. primarysource/usbids/cli.py +65 -0
  274. primarysource/usbids/importer.py +272 -0
  275. primarysource/usbids/parse.py +283 -0
  276. primarysource/usbids/server.py +55 -0
  277. primarysource/usbids/store.py +80 -0
  278. primarysource/usbids/tools.py +174 -0
  279. primarysource/zim/__init__.py +5 -0
  280. primarysource/zim/__main__.py +8 -0
  281. primarysource/zim/_libzim.py +37 -0
  282. primarysource/zim/catalog.py +594 -0
  283. primarysource/zim/cli.py +555 -0
  284. primarysource/zim/collection.py +41 -0
  285. primarysource/zim/errors.py +100 -0
  286. primarysource/zim/groups.py +30 -0
  287. primarysource/zim/library.py +668 -0
  288. primarysource/zim/manage.py +636 -0
  289. primarysource/zim/markdown.py +1247 -0
  290. primarysource/zim/mcp.py +201 -0
  291. primarysource/zim/purl.py +130 -0
  292. primarysource/zim/py.typed +0 -0
  293. primarysource/zim/refs.py +90 -0
  294. primarysource/zim/service.py +1182 -0
  295. primarysource/zim/snapshot.py +197 -0
  296. primarysource/zim/sync.py +269 -0
  297. primarysource/zim/views.py +230 -0
  298. primarysource-0.1.0.dist-info/METADATA +118 -0
  299. primarysource-0.1.0.dist-info/RECORD +303 -0
  300. primarysource-0.1.0.dist-info/WHEEL +4 -0
  301. primarysource-0.1.0.dist-info/entry_points.txt +2 -0
  302. primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
  303. primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
@@ -0,0 +1,315 @@
1
+ r"""A minimal LaTeX reader for the Gentoo Package Manager Specification, whose source is a set of `.tex` files.
2
+
3
+ This is not a TeX engine. It reads the structure the specification uses and nothing else:
4
+
5
+ - `\chapter`, `\section`, `\subsection`, `\subsubsection` (and the starred, unnumbered forms) are sections; the
6
+ numbers are LaTeX's own (chapters 1, 2, ...; after `\appendix`, chapters A, B, ...; then `8.2.4`), and a
7
+ `\label{...}` after a heading becomes the anchor, so `\ref{sec:slot-names}` in the text is replaced by the
8
+ number it names (`section~\ref{...}` reads `section 8.2.4`);
9
+ - `\include{name}` and `\input{name}` are expanded **only from the mapping of files the caller passes in**
10
+ (a file of the same archive), to a depth of four, never twice in one chain and never from disk. A missing name
11
+ becomes the block `[include] name`;
12
+ - blocks (the unit): a paragraph; each `\item` of `description`, `compactdesc`, `itemize`, `compactitem`,
13
+ `enumerate` and `compactenum` (a description item reads `TERM: text`); and each `verbatim`, `listing`,
14
+ `algorithm`, `tabular`, `centertable`, `longtable` environment as one block written as `[environment]` and the
15
+ raw text;
16
+ - text between the preamble and the first heading (the title page's copyright and licence notice) is section `0`.
17
+
18
+ Markup is reduced to its text: `\t{x}`, `\emph{x}`, `\textbf{x}` become `x`; `\cite{k}` becomes `[k]`; `~` is a
19
+ space; `---` and `--` become dashes; escapes (`\_`, `\&`, `\%`, `\$`, `\#`) are resolved. Any other command with
20
+ one argument keeps the argument, any other bare command is dropped, and the document carries one warning naming
21
+ the commands that were dropped, so a reader knows the text was reduced. Math, floats and macros defined in the
22
+ class file are not interpreted. Braces are scanned with a depth counter and a fixed number of passes, so
23
+ unbalanced or deeply nested input costs time linear in its size.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from collections.abc import Mapping
30
+ from dataclasses import dataclass, field
31
+
32
+ from primarysource.model import Unit
33
+ from primarysource.pkgdocs.model import Doc, Sec
34
+ from primarysource.pkgdocs.readers._common import decode, slugify
35
+
36
+ MAX_DEPTH = 4
37
+ MAX_LINES = 200_000
38
+ _PASSES = 8
39
+ _LEVELS = {"chapter": 0, "section": 1, "subsection": 2, "subsubsection": 3}
40
+ _LIST_ENVS = frozenset({"description", "compactdesc", "itemize", "compactitem", "enumerate", "compactenum"})
41
+ _RAW_ENVS = frozenset({"verbatim", "listing", "algorithm", "tabular", "centertable", "longtable"})
42
+ _COMMENT = re.compile(r"(?<!\\)%.*$")
43
+ _INCLUDE = re.compile(r"\s*\\(?:include|input)\{([^{}]*)\}\s*")
44
+ _HEADING = re.compile(r"\s*\\(chapter|section|subsection|subsubsection)(\*?)\s*(?:\[[^\]]*\])?\s*\{")
45
+ _BEGIN = re.compile(r"\s*\\begin\{([A-Za-z*]+)\}")
46
+ _END = re.compile(r"\s*\\end\{([A-Za-z*]+)\}")
47
+ _ITEM = re.compile(r"\s*\\item\b\s*(?:\[([^\]]*)\])?\s*(.*)")
48
+ _LABEL = re.compile(r"\s*\\label\{([^{}]*)\}\s*")
49
+ _ONE_ARG = re.compile(r"\\([A-Za-z]+)\*?(?:\[[^\]]*\])?\{([^{}]*)\}")
50
+ _BARE = re.compile(r"\\([A-Za-z]+)\*?")
51
+ _REF = re.compile(r"\\(?:ref|autoref|pageref)\{([^{}]*)\}")
52
+ _LAYOUT = re.compile(
53
+ r"\s*\\(?:thispagestyle|vspace|tableofcontents|listof\w+|maketitle|bibliography|nobreakpar|clearpage|newpage"
54
+ r"|noindent|small|centering|vfill|hfill)\b.*"
55
+ )
56
+ _SPACE = re.compile(r"[ \t]+")
57
+ _SYMBOLS = (
58
+ ("\\textasciicircum", "^"),
59
+ ("\\textbackslash", "\x01"),
60
+ ("\\textasciitilde", "~"),
61
+ ("\\slash", "/"),
62
+ ("\\allowbreak", ""),
63
+ ("\\dots", "..."),
64
+ )
65
+ _DROP_ARG = frozenset({"featurelabel", "ChangeWhenAddingAnEAPI", "hspace", "vspace", "index"})
66
+ _KEEP_ARG = frozenset(
67
+ {"t", "emph", "textbf", "textit", "texttt", "path", "url", "code", "textsc", "textsf", "underline", "mbox"}
68
+ )
69
+
70
+
71
+ @dataclass(slots=True)
72
+ class _Section:
73
+ id: str
74
+ title: str
75
+ depth: int
76
+ anchor: str
77
+ units: list[tuple[str, bool]] = field(default_factory=list) # (text, already clean)
78
+
79
+
80
+ def _braced(text: str, start: int) -> tuple[str, int]:
81
+ """The brace-balanced group whose `{` is just before `start`, and the index after its `}`."""
82
+ depth = 1
83
+ i = start
84
+ while i < len(text):
85
+ char = text[i]
86
+ if char == "\\":
87
+ i += 2
88
+ continue
89
+ if char == "{":
90
+ depth += 1
91
+ elif char == "}":
92
+ depth -= 1
93
+ if depth == 0:
94
+ return text[start:i], i + 1
95
+ i += 1
96
+ return text[start:], len(text)
97
+
98
+
99
+ def _expand(
100
+ files: Mapping[str, bytes], name: str, chain: tuple[str, ...], out: list[str], missing: list[str]
101
+ ) -> None:
102
+ key = name if name in files else name + ".tex"
103
+ if key not in files or key in chain or len(chain) >= MAX_DEPTH:
104
+ missing.append(name)
105
+ out.append(f"[include] {name}")
106
+ return
107
+ for line in decode(files[key], key).split("\n"):
108
+ if len(out) >= MAX_LINES:
109
+ return
110
+ line = _COMMENT.sub("", line).rstrip()
111
+ found = _INCLUDE.fullmatch(line)
112
+ if found:
113
+ _expand(files, found.group(1).strip(), (*chain, key), out, missing)
114
+ else:
115
+ out.append(line)
116
+
117
+
118
+ def _letter(n: int) -> str:
119
+ return chr(ord("A") + (n - 1) % 26)
120
+
121
+
122
+ def read_latex(files: Mapping[str, bytes], root: str, doc_id: str, title: str) -> Doc:
123
+ """Read `root` (a key of `files`) with its includes into one document. Raises `LibError` for a NUL byte or
124
+ invalid UTF-8 in any file it reads; a `root` that is not in `files` is an empty document with a warning."""
125
+ lines: list[str] = []
126
+ missing: list[str] = []
127
+ _expand(files, root, (), lines, missing)
128
+ started = False
129
+ appendix = False
130
+ counters = [0, 0, 0, 0]
131
+ unnumbered = 0
132
+ sections: list[_Section] = [_Section("0", title, 1, slugify(title) or "preamble")]
133
+ labels: dict[str, str] = {}
134
+ para: list[str] = []
135
+ item: list[str] | None = None
136
+ list_depth = 0
137
+ i, count = 0, len(lines)
138
+
139
+ def emit(text: str, clean: bool = False) -> None:
140
+ if text.strip():
141
+ sections[-1].units.append((text, clean))
142
+
143
+ def flush() -> None:
144
+ if para:
145
+ emit(" ".join(para))
146
+ para.clear()
147
+
148
+ def flush_item() -> None:
149
+ nonlocal item
150
+ if item is not None:
151
+ emit(" ".join(item))
152
+ item = None
153
+
154
+ while i < count:
155
+ line = lines[i]
156
+ i += 1
157
+ if not started:
158
+ if line.strip().startswith("\\begin{document}"):
159
+ started = True
160
+ continue
161
+ if not line.strip():
162
+ flush()
163
+ continue
164
+ if line.strip().startswith("\\end{document}"):
165
+ break
166
+ heading = _HEADING.match(line)
167
+ if heading:
168
+ flush()
169
+ flush_item()
170
+ joined, extra = line, 0
171
+ while joined.count("{") > joined.count("}") and i < count and extra < 3:
172
+ joined += " " + lines[i].strip()
173
+ i += 1
174
+ extra += 1
175
+ start = _HEADING.match(joined)
176
+ if start is None:
177
+ continue
178
+ kind, star = start.group(1), start.group(2)
179
+ text, _ = _braced(joined, start.end())
180
+ level = _LEVELS[kind]
181
+ if star:
182
+ unnumbered += 1
183
+ section_id = f"u{unnumbered}"
184
+ else:
185
+ counters[level] += 1
186
+ for deeper in range(level + 1, 4):
187
+ counters[deeper] = 0
188
+ first = _letter(counters[0]) if appendix else str(counters[0])
189
+ section_id = ".".join([first, *(str(n) for n in counters[1 : level + 1])])
190
+ sections.append(_Section(section_id, _plain(text, labels), level + 1, ""))
191
+ while i < count and not lines[i].strip():
192
+ i += 1
193
+ key = _LABEL.fullmatch(lines[i]) if i < count else None
194
+ if key:
195
+ sections[-1].anchor = key.group(1)
196
+ labels[key.group(1)] = section_id
197
+ i += 1
198
+ continue
199
+ if line.strip() == "\\appendix":
200
+ flush()
201
+ appendix = True
202
+ counters = [0, 0, 0, 0]
203
+ continue
204
+ begin = _BEGIN.match(line)
205
+ if begin and begin.group(1) in _RAW_ENVS:
206
+ flush()
207
+ flush_item()
208
+ env = begin.group(1)
209
+ raw = [line.strip()]
210
+ depth = 1
211
+ while i < count and depth:
212
+ nxt = lines[i]
213
+ i += 1
214
+ raw.append(nxt)
215
+ inner = _BEGIN.match(nxt)
216
+ if inner and inner.group(1) == env:
217
+ depth += 1
218
+ closing = _END.match(nxt)
219
+ if closing and closing.group(1) == env:
220
+ depth -= 1
221
+ emit(f"[{env}]\n" + "\n".join(raw), clean=True)
222
+ continue
223
+ if begin and begin.group(1) in _LIST_ENVS:
224
+ flush()
225
+ flush_item()
226
+ list_depth += 1
227
+ continue
228
+ end = _END.match(line)
229
+ if end and end.group(1) in _LIST_ENVS:
230
+ flush()
231
+ flush_item()
232
+ list_depth = max(0, list_depth - 1)
233
+ continue
234
+ entry = _ITEM.match(line) if list_depth else None
235
+ if entry:
236
+ flush()
237
+ flush_item()
238
+ item = ([f"{entry.group(1)}:"] if entry.group(1) is not None else []) + (
239
+ [entry.group(2)] if entry.group(2) else []
240
+ )
241
+ continue
242
+ if _LAYOUT.fullmatch(line):
243
+ continue
244
+ (item if item is not None else para).append(line.strip())
245
+ flush()
246
+ flush_item()
247
+ unknown: dict[str, int] = {}
248
+ built: list[Sec] = []
249
+ for section in sections:
250
+ units = tuple(t if clean else _plain(t, labels, unknown) for t, clean in section.units)
251
+ units = tuple(u for u in units if u.strip())
252
+ if section.id == "0" and not units:
253
+ continue
254
+ anchor = section.anchor or slugify(section.title) or section.id
255
+ built.append(Sec(section.id, section.title, section.depth, anchor, units))
256
+ warnings: list[str] = []
257
+ if unknown:
258
+ names = ", ".join(sorted(unknown, key=lambda k: (-unknown[k], k))[:12])
259
+ warnings.append(f"LaTeX commands dropped (their arguments kept): {names}")
260
+ if missing:
261
+ warnings.append(f"not in the archive selection: {', '.join(sorted(set(missing)))}")
262
+ return Doc(doc_id, title, Unit.BLOCK, _unique(built), {}, tuple(warnings))
263
+
264
+
265
+ def _unique(sections: list[Sec]) -> tuple[Sec, ...]:
266
+ seen: set[str] = set()
267
+ out: list[Sec] = []
268
+ for section in sections:
269
+ anchor, n = section.anchor, 0
270
+ while anchor in seen:
271
+ n += 1
272
+ anchor = f"{section.anchor}-{n}"
273
+ seen.add(anchor)
274
+ out.append(Sec(section.id, section.title, section.depth, anchor, section.units))
275
+ return tuple(out)
276
+
277
+
278
+ def _plain(text: str, labels: Mapping[str, str], unknown: dict[str, int] | None = None) -> str:
279
+ """Reduce LaTeX markup in `text` to its words (see the module docstring)."""
280
+ text = _REF.sub(lambda m: labels.get(m.group(1), m.group(1)), text)
281
+ text = re.sub(r"\\label\{[^{}]*\}", "", text)
282
+ text = re.sub(r"\\cite\{([^{}]*)\}", r"[\1]", text)
283
+ text = text.replace("\\\\", " ").replace("\\@", "").replace("\\,", " ").replace("\\ ", " ").replace("~", " ")
284
+ for command, plain in _SYMBOLS:
285
+ text = text.replace(command, plain)
286
+ for _ in range(_PASSES):
287
+
288
+ def one(match: re.Match[str]) -> str:
289
+ name, arg = match.group(1), match.group(2)
290
+ if name == "note":
291
+ return "Note: " + arg
292
+ if name in _DROP_ARG:
293
+ return ""
294
+ if name not in _KEEP_ARG and unknown is not None:
295
+ unknown[name] = unknown.get(name, 0) + 1
296
+ return arg
297
+
298
+ after = _ONE_ARG.sub(one, text)
299
+ if after == text:
300
+ break
301
+ text = after
302
+
303
+ def bare(match: re.Match[str]) -> str:
304
+ if unknown is not None:
305
+ unknown[match.group(1)] = unknown.get(match.group(1), 0) + 1
306
+ return ""
307
+
308
+ text = _BARE.sub(bare, text)
309
+ for escape in ("_", "&", "%", "$", "#"):
310
+ text = text.replace("\\" + escape, escape)
311
+ text = text.replace("\\{", "\x02").replace("\\}", "\x03").replace("{", "").replace("}", "")
312
+ text = text.replace("\x02", "{").replace("\x03", "}").replace("\x01", "\\")
313
+ text = text.replace("---", "\u2014").replace("--", "\u2013")
314
+ text = text.replace("``", '"').replace("''", '"')
315
+ return _SPACE.sub(" ", text).strip()
@@ -0,0 +1,173 @@
1
+ """A minimal Markdown reader for rpm's manual and Debian's autopkgtest specification.
2
+
3
+ Headings are ATX (`#` to `######`) or setext (`===` / `---` under a paragraph). Section ids are the ordinal path
4
+ (`2`, `2.1`); the anchor is the GitHub-style slug of the title, duplicates suffixed `-1`, `-2`. That slug is the
5
+ rule used here; this module does not claim CommonMark defines it. Text before the first heading is section `0`,
6
+ titled with the document title. YAML front matter (a first line `---`) is not a section; its `title:` is used
7
+ when no title is given.
8
+
9
+ Blocks (the unit): a paragraph (a run of non-blank lines), a fenced code block as one block with its fences, an
10
+ indented code block, a block quote, a table, and each **top-level** list item with everything nested inside it.
11
+ HTML comments are dropped (a `<!-- -->` hiding a `#` line hides it from the outline too); an unclosed fence or
12
+ comment runs to the end of the file, so no text is lost to a typo. Inline markup is left as written.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+
19
+ from primarysource.model import Unit
20
+ from primarysource.pkgdocs.model import Doc
21
+ from primarysource.pkgdocs.readers._common import Builder, decode
22
+
23
+ _FENCE = re.compile(r" {0,3}(`{3,}|~{3,})")
24
+ _ITEM = re.compile(r" {0,3}(?:[-*+]|\d{1,9}[.)])(?:[ \t]+\S|[ \t]*$)")
25
+ _RULE = re.compile(r" {0,3}(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$")
26
+ _SETEXT = re.compile(r" {0,3}(=+|-+)[ \t]*$")
27
+ _TABLE_SEP = re.compile(r"[ \t|:-]+$")
28
+ _LINK = re.compile(r"\[([^\]]*)\]\([^)]*\)")
29
+
30
+
31
+ def _atx(line: str) -> tuple[int, str] | None:
32
+ stripped = line.lstrip(" ")
33
+ if len(line) - len(stripped) > 3 or not stripped.startswith("#"):
34
+ return None
35
+ body = stripped.lstrip("#")
36
+ level = len(stripped) - len(body)
37
+ if level > 6 or (body and body[0] not in " \t"):
38
+ return None
39
+ title = body.strip()
40
+ if title.endswith("#"):
41
+ shorter = title.rstrip("#")
42
+ if not shorter or shorter[-1] in " \t":
43
+ title = shorter.strip()
44
+ return level, title
45
+
46
+
47
+ def _front_matter(lines: list[str]) -> tuple[int, str]:
48
+ """(first line after the front matter, its `title:`), or (0, "") when there is none."""
49
+ if not lines or lines[0].strip() != "---":
50
+ return 0, ""
51
+ for end in range(1, min(len(lines), 200)):
52
+ if lines[end].strip() in ("---", "..."):
53
+ title = ""
54
+ for line in lines[1:end]:
55
+ key, _, value = line.partition(":")
56
+ if key.strip() == "title" and value.strip():
57
+ title = value.strip().strip("\"'")
58
+ return end + 1, title
59
+ return 0, ""
60
+
61
+
62
+ def read_markdown(data: bytes, doc_id: str, title: str | None = None) -> Doc:
63
+ """Read one Markdown file. Raises `LibError` for NUL bytes or invalid UTF-8; everything else reads."""
64
+ text = decode(data, f"Markdown file {doc_id!r}")
65
+ lines = [ln.rstrip() for ln in text.expandtabs(4).split("\n")]
66
+ i, fm_title = _front_matter(lines)
67
+ doc_title = title or fm_title
68
+ builder = Builder()
69
+ first_h1 = ""
70
+ count = len(lines)
71
+ para: list[str] = []
72
+
73
+ def flush() -> None:
74
+ if para:
75
+ builder.unit("\n".join(para))
76
+ para.clear()
77
+
78
+ def heading(level: int, name: str) -> None:
79
+ nonlocal first_h1
80
+ name = _LINK.sub(chr(92) + "1", name)
81
+ first_h1 = first_h1 or (name if level == 1 else "")
82
+ builder.heading(level, name)
83
+
84
+ while i < count:
85
+ line = lines[i]
86
+ i += 1
87
+ if not line.strip():
88
+ flush()
89
+ continue
90
+ fence = _FENCE.match(line)
91
+ if fence:
92
+ flush()
93
+ marker = fence.group(1)
94
+ block = [line]
95
+ while i < count:
96
+ closing = lines[i].strip()
97
+ block.append(lines[i])
98
+ i += 1
99
+ if closing.startswith(marker[0] * len(marker)) and closing.strip(marker[0]) == "":
100
+ break
101
+ builder.unit("\n".join(block).rstrip("\n"))
102
+ continue
103
+ if line.lstrip().startswith("<!--"):
104
+ flush()
105
+ i -= 1
106
+ while i < count and "-->" not in lines[i]:
107
+ i += 1
108
+ i += 1
109
+ continue
110
+ atx = _atx(line)
111
+ if atx:
112
+ flush()
113
+ if atx[1]:
114
+ heading(*atx)
115
+ continue
116
+ setext = _SETEXT.match(line)
117
+ if setext and para:
118
+ name = " ".join(para)
119
+ para.clear()
120
+ heading(1 if setext.group(1)[0] == "=" else 2, name)
121
+ continue
122
+ if _RULE.match(line) and not para:
123
+ continue
124
+ if para:
125
+ starts = _ITEM.match(line) or line.lstrip().startswith(">") or "|" in line
126
+ if not starts:
127
+ para.append(line)
128
+ continue
129
+ flush()
130
+ if _ITEM.match(line):
131
+ item = [line]
132
+ while i < count:
133
+ nxt = lines[i]
134
+ if not nxt.strip():
135
+ j = i
136
+ while j < count and not lines[j].strip():
137
+ j += 1
138
+ if j < count and (len(lines[j]) - len(lines[j].lstrip(" "))) >= 2:
139
+ item.extend([""] * (j - i))
140
+ i = j
141
+ continue
142
+ break
143
+ indented = (len(nxt) - len(nxt.lstrip(" "))) >= 2
144
+ if not indented and (_ITEM.match(nxt) or _atx(nxt) or _FENCE.match(nxt) or _RULE.match(nxt)):
145
+ break
146
+ item.append(nxt)
147
+ i += 1
148
+ builder.unit("\n".join(item))
149
+ continue
150
+ if line.lstrip().startswith(">"):
151
+ block = [line]
152
+ while i < count and lines[i].strip() and (lines[i].lstrip().startswith(">") or not _atx(lines[i])):
153
+ block.append(lines[i])
154
+ i += 1
155
+ builder.unit("\n".join(block))
156
+ continue
157
+ if "|" in line and i < count and _TABLE_SEP.match(lines[i]) and "-" in lines[i] and "|" in lines[i]:
158
+ block = [line]
159
+ while i < count and lines[i].strip() and "|" in lines[i]:
160
+ block.append(lines[i])
161
+ i += 1
162
+ builder.unit("\n".join(block))
163
+ continue
164
+ if line.startswith(" ") and not para:
165
+ block = [line]
166
+ while i < count and (lines[i].startswith(" ") or not lines[i].strip()):
167
+ block.append(lines[i])
168
+ i += 1
169
+ builder.unit("\n".join(block).rstrip())
170
+ continue
171
+ para.append(line)
172
+ flush()
173
+ return builder.doc(doc_id, doc_title or first_h1 or doc_id, unit=Unit.BLOCK)
@@ -0,0 +1,132 @@
1
+ """A minimal POD reader for the dpkg manual pages, which are `.pod` files in dpkg's `man/` directory.
2
+
3
+ `=head1`..`=head4` are sections (level 1 to 4). Blocks, the unit: an ordinary paragraph, a verbatim (indented)
4
+ paragraph, and an `=item` with every paragraph up to the next item (so a tag and its description are one block,
5
+ the way a man page's `.TP` is). `=over` and `=back` only delimit lists; nesting is flattened. `=begin`/`=end`
6
+ regions and `=for` lines are dropped, as are `=encoding`, `=pod`, `=cut` and the `#` licence comment that dpkg's
7
+ files carry before the first command.
8
+
9
+ Formatting codes (`B<>`, `I<>`, `C<>`, `F<>`, `S<>`, `L<>`, `E<>`, `X<>`, `Z<>`) are resolved to their text,
10
+ including the `C<< ... >>` form, in a fixed number of passes. A paragraph over `MAX_CODED` characters is left
11
+ unresolved and the document carries a warning, so a hostile run of `C<<` costs time linear in the input.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+
18
+ from primarysource.model import Unit
19
+ from primarysource.pkgdocs.model import Doc
20
+ from primarysource.pkgdocs.readers._common import Builder, decode
21
+
22
+ MAX_CODED = 20_000
23
+ _PASSES = 12
24
+ _ENTITIES = {"lt": "<", "gt": ">", "verbar": "|", "sol": "/", "amp": "&", "quot": '"', "apos": "'", "nbsp": " "}
25
+ _WIDE = re.compile(r"([A-Z])(<{2,}) ([^<>]*?) (>{2,})")
26
+ _NARROW = re.compile(r"([A-Z])<([^<>]*)>")
27
+ _CMD = re.compile(r"=([a-z]+\d?)(?:[ \t]+(.*))?$", re.DOTALL)
28
+
29
+
30
+ def _entity(name: str) -> str:
31
+ if name in _ENTITIES:
32
+ return _ENTITIES[name]
33
+ if name.isdigit():
34
+ return chr(int(name)) if 0 < int(name) < 0x110000 else ""
35
+ if name.lower().startswith("0x") and re.fullmatch(r"0[xX][0-9a-fA-F]{1,6}", name):
36
+ return chr(int(name, 16)) if int(name, 16) < 0x110000 else ""
37
+ return f"&{name};"
38
+
39
+
40
+ def _code(match: re.Match[str]) -> str:
41
+ kind, inner = match.group(1), match.group(2) if match.re is _NARROW else match.group(3)
42
+ if kind == "E":
43
+ return _entity(inner.strip())
44
+ if kind in ("X", "Z"):
45
+ return ""
46
+ if kind == "L":
47
+ label = inner.split("|", 1)[0] if "|" in inner else inner
48
+ return label
49
+ if kind in ("B", "I", "C", "F", "S", "U"):
50
+ return inner
51
+ return match.group(0)
52
+
53
+
54
+ def resolve_codes(text: str) -> str:
55
+ """Resolve POD formatting codes to their text. Unknown letters are left as written."""
56
+ for _ in range(_PASSES):
57
+ after = _NARROW.sub(_code, _WIDE.sub(_code, text))
58
+ if after == text:
59
+ break
60
+ text = after
61
+ return text
62
+
63
+
64
+ def read_pod(data: bytes, doc_id: str, title: str | None = None) -> Doc:
65
+ """Read one POD file. Raises `LibError` for NUL bytes or invalid UTF-8."""
66
+ text = decode(data, f"POD file {doc_id!r}")
67
+ builder = Builder()
68
+ uncoded = 0
69
+ paragraphs = re.split(r"\n[ \t]*\n+", text.strip("\n"))
70
+ item: list[str] | None = None
71
+ skip_region = False
72
+ doc_title = title or ""
73
+ in_name = False
74
+ started = False # the first `=` command seen
75
+
76
+ def emit(block: str) -> None:
77
+ nonlocal item
78
+ if item is not None:
79
+ item.append(block)
80
+ else:
81
+ builder.unit(block)
82
+
83
+ def close_item() -> None:
84
+ nonlocal item
85
+ if item is not None:
86
+ builder.unit("\n\n".join(item))
87
+ item = None
88
+
89
+ def coded(block: str) -> str:
90
+ nonlocal uncoded
91
+ if len(block) > MAX_CODED:
92
+ uncoded += 1
93
+ return block
94
+ return resolve_codes(block)
95
+
96
+ for paragraph in paragraphs:
97
+ if not started and not paragraph.startswith("="):
98
+ lines = [ln for ln in paragraph.split("\n") if not ln.startswith("#")]
99
+ if not lines:
100
+ continue # the licence comment block
101
+ paragraph = "\n".join(lines)
102
+ command = _CMD.match(paragraph) if paragraph.startswith("=") else None
103
+ if skip_region:
104
+ skip_region = not (command and command.group(1) == "end")
105
+ continue
106
+ if command:
107
+ started = True
108
+ name, argument = command.group(1), (command.group(2) or "").strip()
109
+ if name.startswith("head") and name[4:].isdigit():
110
+ close_item()
111
+ title_text = coded(argument.replace("\n", " "))
112
+ in_name = title_text.strip().upper() == "NAME"
113
+ builder.heading(int(name[4:]), title_text)
114
+ elif name == "item":
115
+ close_item()
116
+ item = [coded(argument)]
117
+ elif name in ("back",):
118
+ close_item()
119
+ elif name == "begin":
120
+ skip_region = True
121
+ continue
122
+ if paragraph.lstrip().startswith("="):
123
+ continue
124
+ body = coded(paragraph.rstrip()) if paragraph[:1] not in " \t" else paragraph.rstrip()
125
+ if in_name and not doc_title:
126
+ doc_title = " ".join(body.split())
127
+ in_name = False
128
+ emit(body)
129
+ close_item()
130
+ warnings = (f"{uncoded} paragraphs over {MAX_CODED} characters left unresolved",) if uncoded else ()
131
+ builder.set_preamble_title(doc_title or doc_id)
132
+ return builder.doc(doc_id, doc_title or doc_id, unit=Unit.BLOCK, warnings=warnings)