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.
- primarysource/__init__.py +5 -0
- primarysource/bbf/__init__.py +56 -0
- primarysource/bbf/_xml.py +63 -0
- primarysource/bbf/catalogue.json +777 -0
- primarysource/bbf/catalogue.py +137 -0
- primarysource/bbf/cli.py +79 -0
- primarysource/bbf/datamodel.py +281 -0
- primarysource/bbf/importer.py +151 -0
- primarysource/bbf/library.py +380 -0
- primarysource/bbf/server.py +72 -0
- primarysource/budget.py +63 -0
- primarysource/c2sp/__init__.py +53 -0
- primarysource/c2sp/_fetch.py +5 -0
- primarysource/c2sp/catalogue.json +231 -0
- primarysource/c2sp/catalogue.py +95 -0
- primarysource/c2sp/cli.py +62 -0
- primarysource/c2sp/importer.py +90 -0
- primarysource/c2sp/library.py +309 -0
- primarysource/c2sp/markdown.py +31 -0
- primarysource/c2sp/server.py +68 -0
- primarysource/cavp/__init__.py +42 -0
- primarysource/cavp/cli.py +84 -0
- primarysource/cavp/data/vectors.toml +47 -0
- primarysource/cavp/importer.py +156 -0
- primarysource/cavp/model.py +279 -0
- primarysource/cavp/parse.py +269 -0
- primarysource/cavp/paths.py +17 -0
- primarysource/cavp/server.py +67 -0
- primarysource/cavp/tools.py +169 -0
- primarysource/cli.py +107 -0
- primarysource/cursor.py +82 -0
- primarysource/cve/__init__.py +5 -0
- primarysource/cve/__main__.py +6 -0
- primarysource/cve/cli.py +144 -0
- primarysource/cve/collection.py +20 -0
- primarysource/cve/config.py +58 -0
- primarysource/cve/errors.py +16 -0
- primarysource/cve/kernel.py +232 -0
- primarysource/cve/mcp.py +139 -0
- primarysource/cve/parse.py +318 -0
- primarysource/cve/py.typed +0 -0
- primarysource/cve/service.py +760 -0
- primarysource/cve/store.py +214 -0
- primarysource/cve/sync.py +438 -0
- primarysource/cve/views.py +304 -0
- primarysource/debpolicy/__init__.py +16 -0
- primarysource/debpolicy/catalogue.py +263 -0
- primarysource/debpolicy/cli.py +12 -0
- primarysource/debpolicy/server.py +11 -0
- primarysource/draft/__init__.py +11 -0
- primarysource/draft/catalogue.py +117 -0
- primarysource/draft/cli.py +149 -0
- primarysource/draft/collection.py +21 -0
- primarysource/draft/datatracker.py +158 -0
- primarysource/draft/importer.py +163 -0
- primarysource/draft/licence.py +89 -0
- primarysource/draft/names.py +64 -0
- primarysource/draft/net.py +133 -0
- primarysource/draft/segment.py +192 -0
- primarysource/draft/server.py +91 -0
- primarysource/draft/status.py +146 -0
- primarysource/draft/store.py +199 -0
- primarysource/draft/tracked.json +104 -0
- primarysource/draft/views.py +430 -0
- primarysource/draft/xmlmeta.py +74 -0
- primarysource/ecma/__init__.py +52 -0
- primarysource/ecma/_pdf.py +143 -0
- primarysource/ecma/catalogue.json +22 -0
- primarysource/ecma/catalogue.py +100 -0
- primarysource/ecma/cli.py +67 -0
- primarysource/ecma/identify.py +25 -0
- primarysource/ecma/importer.py +108 -0
- primarysource/ecma/library.py +240 -0
- primarysource/ecma/server.py +67 -0
- primarysource/errors.py +29 -0
- primarysource/fetch.py +360 -0
- primarysource/fsutil.py +150 -0
- primarysource/gitdocs/__init__.py +15 -0
- primarysource/gitdocs/catalogue.py +116 -0
- primarysource/gitdocs/cli.py +12 -0
- primarysource/gitdocs/server.py +8 -0
- primarysource/home.py +65 -0
- primarysource/iana/__init__.py +32 -0
- primarysource/iana/catalogue.json +244 -0
- primarysource/iana/catalogue.py +69 -0
- primarysource/iana/cli.py +69 -0
- primarysource/iana/importer.py +157 -0
- primarysource/iana/model.py +325 -0
- primarysource/iana/reader.py +315 -0
- primarysource/iana/server.py +67 -0
- primarysource/iana/store.py +83 -0
- primarysource/iana/xmlsafe.py +47 -0
- primarysource/identify.py +186 -0
- primarysource/ieee/__init__.py +58 -0
- primarysource/ieee/_html.py +49 -0
- primarysource/ieee/catalogue.json +74 -0
- primarysource/ieee/catalogue.py +123 -0
- primarysource/ieee/cli.py +79 -0
- primarysource/ieee/identify.py +24 -0
- primarysource/ieee/ieee802.py +47 -0
- primarysource/ieee/importer.py +305 -0
- primarysource/ieee/posix.py +233 -0
- primarysource/ieee/server.py +79 -0
- primarysource/index.py +446 -0
- primarysource/iso/__init__.py +51 -0
- primarysource/iso/cli.py +98 -0
- primarysource/iso/designation.py +95 -0
- primarysource/iso/identify.py +100 -0
- primarysource/iso/importer.py +224 -0
- primarysource/iso/library.py +249 -0
- primarysource/iso/server.py +69 -0
- primarysource/itu/__init__.py +41 -0
- primarysource/itu/__main__.py +8 -0
- primarysource/itu/asn1mod.py +478 -0
- primarysource/itu/catalog.py +472 -0
- primarysource/itu/cli.py +445 -0
- primarysource/itu/collection.py +20 -0
- primarysource/itu/corpus.py +118 -0
- primarysource/itu/errata.py +16 -0
- primarysource/itu/formal.py +565 -0
- primarysource/itu/identify.py +26 -0
- primarysource/itu/index.py +490 -0
- primarysource/itu/mcp.py +474 -0
- primarysource/itu/py.typed +0 -0
- primarysource/itu/sources.py +600 -0
- primarysource/itu/text.py +609 -0
- primarysource/kconfig/__init__.py +16 -0
- primarysource/kconfig/catalogue.py +98 -0
- primarysource/kconfig/cli.py +12 -0
- primarysource/kconfig/server.py +8 -0
- primarysource/model.py +63 -0
- primarysource/nist/__init__.py +5 -0
- primarysource/nist/collection.py +46 -0
- primarysource/nist/data/acquired-nist.json +1657 -0
- primarysource/nist/data/nist-catalog.json +12090 -0
- primarysource/nist/data/sources.toml +1996 -0
- primarysource/nist/identify.py +41 -0
- primarysource/nist/ingest/__init__.py +1 -0
- primarysource/nist/ingest/acquire_nist.py +128 -0
- primarysource/nist/ingest/controls.py +82 -0
- primarysource/nist/ingest/discover_nist.py +218 -0
- primarysource/nist/ingest/extract.py +48 -0
- primarysource/nist/ingest/fetch.py +211 -0
- primarysource/nist/ingest/index.py +14 -0
- primarysource/nist/ingest/oscal.py +480 -0
- primarysource/nist/ingest/prose_layouts.py +236 -0
- primarysource/nist/ingest/sources.py +94 -0
- primarysource/nist/ingest/status.py +255 -0
- primarysource/nist/models.py +7 -0
- primarysource/nist/paths.py +33 -0
- primarysource/nist/server/__init__.py +3 -0
- primarysource/nist/server/corpus.py +38 -0
- primarysource/nist/server/mcp.py +19 -0
- primarysource/nist/server/tools.py +35 -0
- primarysource/nist/spec.py +38 -0
- primarysource/pdfcorpus/__init__.py +16 -0
- primarysource/pdfcorpus/corpus.py +159 -0
- primarysource/pdfcorpus/fts.py +175 -0
- primarysource/pdfcorpus/index.py +70 -0
- primarysource/pdfcorpus/models.py +30 -0
- primarysource/pdfcorpus/server.py +96 -0
- primarysource/pdfcorpus/spec.py +37 -0
- primarysource/pdfcorpus/split.py +391 -0
- primarysource/pdfcorpus/tools.py +244 -0
- primarysource/pep/__init__.py +15 -0
- primarysource/pep/catalogue.py +191 -0
- primarysource/pep/cli.py +12 -0
- primarysource/pep/server.py +8 -0
- primarysource/pkgdocs/__init__.py +19 -0
- primarysource/pkgdocs/fetch.py +356 -0
- primarysource/pkgdocs/index.py +89 -0
- primarysource/pkgdocs/model.py +76 -0
- primarysource/pkgdocs/readers/__init__.py +1 -0
- primarysource/pkgdocs/readers/_common.py +134 -0
- primarysource/pkgdocs/readers/asciidoc.py +160 -0
- primarysource/pkgdocs/readers/latex.py +315 -0
- primarysource/pkgdocs/readers/markdown.py +173 -0
- primarysource/pkgdocs/readers/pod.py +132 -0
- primarysource/pkgdocs/readers/rst.py +239 -0
- primarysource/pkgdocs/readers/scdoc.py +108 -0
- primarysource/pkgdocs/runtime.py +552 -0
- primarysource/pkgdocs/serving.py +158 -0
- primarysource/pkgdocs/store.py +167 -0
- primarysource/pms/__init__.py +16 -0
- primarysource/pms/catalogue.py +88 -0
- primarysource/pms/cli.py +12 -0
- primarysource/pms/server.py +8 -0
- primarysource/py.typed +0 -0
- primarysource/readers/__init__.py +10 -0
- primarysource/readers/bookmarks.py +192 -0
- primarysource/readers/html.py +1312 -0
- primarysource/readers/markdown.py +189 -0
- primarysource/readers/pdf.py +187 -0
- primarysource/readers/xml.py +230 -0
- primarysource/refs.py +87 -0
- primarysource/registry.py +161 -0
- primarysource/render.py +49 -0
- primarysource/rfc/__init__.py +3 -0
- primarysource/rfc/__main__.py +6 -0
- primarysource/rfc/cite.py +204 -0
- primarysource/rfc/cli.py +903 -0
- primarysource/rfc/collection.py +19 -0
- primarysource/rfc/config.py +138 -0
- primarysource/rfc/errata.py +203 -0
- primarysource/rfc/fsutil.py +82 -0
- primarysource/rfc/graph.py +213 -0
- primarysource/rfc/index/__init__.py +1 -0
- primarysource/rfc/index/chunker.py +281 -0
- primarysource/rfc/index/embed.py +166 -0
- primarysource/rfc/index/hybrid.py +305 -0
- primarysource/rfc/index/lexical.py +223 -0
- primarysource/rfc/index/rfcxml.py +673 -0
- primarysource/rfc/index/search.py +508 -0
- primarysource/rfc/index/txtseg.py +585 -0
- primarysource/rfc/library.py +294 -0
- primarysource/rfc/mcp/__init__.py +1 -0
- primarysource/rfc/mcp/server.py +381 -0
- primarysource/rfc/packs/__init__.py +1 -0
- primarysource/rfc/packs/builder.py +228 -0
- primarysource/rfc/packs/collections/all-current-txt.json +10 -0
- primarysource/rfc/packs/collections/essentials.json +19 -0
- primarysource/rfc/packs/collections/fyi-current.json +10 -0
- primarysource/rfc/packs/collections/standards-current.json +10 -0
- primarysource/rfc/packs/collections/year-2025.json +10 -0
- primarysource/rfc/packs/collections/year-2026.json +10 -0
- primarysource/rfc/packs/collections.py +163 -0
- primarysource/rfc/packs/docsetexport.py +163 -0
- primarysource/rfc/packs/epub.py +159 -0
- primarysource/rfc/packs/export.py +306 -0
- primarysource/rfc/packs/htmlexport.py +82 -0
- primarysource/rfc/packs/serve.py +143 -0
- primarysource/rfc/packs/zimexport.py +171 -0
- primarysource/rfc/query.py +466 -0
- primarysource/rfc/subjects.py +457 -0
- primarysource/rfc/sync/__init__.py +1 -0
- primarysource/rfc/sync/downloader.py +132 -0
- primarysource/rfc/sync/manifest.py +162 -0
- primarysource/rfc/sync/scanner.py +49 -0
- primarysource/rfc/sync/state.py +464 -0
- primarysource/rfc/views.py +1067 -0
- primarysource/rpmdocs/__init__.py +15 -0
- primarysource/rpmdocs/catalogue.py +117 -0
- primarysource/rpmdocs/cli.py +12 -0
- primarysource/rpmdocs/server.py +8 -0
- primarysource/scaffold.py +223 -0
- primarysource/serve.py +75 -0
- primarysource/settings.py +152 -0
- primarysource/usb/__init__.py +5 -0
- primarysource/usb/collection.py +37 -0
- primarysource/usb/data/acquired.json +2025 -0
- primarysource/usb/data/sources-usb.toml +852 -0
- primarysource/usb/data/sources.toml +10 -0
- primarysource/usb/data/usb-catalog.json +1663 -0
- primarysource/usb/data/usb-classification.json +1986 -0
- primarysource/usb/data/usb-corpus-build.json +608 -0
- primarysource/usb/data/usb-pdf-choice.json +839 -0
- primarysource/usb/data/usb-split-measure.json +1803 -0
- primarysource/usb/ingest/__init__.py +1 -0
- primarysource/usb/ingest/acquire.py +130 -0
- primarysource/usb/ingest/discover_usb.py +88 -0
- primarysource/usb/ingest/extract.py +48 -0
- primarysource/usb/ingest/fetch.py +197 -0
- primarysource/usb/ingest/index.py +14 -0
- primarysource/usb/ingest/sources.py +85 -0
- primarysource/usb/models.py +7 -0
- primarysource/usb/paths.py +33 -0
- primarysource/usb/server/__init__.py +3 -0
- primarysource/usb/server/corpus.py +38 -0
- primarysource/usb/server/mcp.py +19 -0
- primarysource/usb/server/tools.py +35 -0
- primarysource/usb/spec.py +20 -0
- primarysource/usbids/__init__.py +56 -0
- primarysource/usbids/cli.py +65 -0
- primarysource/usbids/importer.py +272 -0
- primarysource/usbids/parse.py +283 -0
- primarysource/usbids/server.py +55 -0
- primarysource/usbids/store.py +80 -0
- primarysource/usbids/tools.py +174 -0
- primarysource/zim/__init__.py +5 -0
- primarysource/zim/__main__.py +8 -0
- primarysource/zim/_libzim.py +37 -0
- primarysource/zim/catalog.py +594 -0
- primarysource/zim/cli.py +555 -0
- primarysource/zim/collection.py +41 -0
- primarysource/zim/errors.py +100 -0
- primarysource/zim/groups.py +30 -0
- primarysource/zim/library.py +668 -0
- primarysource/zim/manage.py +636 -0
- primarysource/zim/markdown.py +1247 -0
- primarysource/zim/mcp.py +201 -0
- primarysource/zim/purl.py +130 -0
- primarysource/zim/py.typed +0 -0
- primarysource/zim/refs.py +90 -0
- primarysource/zim/service.py +1182 -0
- primarysource/zim/snapshot.py +197 -0
- primarysource/zim/sync.py +269 -0
- primarysource/zim/views.py +230 -0
- primarysource-0.1.0.dist-info/METADATA +118 -0
- primarysource-0.1.0.dist-info/RECORD +303 -0
- primarysource-0.1.0.dist-info/WHEEL +4 -0
- primarysource-0.1.0.dist-info/entry_points.txt +2 -0
- primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
- primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""EPUB 3 export: one collection -> one .epub (pure stdlib).
|
|
2
|
+
|
|
3
|
+
EPUB is an OCF zip: uncompressed `mimetype` first, container.xml pointing
|
|
4
|
+
at package.opf, EPUB3 nav.xhtml for the table of contents. Chapters are
|
|
5
|
+
the same XHTML pages the HTML export produces, so an RFC's ASCII diagrams
|
|
6
|
+
and section anchors survive intact; search is the reader's job (ReadEra,
|
|
7
|
+
Apple Books, Play Books all index chapter text). No NCX: EPUB3 readers
|
|
8
|
+
use the nav document.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import zipfile
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from xml.sax.saxutils import escape as xesc
|
|
18
|
+
|
|
19
|
+
from .collections import Collection
|
|
20
|
+
from .export import (ExportDoc, STYLE_CSS, make_resolver, render_doc_page,
|
|
21
|
+
slug)
|
|
22
|
+
|
|
23
|
+
MIMETYPE = "application/epub+zip"
|
|
24
|
+
_NAV_MAX_SECTIONS = 150 # nested entries per document chapter
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _now() -> str:
|
|
28
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _opf(coll: Collection, entries: list[tuple[str, str, str]],
|
|
32
|
+
spine: list[str], now: str) -> str:
|
|
33
|
+
"""entries: (id, href, title); spine: manifest ids in reading order."""
|
|
34
|
+
ident = "urn:uuid:" + hashlib.sha1(
|
|
35
|
+
("rfclib:" + coll.id).encode()).hexdigest()[:32]
|
|
36
|
+
manifest = "\n".join(
|
|
37
|
+
f' <item id="{i}" href="{xesc(h)}" '
|
|
38
|
+
f'media-type="application/xhtml+xml"/>' for i, h, _t in entries)
|
|
39
|
+
manifest += ('\n <item id="title" href="title.xhtml" '
|
|
40
|
+
'media-type="application/xhtml+xml"/>'
|
|
41
|
+
'\n <item id="nav" href="nav.xhtml" '
|
|
42
|
+
'media-type="application/xhtml+xml" properties="nav"/>'
|
|
43
|
+
'\n <item id="css" href="style.css" media-type="text/css"/>')
|
|
44
|
+
sp = "\n".join(f' <itemref idref="{i}"/>'
|
|
45
|
+
for i in ["title"] + spine + ["nav"])
|
|
46
|
+
return f"""<?xml version="1.0" encoding="utf-8"?>
|
|
47
|
+
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="bid">
|
|
48
|
+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
49
|
+
<dc:identifier id="bid">{ident}</dc:identifier>
|
|
50
|
+
<dc:title>{xesc(coll.name)}</dc:title>
|
|
51
|
+
<dc:language>en</dc:language>
|
|
52
|
+
<dc:creator>rfclib</dc:creator>
|
|
53
|
+
<dc:publisher>rfclib</dc:publisher>
|
|
54
|
+
<dc:date>{now[:10]}</dc:date>
|
|
55
|
+
<meta property="dcterms:modified">{now}</meta>
|
|
56
|
+
</metadata>
|
|
57
|
+
<manifest>
|
|
58
|
+
{manifest}
|
|
59
|
+
</manifest>
|
|
60
|
+
<spine>
|
|
61
|
+
{sp}
|
|
62
|
+
</spine>
|
|
63
|
+
</package>
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _nav(title_href: str, chapters: list[tuple[str, str, list]]) -> str:
|
|
68
|
+
"""chapters: (href, doc label, [(level, id, text), ...])."""
|
|
69
|
+
items = []
|
|
70
|
+
for href, label, heads in chapters:
|
|
71
|
+
nested = ""
|
|
72
|
+
shown = [h for h in heads if h[0] <= 3][:_NAV_MAX_SECTIONS]
|
|
73
|
+
if shown:
|
|
74
|
+
inner = "".join(
|
|
75
|
+
f'<li><a href="{xesc(href)}#{i}">{xesc(t)}</a></li>'
|
|
76
|
+
for _lvl, i, t in shown)
|
|
77
|
+
nested = f"<ol>{inner}</ol>"
|
|
78
|
+
items.append(f'<li><a href="{xesc(href)}">{xesc(label)}</a>{nested}</li>')
|
|
79
|
+
return f"""<!DOCTYPE html>
|
|
80
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en">
|
|
81
|
+
<head><meta charset="utf-8"/><title>Table of Contents</title></head>
|
|
82
|
+
<body>
|
|
83
|
+
<nav epub:type="toc">
|
|
84
|
+
<h1>Table of Contents</h1>
|
|
85
|
+
<ol>
|
|
86
|
+
<li><a href="{xesc(title_href)}">Cover</a></li>
|
|
87
|
+
{"".join(items)}
|
|
88
|
+
</ol>
|
|
89
|
+
</nav>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _cover(coll: Collection, docs: list[ExportDoc], now: str) -> str:
|
|
96
|
+
rows = "".join(
|
|
97
|
+
f'<tr><td><a href="text/{d.doc_id}.xhtml">{d.doc_id}</a></td>'
|
|
98
|
+
f"<td>{xesc(d.title)}</td></tr>" for d in docs)
|
|
99
|
+
return f"""<!DOCTYPE html>
|
|
100
|
+
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
|
101
|
+
<head><meta charset="utf-8"/><title>{xesc(coll.name)}</title>
|
|
102
|
+
<link rel="stylesheet" type="text/css" href="style.css"/></head>
|
|
103
|
+
<body>
|
|
104
|
+
<h1>{xesc(coll.name)}</h1>
|
|
105
|
+
<p class="meta">{len(docs)} documents · generated {now} · rfclib export</p>
|
|
106
|
+
<p>{xesc(coll.description)}</p>
|
|
107
|
+
<table class="index">
|
|
108
|
+
<tr><th>Doc</th><th>Title</th></tr>
|
|
109
|
+
{rows}
|
|
110
|
+
</table>
|
|
111
|
+
</body>
|
|
112
|
+
</html>
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_epub(coll: Collection, docs: list[ExportDoc], out_path: Path,
|
|
117
|
+
link_refs: str = "internal", progress=None) -> dict:
|
|
118
|
+
"""Write <out_path>/<coll.id>.epub; returns a summary dict."""
|
|
119
|
+
out_path = Path(out_path)
|
|
120
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
known = {d.doc_id: f"text/{d.doc_id}.xhtml" for d in docs}
|
|
122
|
+
resolver = make_resolver(known, link_refs)
|
|
123
|
+
now = _now()
|
|
124
|
+
|
|
125
|
+
chapters: list[tuple[str, str, list]] = []
|
|
126
|
+
pages: list[tuple[str, str]] = [] # (arcname, content)
|
|
127
|
+
for n, doc in enumerate(docs, 1):
|
|
128
|
+
page, heads = render_doc_page(doc, resolver, css_href="../style.css")
|
|
129
|
+
arc = f"OEBPS/text/{doc.doc_id}.xhtml"
|
|
130
|
+
pages.append((arc, page))
|
|
131
|
+
chapters.append((f"text/{doc.doc_id}.xhtml",
|
|
132
|
+
f"{doc.doc_id}: {doc.title}", heads))
|
|
133
|
+
if progress:
|
|
134
|
+
progress(f"epub {n}/{len(docs)} {doc.doc_id}")
|
|
135
|
+
|
|
136
|
+
entries = [(f"doc-{slug(d.doc_id)}", f"text/{d.doc_id}.xhtml", d.title)
|
|
137
|
+
for d in docs]
|
|
138
|
+
spine = [i for i, _h, _t in entries]
|
|
139
|
+
tmp = out_path / (coll.id + ".epub.part")
|
|
140
|
+
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
|
141
|
+
mi = zipfile.ZipInfo("mimetype", date_time=(1980, 1, 1, 0, 0, 0))
|
|
142
|
+
zf.writestr(mi, MIMETYPE, compress_type=zipfile.ZIP_STORED)
|
|
143
|
+
zf.writestr("META-INF/container.xml", """<?xml version="1.0" encoding="utf-8"?>
|
|
144
|
+
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
145
|
+
<rootfiles>
|
|
146
|
+
<rootfile full-path="OEBPS/package.opf" media-type="application/oebps-package+xml"/>
|
|
147
|
+
</rootfiles>
|
|
148
|
+
</container>
|
|
149
|
+
""")
|
|
150
|
+
zf.writestr("OEBPS/package.opf", _opf(coll, entries, spine, now))
|
|
151
|
+
zf.writestr("OEBPS/nav.xhtml", _nav("title.xhtml", chapters))
|
|
152
|
+
zf.writestr("OEBPS/title.xhtml", _cover(coll, docs, now))
|
|
153
|
+
zf.writestr("OEBPS/style.css", STYLE_CSS)
|
|
154
|
+
for arc, content in pages:
|
|
155
|
+
zf.writestr(arc, content)
|
|
156
|
+
final = out_path / (coll.id + ".epub")
|
|
157
|
+
tmp.replace(final)
|
|
158
|
+
return {"id": coll.id, "format": "epub", "path": str(final),
|
|
159
|
+
"docs": len(docs), "bytes": final.stat().st_size}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Shared export layer for third-party reader formats (EPUB / HTML / ZIM / docset).
|
|
2
|
+
|
|
3
|
+
One RFC .txt -> one clean XHTML chapter:
|
|
4
|
+
- page furniture removed (running footers "[Page N]", post-break headers,
|
|
5
|
+
form feeds) while every surviving line keeps its exact whitespace, so
|
|
6
|
+
ASCII diagrams stay pixel-faithful inside <pre>;
|
|
7
|
+
- section anchors reuse chunker.find_headings — the same heuristics that
|
|
8
|
+
back the search index — so EPUB nav / docset TOC / deep links agree with
|
|
9
|
+
search results;
|
|
10
|
+
- optional linkification of "RFC 9000" style references (internal to the
|
|
11
|
+
exported collection and/or rfc-editor.org) plus bare URLs.
|
|
12
|
+
|
|
13
|
+
Every exporter (epub.py, htmlexport.py, later zim/docset) shares
|
|
14
|
+
iter_export_docs() so collection resolution happens exactly once, the same
|
|
15
|
+
way builder.build_pack does.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import html
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Callable, Optional
|
|
25
|
+
|
|
26
|
+
from ..index.chunker import find_headings
|
|
27
|
+
from .builder import MissingFiles, resolve_files
|
|
28
|
+
from .collections import Collection, resolve, sort_key
|
|
29
|
+
|
|
30
|
+
# Running footer (old format: "Bray ... [Page 1]"; new format: bare
|
|
31
|
+
# centered "[Page 7]"); also catches the standalone marker line.
|
|
32
|
+
FOOTER_RE = re.compile(r"\[Page \d+\]\s*$")
|
|
33
|
+
# Post-page-break header, only dropped when seen right after a break:
|
|
34
|
+
# "RFC 8259 JSON December 2017" / "Internet-Draft ...".
|
|
35
|
+
HEADER_RE = re.compile(r"^\s{0,3}(RFC\s+\d+|BCP\s+\d+|STD\s+\d+|FYI\s+\d+|"
|
|
36
|
+
r"Internet-Draft).{0,60}\s{2,}[A-Z][a-z]+ \d{4}\s*$")
|
|
37
|
+
REF_RE = re.compile(r"\b(RFC|BCP|STD|FYI)\s?(\d{1,5})\b")
|
|
38
|
+
URL_RE = re.compile(r"https?://[^\s<>\"'()]+")
|
|
39
|
+
# Ancient RFCs (RFC 768 etc.) have no modern section structure, and the
|
|
40
|
+
# chunker heading heuristics catch their date lines ("28 Aug 1980").
|
|
41
|
+
# Harmless in search chunks, noisy in a nav — reject at export time;
|
|
42
|
+
# rejected lines just flow back into the <pre> body.
|
|
43
|
+
_DATEISH = re.compile(
|
|
44
|
+
r"\d{0,2}\s*(January|February|March|April|May|June|July|August|"
|
|
45
|
+
r"September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|"
|
|
46
|
+
r"Sep|Sept|Oct|Nov|Dec)\s+\d{4}[.,]?\s*\d{0,2}", re.I)
|
|
47
|
+
|
|
48
|
+
LinkResolver = Callable[[str], Optional[str]] # "RFC9000" -> href or None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ExportDoc:
|
|
53
|
+
doc_id: str
|
|
54
|
+
title: str
|
|
55
|
+
status: str
|
|
56
|
+
pub_date: str
|
|
57
|
+
stream: str
|
|
58
|
+
wg: str
|
|
59
|
+
current: bool
|
|
60
|
+
txt_path: Path
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def iter_export_docs(conn, coll: Collection, progress=None) -> list[ExportDoc]:
|
|
64
|
+
"""Resolve a collection to ExportDocs with local txt files.
|
|
65
|
+
|
|
66
|
+
Same pipeline as builder.build_pack: resolve ids, drop content-free
|
|
67
|
+
alias pointers (STD/FYI without files), require local txt (raises
|
|
68
|
+
MissingFiles with sync hint).
|
|
69
|
+
"""
|
|
70
|
+
doc_ids = resolve(conn, coll)
|
|
71
|
+
if not doc_ids:
|
|
72
|
+
raise LookupError(f"collection '{coll.id}' resolves to 0 docs")
|
|
73
|
+
doc_ids = [d for d in doc_ids if d.startswith("RFC") or conn.execute(
|
|
74
|
+
"SELECT 1 FROM files WHERE doc_id=? LIMIT 1", (d,)).fetchone()]
|
|
75
|
+
files = resolve_files(conn, doc_ids, ["txt"])
|
|
76
|
+
docs: list[ExportDoc] = []
|
|
77
|
+
for doc_id in sorted((d for d in doc_ids if d in files), key=sort_key):
|
|
78
|
+
row = conn.execute(
|
|
79
|
+
"""SELECT title, status, pub_date, is_current, stream, wg
|
|
80
|
+
FROM docs WHERE doc_id=?""", (doc_id,)).fetchone()
|
|
81
|
+
if row is None:
|
|
82
|
+
continue
|
|
83
|
+
docs.append(ExportDoc(
|
|
84
|
+
doc_id=doc_id, title=row["title"] or doc_id,
|
|
85
|
+
status=row["status"] or "", pub_date=row["pub_date"] or "",
|
|
86
|
+
stream=row["stream"] or "", wg=row["wg"] or "",
|
|
87
|
+
current=bool(row["is_current"]), txt_path=files[doc_id]["txt"]))
|
|
88
|
+
if progress:
|
|
89
|
+
progress(f"{len(docs)} docs resolved")
|
|
90
|
+
return docs
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _decode(path: Path) -> str:
|
|
94
|
+
raw = path.read_bytes()
|
|
95
|
+
for enc in ("utf-8", "cp1252"):
|
|
96
|
+
try:
|
|
97
|
+
return raw.decode(enc)
|
|
98
|
+
except UnicodeDecodeError:
|
|
99
|
+
continue
|
|
100
|
+
return raw.decode("latin-1", errors="replace")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def slug(s: str) -> str:
|
|
104
|
+
s = re.sub(r"[^A-Za-z0-9]+", "-", s.strip()).strip("-").lower()
|
|
105
|
+
return s or "sec"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _heading_id(num: str, title: str, used: set[str]) -> str:
|
|
109
|
+
if num.startswith("Appendix"):
|
|
110
|
+
base = "app-" + slug(num.split()[-1])
|
|
111
|
+
elif num:
|
|
112
|
+
base = "sec-" + slug(num)
|
|
113
|
+
else:
|
|
114
|
+
base = slug(title)
|
|
115
|
+
hid, n = base, 2
|
|
116
|
+
while hid in used:
|
|
117
|
+
hid, n = f"{base}-{n}", n + 1
|
|
118
|
+
used.add(hid)
|
|
119
|
+
return hid
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _heading_level(num: str) -> int:
|
|
123
|
+
if not num or num.startswith("Appendix"):
|
|
124
|
+
return 2
|
|
125
|
+
parts = num.split(".")
|
|
126
|
+
if parts[0].isalpha(): # "A" / "A.1": letter subsections
|
|
127
|
+
return min(2 + len(parts) - 1, 6)
|
|
128
|
+
return min(2 + len(parts) - 1, 6)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _esc_refs(text: str, resolver: Optional[LinkResolver]) -> str:
|
|
132
|
+
esc = html.escape(text, quote=False)
|
|
133
|
+
if resolver is None:
|
|
134
|
+
return esc
|
|
135
|
+
|
|
136
|
+
def repl(m):
|
|
137
|
+
href = resolver(m.group(1) + m.group(2))
|
|
138
|
+
if not href:
|
|
139
|
+
return m.group(0)
|
|
140
|
+
return (f'<a class="xref" '
|
|
141
|
+
f'href="{html.escape(href, quote=True)}">{m.group(0)}</a>')
|
|
142
|
+
return REF_RE.sub(repl, esc)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def linkify_segment(raw: str, resolver: Optional[LinkResolver]) -> str:
|
|
146
|
+
"""Escape a raw text run, linkifying URLs and RFC/BCP/STD/FYI refs."""
|
|
147
|
+
out, pos = [], 0
|
|
148
|
+
for m in URL_RE.finditer(raw):
|
|
149
|
+
out.append(_esc_refs(raw[pos:m.start()], resolver))
|
|
150
|
+
url = m.group(0)
|
|
151
|
+
trail = ""
|
|
152
|
+
while url and url[-1] in ".,;:!?\"'": # trailing punctuation is prose
|
|
153
|
+
trail = url[-1] + trail
|
|
154
|
+
url = url[:-1]
|
|
155
|
+
out.append(f'<a class="url" href="{html.escape(url, quote=True)}">'
|
|
156
|
+
f'{html.escape(url)}</a>')
|
|
157
|
+
out.append(_esc_refs(trail, resolver))
|
|
158
|
+
pos = m.end()
|
|
159
|
+
out.append(_esc_refs(raw[pos:], resolver))
|
|
160
|
+
return "".join(out)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def render_body(doc: ExportDoc,
|
|
164
|
+
resolver: Optional[LinkResolver] = None,
|
|
165
|
+
) -> tuple[str, list[tuple[int, str, str]]]:
|
|
166
|
+
"""Render the txt body to XHTML.
|
|
167
|
+
|
|
168
|
+
Returns (body_html, headings) where headings are (level, id, text)
|
|
169
|
+
in document order — exporters reuse them for nav/TOC/search indexes.
|
|
170
|
+
"""
|
|
171
|
+
lines = _decode(doc.txt_path).lstrip("\ufeff").splitlines()
|
|
172
|
+
heads = [(i, num, title) for i, num, title in find_headings(lines)
|
|
173
|
+
if not _DATEISH.fullmatch(lines[i].strip())]
|
|
174
|
+
hmap = {i: (num, title) for i, num, title in heads}
|
|
175
|
+
toc_ranges = set()
|
|
176
|
+
for k, (i, _num, title) in enumerate(heads):
|
|
177
|
+
if title == "Table of Contents": # real nav replaces the dot-leader ToC
|
|
178
|
+
nxt = heads[k + 1][0] if k + 1 < len(heads) else len(lines)
|
|
179
|
+
toc_ranges.update(range(i + 1, nxt))
|
|
180
|
+
|
|
181
|
+
parts: list[str] = []
|
|
182
|
+
buf: list[str] = []
|
|
183
|
+
headings: list[tuple[int, str, str]] = []
|
|
184
|
+
used: set[str] = set()
|
|
185
|
+
|
|
186
|
+
def flush():
|
|
187
|
+
while buf and not buf[-1].strip():
|
|
188
|
+
buf.pop()
|
|
189
|
+
if buf and any(ln.strip() for ln in buf):
|
|
190
|
+
parts.append("<pre>" + linkify_segment("\n".join(buf), resolver)
|
|
191
|
+
+ "</pre>")
|
|
192
|
+
buf.clear()
|
|
193
|
+
|
|
194
|
+
after_break = False
|
|
195
|
+
for i, raw in enumerate(lines):
|
|
196
|
+
if i in hmap:
|
|
197
|
+
flush()
|
|
198
|
+
num, title = hmap[i]
|
|
199
|
+
if title == "Table of Contents":
|
|
200
|
+
hid = _heading_id("toc", "toc", used)
|
|
201
|
+
else:
|
|
202
|
+
hid = _heading_id(num, title, used)
|
|
203
|
+
level = _heading_level(num)
|
|
204
|
+
text = re.sub(r"\s{2,}", " ", lines[i].strip())
|
|
205
|
+
parts.append(f'<h{level} id="{hid}">{html.escape(text)}</h{level}>')
|
|
206
|
+
headings.append((level, hid, text))
|
|
207
|
+
after_break = False
|
|
208
|
+
continue
|
|
209
|
+
if i in toc_ranges:
|
|
210
|
+
continue
|
|
211
|
+
was_break = "\f" in raw or bool(FOOTER_RE.search(raw))
|
|
212
|
+
if was_break:
|
|
213
|
+
raw = raw.replace("\f", "")
|
|
214
|
+
if not raw.strip():
|
|
215
|
+
after_break = True
|
|
216
|
+
continue
|
|
217
|
+
if after_break and HEADER_RE.match(raw):
|
|
218
|
+
after_break = False
|
|
219
|
+
continue
|
|
220
|
+
after_break = False
|
|
221
|
+
buf.append(raw.rstrip())
|
|
222
|
+
flush()
|
|
223
|
+
return "\n".join(parts), headings
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def render_doc_page(doc: ExportDoc, resolver: Optional[LinkResolver] = None,
|
|
227
|
+
css_href: str = "../style.css",
|
|
228
|
+
back_href: Optional[str] = None,
|
|
229
|
+
) -> tuple[str, list[tuple[int, str, str]]]:
|
|
230
|
+
"""Full standalone page for one document; also returns its headings."""
|
|
231
|
+
body, headings = render_body(doc, resolver)
|
|
232
|
+
meta_bits = [b for b in (
|
|
233
|
+
doc.status,
|
|
234
|
+
f"published {doc.pub_date}" if doc.pub_date else "",
|
|
235
|
+
doc.stream, doc.wg,
|
|
236
|
+
"current" if doc.current else "obsolete") if b]
|
|
237
|
+
back = (f'<p class="back"><a href="{html.escape(back_href, quote=True)}">'
|
|
238
|
+
f'index</a></p>\n' if back_href else "")
|
|
239
|
+
page = f"""<!DOCTYPE html>
|
|
240
|
+
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
|
241
|
+
<head>
|
|
242
|
+
<meta charset="utf-8"/>
|
|
243
|
+
<title>{html.escape(f'{doc.doc_id}: {doc.title}')}</title>
|
|
244
|
+
<link rel="stylesheet" type="text/css" href="{html.escape(css_href, quote=True)}"/>
|
|
245
|
+
</head>
|
|
246
|
+
<body>
|
|
247
|
+
{back}<header class="doc-head">
|
|
248
|
+
<h1>{html.escape(doc.doc_id)}: {html.escape(doc.title)}</h1>
|
|
249
|
+
<p class="meta">{html.escape(" · ".join(meta_bits))}</p>
|
|
250
|
+
</header>
|
|
251
|
+
{body}
|
|
252
|
+
</body>
|
|
253
|
+
</html>
|
|
254
|
+
"""
|
|
255
|
+
return page, headings
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def make_resolver(known_files: dict[str, str], mode: str) -> Optional[LinkResolver]:
|
|
259
|
+
"""Build the reference resolver for 'none|internal|external' modes."""
|
|
260
|
+
if mode == "none":
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
def resolve_ref(ref: str) -> Optional[str]:
|
|
264
|
+
if ref in known_files:
|
|
265
|
+
return known_files[ref]
|
|
266
|
+
if mode == "external" and ref.startswith("RFC"):
|
|
267
|
+
return "https://www.rfc-editor.org/rfc/" + ref.lower()
|
|
268
|
+
if mode == "external": # BCP/STD/FYI info pages
|
|
269
|
+
return "https://www.rfc-editor.org/info/" + ref.lower()
|
|
270
|
+
return None
|
|
271
|
+
return resolve_ref
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
STYLE_CSS = """\
|
|
275
|
+
body { font-family: Georgia, 'Times New Roman', serif; margin: 1.5em auto;
|
|
276
|
+
max-width: 72ch; padding: 0 1em; line-height: 1.5; color: #1b1f23; }
|
|
277
|
+
h1 { font-size: 1.45em; }
|
|
278
|
+
h2 { font-size: 1.25em; margin-top: 1.6em; border-bottom: 1px solid #d0d7de;
|
|
279
|
+
padding-bottom: .2em; }
|
|
280
|
+
h3, h4, h5, h6 { font-size: 1.08em; margin-top: 1.3em; }
|
|
281
|
+
pre { font-family: ui-monospace, 'Cascadia Mono', Consolas, monospace;
|
|
282
|
+
font-size: .82em; background: #f6f8fa; border: 1px solid #d0d7de;
|
|
283
|
+
border-radius: 6px; padding: .8em; overflow-x: auto;
|
|
284
|
+
white-space: pre; tab-size: 4; }
|
|
285
|
+
code, kbd { font-family: inherit; }
|
|
286
|
+
a { color: #0969da; text-decoration: none; }
|
|
287
|
+
a:hover { text-decoration: underline; }
|
|
288
|
+
.meta, .back { color: #57606a; font-size: .9em; font-family: sans-serif; }
|
|
289
|
+
table.index { border-collapse: collapse; width: 100%; font-size: .95em; }
|
|
290
|
+
table.index th, table.index td { border-bottom: 1px solid #d0d7de;
|
|
291
|
+
padding: .35em .6em; text-align: left; vertical-align: top; }
|
|
292
|
+
table.index td.num { white-space: nowrap; font-family: monospace; }
|
|
293
|
+
@media (prefers-color-scheme: dark) {
|
|
294
|
+
body { color: #e6edf3; background: #0d1117; }
|
|
295
|
+
pre, table.index th, table.index td { border-color: #30363d; }
|
|
296
|
+
pre { background: #161b22; }
|
|
297
|
+
h2 { border-color: #30363d; }
|
|
298
|
+
.meta, .back { color: #8b949e; }
|
|
299
|
+
a { color: #4493f8; }
|
|
300
|
+
}
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
__all__ = ["ExportDoc", "MissingFiles", "STYLE_CSS", "iter_export_docs",
|
|
304
|
+
"linkify_segment", "make_resolver", "render_body",
|
|
305
|
+
"render_doc_page", "slug"]
|
|
306
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Static HTML mirror export: one collection -> a browsable folder tree.
|
|
2
|
+
|
|
3
|
+
<out>/<coll.id>/
|
|
4
|
+
index.html sortable-in-browser table of all docs
|
|
5
|
+
style.css
|
|
6
|
+
text/RFC9000.html one page per doc, section anchors, cross links
|
|
7
|
+
|
|
8
|
+
The point: grep/Everything/Windows-Search friendly, opens in any browser,
|
|
9
|
+
forever — the workflow CHM used to own on Windows without CHM's dead
|
|
10
|
+
tooling. Same page renderer as the EPUB export (export.py), so both stay
|
|
11
|
+
in lockstep.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import html as htmllib
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from .collections import Collection
|
|
21
|
+
from .export import ExportDoc, STYLE_CSS, make_resolver, render_doc_page
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _index_page(coll: Collection, docs: list[ExportDoc], now: str) -> str:
|
|
25
|
+
rows = []
|
|
26
|
+
for d in docs:
|
|
27
|
+
rows.append(
|
|
28
|
+
"<tr>"
|
|
29
|
+
f'<td class="num"><a href="text/{htmllib.escape(d.doc_id)}.html">'
|
|
30
|
+
f"{htmllib.escape(d.doc_id)}</a></td>"
|
|
31
|
+
f"<td>{htmllib.escape(d.title)}</td>"
|
|
32
|
+
f"<td>{htmllib.escape(d.status)}</td>"
|
|
33
|
+
f'<td class="num">{htmllib.escape(d.pub_date)}</td>'
|
|
34
|
+
f"<td>{'current' if d.current else 'obsolete'}</td>"
|
|
35
|
+
"</tr>")
|
|
36
|
+
return f"""<!DOCTYPE html>
|
|
37
|
+
<html lang="en">
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="utf-8"/>
|
|
40
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
41
|
+
<title>{htmllib.escape(coll.name)} — rfclib export</title>
|
|
42
|
+
<link rel="stylesheet" type="text/css" href="style.css"/>
|
|
43
|
+
</head>
|
|
44
|
+
<body>
|
|
45
|
+
<h1>{htmllib.escape(coll.name)}</h1>
|
|
46
|
+
<p class="meta">{len(docs)} documents · generated {now} · rfclib HTML mirror
|
|
47
|
+
(offline; searchable via browser find / grep / Everything)</p>
|
|
48
|
+
<p>{htmllib.escape(coll.description)}</p>
|
|
49
|
+
<table class="index">
|
|
50
|
+
<tr><th>Doc</th><th>Title</th><th>Status</th><th>Published</th><th></th></tr>
|
|
51
|
+
{"".join(rows)}
|
|
52
|
+
</table>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_html(coll: Collection, docs: list[ExportDoc], out_dir: Path,
|
|
59
|
+
link_refs: str = "internal", progress=None) -> dict:
|
|
60
|
+
"""Write <out_dir>/<coll.id>/ HTML mirror; returns a summary dict."""
|
|
61
|
+
root = Path(out_dir) / coll.id
|
|
62
|
+
text_dir = root / "text"
|
|
63
|
+
text_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
known = {d.doc_id: f"{d.doc_id}.html" for d in docs}
|
|
65
|
+
resolver = make_resolver(known, link_refs)
|
|
66
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
67
|
+
|
|
68
|
+
nbytes = 0
|
|
69
|
+
(root / "style.css").write_text(STYLE_CSS, "utf-8")
|
|
70
|
+
for n, doc in enumerate(docs, 1):
|
|
71
|
+
page, _heads = render_doc_page(doc, resolver, css_href="../style.css",
|
|
72
|
+
back_href="../index.html")
|
|
73
|
+
p = text_dir / f"{doc.doc_id}.html"
|
|
74
|
+
p.write_text(page, "utf-8")
|
|
75
|
+
nbytes += len(page.encode("utf-8"))
|
|
76
|
+
if progress:
|
|
77
|
+
progress(f"html {n}/{len(docs)} {doc.doc_id}")
|
|
78
|
+
idx = _index_page(coll, docs, now)
|
|
79
|
+
(root / "index.html").write_text(idx, "utf-8")
|
|
80
|
+
nbytes += len(idx.encode("utf-8")) + len(STYLE_CSS)
|
|
81
|
+
return {"id": coll.id, "format": "html", "path": str(root),
|
|
82
|
+
"docs": len(docs), "bytes": nbytes}
|