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,239 @@
1
+ """A minimal reStructuredText reader for PEPs, Debian Policy, GLEPs and the kernel's Kconfig documents.
2
+
3
+ Sections follow the docutils rule: a title is a text line with an underline (or an underline and an overline of
4
+ the same character) at least as long as the title, and the level of a style is the order in which the style first
5
+ appears in the document, not the character used. A document whose first heading is the only one of its style
6
+ (a chapter title above its sections) has that heading promoted to the document title; with `base` it opens the
7
+ numbered root section, so chapter 5's subsections are `5.1`, `5.1.1`.
8
+
9
+ Blocks (the unit, so `¶n` counts blocks):
10
+
11
+ - a paragraph; a block quote; a literal block (the indented text after a paragraph ending in `::`);
12
+ - a list item (`-`, `*`, `+`, `1.`, `#.`, `(a)`) or a field (`:name: value`) with its indented continuation;
13
+ - a definition-list item (a term and its indented definition);
14
+ - a directive (`.. name:: args` and its indented body) as one block, written `[name] args` then the body;
15
+ - a footnote or citation (`.. [#]`), written `[footnote]`; a grid or simple table.
16
+
17
+ Comments (`.. text`), substitution definitions (`.. |x| ...`), hyperlink targets and transitions are dropped. A
18
+ target `.. _label:` immediately before a heading becomes that section's anchor, the way Sphinx labels are used;
19
+ otherwise the anchor is the GitHub-style slug of the title.
20
+
21
+ Nothing here follows `.. include::`, `.. literalinclude::` or `.. raw::`: they are inert text, and no path a
22
+ document names is ever opened. There is no recursion, so deep indentation costs time linear in the input.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from dataclasses import dataclass
29
+
30
+ from primarysource.model import Unit
31
+ from primarysource.pkgdocs.model import Doc
32
+ from primarysource.pkgdocs.readers._common import Builder, clean_title, decode, slugify
33
+
34
+ _PUNCT = frozenset("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~")
35
+ _MARKER = re.compile(r"(?:[-*+•‣⁃](?:\s+\S|$)|(?:\d+|[a-zA-Z]|#)[.)]\s+\S|\((?:\d+|[a-zA-Z]|#)\)\s+\S)")
36
+ _FIELD = re.compile(r":[^\s:][^:]*:(?:\s|$)")
37
+ _DIRECTIVE = re.compile(r"\.\. +([A-Za-z][\w:+.-]*)::(?: +(.*))?$")
38
+ _TARGET = re.compile(r"\.\. +_(`[^`]+`|[^:`]+):(?: +(.*))?$")
39
+ _FOOTNOTE = re.compile(r"\.\. +\[([^\]]*)\](?: +(.*))?$")
40
+ _SUBST = re.compile(r"\.\. +\|[^|]+\|")
41
+ _SIMPLE_BORDER = re.compile(r"=+(?: +=+)+$")
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class _Heading:
46
+ style: str
47
+ title: str
48
+ anchor: str
49
+
50
+
51
+ def _adornment(line: str) -> str | None:
52
+ """The style character if `line` is one punctuation character repeated at least twice, else None."""
53
+ if len(line) >= 2 and line[0] in _PUNCT and line.count(line[0]) == len(line):
54
+ return line[0]
55
+ return None
56
+
57
+
58
+ def _indent(line: str) -> int:
59
+ return len(line) - len(line.lstrip(" "))
60
+
61
+
62
+ def _dedent(lines: list[str]) -> str:
63
+ live = [line for line in lines if line.strip()]
64
+ if not live:
65
+ return ""
66
+ margin = min(_indent(line) for line in live)
67
+ return "\n".join(line[margin:] if line.strip() else "" for line in lines).strip("\n")
68
+
69
+
70
+ def _take_indented(lines: list[str], i: int) -> tuple[list[str], int]:
71
+ """Lines from `i` that are indented, plus blank lines that are followed by more indented lines."""
72
+ body: list[str] = []
73
+ count = len(lines)
74
+ while i < count:
75
+ line = lines[i]
76
+ if line.strip() and _indent(line) > 0:
77
+ body.append(line)
78
+ i += 1
79
+ continue
80
+ if not line.strip():
81
+ j = i
82
+ while j < count and not lines[j].strip():
83
+ j += 1
84
+ if j < count and _indent(lines[j]) > 0:
85
+ body.extend([""] * (j - i))
86
+ i = j
87
+ continue
88
+ break
89
+ return body, i
90
+
91
+
92
+ def _parse(lines: list[str]) -> list[_Heading | str]:
93
+ events: list[_Heading | str] = []
94
+ pending_label = ""
95
+ count = len(lines)
96
+ i = 0
97
+ while i < count:
98
+ line = lines[i]
99
+ if not line.strip():
100
+ i += 1
101
+ continue
102
+ if _indent(line) > 0: # a block quote
103
+ body, i = _take_indented(lines, i)
104
+ events.append(_dedent(body))
105
+ pending_label = ""
106
+ continue
107
+ # headings: text over an underline, or an overlined title
108
+ style = _adornment(line)
109
+ if style and i + 2 < count and lines[i + 1].strip() and lines[i + 2].rstrip() == line:
110
+ title = lines[i + 1].strip()
111
+ if _indent(lines[i + 1]) == 0 and _adornment(lines[i + 1]) is None and len(line) >= len(title):
112
+ events.append(_Heading(style + "o", title, pending_label or slugify(title)))
113
+ pending_label = ""
114
+ i += 3
115
+ continue
116
+ if style is None and i + 1 < count and not line.startswith(".."):
117
+ under = _adornment(lines[i + 1])
118
+ if under and len(lines[i + 1]) >= len(line.strip()) and _MARKER.match(line) is None:
119
+ title = line.strip()
120
+ events.append(_Heading(under, title, pending_label or slugify(title)))
121
+ pending_label = ""
122
+ i += 2
123
+ continue
124
+ if style and len(line) >= 4 and (i + 1 >= count or not lines[i + 1].strip()):
125
+ i += 1 # a transition
126
+ continue
127
+ if line.startswith(".."):
128
+ i, pending_label = _explicit(lines, i, events, pending_label)
129
+ continue
130
+ if line.startswith(("+-", "+=")): # a grid table
131
+ start = i
132
+ while i < count and lines[i].strip():
133
+ i += 1
134
+ events.append("\n".join(lines[start:i]))
135
+ pending_label = ""
136
+ continue
137
+ if _SIMPLE_BORDER.match(line):
138
+ start, borders = i, 0
139
+ while i < count:
140
+ if _SIMPLE_BORDER.match(lines[i]):
141
+ borders += 1
142
+ if not lines[i].strip() and borders >= 2:
143
+ break
144
+ i += 1
145
+ events.append("\n".join(lines[start:i]).rstrip())
146
+ pending_label = ""
147
+ continue
148
+ # a paragraph, list item, field or definition item
149
+ start = i
150
+ first = _MARKER.match(line) is not None or _FIELD.match(line) is not None
151
+ i += 1
152
+ if not first:
153
+ while i < count and lines[i].strip() and _indent(lines[i]) == 0:
154
+ if _MARKER.match(lines[i]) or _FIELD.match(lines[i]):
155
+ break
156
+ i += 1
157
+ text_lines = lines[start:i]
158
+ if i < count and lines[i].strip() and _indent(lines[i]) > 0: # a definition or a list continuation
159
+ body, i = _take_indented(lines, i)
160
+ text_lines = [*text_lines, *body]
161
+ events.append(_dedent(text_lines) if first else "\n".join(text_lines).strip("\n"))
162
+ elif text_lines[-1].rstrip().endswith("::"):
163
+ paragraph = "\n".join(text_lines)
164
+ if paragraph.strip() != "::":
165
+ events.append(paragraph)
166
+ body, i = _take_indented(lines, _skip_blank(lines, i))
167
+ if body:
168
+ events.append(_dedent(body))
169
+ else:
170
+ events.append("\n".join(text_lines))
171
+ pending_label = ""
172
+ return events
173
+
174
+
175
+ def _skip_blank(lines: list[str], i: int) -> int:
176
+ while i < len(lines) and not lines[i].strip():
177
+ i += 1
178
+ return i
179
+
180
+
181
+ def _explicit(lines: list[str], i: int, events: list[_Heading | str], label: str) -> tuple[int, str]:
182
+ """An explicit markup block (`.. ...`): returns the next index and the label still pending."""
183
+ head = lines[i]
184
+ body, nxt = _take_indented(lines, i + 1)
185
+ target = _TARGET.match(head)
186
+ if target:
187
+ name = target.group(1).strip("`")
188
+ if not (target.group(2) or "").strip() and not body:
189
+ return nxt, name
190
+ return nxt, label
191
+ directive = _DIRECTIVE.match(head)
192
+ if directive:
193
+ text = f"[{directive.group(1)}]" + (f" {directive.group(2).strip()}" if directive.group(2) else "")
194
+ if body:
195
+ text += "\n" + _dedent(body)
196
+ events.append(text)
197
+ return nxt, ""
198
+ footnote = _FOOTNOTE.match(head)
199
+ if footnote:
200
+ first = (footnote.group(2) or "").strip()
201
+ text = f"[footnote {footnote.group(1)}] " + first
202
+ if body:
203
+ text += "\n" + _dedent(body)
204
+ events.append(text.rstrip())
205
+ return nxt, ""
206
+ if _SUBST.match(head):
207
+ return nxt, label
208
+ return nxt, label # a comment: dropped
209
+
210
+
211
+ def read_rst(data: bytes, doc_id: str, title: str | None = None, *, base: str = "") -> Doc:
212
+ """Read one reStructuredText file. Raises `LibError` for NUL bytes or invalid UTF-8; everything else reads."""
213
+ text = decode(data, f"reStructuredText file {doc_id!r}")
214
+ lines = [line.rstrip() for line in text.expandtabs(8).split("\n")]
215
+ events = _parse(lines)
216
+ headings = [e for e in events if isinstance(e, _Heading)]
217
+ promote = bool(headings) and events[0] is headings[0] and sum(h.style == headings[0].style for h in headings) == 1
218
+ order: list[str] = []
219
+ for heading in headings[1 if promote else 0 :]:
220
+ if heading.style not in order:
221
+ order.append(heading.style)
222
+ builder = Builder(base)
223
+ doc_title = title or ""
224
+ rest = events
225
+ if promote:
226
+ first = headings[0]
227
+ doc_title = title or clean_title(first.title)
228
+ builder.root(first.title, first.anchor)
229
+ rest = events[1:]
230
+ else:
231
+ builder.set_preamble_title(doc_title or doc_id)
232
+ if base:
233
+ builder.root(doc_title or doc_id)
234
+ for event in rest:
235
+ if isinstance(event, _Heading):
236
+ builder.heading(order.index(event.style) + 1, event.title, event.anchor)
237
+ else:
238
+ builder.unit(event)
239
+ return builder.doc(doc_id, doc_title or doc_id, unit=Unit.BLOCK)
@@ -0,0 +1,108 @@
1
+ """A minimal scdoc reader for rpm's manual pages (`docs/man/*.scd`).
2
+
3
+ scdoc is the small man-page markup rpm uses in place of roff. The first line is `NAME(section)` (optionally with
4
+ quoted footer strings); `#`, `##` and `###` start sections; `;` at the start of a line is a comment.
5
+
6
+ Blocks (the unit): a paragraph, a list item (`- ` or `. ` with its indented continuation and nested items), a
7
+ literal block (a fenced ``` block, or lines indented by a tab or four spaces) and a table (`[[` to the blank
8
+ line). Inline markup (`*bold*`, `_underline_`, `\\` escapes) is left as written.
9
+
10
+ The document title is the header line, lower-cased: `rpm-macros(7)`. The collection adds the NAME description.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+
17
+ from primarysource.model import Unit
18
+ from primarysource.pkgdocs.model import Doc
19
+ from primarysource.pkgdocs.readers._common import Builder, decode
20
+
21
+ _HEADER = re.compile(r"([A-Za-z0-9_.+-]+)\((\d)\)(?:\s+\"[^\"]*\"){0,2}")
22
+ _ITEM = re.compile(r"(?:-|\.)\s+\S")
23
+ _HEADING = re.compile(r"(#{1,3})\s+(\S.*)")
24
+
25
+
26
+ def read_scdoc(data: bytes, doc_id: str, title: str | None = None) -> Doc:
27
+ """Read one scdoc file. Raises `LibError` for NUL bytes or invalid UTF-8; everything else reads."""
28
+ text = decode(data, f"scdoc file {doc_id!r}")
29
+ lines = [ln.rstrip() for ln in text.expandtabs(4).split("\n")]
30
+ count = len(lines)
31
+ i = 0
32
+ doc_title = title or ""
33
+ while i < count and (not lines[i].strip() or lines[i].startswith(";")):
34
+ i += 1
35
+ header = _HEADER.fullmatch(lines[i].strip()) if i < count else None
36
+ if header:
37
+ doc_title = doc_title or f"{header.group(1).lower()}({header.group(2)})"
38
+ i += 1
39
+ builder = Builder()
40
+ builder.set_preamble_title(doc_title or doc_id)
41
+ para: list[str] = []
42
+
43
+ def flush() -> None:
44
+ if para:
45
+ builder.unit("\n".join(para))
46
+ para.clear()
47
+
48
+ while i < count:
49
+ line = lines[i]
50
+ i += 1
51
+ if line.startswith(";"):
52
+ continue
53
+ if not line.strip():
54
+ flush()
55
+ continue
56
+ if line.startswith("```"):
57
+ flush()
58
+ block = [line]
59
+ while i < count:
60
+ block.append(lines[i])
61
+ i += 1
62
+ if block[-1].startswith("```"):
63
+ break
64
+ builder.unit("\n".join(block))
65
+ continue
66
+ heading = _HEADING.fullmatch(line)
67
+ if heading:
68
+ flush()
69
+ builder.heading(len(heading.group(1)), heading.group(2))
70
+ continue
71
+ if line.startswith("[["):
72
+ flush()
73
+ block = [line]
74
+ while i < count and lines[i].strip():
75
+ block.append(lines[i])
76
+ i += 1
77
+ builder.unit("\n".join(block))
78
+ continue
79
+ if line.startswith(" ") and not para:
80
+ block = [line]
81
+ while i < count and (lines[i].startswith(" ") or not lines[i].strip()):
82
+ block.append(lines[i])
83
+ i += 1
84
+ builder.unit("\n".join(block).rstrip())
85
+ continue
86
+ if _ITEM.match(line):
87
+ flush()
88
+ item = [line]
89
+ while i < count:
90
+ nxt = lines[i]
91
+ if nxt.strip() and (nxt.startswith(" ") or not _ITEM.match(nxt) and not _HEADING.fullmatch(nxt)):
92
+ item.append(nxt)
93
+ i += 1
94
+ continue
95
+ if not nxt.strip():
96
+ j = i
97
+ while j < count and not lines[j].strip():
98
+ j += 1
99
+ if j < count and lines[j].startswith(" "):
100
+ item.extend([""] * (j - i))
101
+ i = j
102
+ continue
103
+ break
104
+ builder.unit("\n".join(item))
105
+ continue
106
+ para.append(line)
107
+ flush()
108
+ return builder.doc(doc_id, doc_title or doc_id, unit=Unit.BLOCK)