diff options
| -rw-r--r-- | epubs/zig-0.11.0.epub | bin | 1086285 -> 1081154 bytes | |||
| -rw-r--r-- | epubs/zig-0.12.0.epub | bin | 928020 -> 922787 bytes | |||
| -rw-r--r-- | epubs/zig-0.12.1.epub | bin | 938646 -> 933413 bytes | |||
| -rw-r--r-- | epubs/zig-0.13.0.epub | bin | 938695 -> 933462 bytes | |||
| -rw-r--r-- | epubs/zig-0.14.0.epub | bin | 969788 -> 964256 bytes | |||
| -rw-r--r-- | epubs/zig-0.14.1.epub | bin | 973091 -> 967559 bytes | |||
| -rw-r--r-- | epubs/zig-0.15.1.epub | bin | 976374 -> 970838 bytes | |||
| -rw-r--r-- | epubs/zig-0.15.2.epub | bin | 975597 -> 970073 bytes | |||
| -rw-r--r-- | epubs/zig-0.16.0.epub | bin | 989455 -> 983760 bytes | |||
| -rw-r--r-- | epubs/zig-0.9.0.epub | bin | 1118090 -> 1113072 bytes | |||
| -rw-r--r-- | epubs/zig-0.9.1.epub | bin | 1118252 -> 1113222 bytes | |||
| -rw-r--r-- | epubs/zig-master.epub | bin | 972486 -> 966895 bytes | |||
| -rw-r--r-- | src/html.zig | 71 | ||||
| -rwxr-xr-x | validate.sh | 44 |
14 files changed, 73 insertions, 42 deletions
diff --git a/epubs/zig-0.11.0.epub b/epubs/zig-0.11.0.epub Binary files differindex 8751a77..ee5b585 100644 --- a/epubs/zig-0.11.0.epub +++ b/epubs/zig-0.11.0.epub diff --git a/epubs/zig-0.12.0.epub b/epubs/zig-0.12.0.epub Binary files differindex 9dca6f3..fab617f 100644 --- a/epubs/zig-0.12.0.epub +++ b/epubs/zig-0.12.0.epub diff --git a/epubs/zig-0.12.1.epub b/epubs/zig-0.12.1.epub Binary files differindex 64b2179..d2d0be1 100644 --- a/epubs/zig-0.12.1.epub +++ b/epubs/zig-0.12.1.epub diff --git a/epubs/zig-0.13.0.epub b/epubs/zig-0.13.0.epub Binary files differindex f242a27..57cf9ca 100644 --- a/epubs/zig-0.13.0.epub +++ b/epubs/zig-0.13.0.epub diff --git a/epubs/zig-0.14.0.epub b/epubs/zig-0.14.0.epub Binary files differindex 1af8ecd..60ce73d 100644 --- a/epubs/zig-0.14.0.epub +++ b/epubs/zig-0.14.0.epub diff --git a/epubs/zig-0.14.1.epub b/epubs/zig-0.14.1.epub Binary files differindex 378dbb1..7279242 100644 --- a/epubs/zig-0.14.1.epub +++ b/epubs/zig-0.14.1.epub diff --git a/epubs/zig-0.15.1.epub b/epubs/zig-0.15.1.epub Binary files differindex 564ab1e..d92935f 100644 --- a/epubs/zig-0.15.1.epub +++ b/epubs/zig-0.15.1.epub diff --git a/epubs/zig-0.15.2.epub b/epubs/zig-0.15.2.epub Binary files differindex 152f4e4..1d98fee 100644 --- a/epubs/zig-0.15.2.epub +++ b/epubs/zig-0.15.2.epub diff --git a/epubs/zig-0.16.0.epub b/epubs/zig-0.16.0.epub Binary files differindex 114bee7..80fd87b 100644 --- a/epubs/zig-0.16.0.epub +++ b/epubs/zig-0.16.0.epub diff --git a/epubs/zig-0.9.0.epub b/epubs/zig-0.9.0.epub Binary files differindex b6f6dd4..f2203e7 100644 --- a/epubs/zig-0.9.0.epub +++ b/epubs/zig-0.9.0.epub diff --git a/epubs/zig-0.9.1.epub b/epubs/zig-0.9.1.epub Binary files differindex fbc087d..14fc4b1 100644 --- a/epubs/zig-0.9.1.epub +++ b/epubs/zig-0.9.1.epub diff --git a/epubs/zig-master.epub b/epubs/zig-master.epub Binary files differindex 2dd2111..2dad509 100644 --- a/epubs/zig-master.epub +++ b/epubs/zig-master.epub diff --git a/src/html.zig b/src/html.zig index fe7f287..8ec8564 100644 --- a/src/html.zig +++ b/src/html.zig @@ -8,7 +8,8 @@ //! 4. normalizes HTML named entities (e.g. `—`) to numeric references, //! since XML predefines only `& < > " '`, //! 5. removes invalid control characters, -//! 6. extracts the in-page table of contents for use as the EPUB navigation, +//! 6. extracts the in-page table of contents (flattened to one level, since +//! Amazon's kindlegen rejects nested nav TOCs) for the EPUB navigation, //! 7. removes the `#navigation` sidebar (it is `position: fixed`, so it bleeds //! onto every page, and the version-switcher dropdown is dead in an EPUB), //! 8. self-closes void elements (`<meta>` -> `<meta/>`, etc.). @@ -94,48 +95,52 @@ fn extractToc(allocator: std.mem.Allocator, html: []const u8) !?[]u8 { const m = std.mem.indexOf(u8, html, marker) orelse return null; const ul_open = findTagOpen(html, m, "ul") orelse return null; const ul_end = matchingClose(html, ul_open, "ul") orelse return null; - return try rewriteToc(allocator, html[ul_open..ul_end]); + return try flattenToc(allocator, html[ul_open..ul_end]); } -/// Rewrite an extracted TOC list: `<ul>` → `<ol>` (EPUB nav requires ordered -/// lists) and `href="#X"` → `href="index.xhtml#X"`. Caller owns the result. -fn rewriteToc(allocator: std.mem.Allocator, list: []const u8) ![]u8 { +/// Build a single-level `<ol>` from the (possibly nested) TOC region: collect +/// every `<a>...</a>` link, rewrite `href="#X"` → `href="index.xhtml#X"`, and +/// wrap each in its own `<li>`. The list is flattened on purpose — Amazon's +/// kindlegen rejects nested navigation TOCs (error E24011), which is what breaks +/// "Send to Kindle". Returns null if the region has no links. Caller owns it. +fn flattenToc(allocator: std.mem.Allocator, region: []const u8) !?[]u8 { var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); + try out.writer.writeAll("<ol>\n"); var i: usize = 0; - while (i < list.len) { - const lt = std.mem.indexOfScalarPos(u8, list, i, '<') orelse { - try out.writer.writeAll(list[i..]); - break; - }; - try out.writer.writeAll(list[i..lt]); - const gt = tagEnd(list, lt) orelse return error.MalformedToc; - const tag = list[lt .. gt + 1]; - try writeTocTag(&out.writer, tag); - i = gt + 1; + var count: usize = 0; + while (std.mem.indexOfPos(u8, region, i, "<a")) |a_lt| { + // Confirm this is an <a> element start (delimiter after the name). + const after = a_lt + 2; + if (after >= region.len or (region[after] != ' ' and region[after] != '>' and + region[after] != '\t' and region[after] != '\n')) + { + i = after; + continue; + } + const close = std.mem.indexOfPos(u8, region, a_lt, "</a>") orelse break; + try out.writer.writeAll("<li>"); + try writeAnchor(&out.writer, region[a_lt .. close + 4]); + try out.writer.writeAll("</li>\n"); + i = close + 4; + count += 1; } - return out.toOwnedSlice(); -} -fn writeTocTag(w: *std.Io.Writer, tag: []const u8) !void { - const closing = tag[1] == '/'; - const name = tagName(tag); + try out.writer.writeAll("</ol>\n"); + if (count == 0) return null; + return try out.toOwnedSlice(); +} - if (std.ascii.eqlIgnoreCase(name, "ul")) { - try w.writeAll(if (closing) "</ol" else "<ol"); - try w.writeAll(tag[1 + @as(usize, if (closing) 1 else 0) + name.len ..]); - return; +/// Write an `<a>...</a>`, rewriting a leading `href="#"` to target the content. +fn writeAnchor(w: *std.Io.Writer, anchor: []const u8) !void { + if (std.mem.indexOf(u8, anchor, "href=\"#")) |p| { + try w.writeAll(anchor[0 .. p + 6]); // through the opening quote + try w.writeAll(content_href); + try w.writeAll(anchor[p + 6 ..]); // from '#' onward + } else { + try w.writeAll(anchor); } - if (!closing and std.ascii.eqlIgnoreCase(name, "a")) { - if (std.mem.indexOf(u8, tag, "href=\"#")) |p| { - try w.writeAll(tag[0 .. p + 6]); // through the opening quote - try w.writeAll(content_href); - try w.writeAll(tag[p + 6 ..]); // from '#' onward - return; - } - } - try w.writeAll(tag); } /// Remove the `<div id="navigation">…</div>` sidebar. If absent, returns an diff --git a/validate.sh b/validate.sh index f932f11..5bc0224 100755 --- a/validate.sh +++ b/validate.sh @@ -1,8 +1,17 @@ #!/usr/bin/env bash -# Validate every generated EPUB with epubcheck (the official EPUB validator). -# Exits non-zero if any EPUB has errors, so it can gate CI. +# Validate every generated EPUB. # -# Requires epubcheck on PATH (e.g. `brew install epubcheck`). +# 1. epubcheck — official EPUB3 conformance (always, required on PATH). +# 2. kindlegen — Amazon's converter, the engine behind "Send to Kindle". +# Optional but recommended: epubcheck does NOT catch the +# Kindle-specific issues that cause "E999" delivery failures. +# +# epubcheck: brew install epubcheck +# kindlegen: bundled inside Kindle Previewer 3 at +# ".../Kindle Previewer 3.app/Contents/lib/fc/bin/kindlegen". +# Put it on PATH or point KINDLEGEN at it to enable the Kindle check. +# +# Exits non-zero if any EPUB fails a check, so it can gate CI. set -uo pipefail @@ -11,6 +20,8 @@ if ! command -v epubcheck >/dev/null 2>&1; then exit 127 fi +KINDLEGEN="${KINDLEGEN:-$(command -v kindlegen 2>/dev/null || true)}" + shopt -s nullglob epubs=(epubs/*.epub) if [ ${#epubs[@]} -eq 0 ]; then @@ -20,13 +31,28 @@ fi fail=0 for f in "${epubs[@]}"; do - if epubcheck "$f" >/dev/null 2>&1; then - echo "PASS $f" - else - echo "FAIL $f" - epubcheck "$f" 2>&1 | grep -E '^(ERROR|FATAL|WARNING)' | sed 's/^/ /' - fail=1 + ok=1 + + if ! epubcheck "$f" >/dev/null 2>&1; then + ok=0 + echo "FAIL (epubcheck) $f" + epubcheck "$f" 2>&1 | grep -E '^(ERROR|FATAL)' | sed 's/^/ /' fi + + if [ -n "$KINDLEGEN" ]; then + # kindlegen writes output next to the input; -o takes a bare filename. + out=$("$KINDLEGEN" "$f" -o "$(basename "${f%.epub}").mobi" 2>&1) + rm -f "${f%.epub}.mobi" + if echo "$out" | grep -q "could not be generated"; then + ok=0 + echo "FAIL (kindlegen) $f" + echo "$out" | grep -E '^Error|E[0-9]{4,}' | sed 's/^/ /' + fi + fi + + if [ $ok -eq 1 ]; then echo "PASS $f"; else fail=1; fi done +[ -z "$KINDLEGEN" ] && echo "note: kindlegen not found; ran epubcheck only (set KINDLEGEN to also test Kindle conversion)." >&2 + exit $fail |
