Fix footnote formatting for ODT and update document builder tests

This commit is contained in:
Veronica Berglyd Olsen
2024-04-27 19:05:13 +02:00
parent db9305c230
commit e9596e1bbc
6 changed files with 278 additions and 75 deletions
+81 -8
View File
@@ -307,7 +307,7 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
@pytest.mark.core
def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
def testCoreIndex_ScanText(monkeypatch, mockGUI, fncPath, mockRnd):
"""Check the index text scanner."""
project = NWProject()
mockRnd.reset()
@@ -377,12 +377,14 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
"@char: Jane\n\n"
"% this is a comment\n\n"
"This is a story about Jane Smith.\n\n"
"Well, not really.\n"
"Well, not really.[footnote:key]\n\n"
"%Footnote.key: Footnote text.\n\n"
))
assert index._tagsIndex.tagHandle("Jane") == cHandle
assert index._tagsIndex.tagHeading("Jane") == "T0001"
assert index._tagsIndex.tagClass("Jane") == "CHARACTER"
assert index.getItemHeading(nHandle, "T0001").title == "Hello World!" # type: ignore
assert index._itemIndex[nHandle].noteKeys("footnotes") == {"key"} # type: ignore
# Title Indexing
# ==============
@@ -549,6 +551,45 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
# END Test testCoreIndex_ScanText
@pytest.mark.core
def testCoreIndex_CommentKeys(monkeypatch, mockGUI, fncPath, mockRnd):
"""Check the index comment key generator."""
project = NWProject()
mockRnd.reset()
buildTestProject(project, fncPath)
index = project.index
nKeys = 1000
# Generate footnote keys
keys = set()
for _ in range(nKeys):
key = index.newCommentKey(C.hSceneDoc, nwComment.FOOTNOTE)
assert key not in keys
assert key != "err"
keys.add(key)
assert len(keys) == nKeys
# Generate comment keys
keys = set()
for _ in range(nKeys):
key = index.newCommentKey(C.hSceneDoc, nwComment.COMMENT)
assert key not in keys
keys.add(key)
assert len(keys) == nKeys
# Induce collision
with monkeypatch.context() as mp:
mp.setattr("random.choices", lambda *a, **k: "aaaa")
assert index.newCommentKey(C.hSceneDoc, nwComment.FOOTNOTE) == "faaaa"
assert index.newCommentKey(C.hSceneDoc, nwComment.FOOTNOTE) == "err"
# Check invalid comment style
assert index.newCommentKey(C.hSceneDoc, None) == "err" # type: ignore
# END Test testCoreIndex_CommentKeys
@pytest.mark.core
def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
"""Check the index data extraction functions."""
@@ -1250,12 +1291,14 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
itemIndex.clear()
# Data must be dictionary
with pytest.raises(ValueError):
with pytest.raises(ValueError) as exc:
itemIndex.unpackData("stuff") # type: ignore
assert str(exc.value) == "itemIndex is not a dict"
# Keys must be valid handles
with pytest.raises(ValueError):
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({"stuff": "more stuff"})
assert str(exc.value) == "itemIndex keys must be handles"
# Unknown keys should be skipped
itemIndex.unpackData({C.hInvalid: {}})
@@ -1267,8 +1310,9 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
assert itemIndex[nHandle].handle == nHandle # type: ignore
# Title tags must be valid
with pytest.raises(ValueError):
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({cHandle: {"headings": {"TTTTTTT": {}}}})
assert str(exc.value) == "The itemIndex contains an invalid title key"
# Reference without a heading should be rejected
itemIndex.unpackData({
@@ -1282,37 +1326,66 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
itemIndex.clear()
# Tag keys must be strings
with pytest.raises(ValueError):
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {1234: "@pov"}},
"notes": {"footnotes": [], "comments": []},
}
})
assert str(exc.value) == "itemIndex reference key must be a string"
# Type must be strings
with pytest.raises(ValueError):
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": []}},
"notes": {"footnotes": [], "comments": []},
}
})
assert str(exc.value) == "itemIndex reference type must be a string"
# Types must be valid
with pytest.raises(ValueError):
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": "@pov,@char,@stuff"}},
"notes": {"footnotes": [], "comments": []},
}
})
assert str(exc.value) == "The itemIndex contains an invalid reference type"
# Note type must be valid
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": "@pov,@char"}},
"notes": {"stuff": [], "comments": []},
}
})
assert str(exc.value) == "The notes style is invalid"
# Note keys must be all strings
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": "@pov,@char"}},
"notes": {"footnotes": ["fkey", 1], "comments": []},
}
})
assert str(exc.value) == "The notes keys must be a list of strings"
# This should pass
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": "@pov,@char"}},
"notes": {"footnotes": ["fkey"], "comments": ["ckey"]},
}
})
+81 -37
View File
@@ -20,9 +20,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
import json
from tools import readFile
import pytest
from novelwriter.core.project import NWProject
from novelwriter.core.tohtml import ToHtml
@@ -225,6 +225,22 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
"<a href='#tag_Bod'>Bod</a>, <a href='#tag_Jane'>Jane</a></p>\n"
)
# Tags
html._text = "@tag: Bod\n"
html.tokenizeText()
html.doConvert()
assert html.result == (
"<p class='meta meta-tag'><span class='tags'>Tag:</span> <a name='tag_Bod'>Bod</a></p>\n"
)
html._text = "@tag: Bod | Nobody Owens\n"
html.tokenizeText()
html.doConvert()
assert html.result == (
"<p class='meta meta-tag'><span class='tags'>Tag:</span> <a name='tag_Bod'>Bod</a> "
"| <span class='optional'>Nobody Owens</a></p>\n"
)
# Multiple Keywords
html._isFirst = False
html.setKeywords(True)
@@ -241,6 +257,30 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
"<span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></p>\n"
)
# Footnotes
# =========
html._text = (
"Text with one[footnote:fa] or two[footnote:fb] footnotes.\n\n"
"%footnote.fa: Footnote text A.\n\n"
)
html.tokenizeText()
html.doConvert()
assert html.result == (
"<p>Text with one<sup><a href='#footnote_1'>1</a></sup> "
"or two<sup>ERR</sup> footnotes.</p>\n"
)
html.appendFootnotes()
assert html.result == (
"<p>Text with one<sup><a href='#footnote_1'>1</a></sup> "
"or two<sup>ERR</sup> footnotes.</p>\n"
"<h3>Footnotes</h3>\n"
"<ol>\n"
"<li id='footnote_1'><p>Footnote text A.</p></li>\n"
"</ol>\n"
)
# Preview Mode
# ============
@@ -480,7 +520,7 @@ def testCoreToHtml_SpecialCases(mockGUI):
@pytest.mark.core
def testCoreToHtml_Complex(mockGUI, fncPath):
def testCoreToHtml_Save(mockGUI, fncPath):
"""Test the save method of the ToHtml class."""
project = NWProject()
html = ToHtml(project)
@@ -498,36 +538,28 @@ def testCoreToHtml_Complex(mockGUI, fncPath):
"### Scene 2\n\nThe text of scene two.\n",
"#### A Section\n\n\tMore text in scene two.\n",
]
resText = [
(
"<h1 class='title' style='text-align: center;'>My Novel</h1>\n"
"<p><strong>By Jane Doh</strong></p>\n"
),
(
"<h1 style='page-break-before: always;'>Chapter 1</h1>\n"
"<p>The text of chapter one.</p>\n"
),
(
"<h2>Scene 1</h2>\n"
"<p>The text of scene one.</p>\n"
),
(
"<h3>A Section</h3>\n"
"<p>More text in scene one.</p>\n"
),
(
"<h1 style='page-break-before: always;'>Chapter 2</h1>\n"
"<p>The text of chapter two.</p>\n"
),
(
"<h2>Scene 2</h2>\n"
"<p>The text of scene two.</p>\n"
),
(
"<h3>A Section</h3>\n"
"<p>\tMore text in scene two.</p>\n"
),
]
resText = [(
"<h1 class='title' style='text-align: center;'>My Novel</h1>\n"
"<p><strong>By Jane Doh</strong></p>\n"
), (
"<h1 style='page-break-before: always;'>Chapter 1</h1>\n"
"<p>The text of chapter one.</p>\n"
), (
"<h2>Scene 1</h2>\n"
"<p>The text of scene one.</p>\n"
), (
"<h3>A Section</h3>\n"
"<p>More text in scene one.</p>\n"
), (
"<h1 style='page-break-before: always;'>Chapter 2</h1>\n"
"<p>The text of chapter two.</p>\n"
), (
"<h2>Scene 2</h2>\n"
"<p>The text of scene two.</p>\n"
), (
"<h3>A Section</h3>\n"
"<p>\tMore text in scene two.</p>\n"
)]
for i in range(len(docText)):
html._text = docText[i]
@@ -541,9 +573,10 @@ def testCoreToHtml_Complex(mockGUI, fncPath):
html.replaceTabs(nSpaces=2, spaceChar="&nbsp;")
resText[6] = "<h3>A Section</h3>\n<p>&nbsp;&nbsp;More text in scene two.</p>\n"
# Check File
# ==========
# Check Files
# ===========
# HTML
hStyle = html.getStyleSheet()
htmlDoc = (
"<!DOCTYPE html>\n"
@@ -568,9 +601,20 @@ def testCoreToHtml_Complex(mockGUI, fncPath):
saveFile = fncPath / "outFile.htm"
html.saveHtml5(saveFile)
assert readFile(saveFile) == htmlDoc
assert saveFile.read_text(encoding="utf-8") == htmlDoc
# END Test testCoreToHtml_Complex
# JSON + HTML
saveFile = fncPath / "outFile.json"
html.saveHtmlJson(saveFile)
data = json.loads(saveFile.read_text(encoding="utf-8"))
assert data["meta"]["projectName"] == ""
assert data["meta"]["novelAuthor"] == ""
assert data["meta"]["buildTime"] > 0
assert data["meta"]["buildTimeStr"] != ""
assert data["text"]["css"] == hStyle
assert len(data["text"]["html"]) == len(resText)
# END Test testCoreToHtml_Save
@pytest.mark.core
+30 -7
View File
@@ -22,10 +22,8 @@ from __future__ import annotations
import pytest
from tools import readFile
from novelwriter.core.tomd import ToMarkdown
from novelwriter.core.project import NWProject
from novelwriter.core.tomd import ToMarkdown
@pytest.mark.core
@@ -134,6 +132,13 @@ def testCoreToMarkdown_ConvertParagraphs(mockGUI):
toMD.doConvert()
assert toMD.result == "Line one \nLine two \nLine three\n\n"
# Text wo/Hard Break
toMD._text = "Line one \nLine two \nLine three\n"
toMD.setPreserveBreaks(False)
toMD.tokenizeText()
toMD.doConvert()
assert toMD.result == "Line one Line two Line three\n\n"
# Synopsis, Short
toMD._text = "%synopsis: The synopsis ...\n"
toMD.tokenizeText()
@@ -188,6 +193,22 @@ def testCoreToMarkdown_ConvertParagraphs(mockGUI):
"**Locations:** Europe\n\n"
)
# Footnotes
toMD._text = (
"Text with one[footnote:fa] or two[footnote:fb] footnotes.\n\n"
"%footnote.fa: Footnote text A.\n\n"
)
toMD.tokenizeText()
toMD.doConvert()
assert toMD.result == "Text with one[1] or two[ERR] footnotes.\n\n"
toMD.appendFootnotes()
assert toMD.result == (
"Text with one[1] or two[ERR] footnotes.\n\n"
"### Footnotes\n\n"
"1. Footnote text A.\n\n"
)
# END Test testCoreToMarkdown_ConvertParagraphs
@@ -233,17 +254,18 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
@pytest.mark.core
def testCoreToMarkdown_Complex(mockGUI, fncPath):
def testCoreToMarkdown_Save(mockGUI, fncPath):
"""Test the save method of the ToMarkdown class."""
project = NWProject()
toMD = ToMarkdown(project)
toMD.setKeepMarkdown(True)
toMD._isNovel = True
# Build Project
# =============
docText = [
"# My Novel\n**By Jane Doh**\n",
"# My Novel\n\n**By Jane Doh**\n",
"## Chapter 1\n\nThe text of chapter one.\n",
"### Scene 1\n\nThe text of scene one.\n",
"#### A Section\n\nMore text in scene one.\n",
@@ -273,15 +295,16 @@ def testCoreToMarkdown_Complex(mockGUI, fncPath):
toMD.replaceTabs(nSpaces=4, spaceChar=" ")
resText[6] = "#### A Section\n\n More text in scene two.\n\n"
assert toMD.allMarkdown == resText
# Check File
# ==========
saveFile = fncPath / "outFile.md"
toMD.saveMarkdown(saveFile)
assert readFile(saveFile) == "".join(resText)
assert saveFile.read_text(encoding="utf-8") == "".join(resText)
# END Test testCoreToHtml_Complex
# END Test testCoreToMarkdown_Save
@pytest.mark.core
+37
View File
@@ -621,6 +621,43 @@ def testCoreToOdt_ConvertParagraphs(mockGUI):
'</office:text>'
)
# Footnotes
odt._text = (
"Text with one[footnote:fa], **two**[footnote:fd], "
"or three[footnote:fb] footnotes.[footnote:fe]\n\n"
"%footnote.fa: Footnote text A.[footnote:fc]\n\n"
"%footnote.fc: This footnote is skipped.\n\n"
"%footnote.fd: Another footnote.\n\n"
"%footnote.fe: Again?\n\n"
)
odt.tokenizeText()
odt.initDocument()
odt.doConvert()
odt.closeDocument()
assert xmlToText(odt._xText) == (
'<office:text>'
'<text:p text:style-name="Text_20_body">Text with one'
'<text:note text:id="ftn1" text:note-class="footnote">'
'<text:note-citation>1</text:note-citation>'
'<text:note-body>'
'<text:p text:style-name="Footnote">Footnote text A.</text:p>'
'</text:note-body>'
'</text:note>, <text:span text:style-name="T9">two</text:span>'
'<text:note text:id="ftn2" text:note-class="footnote">'
'<text:note-citation>2</text:note-citation>'
'<text:note-body>'
'<text:p text:style-name="Footnote">Another footnote.</text:p>'
'</text:note-body>'
'</text:note>, or three footnotes.'
'<text:note text:id="ftn3" text:note-class="footnote">'
'<text:note-citation>3</text:note-citation>'
'<text:note-body>'
'<text:p text:style-name="Footnote">Again?</text:p>'
'</text:note-body>'
'</text:note></text:p>'
'</office:text>'
)
# Test for issue #1412
# ====================
# See: https://github.com/vkbo/novelWriter/issues/1412