Add nested Epub nav table (#3130)
This commit is contained in:
@@ -87,7 +87,7 @@ class ToEPub(ToHtml):
|
||||
Extend the ToHtml class to write EPub output.
|
||||
"""
|
||||
|
||||
__slots__ = ("_isFront", "_nextFnIndex", "_section", "_sections")
|
||||
__slots__ = ("_isFront", "_nestedToc", "_nextFnIndex", "_section", "_sections")
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
super().__init__(project)
|
||||
@@ -97,18 +97,20 @@ class ToEPub(ToHtml):
|
||||
self._brTag = "<br />"
|
||||
self._nextFnIndex = 1
|
||||
self._useAnchors = False
|
||||
self._nestedToc = False
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setReplaceUnicode(self, doReplace: bool) -> None:
|
||||
"""Set translation map.
|
||||
|
||||
Replace Unicode is ignored for EPub.
|
||||
"""
|
||||
"""Set translation map. Replace Unicode is ignored for EPub."""
|
||||
self._trMap = str.maketrans({"&": "&"})
|
||||
|
||||
def setNestedToC(self, value: bool) -> None:
|
||||
"""Set a nested table of contents."""
|
||||
self._nestedToc = value
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
@@ -382,15 +384,42 @@ class ToEPub(ToHtml):
|
||||
xHtml.append("</header>")
|
||||
xHtml.append('<nav epub:type="toc" role="doc-toc" aria-labelledby="toc">')
|
||||
xHtml.append('<ol epub:type="list">')
|
||||
xHtml.extend(
|
||||
f'<li><a href="xhtml/{section.name}.xhtml#{section.sectionID}">{title}</a></li>'
|
||||
for section in self._sections
|
||||
if (title := section.title) and section.epubType != EPubType.COVER
|
||||
)
|
||||
|
||||
nested = self._nestedToc and any(s.epubType == EPubType.PART for s in self._sections)
|
||||
inPart = False
|
||||
inList = False
|
||||
for section in self._sections:
|
||||
if not (title := section.title) or section.epubType == EPubType.COVER:
|
||||
continue
|
||||
link = f'<a href="xhtml/{section.name}.xhtml#{section.sectionID}">{title}</a>'
|
||||
if inPart and section.epubType == EPubType.CHAPTER:
|
||||
if not inList:
|
||||
# A nested list must not be empty, so open it on first child
|
||||
xHtml.append("<ol>")
|
||||
inList = True
|
||||
xHtml.append(f"<li>{link}</li>")
|
||||
continue
|
||||
if inList:
|
||||
xHtml.append("</ol>")
|
||||
inList = False
|
||||
if inPart:
|
||||
xHtml.append("</li>")
|
||||
inPart = False
|
||||
if nested and section.epubType == EPubType.PART:
|
||||
xHtml.append(f"<li>{link}")
|
||||
inPart = True
|
||||
else:
|
||||
xHtml.append(f"<li>{link}</li>")
|
||||
if inList:
|
||||
xHtml.append("</ol>")
|
||||
if inPart:
|
||||
xHtml.append("</li>")
|
||||
|
||||
xHtml.append("</ol>")
|
||||
xHtml.append("</nav>")
|
||||
xHtml.append("</body>")
|
||||
xHtml.append("</html>")
|
||||
|
||||
return "\n".join(xHtml)
|
||||
|
||||
def _generateStyleSheet(self) -> str:
|
||||
|
||||
@@ -138,6 +138,8 @@ SETTINGS_TEMPLATE: dict[str, tuple[type, T_BuildValue]] = {
|
||||
"doc.pageCountOffset": (int, 0),
|
||||
"doc.metaLanguage": (str, ""),
|
||||
|
||||
"epub.nestedToc": (bool, False),
|
||||
|
||||
"html.addStyles": (bool, True),
|
||||
"html.preserveTabs": (bool, False),
|
||||
}
|
||||
@@ -213,6 +215,9 @@ SETTINGS_LABELS = {
|
||||
"doc.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page counter offset"),
|
||||
"doc.metaLanguage": QT_TRANSLATE_NOOP("Builds", "Override document language"),
|
||||
|
||||
"epub": QT_TRANSLATE_NOOP("Builds", "EPUB Options"),
|
||||
"epub.nestedToc": QT_TRANSLATE_NOOP("Builds", "Nested table of contents"),
|
||||
|
||||
"html": QT_TRANSLATE_NOOP("Builds", "HTML Options"),
|
||||
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS styles"),
|
||||
"html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve tab characters"),
|
||||
|
||||
@@ -348,6 +348,9 @@ class DocumentBuilder:
|
||||
bldObj.setCommentType(nwComment.STORY, self._build.getBool("text.includeStory"))
|
||||
bldObj.setCommentType(nwComment.NOTE, self._build.getBool("text.includeNotes"))
|
||||
|
||||
if isinstance(bldObj, ToEPub):
|
||||
bldObj.setNestedToC(self._build.getBool("epub.nestedToc"))
|
||||
|
||||
if isinstance(bldObj, ToHtml):
|
||||
bldObj.setStyles(self._build.getBool("html.addStyles"))
|
||||
bldObj.setReplaceUnicode(self._build.getBool("format.stripUnicode"))
|
||||
|
||||
@@ -1524,6 +1524,18 @@ class _FormattingTab(NScrollableForm):
|
||||
|
||||
self.addRow(self._build.getLabel("doc.metaLanguage"), [self.lblMetaLanguage, 8, self.metaLanguage])
|
||||
|
||||
# EPUB Document
|
||||
# =============
|
||||
|
||||
title = self._build.getLabel("epub")
|
||||
section += 1
|
||||
self._sidebar.addButton(title, section)
|
||||
self.addGroupLabel(title, section)
|
||||
|
||||
self.nestedToc = NSwitch(self, height=iPx)
|
||||
|
||||
self.addRow(self._build.getLabel("epub.nestedToc"), self.nestedToc)
|
||||
|
||||
# HTML Document
|
||||
# =============
|
||||
|
||||
@@ -1597,8 +1609,6 @@ class _FormattingTab(NScrollableForm):
|
||||
def loadContent(self) -> None:
|
||||
"""Populate the widgets."""
|
||||
# Text Content
|
||||
# ============
|
||||
|
||||
self.incBodyText.setChecked(self._build.getBool("text.includeBodyText"))
|
||||
self.incSynopsis.setChecked(self._build.getBool("text.includeSynopsis"))
|
||||
self.incComments.setChecked(self._build.getBool("text.includeComments"))
|
||||
@@ -1610,8 +1620,6 @@ class _FormattingTab(NScrollableForm):
|
||||
self._updateIgnoredKeywords()
|
||||
|
||||
# Text Format
|
||||
# ===========
|
||||
|
||||
font = QFont()
|
||||
font.fromString(self._build.getStr("format.textFont"))
|
||||
self._textFont = fontMatcher(font)
|
||||
@@ -1629,23 +1637,17 @@ class _FormattingTab(NScrollableForm):
|
||||
self.showDialogue.setChecked(self._build.getBool("format.showDialogue"))
|
||||
|
||||
# Heading Format
|
||||
# ==============
|
||||
|
||||
self.colorHeadings.setChecked(self._build.getBool("format.colorHeadings"))
|
||||
self.boldHeadings.setChecked(self._build.getBool("format.boldHeadings"))
|
||||
self.upperHeadings.setChecked(self._build.getBool("format.upperHeadings"))
|
||||
self.showStatus.setChecked(self._build.getBool("format.showStatus"))
|
||||
|
||||
# First Line Indent
|
||||
# =================
|
||||
|
||||
self.firstIndent.setChecked(self._build.getBool("format.firstLineIndent"))
|
||||
self.indentWidth.setValue(self._build.getFloat("format.firstIndentWidth"))
|
||||
self.indentFirstPar.setChecked(self._build.getBool("format.indentFirstPar"))
|
||||
|
||||
# Text Margins
|
||||
# ============
|
||||
|
||||
self.titleSize.setValue(self._build.getFloat("format.titleSize"))
|
||||
self.h1Size.setValue(self._build.getFloat("format.h1Size"))
|
||||
self.h2Size.setValue(self._build.getFloat("format.h2Size"))
|
||||
@@ -1673,8 +1675,6 @@ class _FormattingTab(NScrollableForm):
|
||||
self._updateTextMarginState()
|
||||
|
||||
# Page Layout
|
||||
# ===========
|
||||
|
||||
pageUnit = self._build.getStr("format.pageUnit")
|
||||
index = self.pageUnit.findData(pageUnit)
|
||||
if index >= 0:
|
||||
@@ -1703,8 +1703,6 @@ class _FormattingTab(NScrollableForm):
|
||||
self.pageSize.currentIndexChanged.connect(self._changePageSize)
|
||||
|
||||
# Document
|
||||
# ========
|
||||
|
||||
self.metaLanguage.setText(processLangCode(self._build.getStr("doc.metaLanguage")))
|
||||
self.pageHeader.setText(self._build.getStr("doc.pageHeader"))
|
||||
self.pageHeader.setCursorPosition(0)
|
||||
@@ -1712,9 +1710,10 @@ class _FormattingTab(NScrollableForm):
|
||||
self.pageHeaderEven.setCursorPosition(0)
|
||||
self.pageCountOffset.setValue(self._build.getInt("doc.pageCountOffset"))
|
||||
|
||||
# HTML Document
|
||||
# =============
|
||||
# EPUB Document
|
||||
self.nestedToc.setChecked(self._build.getBool("epub.nestedToc"))
|
||||
|
||||
# HTML Document
|
||||
self.htmlAddStyles.setChecked(self._build.getBool("html.addStyles"))
|
||||
self.htmlPreserveTabs.setChecked(self._build.getBool("html.preserveTabs"))
|
||||
|
||||
@@ -1801,6 +1800,9 @@ class _FormattingTab(NScrollableForm):
|
||||
|
||||
self.metaLanguage.setText(metaLanguage)
|
||||
|
||||
# EPUB Document
|
||||
self._build.setValue("epub.nestedToc", self.nestedToc.isChecked())
|
||||
|
||||
# HTML Document
|
||||
self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked())
|
||||
self._build.setValue("html.preserveTabs", self.htmlPreserveTabs.isChecked())
|
||||
|
||||
@@ -622,6 +622,83 @@ def testToEPub_ConvertDirect(mockGUI: MockGuiMain):
|
||||
assert epub._sections[-1].text[-1] == "<p style='text-indent: 1.40em;'>Some text ...</p>"
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testToEPub_NavPage(mockGUI: MockGuiMain):
|
||||
"""Test the nav page table of contents."""
|
||||
project = NWProject()
|
||||
|
||||
def build(*docs: tuple[bool, str]) -> ToEPub:
|
||||
epub = ToEPub(project)
|
||||
epub.initDocument()
|
||||
epub.setPartitionFormat(nwHeadFmt.TITLE)
|
||||
epub.setChapterFormat(nwHeadFmt.TITLE)
|
||||
for isNovel, text in docs:
|
||||
epub._isNovel = isNovel
|
||||
epub._text = text
|
||||
epub.tokenizeText()
|
||||
epub.doConvert()
|
||||
epub.closeDocument()
|
||||
return epub
|
||||
|
||||
def navList(epub: ToEPub) -> str:
|
||||
text = epub._generateNavPage()
|
||||
return text[text.index('<ol epub:type="list">') + 22 : text.index("</nav>") - 6]
|
||||
|
||||
# No parts
|
||||
epub = build((True, "## Chapter One\n\n## Chapter Two\n\n"))
|
||||
epub.setNestedToC(True)
|
||||
assert navList(epub) == (
|
||||
'<li><a href="xhtml/chapter1.xhtml#sec_1">Chapter One</a></li>\n'
|
||||
'<li><a href="xhtml/chapter2.xhtml#sec_2">Chapter Two</a></li>\n'
|
||||
)
|
||||
|
||||
# With parts, ending inside a part
|
||||
text = (
|
||||
"## Prologue\n\n# Part One\n\n## Chapter One\n\n## Chapter Two\n\n"
|
||||
"# Part Two\n\n# Part Three\n\n## Chapter Three\n\n"
|
||||
)
|
||||
epub = build((True, text))
|
||||
assert navList(epub) == (
|
||||
'<li><a href="xhtml/chapter1.xhtml#sec_1">Prologue</a></li>\n'
|
||||
'<li><a href="xhtml/part1.xhtml#sec_2">Part One</a></li>\n'
|
||||
'<li><a href="xhtml/chapter2.xhtml#sec_3">Chapter One</a></li>\n'
|
||||
'<li><a href="xhtml/chapter3.xhtml#sec_4">Chapter Two</a></li>\n'
|
||||
'<li><a href="xhtml/part2.xhtml#sec_5">Part Two</a></li>\n'
|
||||
'<li><a href="xhtml/part3.xhtml#sec_6">Part Three</a></li>\n'
|
||||
'<li><a href="xhtml/chapter4.xhtml#sec_7">Chapter Three</a></li>\n'
|
||||
)
|
||||
|
||||
epub.setNestedToC(True)
|
||||
assert navList(epub) == (
|
||||
'<li><a href="xhtml/chapter1.xhtml#sec_1">Prologue</a></li>\n'
|
||||
'<li><a href="xhtml/part1.xhtml#sec_2">Part One</a>\n'
|
||||
"<ol>\n"
|
||||
'<li><a href="xhtml/chapter2.xhtml#sec_3">Chapter One</a></li>\n'
|
||||
'<li><a href="xhtml/chapter3.xhtml#sec_4">Chapter Two</a></li>\n'
|
||||
"</ol>\n"
|
||||
"</li>\n"
|
||||
'<li><a href="xhtml/part2.xhtml#sec_5">Part Two</a>\n'
|
||||
"</li>\n"
|
||||
'<li><a href="xhtml/part3.xhtml#sec_6">Part Three</a>\n'
|
||||
"<ol>\n"
|
||||
'<li><a href="xhtml/chapter4.xhtml#sec_7">Chapter Three</a></li>\n'
|
||||
"</ol>\n"
|
||||
"</li>\n"
|
||||
)
|
||||
|
||||
# Notes after a part are not nested
|
||||
epub = build((True, "# Part One\n\n## Chapter One\n\n"), (False, "#! Notes\n\n"))
|
||||
epub.setNestedToC(True)
|
||||
assert navList(epub) == (
|
||||
'<li><a href="xhtml/part1.xhtml#sec_1">Part One</a>\n'
|
||||
"<ol>\n"
|
||||
'<li><a href="xhtml/chapter1.xhtml#sec_2">Chapter One</a></li>\n'
|
||||
"</ol>\n"
|
||||
"</li>\n"
|
||||
'<li><a href="xhtml/notes1.xhtml#sec_3">Notes</a></li>\n'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testToEPub_Save(mockGUI: MockGuiMain, fncPath: Path, tstPaths: TstPaths, ipsumText: list[str]):
|
||||
"""Test the save method of the ToEPub class."""
|
||||
|
||||
@@ -380,6 +380,47 @@ def testDocumentBuilder_DocX(mockGUI: MockGuiMain, prjLipsum: Path, fncPath: Pat
|
||||
assert zipfile.is_zipfile(docFile)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testDocumentBuilder_EPub(mockGUI: MockGuiMain, prjLipsum: Path, fncPath: Path):
|
||||
"""Test building an EPub manuscript."""
|
||||
project = NWProject()
|
||||
project.openProject(prjLipsum)
|
||||
|
||||
build = BuildSettings()
|
||||
build.unpack(BUILD_CONF)
|
||||
build.setValue("epub.nestedToc", True)
|
||||
|
||||
docBuild = DocumentBuilder(project, build)
|
||||
docBuild.queueAll()
|
||||
|
||||
assert len(docBuild) == 19
|
||||
|
||||
# Check Build
|
||||
# ===========
|
||||
|
||||
docFile = fncPath / "Lorem Ipsum.epub"
|
||||
|
||||
count = 0
|
||||
error = []
|
||||
for _, success in docBuild.iterBuildDocument(docFile, nwBuildFmt.EPUB):
|
||||
count += 1 if success else 0
|
||||
if docBuild.error:
|
||||
error.append(docBuild.error)
|
||||
|
||||
assert count == 19
|
||||
assert error == []
|
||||
|
||||
assert docFile.is_file()
|
||||
assert zipfile.is_zipfile(docFile)
|
||||
with zipfile.ZipFile(docFile, mode="r") as zipObj:
|
||||
navText = zipObj.read("OEBPS/nav.xhtml").decode("utf-8")
|
||||
assert (
|
||||
'<li><a href="xhtml/part1.xhtml#sec_3">Part: Act One</a>\n'
|
||||
"<ol>\n"
|
||||
'<li><a href="xhtml/chapter2.xhtml#sec_4">Chapter: Chapter One</a></li>\n'
|
||||
) in navText
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testDocumentBuilder_PDF(mockGUI: MockGuiMain, prjLipsum: Path, fncPath: Path):
|
||||
"""Test building a PDF manuscript."""
|
||||
|
||||
@@ -1088,6 +1088,8 @@ def testGuiBuildSettings_FormatOutput(qtbot: QtBot, nwGUI: GuiMain):
|
||||
build.setValue("doc.pageHeaderEven", nwHeadFmt.DOC_AUTO_EVEN)
|
||||
build.setValue("doc.pageCountOffset", 0)
|
||||
|
||||
build.setValue("epub.nestedToc", False)
|
||||
|
||||
build.setValue("html.addStyles", False)
|
||||
build.setValue("html.preserveTabs", False)
|
||||
|
||||
@@ -1114,6 +1116,8 @@ def testGuiBuildSettings_FormatOutput(qtbot: QtBot, nwGUI: GuiMain):
|
||||
assert fmtTab.pageHeaderEven.isEnabled() is True
|
||||
assert fmtTab.btnPageHeaderEven.isEnabled() is True
|
||||
|
||||
assert fmtTab.nestedToc.isChecked() is False
|
||||
|
||||
assert fmtTab.htmlAddStyles.isChecked() is False
|
||||
assert fmtTab.htmlPreserveTabs.isChecked() is False
|
||||
|
||||
@@ -1127,6 +1131,7 @@ def testGuiBuildSettings_FormatOutput(qtbot: QtBot, nwGUI: GuiMain):
|
||||
assert fmtTab.lblMetaLanguage.text() == "British English"
|
||||
|
||||
# Toggle all
|
||||
fmtTab.nestedToc.setChecked(True)
|
||||
fmtTab.htmlAddStyles.setChecked(True)
|
||||
fmtTab.htmlPreserveTabs.setChecked(True)
|
||||
|
||||
@@ -1140,6 +1145,8 @@ def testGuiBuildSettings_FormatOutput(qtbot: QtBot, nwGUI: GuiMain):
|
||||
assert sBuild.getInt("doc.pageCountOffset") == 1
|
||||
assert sBuild.getStr("doc.metaLanguage") == "en-GB"
|
||||
|
||||
assert sBuild.getBool("epub.nestedToc") is True
|
||||
|
||||
assert sBuild.getBool("html.addStyles") is True
|
||||
assert sBuild.getBool("html.preserveTabs") is True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user