From aa8e0a8a564808b2de48bbb032ab7b21cffe3f62 Mon Sep 17 00:00:00 2001 From: Sergiy Egoshyn Date: Mon, 2 Oct 2023 17:15:34 +0300 Subject: [PATCH] Fb2Kindle: auto-depth for navpoints --- Fb2Kindle/Config.cs | 3 +- Fb2Kindle/Convertor.cs | 139 ++++++++++++++++++++++------------------- Fb2Kindle/Program.cs | 11 +--- 3 files changed, 75 insertions(+), 78 deletions(-) diff --git a/Fb2Kindle/Config.cs b/Fb2Kindle/Config.cs index 5bb8e76..a8bfba1 100644 --- a/Fb2Kindle/Config.cs +++ b/Fb2Kindle/Config.cs @@ -14,7 +14,7 @@ public class Config { public bool NoChapters { get; set; } public bool DropCaps { get; set; } public bool NoImages { get; set; } - public bool NoToc { get; set; } + public bool SkipToc { get; set; } public byte CompressionLevel { get; set; } public bool AddSequenceInfo { get; set; } public bool Grayscaled { get; set; } @@ -27,7 +27,6 @@ public class Config { public int SmtpTimeout { get; set; } = 100000; public bool CheckUpdates { get; set; } - public int NavbarDepth { get; set; } = 3; } internal class AppOptions { diff --git a/Fb2Kindle/Convertor.cs b/Fb2Kindle/Convertor.cs index 5546e04..76f8208 100644 --- a/Fb2Kindle/Convertor.cs +++ b/Fb2Kindle/Convertor.cs @@ -17,8 +17,26 @@ namespace Fb2Kindle { internal class Convertor { - private const string TocElement = "ul"; //"ol"; - private const string NcxName = "toc.ncx"; + internal class TocItem { + internal string Name { get; } + internal string Href { get; } + internal List SubItems { get; } + + public bool IsLastCollapsibleLevel => SubItems.Count > 0 && SubItems.All(si => si.SubItems.Count == 0); + + internal TocItem(string name, string href) { + Name = name; + Href = href; + SubItems = new List(); + } + + internal TocItem Add(string name, string href) { + var subItem = new TocItem(name, href); + SubItems.Add(subItem); + return subItem; + } + } + private const string DropCap = "АБВГДЕЖЗИКЛМНОПРСТУФХЦЧЩШЭЮЯ"; //"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧЩШЬЪЫЭЮЯQWERTYUIOPASDFGHJKLZXCVBNM"; private const string NoAuthorText = "без автора"; private XElement opfFile; @@ -57,7 +75,7 @@ internal bool ConvertBookSequence(List books) { File.WriteAllText(options.TempFolder + @"\book.css", options.Css); var coverDone = false; - XElement tocEl = null; + TocItem rootToc = null; for (var idx = 0; idx < books.Count; idx++) { var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(books[idx]); if (fileNameWithoutExtension != null) { @@ -70,7 +88,7 @@ internal bool ConvertBookSequence(List books) { options.TargetName = fileName; //create instances opfFile = GetEmptyPackage(book, books.Count > 1); - AddPackItem("ncx", NcxName, "application/x-dtbncx+xml", false); + AddPackItem("ncx", "toc.ncx", "application/x-dtbncx+xml", false); } var bookPostfix = idx == 0 ? "" : $"_{idx}"; @@ -104,23 +122,21 @@ internal bool ConvertBookSequence(List books) { AddPackItem("it" + bookPostfix, bookFileName); var bookTitle = GetTitle(book); //add to TOC - var bookLi = GetListItem(bookTitle, bookFileName); - if (tocEl == null) - tocEl = GetEmptyToc(bookTitle); - tocEl.Elements("body").Elements(TocElement).First().Add(bookLi); - ProcessAllData(book, bookRoot, bookPostfix, bookLi, bookFileName); + if (rootToc == null) + rootToc = new TocItem(bookTitle, null); + var tocItem = rootToc.Add(bookTitle, bookFileName); + ProcessAllData(book, bookRoot, bookPostfix, tocItem, bookFileName); ConvertTagsToHtml(bookRoot, true); SaveAsHtmlBook(bookRoot, $"{options.TempFolder}\\{bookFileName}", bookTitle); } } - CreateNcxFile(tocEl, options.Config.NoToc, books.Count > 1); + CreateNcxFile(rootToc); - if (!options.Config.NoToc) { - SaveXmlToFile(tocEl, $@"{options.TempFolder}\toc.html"); + if (rootToc != null && !options.Config.SkipToc) { + GenerateTocFile(rootToc); AddPackItem("content", "toc.html"); AddGuideItem("toc", "toc.html", "toc"); - tocEl?.RemoveAll(); } SaveXmlToFile(opfFile, $@"{options.TempFolder}\content.opf"); @@ -196,7 +212,7 @@ private static XElement AddNcxItem(XElement parent, int playOrder, string label, return navPoint; } - private void CreateNcxFile(XElement toc, bool addToc, bool sequenceMode) { + private void CreateNcxFile(TocItem tocItem) { var ncx = new XElement("ncx"); var head = new XElement("head", ""); head.Add(new XElement("meta", new XAttribute("name", "dtb:uid"), new XAttribute("content", "BookId"))); @@ -209,31 +225,22 @@ private void CreateNcxFile(XElement toc, bool addToc, bool sequenceMode) { var navMap = new XElement("navMap", ""); AddNcxItem(navMap, 0, "Описание", "book.html#it"); var playOrder = 2; - var listEls = toc.Elements("body").Elements(TocElement); - AddTocListItems(listEls, navMap, ref playOrder, sequenceMode ? options.Epub ? 3 : 2 : 1); - if (!addToc) + AddTocListItems(tocItem, navMap, ref playOrder); + if (!options.Config.SkipToc) AddNcxItem(navMap, 1, "Содержание", "toc.html#toc"); ncx.Add(navMap); - SaveXmlToFile(ncx, $"{options.TempFolder}\\{NcxName}"); + SaveXmlToFile(ncx, $"{options.TempFolder}\\toc.ncx"); ncx.RemoveAll(); } - private void AddTocListItems(IEnumerable listEls, XElement navMap, ref int playOrder, int depth) { - foreach (var list in listEls) { - AddTocListItems(list.Elements(TocElement), navMap, ref playOrder, depth + 1); - foreach (var li in list.Elements("li")) { - var navPoint = navMap; - var a = li.Elements("a").FirstOrDefault(); - if (a != null) { - if (depth == options.Config.NavbarDepth) { - navPoint = AddNcxItem(navMap, playOrder++, a.Value, (string) a.Attribute("href")); - } - else { - AddNcxItem(navMap, playOrder++, a.Value, (string)a.Attribute("href")); - } - } - AddTocListItems(li.Elements(TocElement), navPoint, ref playOrder, depth + 1); - } + private void AddTocListItems(TocItem tocItem, XElement navMap, ref int playOrder) { + foreach (var subItem in tocItem.SubItems) { + var navPoint = navMap; + if (subItem.IsLastCollapsibleLevel) + navPoint = AddNcxItem(navMap, playOrder++, subItem.Name, subItem.Href); + else + AddNcxItem(navMap, playOrder++, subItem.Name, subItem.Href); + AddTocListItems(subItem, navPoint, ref playOrder); } } @@ -256,7 +263,7 @@ private void UpdateLinksInBook(XElement book, string filename) { } } - private static int SaveSubSections(XElement section, int bookNum, XElement toc, string postfix, string bookFileName) { + private static int SaveSubSections(XElement section, int bookNum, TocItem parent, string postfix, string bookFileName) { var bookId = "i" + bookNum + postfix; var t = section.Elements("title").FirstOrDefault(el => !string.IsNullOrWhiteSpace(el.Value)); //var t = section.Descendants("title").FirstOrDefault(el => !string.IsNullOrWhiteSpace(el.Value)); @@ -273,25 +280,16 @@ private static int SaveSubSections(XElement section, int bookNum, XElement toc, t.RemoveNodes(); t.Add(inner); //t.SetAttributeValue("id", string.Format("title{0}", bookNum + 2)); - var li = GetListItem(t.Value.Trim(), $"{bookFileName}#{bookId}"); - toc.Add(li); - toc = li; + parent = parent.Add(t.Value.Trim(), $"{bookFileName}#{bookId}"); } bookNum++; foreach (var subSection in section.Descendants("section")) { - var si = toc.Descendants(TocElement).FirstOrDefault(); - if (si == null) { - si = new XElement(TocElement); - toc.Add(si); - } - bookNum = SaveSubSections(subSection, bookNum, si, postfix, bookFileName); + bookNum = SaveSubSections(subSection, bookNum, parent, postfix, bookFileName); } return bookNum; } - private void ProcessAllData(XElement book, XElement bookRoot, string postfix, XElement bookLi, string bookFileName) { - var listItem = new XElement(TocElement, ""); - bookLi.Add(listItem); + private void ProcessAllData(XElement book, XElement bookRoot, string postfix, TocItem parent, string bookFileName) { Util.Write("FB2 to HTML...", ConsoleColor.White); UpdateLinksInBook(book, bookFileName); @@ -319,7 +317,7 @@ private void ProcessAllData(XElement book, XElement bookRoot, string postfix, XE var ts = bodies[0].Descendants("title"); foreach (var t in ts) { if (!string.IsNullOrEmpty(t.Value)) - listItem.Add(GetListItem(t.Value.Trim(), $"{bookFileName}#title{i + 2}")); + parent.Add(t.Value.Trim(), $"{bookFileName}#title{i + 2}"); Util.RenameTag(t, "div", "title"); var inner = new XElement("div"); inner.SetAttributeValue("class", i == 0 ? "title0" : "title1"); @@ -332,7 +330,7 @@ private void ProcessAllData(XElement book, XElement bookRoot, string postfix, XE } } else { - SaveSubSections(bodies[0], 0, listItem, postfix, bookFileName); + SaveSubSections(bodies[0], 0, parent, postfix, bookFileName); } bookRoot.Add(bodies[0]); @@ -348,7 +346,7 @@ private void ProcessAllData(XElement book, XElement bookRoot, string postfix, XE bodyName = (string)item.Attribute("name"); } bookRoot.Add(item); - listItem.Add(GetListItem(bodyName, $"{bookFileName}#{part.Key}")); + parent.Add(bodyName, $"{bookFileName}#{part.Key}"); } Util.WriteLine("(OK)", ConsoleColor.Green); @@ -646,11 +644,11 @@ private static string GetTitle(XElement book) { } private XElement GetEmptyPackage(XElement book, bool useSequenceNameOnly = false) { - var opfFile = new XElement("package"); - opfFile.Add(new XAttribute("unique-identifier", "DOI")); - opfFile.Add(new XAttribute(XNamespace.Get("http://www.w3.org/2000/xmlns/").GetName("fo"), "http://www.w3.org/1999/XSL/Format")); - opfFile.Add(new XAttribute(XNamespace.Get("http://www.w3.org/2000/xmlns/").GetName("fb"), "http://www.gribuser.ru/xml/fictionbook/2.0")); - opfFile.Add(new XAttribute(XNamespace.Get("http://www.w3.org/2000/xmlns/").GetName("xlink"), "http://www.w3.org/1999/xlink")); + var package = new XElement("package"); + package.Add(new XAttribute("unique-identifier", "DOI")); + package.Add(new XAttribute(XNamespace.Get("http://www.w3.org/2000/xmlns/").GetName("fo"), "http://www.w3.org/1999/XSL/Format")); + package.Add(new XAttribute(XNamespace.Get("http://www.w3.org/2000/xmlns/").GetName("fb"), "http://www.gribuser.ru/xml/fictionbook/2.0")); + package.Add(new XAttribute(XNamespace.Get("http://www.w3.org/2000/xmlns/").GetName("xlink"), "http://www.w3.org/1999/xlink")); var linkEl = new XElement("meta", new XAttribute("name", "zero-gutter"), new XAttribute("content", "true")); var headEl = new XElement("metadata", linkEl); linkEl = new XElement("meta", new XAttribute("name", "zero-margin"), new XAttribute("content", "true")); @@ -696,28 +694,37 @@ private XElement GetEmptyPackage(XElement book, bool useSequenceNameOnly = false linkEl.Add(new XElement(dc + "Description", Util.Value(book.Elements("description").Elements("title-info").Elements("annotation")))); headEl.Add(linkEl); headEl.Add(new XElement("x-metadata", new XElement("output", new XAttribute("encoding", "utf-8")))); - opfFile.Add(headEl); + package.Add(headEl); - opfFile.Add(new XElement("manifest")); - opfFile.Add(new XElement("spine", new XAttribute("toc", "ncx"))); - return opfFile; + package.Add(new XElement("manifest")); + package.Add(new XElement("spine", new XAttribute("toc", "ncx"))); + return package; } - private static XElement GetEmptyToc(string title) { + private void GenerateTocFile(TocItem rootToc) { var toc = new XElement("html", new XAttribute("type", "toc")); var head = new XElement("head", ""); head.Add(new XElement("meta", new XAttribute("charset", "utf-8"))); - head.Add(new XElement("title", $"{title} - Содержание"), + head.Add(new XElement("title", $"{rootToc.Name} - Содержание"), new XElement("link", new XAttribute("type", "text/css"), new XAttribute("href", "book.css"), new XAttribute("rel", "Stylesheet"))); toc.Add(head); + var ul = new XElement("ul", ""); toc.Add(new XElement("body", new XElement("div", new XAttribute("class", "title"), - new XAttribute("id", "toc"), "Содержание"), new XElement(TocElement, ""))); - return toc; + new XAttribute("id", "toc"), "Содержание"), ul)); + AddTocSubItems(rootToc, ul); + SaveXmlToFile(toc, $@"{options.TempFolder}\toc.html"); } - private static XElement GetListItem(string name, string href) { - return new XElement("li", new XElement("a", new XAttribute("href", href), name)); - } + private static void AddTocSubItems(TocItem tocItem, XElement tocEl) { + foreach (var subItem in tocItem.SubItems) { + var li = new XElement("li", new XElement("a", new XAttribute("href", subItem.Href), subItem.Name)); + tocEl.Add(li); + if (!subItem.SubItems.Any()) continue; + var ul = new XElement("ul", ""); + li.Add(ul); + AddTocSubItems(subItem, ul); + } + } private void AddPackItem(string id, string href, string mediaType = "text/x-oeb1-document", bool addSpine = true) { var packEl = new XElement("item"); diff --git a/Fb2Kindle/Program.cs b/Fb2Kindle/Program.cs index 7b03fd3..6f02e0c 100644 --- a/Fb2Kindle/Program.cs +++ b/Fb2Kindle/Program.cs @@ -36,7 +36,6 @@ private static void ShowHelpText() { Util.WriteLine("\t-jpeg: save images in jpeg"); Util.WriteLine("\t-ntoc: no table of content"); Util.WriteLine("\t-nch: no chapters"); - // Util.WriteLine("\t-depth : set kindle chapters navigation menu fold level (default is 3)"); Util.WriteLine(); } @@ -124,7 +123,7 @@ public static void Main(string[] args) { options.Config.Jpeg = true; break; case "-ntoc": - options.Config.NoToc = true; + options.Config.SkipToc = true; break; case "-c": case "-c1": @@ -139,14 +138,6 @@ public static void Main(string[] args) { case "-d": options.Config.DeleteOriginal = true; break; - case "-depth": - if (args.Length > j + 1) { - if (int.TryParse(args[j + 1], out var value)) { - options.Config.NavbarDepth = value; - j++; - } - } - break; #endregion