aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/html.zig71
1 files changed, 38 insertions, 33 deletions
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