aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--epubs/zig-0.11.0.epubbin1086285 -> 1081154 bytes
-rw-r--r--epubs/zig-0.12.0.epubbin928020 -> 922787 bytes
-rw-r--r--epubs/zig-0.12.1.epubbin938646 -> 933413 bytes
-rw-r--r--epubs/zig-0.13.0.epubbin938695 -> 933462 bytes
-rw-r--r--epubs/zig-0.14.0.epubbin969788 -> 964256 bytes
-rw-r--r--epubs/zig-0.14.1.epubbin973091 -> 967559 bytes
-rw-r--r--epubs/zig-0.15.1.epubbin976374 -> 970838 bytes
-rw-r--r--epubs/zig-0.15.2.epubbin975597 -> 970073 bytes
-rw-r--r--epubs/zig-0.16.0.epubbin989455 -> 983760 bytes
-rw-r--r--epubs/zig-0.9.0.epubbin1118090 -> 1113072 bytes
-rw-r--r--epubs/zig-0.9.1.epubbin1118252 -> 1113222 bytes
-rw-r--r--epubs/zig-master.epubbin972486 -> 966895 bytes
-rw-r--r--src/html.zig71
-rwxr-xr-xvalidate.sh44
14 files changed, 73 insertions, 42 deletions
diff --git a/epubs/zig-0.11.0.epub b/epubs/zig-0.11.0.epub
index 8751a77..ee5b585 100644
--- a/epubs/zig-0.11.0.epub
+++ b/epubs/zig-0.11.0.epub
Binary files differ
diff --git a/epubs/zig-0.12.0.epub b/epubs/zig-0.12.0.epub
index 9dca6f3..fab617f 100644
--- a/epubs/zig-0.12.0.epub
+++ b/epubs/zig-0.12.0.epub
Binary files differ
diff --git a/epubs/zig-0.12.1.epub b/epubs/zig-0.12.1.epub
index 64b2179..d2d0be1 100644
--- a/epubs/zig-0.12.1.epub
+++ b/epubs/zig-0.12.1.epub
Binary files differ
diff --git a/epubs/zig-0.13.0.epub b/epubs/zig-0.13.0.epub
index f242a27..57cf9ca 100644
--- a/epubs/zig-0.13.0.epub
+++ b/epubs/zig-0.13.0.epub
Binary files differ
diff --git a/epubs/zig-0.14.0.epub b/epubs/zig-0.14.0.epub
index 1af8ecd..60ce73d 100644
--- a/epubs/zig-0.14.0.epub
+++ b/epubs/zig-0.14.0.epub
Binary files differ
diff --git a/epubs/zig-0.14.1.epub b/epubs/zig-0.14.1.epub
index 378dbb1..7279242 100644
--- a/epubs/zig-0.14.1.epub
+++ b/epubs/zig-0.14.1.epub
Binary files differ
diff --git a/epubs/zig-0.15.1.epub b/epubs/zig-0.15.1.epub
index 564ab1e..d92935f 100644
--- a/epubs/zig-0.15.1.epub
+++ b/epubs/zig-0.15.1.epub
Binary files differ
diff --git a/epubs/zig-0.15.2.epub b/epubs/zig-0.15.2.epub
index 152f4e4..1d98fee 100644
--- a/epubs/zig-0.15.2.epub
+++ b/epubs/zig-0.15.2.epub
Binary files differ
diff --git a/epubs/zig-0.16.0.epub b/epubs/zig-0.16.0.epub
index 114bee7..80fd87b 100644
--- a/epubs/zig-0.16.0.epub
+++ b/epubs/zig-0.16.0.epub
Binary files differ
diff --git a/epubs/zig-0.9.0.epub b/epubs/zig-0.9.0.epub
index b6f6dd4..f2203e7 100644
--- a/epubs/zig-0.9.0.epub
+++ b/epubs/zig-0.9.0.epub
Binary files differ
diff --git a/epubs/zig-0.9.1.epub b/epubs/zig-0.9.1.epub
index fbc087d..14fc4b1 100644
--- a/epubs/zig-0.9.1.epub
+++ b/epubs/zig-0.9.1.epub
Binary files differ
diff --git a/epubs/zig-master.epub b/epubs/zig-master.epub
index 2dd2111..2dad509 100644
--- a/epubs/zig-master.epub
+++ b/epubs/zig-master.epub
Binary files differ
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