diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/html.zig | 71 |
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 |
