Fix footnote formatting for ODT and update document builder tests
This commit is contained in:
+20
-13
@@ -646,8 +646,14 @@ class ToOdt(Tokenizer):
|
|||||||
xFmt = 0x00
|
xFmt = 0x00
|
||||||
tFrag = ""
|
tFrag = ""
|
||||||
fLast = 0
|
fLast = 0
|
||||||
|
xNode = None
|
||||||
for fPos, fFmt, fData in tFmt:
|
for fPos, fFmt, fData in tFmt:
|
||||||
|
|
||||||
|
# Add any extra nodes
|
||||||
|
if xNode:
|
||||||
|
parProc.appendNode(xNode)
|
||||||
|
xNode = None
|
||||||
|
|
||||||
# Add the text up to the current fragment
|
# Add the text up to the current fragment
|
||||||
if tFrag := tText[fLast:fPos]:
|
if tFrag := tText[fLast:fPos]:
|
||||||
if xFmt == 0x00:
|
if xFmt == 0x00:
|
||||||
@@ -685,7 +691,7 @@ class ToOdt(Tokenizer):
|
|||||||
elif fFmt == self.FMT_SUB_E:
|
elif fFmt == self.FMT_SUB_E:
|
||||||
xFmt &= M_SUB
|
xFmt &= M_SUB
|
||||||
elif fFmt == self.FMT_FNOTE:
|
elif fFmt == self.FMT_FNOTE:
|
||||||
parProc.appendNode(self._generateFootnote(fData))
|
xNode = self._generateFootnote(fData)
|
||||||
elif fFmt == self.FMT_STRIP:
|
elif fFmt == self.FMT_STRIP:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -693,6 +699,9 @@ class ToOdt(Tokenizer):
|
|||||||
|
|
||||||
fLast = fPos
|
fLast = fPos
|
||||||
|
|
||||||
|
if xNode:
|
||||||
|
parProc.appendNode(xNode)
|
||||||
|
|
||||||
if tFrag := tText[fLast:]:
|
if tFrag := tText[fLast:]:
|
||||||
if xFmt == 0x00:
|
if xFmt == 0x00:
|
||||||
parProc.appendText(tFrag)
|
parProc.appendText(tFrag)
|
||||||
@@ -1575,18 +1584,16 @@ class XMLParagraph:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def appendNode(self, xNode: ET.Element | None) -> None:
|
def appendNode(self, xNode: ET.Element | None) -> None:
|
||||||
"""Append an XML node to the paragraph."""
|
"""Append an XML node to the paragraph. We only check for the
|
||||||
if xNode:
|
X_ROOT_TEXT and X_ROOT_TAIL states. X_SPAN_TEXT is not possible
|
||||||
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
|
at all, and X_SPAN_SING only happens internally in an appendSpan
|
||||||
self._xRoot.append(xNode)
|
call, returning us to an X_ROOT_TAIL state.
|
||||||
self._xTail = xNode
|
"""
|
||||||
self._xTail.tail = ""
|
if xNode and self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
|
||||||
self._nState = X_ROOT_TAIL
|
self._xRoot.append(xNode)
|
||||||
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
|
self._xTail = xNode
|
||||||
self._xTail.append(xNode)
|
self._xTail.tail = ""
|
||||||
self._xSing = xNode
|
self._nState = X_ROOT_TAIL
|
||||||
self._xSing.tail = ""
|
|
||||||
self._nState = X_SPAN_SING
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def checkError(self) -> tuple[int, str]:
|
def checkError(self) -> tuple[int, str]:
|
||||||
|
|||||||
@@ -21,24 +21,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import pytest
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
from tools import writeFile
|
import pytest
|
||||||
from mocked import causeOSError
|
|
||||||
|
|
||||||
from PyQt5.QtGui import QColor, QDesktopServices
|
from mocked import causeOSError
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
|
from PyQt5.QtGui import QColor, QDesktopServices
|
||||||
|
from tools import writeFile
|
||||||
|
|
||||||
from novelwriter.common import (
|
from novelwriter.common import (
|
||||||
checkBool, checkFloat, checkInt, checkIntTuple, checkPath, checkString,
|
NWConfigParser, checkBool, checkFloat, checkInt, checkIntTuple, checkPath,
|
||||||
checkStringNone, checkUuid, cssCol, formatFileFilter, formatInt,
|
checkString, checkStringNone, checkUuid, cssCol, formatFileFilter,
|
||||||
formatTime, formatTimeStamp, formatVersion, fuzzyTime, getFileSize,
|
formatInt, formatTime, formatTimeStamp, formatVersion, fuzzyTime,
|
||||||
hexToInt, isHandle, isItemClass, isItemLayout, isItemType, isTitleTag,
|
getFileSize, hexToInt, isHandle, isItemClass, isItemLayout, isItemType,
|
||||||
jsonEncode, makeFileNameSafe, minmax, numberToRoman, NWConfigParser,
|
isListInstance, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
|
||||||
openExternalPath, readTextFile, simplified, transferCase, xmlIndent, yesNo
|
numberToRoman, openExternalPath, readTextFile, simplified, transferCase,
|
||||||
|
xmlIndent, yesNo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -272,6 +273,24 @@ def testBaseCommon_isItemLayout():
|
|||||||
# END Test testBaseCommon_isItemLayout
|
# END Test testBaseCommon_isItemLayout
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_isListInstance():
|
||||||
|
"""Test the isListInstance function."""
|
||||||
|
# String
|
||||||
|
assert isListInstance("stuff", str) is False
|
||||||
|
assert isListInstance(["stuff"], str) is True
|
||||||
|
|
||||||
|
# Int
|
||||||
|
assert isListInstance(1, int) is False
|
||||||
|
assert isListInstance([1], int) is True
|
||||||
|
|
||||||
|
# Mixed
|
||||||
|
assert isListInstance([1], str) is False
|
||||||
|
assert isListInstance(["stuff"], int) is False
|
||||||
|
|
||||||
|
# END Test testBaseCommon_isListInstance
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_hexToInt():
|
def testBaseCommon_hexToInt():
|
||||||
"""Test the hexToInt function."""
|
"""Test the hexToInt function."""
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
|
def testCoreIndex_ScanText(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||||
"""Check the index text scanner."""
|
"""Check the index text scanner."""
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
mockRnd.reset()
|
mockRnd.reset()
|
||||||
@@ -377,12 +377,14 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
|
|||||||
"@char: Jane\n\n"
|
"@char: Jane\n\n"
|
||||||
"% this is a comment\n\n"
|
"% this is a comment\n\n"
|
||||||
"This is a story about Jane Smith.\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.tagHandle("Jane") == cHandle
|
||||||
assert index._tagsIndex.tagHeading("Jane") == "T0001"
|
assert index._tagsIndex.tagHeading("Jane") == "T0001"
|
||||||
assert index._tagsIndex.tagClass("Jane") == "CHARACTER"
|
assert index._tagsIndex.tagClass("Jane") == "CHARACTER"
|
||||||
assert index.getItemHeading(nHandle, "T0001").title == "Hello World!" # type: ignore
|
assert index.getItemHeading(nHandle, "T0001").title == "Hello World!" # type: ignore
|
||||||
|
assert index._itemIndex[nHandle].noteKeys("footnotes") == {"key"} # type: ignore
|
||||||
|
|
||||||
# Title Indexing
|
# Title Indexing
|
||||||
# ==============
|
# ==============
|
||||||
@@ -549,6 +551,45 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
|
|||||||
# END Test testCoreIndex_ScanText
|
# 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
|
@pytest.mark.core
|
||||||
def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
|
def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
|
||||||
"""Check the index data extraction functions."""
|
"""Check the index data extraction functions."""
|
||||||
@@ -1250,12 +1291,14 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
|
|||||||
itemIndex.clear()
|
itemIndex.clear()
|
||||||
|
|
||||||
# Data must be dictionary
|
# Data must be dictionary
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError) as exc:
|
||||||
itemIndex.unpackData("stuff") # type: ignore
|
itemIndex.unpackData("stuff") # type: ignore
|
||||||
|
assert str(exc.value) == "itemIndex is not a dict"
|
||||||
|
|
||||||
# Keys must be valid handles
|
# Keys must be valid handles
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError) as exc:
|
||||||
itemIndex.unpackData({"stuff": "more stuff"})
|
itemIndex.unpackData({"stuff": "more stuff"})
|
||||||
|
assert str(exc.value) == "itemIndex keys must be handles"
|
||||||
|
|
||||||
# Unknown keys should be skipped
|
# Unknown keys should be skipped
|
||||||
itemIndex.unpackData({C.hInvalid: {}})
|
itemIndex.unpackData({C.hInvalid: {}})
|
||||||
@@ -1267,8 +1310,9 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
|
|||||||
assert itemIndex[nHandle].handle == nHandle # type: ignore
|
assert itemIndex[nHandle].handle == nHandle # type: ignore
|
||||||
|
|
||||||
# Title tags must be valid
|
# Title tags must be valid
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError) as exc:
|
||||||
itemIndex.unpackData({cHandle: {"headings": {"TTTTTTT": {}}}})
|
itemIndex.unpackData({cHandle: {"headings": {"TTTTTTT": {}}}})
|
||||||
|
assert str(exc.value) == "The itemIndex contains an invalid title key"
|
||||||
|
|
||||||
# Reference without a heading should be rejected
|
# Reference without a heading should be rejected
|
||||||
itemIndex.unpackData({
|
itemIndex.unpackData({
|
||||||
@@ -1282,37 +1326,66 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
|
|||||||
itemIndex.clear()
|
itemIndex.clear()
|
||||||
|
|
||||||
# Tag keys must be strings
|
# Tag keys must be strings
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError) as exc:
|
||||||
itemIndex.unpackData({
|
itemIndex.unpackData({
|
||||||
cHandle: {
|
cHandle: {
|
||||||
"headings": {"T0001": {}},
|
"headings": {"T0001": {}},
|
||||||
"references": {"T0001": {1234: "@pov"}},
|
"references": {"T0001": {1234: "@pov"}},
|
||||||
|
"notes": {"footnotes": [], "comments": []},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
assert str(exc.value) == "itemIndex reference key must be a string"
|
||||||
|
|
||||||
# Type must be strings
|
# Type must be strings
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError) as exc:
|
||||||
itemIndex.unpackData({
|
itemIndex.unpackData({
|
||||||
cHandle: {
|
cHandle: {
|
||||||
"headings": {"T0001": {}},
|
"headings": {"T0001": {}},
|
||||||
"references": {"T0001": {"John": []}},
|
"references": {"T0001": {"John": []}},
|
||||||
|
"notes": {"footnotes": [], "comments": []},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
assert str(exc.value) == "itemIndex reference type must be a string"
|
||||||
|
|
||||||
# Types must be valid
|
# Types must be valid
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError) as exc:
|
||||||
itemIndex.unpackData({
|
itemIndex.unpackData({
|
||||||
cHandle: {
|
cHandle: {
|
||||||
"headings": {"T0001": {}},
|
"headings": {"T0001": {}},
|
||||||
"references": {"T0001": {"John": "@pov,@char,@stuff"}},
|
"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
|
# This should pass
|
||||||
itemIndex.unpackData({
|
itemIndex.unpackData({
|
||||||
cHandle: {
|
cHandle: {
|
||||||
"headings": {"T0001": {}},
|
"headings": {"T0001": {}},
|
||||||
"references": {"T0001": {"John": "@pov,@char"}},
|
"references": {"T0001": {"John": "@pov,@char"}},
|
||||||
|
"notes": {"footnotes": ["fkey"], "comments": ["ckey"]},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import json
|
||||||
|
|
||||||
from tools import readFile
|
import pytest
|
||||||
|
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
from novelwriter.core.tohtml import ToHtml
|
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"
|
"<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
|
# Multiple Keywords
|
||||||
html._isFirst = False
|
html._isFirst = False
|
||||||
html.setKeywords(True)
|
html.setKeywords(True)
|
||||||
@@ -241,6 +257,30 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
|
|||||||
"<span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></p>\n"
|
"<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
|
# Preview Mode
|
||||||
# ============
|
# ============
|
||||||
|
|
||||||
@@ -480,7 +520,7 @@ def testCoreToHtml_SpecialCases(mockGUI):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreToHtml_Complex(mockGUI, fncPath):
|
def testCoreToHtml_Save(mockGUI, fncPath):
|
||||||
"""Test the save method of the ToHtml class."""
|
"""Test the save method of the ToHtml class."""
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
html = ToHtml(project)
|
html = ToHtml(project)
|
||||||
@@ -498,36 +538,28 @@ def testCoreToHtml_Complex(mockGUI, fncPath):
|
|||||||
"### Scene 2\n\nThe text of scene two.\n",
|
"### Scene 2\n\nThe text of scene two.\n",
|
||||||
"#### A Section\n\n\tMore text in scene two.\n",
|
"#### A Section\n\n\tMore text in scene two.\n",
|
||||||
]
|
]
|
||||||
resText = [
|
resText = [(
|
||||||
(
|
"<h1 class='title' style='text-align: center;'>My Novel</h1>\n"
|
||||||
"<h1 class='title' style='text-align: center;'>My Novel</h1>\n"
|
"<p><strong>By Jane Doh</strong></p>\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"
|
||||||
"<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"
|
||||||
(
|
), (
|
||||||
"<h2>Scene 1</h2>\n"
|
"<h3>A Section</h3>\n"
|
||||||
"<p>The text of scene one.</p>\n"
|
"<p>More text in scene one.</p>\n"
|
||||||
),
|
), (
|
||||||
(
|
"<h1 style='page-break-before: always;'>Chapter 2</h1>\n"
|
||||||
"<h3>A Section</h3>\n"
|
"<p>The text of chapter two.</p>\n"
|
||||||
"<p>More text in scene one.</p>\n"
|
), (
|
||||||
),
|
"<h2>Scene 2</h2>\n"
|
||||||
(
|
"<p>The text of scene two.</p>\n"
|
||||||
"<h1 style='page-break-before: always;'>Chapter 2</h1>\n"
|
), (
|
||||||
"<p>The text of chapter two.</p>\n"
|
"<h3>A Section</h3>\n"
|
||||||
),
|
"<p>\tMore text in scene 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)):
|
for i in range(len(docText)):
|
||||||
html._text = docText[i]
|
html._text = docText[i]
|
||||||
@@ -541,9 +573,10 @@ def testCoreToHtml_Complex(mockGUI, fncPath):
|
|||||||
html.replaceTabs(nSpaces=2, spaceChar=" ")
|
html.replaceTabs(nSpaces=2, spaceChar=" ")
|
||||||
resText[6] = "<h3>A Section</h3>\n<p> More text in scene two.</p>\n"
|
resText[6] = "<h3>A Section</h3>\n<p> More text in scene two.</p>\n"
|
||||||
|
|
||||||
# Check File
|
# Check Files
|
||||||
# ==========
|
# ===========
|
||||||
|
|
||||||
|
# HTML
|
||||||
hStyle = html.getStyleSheet()
|
hStyle = html.getStyleSheet()
|
||||||
htmlDoc = (
|
htmlDoc = (
|
||||||
"<!DOCTYPE html>\n"
|
"<!DOCTYPE html>\n"
|
||||||
@@ -568,9 +601,20 @@ def testCoreToHtml_Complex(mockGUI, fncPath):
|
|||||||
|
|
||||||
saveFile = fncPath / "outFile.htm"
|
saveFile = fncPath / "outFile.htm"
|
||||||
html.saveHtml5(saveFile)
|
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
|
@pytest.mark.core
|
||||||
|
|||||||
@@ -22,10 +22,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tools import readFile
|
|
||||||
|
|
||||||
from novelwriter.core.tomd import ToMarkdown
|
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
|
from novelwriter.core.tomd import ToMarkdown
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
@@ -134,6 +132,13 @@ def testCoreToMarkdown_ConvertParagraphs(mockGUI):
|
|||||||
toMD.doConvert()
|
toMD.doConvert()
|
||||||
assert toMD.result == "Line one \nLine two \nLine three\n\n"
|
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
|
# Synopsis, Short
|
||||||
toMD._text = "%synopsis: The synopsis ...\n"
|
toMD._text = "%synopsis: The synopsis ...\n"
|
||||||
toMD.tokenizeText()
|
toMD.tokenizeText()
|
||||||
@@ -188,6 +193,22 @@ def testCoreToMarkdown_ConvertParagraphs(mockGUI):
|
|||||||
"**Locations:** Europe\n\n"
|
"**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
|
# END Test testCoreToMarkdown_ConvertParagraphs
|
||||||
|
|
||||||
|
|
||||||
@@ -233,17 +254,18 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreToMarkdown_Complex(mockGUI, fncPath):
|
def testCoreToMarkdown_Save(mockGUI, fncPath):
|
||||||
"""Test the save method of the ToMarkdown class."""
|
"""Test the save method of the ToMarkdown class."""
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
toMD = ToMarkdown(project)
|
toMD = ToMarkdown(project)
|
||||||
|
toMD.setKeepMarkdown(True)
|
||||||
toMD._isNovel = True
|
toMD._isNovel = True
|
||||||
|
|
||||||
# Build Project
|
# Build Project
|
||||||
# =============
|
# =============
|
||||||
|
|
||||||
docText = [
|
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",
|
"## Chapter 1\n\nThe text of chapter one.\n",
|
||||||
"### Scene 1\n\nThe text of scene one.\n",
|
"### Scene 1\n\nThe text of scene one.\n",
|
||||||
"#### A Section\n\nMore text in 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=" ")
|
toMD.replaceTabs(nSpaces=4, spaceChar=" ")
|
||||||
resText[6] = "#### A Section\n\n More text in scene two.\n\n"
|
resText[6] = "#### A Section\n\n More text in scene two.\n\n"
|
||||||
|
assert toMD.allMarkdown == resText
|
||||||
|
|
||||||
# Check File
|
# Check File
|
||||||
# ==========
|
# ==========
|
||||||
|
|
||||||
saveFile = fncPath / "outFile.md"
|
saveFile = fncPath / "outFile.md"
|
||||||
toMD.saveMarkdown(saveFile)
|
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
|
@pytest.mark.core
|
||||||
|
|||||||
@@ -621,6 +621,43 @@ def testCoreToOdt_ConvertParagraphs(mockGUI):
|
|||||||
'</office:text>'
|
'</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
|
# Test for issue #1412
|
||||||
# ====================
|
# ====================
|
||||||
# See: https://github.com/vkbo/novelWriter/issues/1412
|
# See: https://github.com/vkbo/novelWriter/issues/1412
|
||||||
|
|||||||
Reference in New Issue
Block a user