Add forced line breaks feature (#2063)

This commit is contained in:
Veronica Berglyd Olsen
2024-10-24 17:55:56 +02:00
committed by GitHub
28 changed files with 515 additions and 279 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"Short Description": "Short Description",
"Footnotes": "Footnotes",
"Comment": "Comment",
"Note": "Note",
"Notes": "Notes",
"Tag": "Tag",
"Point of View": "Point of View",
"Focus": "Focus",
+3 -1
View File
@@ -61,10 +61,11 @@ class nwConst:
class nwRegEx:
WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b"
BREAK = r"(?i)(?<!\\)(\[br\]\n?)"
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])"
FMT_SC = r"(?i)(?<!\\)(\[(?:b|/b|i|/i|s|/s|u|/u|m|/m|sup|/sup|sub|/sub|br)\])"
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
@@ -84,6 +85,7 @@ class nwShortcode:
SUP_C = "[/sup]"
SUB_O = "[sub]"
SUB_C = "[/sub]"
BREAK = "[br]"
FOOTNOTE_B = "[footnote:"
+1 -1
View File
@@ -67,7 +67,7 @@ SETTINGS_TEMPLATE: dict[str, tuple[type, str | int | float | bool]] = {
"headings.centerPart": (bool, True),
"headings.centerChapter": (bool, False),
"headings.centerScene": (bool, False),
"headings.breakTitle": (bool, True),
"headings.breakTitle": (bool, False),
"headings.breakPart": (bool, True),
"headings.breakChapter": (bool, True),
"headings.breakScene": (bool, False),
+2 -2
View File
@@ -267,8 +267,8 @@ class NWBuildDocument:
self._build.getBool("headings.hideSection")
)
bldObj.setTitleStyle(
self._build.getBool("headings.centerPart"),
self._build.getBool("headings.breakPart")
self._build.getBool("headings.centerTitle"),
self._build.getBool("headings.breakTitle")
)
bldObj.setPartitionStyle(
self._build.getBool("headings.centerPart"),
+1
View File
@@ -137,6 +137,7 @@ class nwDocInsert(Enum):
VSPACE_M = 9
LIPSUM = 10
FOOTNOTE = 11
LINE_BRK = 12
class nwView(Enum):
+5 -10
View File
@@ -255,24 +255,19 @@ class ToDocX(Tokenizer):
self._processFragments(par, S_NORM, tText, tFormat)
elif tType == BlockTyp.TITLE:
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._processFragments(par, S_TITLE, tHead, tFormat)
self._processFragments(par, S_TITLE, tText, tFormat)
elif tType == BlockTyp.HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._processFragments(par, S_HEAD1, tHead, tFormat)
self._processFragments(par, S_HEAD1, tText, tFormat)
elif tType == BlockTyp.HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._processFragments(par, S_HEAD2, tHead, tFormat)
self._processFragments(par, S_HEAD2, tText, tFormat)
elif tType == BlockTyp.HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._processFragments(par, S_HEAD3, tHead, tFormat)
self._processFragments(par, S_HEAD3, tText, tFormat)
elif tType == BlockTyp.HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._processFragments(par, S_HEAD4, tHead, tFormat)
self._processFragments(par, S_HEAD4, tText, tFormat)
elif tType == BlockTyp.SEP:
self._processFragments(par, S_SEP, tText)
+6 -6
View File
@@ -30,7 +30,7 @@ from pathlib import Path
from time import time
from novelwriter.common import formatTimeStamp
from novelwriter.constants import nwHeadFmt, nwHtmlUnicode
from novelwriter.constants import nwHtmlUnicode
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
from novelwriter.formats.tokenizer import Tokenizer
@@ -211,23 +211,23 @@ class ToHtml(Tokenizer):
lines.append(f"<p{hStyle}>{self._formatText(tText, tFmt)}</p>\n")
elif tType == BlockTyp.TITLE:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
tHead = tText.replace("\n", "<br>")
lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
elif tType == BlockTyp.HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
tHead = tText.replace("\n", "<br>")
lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
elif tType == BlockTyp.HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
tHead = tText.replace("\n", "<br>")
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
elif tType == BlockTyp.HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
tHead = tText.replace("\n", "<br>")
lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
elif tType == BlockTyp.HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
tHead = tText.replace("\n", "<br>")
lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
elif tType == BlockTyp.SEP:
+52 -42
View File
@@ -56,7 +56,6 @@ class ComStyle(NamedTuple):
textClass: str = ""
B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE)
COMMENT_STYLE = {
nwComment.PLAIN: ComStyle("Comment", "comment", "comment"),
nwComment.IGNORE: ComStyle(),
@@ -67,13 +66,12 @@ COMMENT_STYLE = {
nwComment.COMMENT: ComStyle(),
nwComment.STORY: ComStyle("", "modifier", "note"),
}
# Lookups
HEADINGS = [BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.HEAD4]
SKIP_INDENT = [
BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD2, BlockTyp.HEAD3,
BlockTyp.HEAD4, BlockTyp.SEP, BlockTyp.SKIP,
]
B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE)
class Tokenizer(ABC):
@@ -182,8 +180,6 @@ class Tokenizer(ABC):
(REGEX_PATTERNS.markdownBold, [0, TextFmt.B_B, 0, TextFmt.B_E]),
(REGEX_PATTERNS.markdownStrike, [0, TextFmt.D_B, 0, TextFmt.D_E]),
]
self._rxShortCodes = REGEX_PATTERNS.shortcodePlain
self._rxShortCodeVals = REGEX_PATTERNS.shortcodeValue
self._shortCodeFmt = {
nwShortcode.ITALIC_O: TextFmt.I_B, nwShortcode.ITALIC_C: TextFmt.I_E,
@@ -456,20 +452,22 @@ class Tokenizer(ABC):
self._text = ""
self._handle = None
if (tItem := self._project.tree[tHandle]) and tItem.isRootType():
if (item := self._project.tree[tHandle]) and item.isRootType():
self._handle = tHandle
style = BlockFmt.CENTRE
if self._isFirst:
textAlign = BlockFmt.CENTRE
self._isFirst = False
else:
textAlign = BlockFmt.PBB | BlockFmt.CENTRE
style |= BlockFmt.PBB
trNotes = self._localLookup("Notes")
title = f"{trNotes}: {tItem.itemName}"
self._blocks = []
self._blocks.append((
BlockTyp.TITLE, f"{self._handle}:T0001", title, [], textAlign
))
title = item.itemName
if not item.isNovelLike():
notes = self._localLookup("Notes")
title = f"{notes}: {title}"
self._blocks = [(
BlockTyp.TITLE, f"{self._handle}:T0001", title, [], style
)]
if self._keepRaw:
self._raw.append(f"#! {title}\n\n")
@@ -523,25 +521,30 @@ class Tokenizer(ABC):
isNovel = self._isNovel
keepRaw = self._keepRaw
doJustify = self._doJustify
keepBreaks = self._keepBreaks
indentFirst = self._indentFirst
firstIndent = self._firstIndent
if self._isNovel:
self._hFormatter.setHandle(self._handle)
# Replace all instances of [br] with a placeholder character
text = REGEX_PATTERNS.lineBreak.sub("\uffff", self._text)
nHead = 0
breakNext = False
tmpMarkdown = []
rawText = []
tHandle = self._handle or ""
tBlocks: list[T_Block] = [B_EMPTY]
for aLine in self._text.splitlines():
for bLine in text.splitlines():
aLine = bLine.replace("\uffff", "") # Remove placeholder characters
sLine = aLine.strip().lower()
# Check for blank lines
if not sLine:
tBlocks.append(B_EMPTY)
if keepRaw:
tmpMarkdown.append("\n")
rawText.append("\n")
continue
if breakNext:
@@ -607,13 +610,13 @@ class Tokenizer(ABC):
BlockTyp.COMMENT, "", tLine, tFmt, sAlign
))
if keepRaw:
tmpMarkdown.append(f"{aLine}\n")
rawText.append(f"{aLine}\n")
elif cStyle == nwComment.FOOTNOTE:
tLine, tFmt = self._extractFormats(cText, skip=TextFmt.FNOTE)
self._footnotes[f"{tHandle}:{cKey}"] = (tLine, tFmt)
if keepRaw:
tmpMarkdown.append(f"{aLine}\n")
rawText.append(f"{aLine}\n")
elif aLine.startswith("@"):
# Keywords
@@ -628,7 +631,7 @@ class Tokenizer(ABC):
BlockTyp.KEYWORD, tTag[1:], tLine, tFmt, sAlign
))
if keepRaw:
tmpMarkdown.append(f"{aLine}\n")
rawText.append(f"{aLine}\n")
elif aLine.startswith(("# ", "#! ")):
# Title or Partition Headings
@@ -664,7 +667,7 @@ class Tokenizer(ABC):
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
))
if keepRaw:
tmpMarkdown.append(f"{aLine}\n")
rawText.append(f"{aLine}\n")
elif aLine.startswith(("## ", "##! ")):
# (Unnumbered) Chapter Headings
@@ -699,7 +702,7 @@ class Tokenizer(ABC):
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
))
if keepRaw:
tmpMarkdown.append(f"{aLine}\n")
rawText.append(f"{aLine}\n")
elif aLine.startswith(("### ", "###! ")):
# (Alternative) Scene Headings
@@ -740,7 +743,7 @@ class Tokenizer(ABC):
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
))
if keepRaw:
tmpMarkdown.append(f"{aLine}\n")
rawText.append(f"{aLine}\n")
elif aLine.startswith("#### "):
# Section Headings
@@ -770,7 +773,7 @@ class Tokenizer(ABC):
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
))
if keepRaw:
tmpMarkdown.append(f"{aLine}\n")
rawText.append(f"{aLine}\n")
else:
# Text Lines
@@ -786,19 +789,19 @@ class Tokenizer(ABC):
alnRight = False
indLeft = False
indRight = False
if aLine.startswith(">>"):
if bLine.startswith(">>"):
alnRight = True
aLine = aLine[2:].lstrip(" ")
elif aLine.startswith(">"):
bLine = bLine[2:].lstrip(" ")
elif bLine.startswith(">"):
indLeft = True
aLine = aLine[1:].lstrip(" ")
bLine = bLine[1:].lstrip(" ")
if aLine.endswith("<<"):
if bLine.endswith("<<"):
alnLeft = True
aLine = aLine[:-2].rstrip(" ")
elif aLine.endswith("<"):
bLine = bLine[:-2].rstrip(" ")
elif bLine.endswith("<"):
indRight = True
aLine = aLine[:-1].rstrip(" ")
bLine = bLine[:-1].rstrip(" ")
if alnLeft and alnRight:
sAlign |= BlockFmt.CENTRE
@@ -813,12 +816,12 @@ class Tokenizer(ABC):
sAlign |= BlockFmt.IND_R
# Process formats
tLine, tFmt = self._extractFormats(aLine, hDialog=isNovel)
tLine, tFmt = self._extractFormats(bLine, hDialog=isNovel)
tBlocks.append((
BlockTyp.TEXT, "", tLine, tFmt, sAlign
))
if keepRaw:
tmpMarkdown.append(f"{aLine}\n")
rawText.append(f"{aLine}\n")
# If we have content, turn off the first page flag
if self._isFirst and len(tBlocks) > 1:
@@ -834,8 +837,8 @@ class Tokenizer(ABC):
# Always add an empty line at the end of the file
tBlocks.append(B_EMPTY)
if keepRaw:
tmpMarkdown.append("\n")
self._raw.append("".join(tmpMarkdown))
rawText.append("\n")
self._raw.append("".join(rawText))
# Second Pass
# ===========
@@ -844,7 +847,7 @@ class Tokenizer(ABC):
# It also ensures that there isn't paragraph spacing between
# meta data lines for formats that have spacing.
lineSep = "\n" if self._keepBreaks else " "
lineSep = "\n" if keepBreaks else " "
pLines: list[T_Block] = []
sBlocks: list[T_Block] = []
@@ -894,9 +897,12 @@ class Tokenizer(ABC):
# enabled, and there is no alignment, we apply it.
if doJustify and not cStyle & BlockFmt.ALIGNED:
cStyle |= BlockFmt.JUSTIFY
pTxt = pLines[0][2].replace("\uffff", "\n")
sBlocks.append((
BlockTyp.TEXT, pLines[0][1], pLines[0][2], pLines[0][3], cStyle
BlockTyp.TEXT, pLines[0][1], pTxt, pLines[0][3], cStyle
))
elif nLines > 1:
# The paragraph contains multiple lines, so we need to
# join them according to the line break policy, and
@@ -907,8 +913,11 @@ class Tokenizer(ABC):
tLen = len(tTxt)
tTxt += f"{aBlock[2]}{lineSep}"
tFmt.extend((p+tLen, fmt, key) for p, fmt, key in aBlock[3])
cStyle |= aBlock[4]
pTxt = tTxt[:-1].replace("\uffff", "\n")
sBlocks.append((
BlockTyp.TEXT, pLines[0][1], tTxt[:-1], tFmt, cStyle
BlockTyp.TEXT, pLines[0][1], pTxt, tFmt, cStyle
))
# Reset buffer and make sure text indent is on for next pass
@@ -1136,12 +1145,12 @@ class Tokenizer(ABC):
# Post-process text and format
result = text
formats = []
for pos, end, fmt, key in reversed(sorted(temp, key=lambda x: x[0])):
for pos, end, fmt, meta in reversed(sorted(temp, key=lambda x: x[0])):
if fmt > 0:
if end > pos:
result = result[:pos] + result[end:]
formats = [(p+pos-end if p > pos else p, f, k) for p, f, k in formats]
formats.insert(0, (pos, fmt, key))
formats = [(p+pos-end if p > pos else p, f, m) for p, f, m in formats]
formats.insert(0, (pos, fmt, meta))
return result, formats
@@ -1187,6 +1196,7 @@ class HeadingFormatter:
def apply(self, hFormat: str, text: str, nHead: int) -> str:
"""Apply formatting to a specific heading."""
hFormat = hFormat.replace(nwHeadFmt.TITLE, text)
hFormat = hFormat.replace(nwHeadFmt.BR, "\n")
hFormat = hFormat.replace(nwHeadFmt.CH_NUM, str(self._chCount))
hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount))
hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount))
+6 -6
View File
@@ -27,7 +27,7 @@ import logging
from pathlib import Path
from novelwriter.constants import nwHeadFmt, nwUnicode
from novelwriter.constants import nwUnicode
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
from novelwriter.formats.tokenizer import Tokenizer
@@ -113,23 +113,23 @@ class ToMarkdown(Tokenizer):
lines.append(f"{tTemp}\n\n")
elif tType == BlockTyp.TITLE:
tHead = tText.replace(nwHeadFmt.BR, "\n")
tHead = tText.replace("\n", " - ")
lines.append(f"# {tHead}\n\n")
elif tType == BlockTyp.HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "\n")
tHead = tText.replace("\n", " - ")
lines.append(f"# {tHead}\n\n")
elif tType == BlockTyp.HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "\n")
tHead = tText.replace("\n", " - ")
lines.append(f"## {tHead}\n\n")
elif tType == BlockTyp.HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "\n")
tHead = tText.replace("\n", " - ")
lines.append(f"### {tHead}\n\n")
elif tType == BlockTyp.HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "\n")
tHead = tText.replace("\n", " - ")
lines.append(f"#### {tHead}\n\n")
elif tType == BlockTyp.SEP:
+5 -10
View File
@@ -444,24 +444,19 @@ class ToOdt(Tokenizer):
elif tType == BlockTyp.TITLE:
# Title must be text:p
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._addTextPar(xText, S_TITLE, oStyle, tHead, isHead=False)
self._addTextPar(xText, S_TITLE, oStyle, tText, isHead=False)
elif tType == BlockTyp.HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._addTextPar(xText, S_HEAD1, oStyle, tHead, isHead=True, oLevel="1")
self._addTextPar(xText, S_HEAD1, oStyle, tText, isHead=True, oLevel="1")
elif tType == BlockTyp.HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._addTextPar(xText, S_HEAD2, oStyle, tHead, isHead=True, oLevel="2")
self._addTextPar(xText, S_HEAD2, oStyle, tText, isHead=True, oLevel="2")
elif tType == BlockTyp.HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._addTextPar(xText, S_HEAD3, oStyle, tHead, isHead=True, oLevel="3")
self._addTextPar(xText, S_HEAD3, oStyle, tText, isHead=True, oLevel="3")
elif tType == BlockTyp.HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "\n")
self._addTextPar(xText, S_HEAD4, oStyle, tHead, isHead=True, oLevel="4")
self._addTextPar(xText, S_HEAD4, oStyle, tText, isHead=True, oLevel="4")
elif tType == BlockTyp.SEP:
self._addTextPar(xText, S_SEP, oStyle, tText)
+2 -2
View File
@@ -34,7 +34,7 @@ from PyQt5.QtGui import (
)
from PyQt5.QtPrintSupport import QPrinter
from novelwriter.constants import nwHeadFmt, nwStyles, nwUnicode
from novelwriter.constants import nwStyles, nwUnicode
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
from novelwriter.formats.tokenizer import HEADINGS, Tokenizer
@@ -217,7 +217,7 @@ class ToQTextDocument(Tokenizer):
elif tType in HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt)
newBlock(cursor, bFmt)
cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt)
cursor.insertText(tText, cFmt)
elif tType == BlockTyp.SEP:
newBlock(cursor, bFmt)
+2
View File
@@ -877,6 +877,8 @@ class GuiDocEditor(QPlainTextEdit):
after = False
elif insert == nwDocInsert.FOOTNOTE:
self._insertCommentStructure(nwComment.FOOTNOTE)
elif insert == nwDocInsert.LINE_BRK:
text = nwShortcode.BREAK
if text:
if block:
+8 -2
View File
@@ -570,8 +570,8 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocInsert.emit(nwDocInsert.SHORT)
)
# Insert > Symbols
self.mInsBreaks = self.insMenu.addMenu(self.tr("Page Break and Space"))
# Insert > Breaks and Vertical Space
self.mInsBreaks = self.insMenu.addMenu(self.tr("Breaks and Vertical Space"))
# Insert > New Page
self.aInsNewPage = self.mInsBreaks.addAction(self.tr("Page Break"))
@@ -579,6 +579,12 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocInsert.emit(nwDocInsert.NEW_PAGE)
)
# Insert > Forced Line Break
self.aInsLineBreak = self.mInsBreaks.addAction(self.tr("Forced Line Break"))
self.aInsLineBreak.triggered.connect(
lambda: self.requestDocInsert.emit(nwDocInsert.LINE_BRK)
)
# Insert > Vertical Space (Single)
self.aInsVSpaceS = self.mInsBreaks.addAction(self.tr("Vertical Space (Single)"))
self.aInsVSpaceS.triggered.connect(
+6
View File
@@ -33,6 +33,7 @@ class RegExPatterns:
# Static RegExes
_rxWords = re.compile(nwRegEx.WORDS, re.UNICODE)
_rxBreak = re.compile(nwRegEx.BREAK, re.UNICODE)
_rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE)
_rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE)
_rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE)
@@ -44,6 +45,11 @@ class RegExPatterns:
"""Split text into words."""
return self._rxWords
@property
def lineBreak(self) -> re.Pattern:
"""Find forced line break."""
return self._rxBreak
@property
def markdownItalic(self) -> re.Pattern:
"""Markdown italic style."""
+10 -3
View File
@@ -1,11 +1,18 @@
%%~name: Title Page
%%~path: 7031beac91f75/53b69b83cdafc
%%~kind: NOVEL/DOCUMENT
%%~hash: c5dc35d18ecb074a9e41a1410d1bff8021cf0a5b
%%~date: Unknown/2023-08-25 16:51:52
%%~hash: 4072adb6d21ff877577f033f19714d9bd01396f3
%%~date: Unknown/2024-10-24 16:25:44
Jane Smith[br]
42 Main Street[br]
1234 Capital City <<
[vspace:5]
#! My Novel
>> **By Jane Smith** <<
>> This is the title page. <<
>> It should be the first document of the project. <<
>> It should be the first document of the project. <<
+13 -13
View File
@@ -1,24 +1,24 @@
%%~name: Interlude
%%~path: 7031beac91f75/ba8a28a246524
%%~kind: NOVEL/DOCUMENT
%%~hash: 721d3d15e0233186354ac6fa61f27db10f38e6bc
%%~date: Unknown/2024-03-14 22:55:04
%%~hash: 5c8f68d48573b576dcaacf6d6928c496ec361b9d
%%~date: Unknown/2024-10-24 01:22:15
##! Interlude
% Notice that this document has a title with a ! in it in addition to the two hash symbols. This means it is an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue.
I am the very model of a modern Major-General
I've information vegetable, animal, and mineral
I know the kings of England, and I quote the fights historical
From Marathon to Waterloo, in order categorical
I am the very model of a modern Major-General[br]
I've information vegetable, animal, and mineral[br]
I know the kings of England, and I quote the fights historical[br]
From Marathon to Waterloo, in order categorical <<
I'm very well acquainted, too, with matters mathematical <<
I understand equations, both the simple and quadratical
About binomial theorem I'm teeming with a lot o news
With many cheerful facts about the square of the hypotenuse
I'm very well acquainted, too, with matters mathematical[br]
I understand equations, both the simple and quadratical[br]
About binomial theorem I'm teeming with a lot o news[br]
With many cheerful facts about the square of the hypotenuse <<
With many cheerful facts about the square of the hypotenuse <<
With many cheerful facts about the square of the hypotenuse
With many cheerful facts about the square of the hypotepotenuse
With many cheerful facts about the square of the hypotenuse[br]
With many cheerful facts about the square of the hypotenuse[br]
With many cheerful facts about the square of the hypotepotenuse <<
% Notice that the lines in the verse end in a single line break. Single line breaks do not create a new paragraph but instead insert a break within the paragraph. Press Ctrl+R to see what this renders like.
+6 -6
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.6a2" hexVersion="0x020600a2" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-23 17:41:09">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2074" autoCount="277" editTime="93039">
<novelWriterXML appVersion="2.6a2" hexVersion="0x020600a2" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-24 16:26:12">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2077" autoCount="279" editTime="93511">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
@@ -36,13 +36,13 @@
<entry key="i56be10" count="1" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="31" novelWords="996" notesWords="416">
<content items="31" novelWords="1004" notesWords="416">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name>
</item>
<item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H1" charCount="95" wordCount="19" paraCount="2" cursorPos="31" />
<meta expanded="no" heading="H1" charCount="136" wordCount="27" paraCount="3" cursorPos="69" />
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -58,7 +58,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2953" wordCount="520" paraCount="15" cursorPos="66" />
<meta expanded="no" heading="H3" charCount="2937" wordCount="520" paraCount="15" cursorPos="4" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -66,7 +66,7 @@
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="357" />
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="1182" />
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
</item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
@@ -2,19 +2,19 @@
"meta": {
"projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
"buildTime": 1729029144,
"buildTimeStr": "2024-10-15 23:52:24"
"buildTime": 1729725334,
"buildTimeStr": "2024-10-24 01:15:34"
},
"text": {
"nwd": [
[
"#! Lorem Ipsum",
"",
"**By lipsum.com**",
">> **By lipsum.com** <<",
"",
"\u201cNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u2026\u201d",
">> \u201cNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u2026\u201d <<",
"",
"\u201cThere is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain\u2026\u201d"
">> \u201cThere is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain\u2026\u201d <<"
],
[
"",
@@ -36,7 +36,7 @@
[
"# Act One",
"",
"\u201cFusce maximus felis libero\u201d"
">> \u201cFusce maximus felis libero\u201d <<"
],
[
"## Chapter One",
@@ -1,10 +1,10 @@
#! Lorem Ipsum
**By lipsum.com**
>> **By lipsum.com** <<
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
>> “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” <<
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
>> “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” <<
% Exctracted from the lipsum.com website.
@@ -23,7 +23,7 @@ _Lorem Ipsum_ is simply dummy text[footnote:f9kgf] of the printing and typesetti
# Act One
“Fusce maximus felis libero”
>> “Fusce maximus felis libero” <<
## Chapter One
+3 -3
View File
@@ -412,7 +412,7 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
assert error == []
assert docFile.read_text(encoding="utf-8") == (
"#! New Novel\n\n"
"By Jane Doe\n\n"
">> By Jane Doe <<\n\n"
"## New Chapter\n\n\n"
"### New Scene\n\n\n"
)
@@ -442,7 +442,7 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
assert error == []
assert docFile.read_text(encoding="utf-8") == (
"#! New Novel\n\n"
"By Jane Doe\n\n"
">> By Jane Doe <<\n\n"
"## New Chapter\n\n\n"
"### New Scene\n\n\n"
)
@@ -566,7 +566,7 @@ def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd):
assert isinstance(docBuild.lastBuild, ToRaw)
assert docFile.read_text(encoding="utf-8") == (
"#! New Novel\n\n"
"By Jane Doe\n\n"
">> By Jane Doe <<\n\n"
"## New Chapter\n\n\n"
"### New Scene\n\n\n"
"#! Notes: Plot\n\n"
+145 -83
View File
@@ -32,10 +32,7 @@ from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.project import NWProject
from novelwriter.enum import nwBuildFmt
from novelwriter.formats.shared import BlockFmt, BlockTyp
from novelwriter.formats.todocx import (
S_FNOTE, S_HEAD1, S_HEAD2, S_HEAD3, S_HEAD4, S_META, S_NORM, S_SEP,
S_TITLE, ToDocX, _mkTag, _wTag
)
from novelwriter.formats.todocx import ToDocX, _mkTag, _wTag
from tests.tools import DOCX_IGNORE, cmpFiles
@@ -59,6 +56,120 @@ def xmlToText(xElem):
return rTxt
@pytest.mark.core
def testFmtToDocX_HeadingStyles(mockGUI):
"""Test formatting of headings."""
project = NWProject()
doc = ToDocX(project)
doc._isNovel = True
doc.initDocument()
# Title
# =====
xTest = ET.Element(_wTag("body"))
doc._text = "#! Hello World"
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
'<w:body><w:p><w:pPr><w:pStyle w:val="Title" /><w:jc w:val="center" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 1
# ===============
doc._text = "# Hello World"
# Plain
xTest = ET.Element(_wTag("body"))
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
'<w:body><w:p><w:pPr><w:pStyle w:val="Heading1" /><w:jc w:val="center" /></w:pPr>'
'<w:r><w:br w:type="page" /></w:r>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Formatted
xTest = ET.Element(_wTag("body"))
doc.setPartitionFormat(f"Part{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
'<w:body><w:p><w:pPr><w:pStyle w:val="Heading1" /><w:jc w:val="center" /></w:pPr>'
'<w:r><w:br w:type="page" /></w:r>'
'<w:r><w:rPr /><w:t>Part</w:t><w:br /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 2
# ===============
doc._text = "## Hello World"
# Plain
xTest = ET.Element(_wTag("body"))
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
'<w:body><w:p><w:pPr><w:pStyle w:val="Heading2" /></w:pPr>'
'<w:r><w:br w:type="page" /></w:r>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Formatted
xTest = ET.Element(_wTag("body"))
doc.setChapterFormat(f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
'<w:body><w:p><w:pPr><w:pStyle w:val="Heading2" /></w:pPr>'
'<w:r><w:br w:type="page" /></w:r>'
'<w:r><w:rPr /><w:t>Chapter 2</w:t><w:br /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 3
# ===============
doc._text = "### Hello World"
# Plain
xTest = ET.Element(_wTag("body"))
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
'<w:body><w:p><w:pPr><w:pStyle w:val="Heading3" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Formatted
xTest = ET.Element(_wTag("body"))
doc.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
'<w:body><w:p><w:pPr><w:pStyle w:val="Heading3" /></w:pPr>'
'<w:r><w:rPr /><w:t>Scene 2</w:t><w:br /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 4
# ===============
doc._text = "#### Hello World"
xTest = ET.Element(_wTag("body"))
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
'<w:body><w:p><w:pPr><w:pStyle w:val="Heading4" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@pytest.mark.core
def testFmtToDocX_ParagraphStyles(mockGUI):
"""Test formatting of paragraphs."""
@@ -71,61 +182,12 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
# Normal Text
xTest = ET.Element(_wTag("body"))
doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.NONE)]
doc._text = "Hello World"
doc.tokenizeText()
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Title
xTest = ET.Element(_wTag("body"))
doc._blocks = [(BlockTyp.TITLE, "", "Hello World", [], BlockFmt.NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_TITLE}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 1
xTest = ET.Element(_wTag("body"))
doc._blocks = [(BlockTyp.HEAD1, "", "Hello World", [], BlockFmt.NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD1}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 2
xTest = ET.Element(_wTag("body"))
doc._blocks = [(BlockTyp.HEAD2, "", "Hello World", [], BlockFmt.NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD2}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 3
xTest = ET.Element(_wTag("body"))
doc._blocks = [(BlockTyp.HEAD3, "", "Hello World", [], BlockFmt.NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD3}" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
# Heading Level 4
xTest = ET.Element(_wTag("body"))
doc._blocks = [(BlockTyp.HEAD4, "", "Hello World", [], BlockFmt.NONE)]
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_HEAD4}" /></w:pPr><w:r><w:rPr />'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr><w:r><w:rPr />'
'<w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@@ -135,7 +197,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_SEP}" /></w:pPr><w:r><w:rPr />'
'<w:body><w:p><w:pPr><w:pStyle w:val="Separator" /></w:pPr><w:r><w:rPr />'
'<w:t>* * *</w:t></w:r></w:p></w:body>'
)
@@ -145,7 +207,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr></w:p></w:body>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr></w:p></w:body>'
)
# Synopsis
@@ -155,7 +217,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="MetaText" /></w:pPr>'
'<w:r><w:rPr><w:b /><w:color w:val="813709" /></w:rPr><w:t>Synopsis:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> </w:t></w:r>'
'<w:r><w:rPr><w:color w:val="813709" /></w:rPr><w:t>Hello World</w:t></w:r>'
@@ -169,7 +231,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="MetaText" /></w:pPr>'
'<w:r><w:rPr><w:b /><w:color w:val="813709" /></w:rPr><w:t>Short Description:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> </w:t></w:r>'
'<w:r><w:rPr><w:color w:val="813709" /></w:rPr><w:t>Hello World</w:t></w:r>'
@@ -183,7 +245,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="MetaText" /></w:pPr>'
'<w:r><w:rPr><w:b /><w:color w:val="646464" /></w:rPr><w:t>Comment:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> </w:t></w:r>'
'<w:r><w:rPr><w:color w:val="646464" /></w:rPr><w:t>Hello World</w:t></w:r>'
@@ -197,7 +259,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="MetaText" /></w:pPr>'
'<w:r><w:rPr><w:b /><w:color w:val="f5871f" /></w:rPr><w:t>Tag:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> </w:t></w:r>'
'<w:r><w:rPr><w:color w:val="4271ae" /></w:rPr><w:t>Stuff</w:t></w:r>'
@@ -211,7 +273,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_META}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="MetaText" /></w:pPr>'
'<w:r><w:rPr><w:b /><w:color w:val="f5871f" /></w:rPr><w:t>Characters:</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> </w:t></w:r>'
'<w:r><w:rPr><w:color w:val="4271ae" /></w:rPr><w:t>Jane</w:t></w:r>'
@@ -245,7 +307,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="left" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /><w:jc w:val="left" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@@ -255,7 +317,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="right" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /><w:jc w:val="right" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@@ -265,7 +327,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="center" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /><w:jc w:val="center" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@@ -275,7 +337,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /><w:jc w:val="both" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /><w:jc w:val="both" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@@ -285,7 +347,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:br w:type="page" /></w:r>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r>'
'</w:p></w:body>'
@@ -297,7 +359,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r>'
'<w:r><w:br w:type="page" /></w:r>'
'</w:p></w:body>'
@@ -309,7 +371,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" />'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" />'
'<w:spacing w:before="0" w:after="0" w:line="252" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@@ -320,7 +382,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" />'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" />'
'<w:ind w:left="880" w:right="880" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@@ -331,7 +393,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" />'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" />'
'<w:ind w:firstLine="308" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>'
)
@@ -351,7 +413,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Text </w:t></w:r>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>bold</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve">, </w:t></w:r>'
@@ -369,7 +431,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:strike /></w:rPr><w:t xml:space="preserve">nested </w:t></w:r>'
'<w:r><w:rPr><w:b /><w:strike /></w:rPr><w:t>bold</w:t></w:r>'
@@ -389,7 +451,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:t>Some super</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr><w:t>script</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve"> and sub</w:t></w:r>'
@@ -405,7 +467,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:u w:val="single" /></w:rPr>'
'<w:t xml:space="preserve">underlined and </w:t></w:r>'
@@ -422,7 +484,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:t>Some text.</w:t><w:br /><w:t>Next line</w:t></w:r>'
'</w:p></w:body>'
)
@@ -434,7 +496,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:tab /><w:t>Item 1</w:t><w:tab /><w:t>Item 2</w:t></w:r>'
'</w:p></w:body>'
)
@@ -446,7 +508,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:b /></w:rPr><w:t>bold</w:t><w:tab /><w:t>text</w:t></w:r>'
'</w:p></w:body>'
@@ -474,7 +536,7 @@ def testFmtToDocX_Footnotes(mockGUI):
doc.doConvert()
doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></w:pPr>'
'<w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:t>Text with one</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr>'
'<w:footnoteReference w:id="1" /></w:r>'
@@ -493,11 +555,11 @@ def testFmtToDocX_Footnotes(mockGUI):
doc._footnotesXml()
assert xmlToText(doc._files["footnotes.xml"].xml) == (
'<w:footnotes>'
f'<w:footnote w:id="1"><w:p><w:pPr><w:pStyle w:val="{S_FNOTE}" /></w:pPr>'
'<w:footnote w:id="1"><w:p><w:pPr><w:pStyle w:val="FootnoteText" /></w:pPr>'
'<w:r><w:rPr /><w:t>Footnote text A.</w:t></w:r></w:p></w:footnote>'
f'<w:footnote w:id="2"><w:p><w:pPr><w:pStyle w:val="{S_FNOTE}" /></w:pPr>'
'<w:footnote w:id="2"><w:p><w:pPr><w:pStyle w:val="FootnoteText" /></w:pPr>'
'<w:r><w:rPr /><w:t>Another footnote.</w:t></w:r></w:p></w:footnote>'
f'<w:footnote w:id="3"><w:p><w:pPr><w:pStyle w:val="{S_FNOTE}" /></w:pPr>'
'<w:footnote w:id="3"><w:p><w:pPr><w:pStyle w:val="FootnoteText" /></w:pPr>'
'<w:r><w:rPr /><w:t>Again?</w:t></w:r></w:p></w:footnote>'
'</w:footnotes>'
)
+12 -8
View File
@@ -25,6 +25,7 @@ import json
import pytest
from novelwriter import CONFIG
from novelwriter.constants import nwHeadFmt
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp
from novelwriter.formats.tohtml import ToHtml
@@ -44,32 +45,35 @@ def testFmtToHtml_ConvertHeaders(mockGUI):
html._isFirst = True
# Header 1
html._text = "# Partition\n"
html._text = "# Title\n"
html.setPartitionFormat(f"Part{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
html.tokenizeText()
html.doConvert()
assert html._pages[-1] == (
"<h1 class='title' style='text-align: center;'>Partition</h1>\n"
"<h1 class='title' style='text-align: center;'>Part<br>Title</h1>\n"
)
# Header 2
html._text = "## Chapter Title\n"
html._text = "## Title\n"
html.setChapterFormat(f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
html.tokenizeText()
html.doConvert()
assert html._pages[-1] == (
"<h1 style='page-break-before: always;'>Chapter Title</h1>\n"
"<h1 style='page-break-before: always;'>Chapter 1<br>Title</h1>\n"
)
# Header 3
html._text = "### Scene Title\n"
html._text = "### Title\n"
html.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
html.tokenizeText()
html.doConvert()
assert html._pages[-1] == "<h2>Scene Title</h2>\n"
assert html._pages[-1] == "<h2>Scene 1<br>Title</h2>\n"
# Header 4
html._text = "#### Section Title\n"
html._text = "#### Title\n"
html.tokenizeText()
html.doConvert()
assert html._pages[-1] == "<h3>Section Title</h3>\n"
assert html._pages[-1] == "<h3>Title</h3>\n"
# Title
html._text = "#! Title\n"
+172 -53
View File
@@ -421,8 +421,8 @@ def testFmtToken_HeaderFormat(mockGUI):
@pytest.mark.core
def testFmtToken_HeaderStyle(mockGUI):
"""Test the styling of headers in the Tokenizer class."""
def testFmtToken_HeaderStyleNone(mockGUI):
"""Test header styling disabled."""
project = NWProject()
tokens = BareTokenizer(project)
@@ -432,13 +432,12 @@ def testFmtToken_HeaderStyle(mockGUI):
tokens.tokenizeText()
return tokens._blocks[0][4]
# No Styles
# =========
tokens.setTitleStyle(False, False)
tokens.setPartitionStyle(False, False)
tokens.setChapterStyle(False, False)
tokens.setSceneStyle(False, False)
assert tokens._titleStyle == BlockFmt.NONE
assert tokens._partStyle == BlockFmt.NONE
assert tokens._chapterStyle == BlockFmt.NONE
assert tokens._sceneStyle == BlockFmt.NONE
@@ -451,7 +450,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#! My Novel\n", False) == BlockFmt.NONE
assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# First Document is True
@@ -459,7 +458,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", True) == BlockFmt.NONE
assert processStyle("### Scene\n", True) == BlockFmt.NONE
assert processStyle("#### Section\n", True) == BlockFmt.NONE
assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE
assert processStyle("#! My Novel\n", True) == BlockFmt.NONE
assert processStyle("##! Prologue\n", True) == BlockFmt.NONE
# Note Docs
@@ -470,7 +469,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#! My Novel\n", False) == BlockFmt.NONE
assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# First Document is True
@@ -478,16 +477,28 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", True) == BlockFmt.NONE
assert processStyle("### Scene\n", True) == BlockFmt.NONE
assert processStyle("#### Section\n", True) == BlockFmt.NONE
assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE
assert processStyle("#! My Novel\n", True) == BlockFmt.NONE
assert processStyle("##! Prologue\n", True) == BlockFmt.NONE
# Center Headers
# ==============
@pytest.mark.core
def testFmtToken_HeaderStyleCenter(mockGUI):
"""Test header styling centred."""
project = NWProject()
tokens = BareTokenizer(project)
def processStyle(text: str, first: bool) -> BlockFmt:
tokens._text = text
tokens._isFirst = first
tokens.tokenizeText()
return tokens._blocks[0][4]
tokens.setTitleStyle(True, False)
tokens.setPartitionStyle(True, False)
tokens.setChapterStyle(True, False)
tokens.setSceneStyle(True, False)
assert tokens._titleStyle == BlockFmt.CENTRE
assert tokens._partStyle == BlockFmt.CENTRE
assert tokens._chapterStyle == BlockFmt.CENTRE
assert tokens._sceneStyle == BlockFmt.CENTRE
@@ -500,7 +511,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.CENTRE
assert processStyle("### Scene\n", False) == BlockFmt.CENTRE
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE
assert processStyle("##! Prologue\n", False) == BlockFmt.CENTRE
# First Document is True
@@ -519,7 +530,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE
assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# First Document is True
@@ -530,13 +541,25 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE
assert processStyle("##! Prologue\n", True) == BlockFmt.NONE
# Page Break Headers
# ==================
@pytest.mark.core
def testFmtToken_HeaderStylePageBreak(mockGUI):
"""Test header styling page break."""
project = NWProject()
tokens = BareTokenizer(project)
def processStyle(text: str, first: bool) -> BlockFmt:
tokens._text = text
tokens._isFirst = first
tokens.tokenizeText()
return tokens._blocks[0][4]
tokens.setTitleStyle(False, True)
tokens.setPartitionStyle(False, True)
tokens.setChapterStyle(False, True)
tokens.setSceneStyle(False, True)
assert tokens._titleStyle == BlockFmt.PBB
assert tokens._partStyle == BlockFmt.PBB
assert tokens._chapterStyle == BlockFmt.PBB
assert tokens._sceneStyle == BlockFmt.PBB
@@ -549,7 +572,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.PBB
assert processStyle("### Scene\n", False) == BlockFmt.PBB
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#! My Novel\n", False) == BlockFmt.PBB
assert processStyle("##! Prologue\n", False) == BlockFmt.PBB
# First Document is True
@@ -557,7 +580,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", True) == BlockFmt.NONE
assert processStyle("### Scene\n", True) == BlockFmt.NONE
assert processStyle("#### Section\n", True) == BlockFmt.NONE
assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE
assert processStyle("#! My Novel\n", True) == BlockFmt.NONE
assert processStyle("##! Prologue\n", True) == BlockFmt.NONE
# Note Docs
@@ -568,7 +591,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#! My Novel\n", False) == BlockFmt.PBB
assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# First Document is True
@@ -576,16 +599,28 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", True) == BlockFmt.NONE
assert processStyle("### Scene\n", True) == BlockFmt.NONE
assert processStyle("#### Section\n", True) == BlockFmt.NONE
assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE
assert processStyle("#! My Novel\n", True) == BlockFmt.NONE
assert processStyle("##! Prologue\n", True) == BlockFmt.NONE
# Page Break and Centre Headers
# =============================
@pytest.mark.core
def testFmtToken_HeaderStylePageBreakCenter(mockGUI):
"""Test header styling page break and centred."""
project = NWProject()
tokens = BareTokenizer(project)
def processStyle(text: str, first: bool) -> BlockFmt:
tokens._text = text
tokens._isFirst = first
tokens.tokenizeText()
return tokens._blocks[0][4]
tokens.setTitleStyle(True, True)
tokens.setPartitionStyle(True, True)
tokens.setChapterStyle(True, True)
tokens.setSceneStyle(True, True)
assert tokens._titleStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._partStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._chapterStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._sceneStyle == BlockFmt.CENTRE | BlockFmt.PBB
@@ -628,15 +663,46 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE
assert processStyle("##! Prologue\n", True) == BlockFmt.NONE
# Check Separation
# ================
@pytest.mark.core
def testFmtToken_HeaderStyleSeparation(mockGUI):
"""Test header styling separation."""
project = NWProject()
tokens = BareTokenizer(project)
def processStyle(text: str, first: bool) -> BlockFmt:
tokens._text = text
tokens._isFirst = first
tokens.tokenizeText()
return tokens._blocks[0][4]
tokens._isNovel = True
# Title Styles
tokens.setTitleStyle(True, True)
tokens.setPartitionStyle(False, False)
tokens.setChapterStyle(False, False)
tokens.setSceneStyle(False, False)
assert tokens._titleStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._partStyle == BlockFmt.NONE
assert tokens._chapterStyle == BlockFmt.NONE
assert tokens._sceneStyle == BlockFmt.NONE
assert processStyle("# Title\n", False) == BlockFmt.NONE
assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# Partition Styles
tokens.setTitleStyle(False, False)
tokens.setPartitionStyle(True, True)
tokens.setChapterStyle(False, False)
tokens.setSceneStyle(False, False)
assert tokens._titleStyle == BlockFmt.NONE
assert tokens._partStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._chapterStyle == BlockFmt.NONE
assert tokens._sceneStyle == BlockFmt.NONE
@@ -645,14 +711,16 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#! My Novel\n", False) == BlockFmt.NONE
assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# Chapter Styles
tokens.setTitleStyle(False, False)
tokens.setPartitionStyle(False, False)
tokens.setChapterStyle(True, True)
tokens.setSceneStyle(False, False)
assert tokens._titleStyle == BlockFmt.NONE
assert tokens._partStyle == BlockFmt.NONE
assert tokens._chapterStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._sceneStyle == BlockFmt.NONE
@@ -661,14 +729,16 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#! My Novel\n", False) == BlockFmt.NONE
assert processStyle("##! Prologue\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
# Scene Styles
tokens.setTitleStyle(False, False)
tokens.setPartitionStyle(False, False)
tokens.setChapterStyle(False, False)
tokens.setSceneStyle(True, True)
assert tokens._titleStyle == BlockFmt.NONE
assert tokens._partStyle == BlockFmt.NONE
assert tokens._chapterStyle == BlockFmt.NONE
assert tokens._sceneStyle == BlockFmt.CENTRE | BlockFmt.PBB
@@ -677,7 +747,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#### Section\n", False) == BlockFmt.NONE
assert processStyle("#! My Novel\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#! My Novel\n", False) == BlockFmt.NONE
assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
@@ -842,13 +912,13 @@ def testFmtToken_MarginFormat(mockGUI):
]
assert tokens._raw[-1] == (
"Some regular text\n\n"
"Some left-aligned text\n\n"
"Some right-aligned text\n\n"
"Some centered text\n\n"
"Left-indented block\n\n"
"Right-indented block\n\n"
"Double-indented block\n\n"
"Right-indent, right-aligned\n\n\n"
"Some left-aligned text <<\n\n"
">> Some right-aligned text\n\n"
">> Some centered text <<\n\n"
"> Left-indented block\n\n"
"Right-indented block <\n\n"
"> Double-indented block <\n\n"
">> Right-indent, right-aligned <\n\n\n"
)
@@ -1056,72 +1126,121 @@ def testFmtToken_TextFormat(mockGUI):
tokens._text = "Some **bolded text** on this lines\n"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.TEXT, "", "Some bolded text on this lines",
[
BlockTyp.TEXT, "", "Some bolded text on this lines", [
(5, TextFmt.B_B, ""),
(16, TextFmt.B_E, ""),
],
BlockFmt.NONE
], BlockFmt.NONE
)]
assert tokens._raw[-1] == "Some **bolded text** on this lines\n\n"
tokens._text = "Some _italic text_ on this lines\n"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.TEXT, "", "Some italic text on this lines",
[
BlockTyp.TEXT, "", "Some italic text on this lines", [
(5, TextFmt.I_B, ""),
(16, TextFmt.I_E, ""),
],
BlockFmt.NONE
], BlockFmt.NONE
)]
assert tokens._raw[-1] == "Some _italic text_ on this lines\n\n"
tokens._text = "Some **_bold italic text_** on this lines\n"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.TEXT, "", "Some bold italic text on this lines",
[
BlockTyp.TEXT, "", "Some bold italic text on this lines", [
(5, TextFmt.B_B, ""),
(5, TextFmt.I_B, ""),
(21, TextFmt.I_E, ""),
(21, TextFmt.B_E, ""),
],
BlockFmt.NONE
], BlockFmt.NONE
)]
assert tokens._raw[-1] == "Some **_bold italic text_** on this lines\n\n"
tokens._text = "Some ~~strikethrough text~~ on this lines\n"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.TEXT, "", "Some strikethrough text on this lines",
[
BlockTyp.TEXT, "", "Some strikethrough text on this lines", [
(5, TextFmt.D_B, ""),
(23, TextFmt.D_E, ""),
],
BlockFmt.NONE
], BlockFmt.NONE
)]
assert tokens._raw[-1] == "Some ~~strikethrough text~~ on this lines\n\n"
tokens._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.TEXT, "", "Some nested bold and italic and strikethrough text here",
[
BlockTyp.TEXT, "", "Some nested bold and italic and strikethrough text here", [
(5, TextFmt.B_B, ""),
(21, TextFmt.I_B, ""),
(27, TextFmt.I_E, ""),
(32, TextFmt.D_B, ""),
(45, TextFmt.D_E, ""),
(50, TextFmt.B_E, ""),
],
BlockFmt.NONE
], BlockFmt.NONE
)]
assert tokens._raw[-1] == (
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
)
@pytest.mark.core
def testFmtToken_LineBreak(mockGUI):
"""Test processing of forced line breaks in the Tokenizer class."""
project = NWProject()
tokens = BareTokenizer(project)
tokens._handle = TMH
tokens.setComments(True)
# They are stripped in headers
tokens._text = "## Hello[br] World"
tokens.tokenizeText()
assert tokens._blocks == [
(BlockTyp.HEAD2, TM1, "Hello World", [], BlockFmt.NONE)
]
# They are stripped in comments
tokens._text = "% Hello[br] World"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.COMMENT, "", "Comment: Hello World", [
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "comment"),
(8, TextFmt.COL_E, ""), (8, TextFmt.B_E, ""),
(9, TextFmt.COL_B, "comment"), (20, TextFmt.COL_E, ""),
], BlockFmt.NONE
)]
# They are used in text, with breaks enabled
tokens.setKeepLineBreaks(True)
tokens._text = "Hello[br]\nWorld"
tokens.tokenizeText()
assert tokens._blocks == [
(BlockTyp.TEXT, "", "Hello\nWorld", [], BlockFmt.NONE)
]
# They are used in text, with breaks disabled
tokens.setKeepLineBreaks(False)
tokens._text = "Hello[br]\nWorld"
tokens.tokenizeText()
assert tokens._blocks == [
(BlockTyp.TEXT, "", "Hello\nWorld", [], BlockFmt.NONE)
]
# Without forced breaks, they are preserved with breaks enabled
tokens.setKeepLineBreaks(True)
tokens._text = "Hello\nWorld"
tokens.tokenizeText()
assert tokens._blocks == [
(BlockTyp.TEXT, "", "Hello\nWorld", [], BlockFmt.NONE)
]
# Without forced breaks, they are not preserved with breaks disabled
tokens.setKeepLineBreaks(False)
tokens._text = "Hello\nWorld"
tokens.tokenizeText()
assert tokens._blocks == [
(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.NONE)
]
@pytest.mark.core
def testFmtToken_Dialogue(mockGUI):
"""Test the tokenization of dialogue in the Tokenizer class."""
+10 -6
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import pytest
from novelwriter.constants import nwHeadFmt
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp
from novelwriter.formats.tomarkdown import ToMarkdown
@@ -37,22 +38,25 @@ def testFmtToMarkdown_ConvertHeaders(mockGUI):
md._isFirst = True
# Header 1
md._text = "# Partition\n"
md._text = "# Title\n"
md.setPartitionFormat(f"Part{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
md.tokenizeText()
md.doConvert()
assert md._pages[-1] == "# Partition\n\n"
assert md._pages[-1] == "# Part - Title\n\n"
# Header 2
md._text = "## Chapter Title\n"
md._text = "## Title\n"
md.setChapterFormat(f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
md.tokenizeText()
md.doConvert()
assert md._pages[-1] == "## Chapter Title\n\n"
assert md._pages[-1] == "## Chapter 1 - Title\n\n"
# Header 3
md._text = "### Scene Title\n"
md._text = "### Title\n"
md.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
md.tokenizeText()
md.doConvert()
assert md._pages[-1] == "### Scene Title\n\n"
assert md._pages[-1] == "### Scene 1 - Title\n\n"
# Header 4
md._text = "#### Section Title\n"
+13 -7
View File
@@ -281,6 +281,7 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
# Header 1
odt._text = "# Title\n"
odt.setPartitionFormat(f"Part{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
odt.tokenizeText()
odt.initDocument()
odt.doConvert()
@@ -288,12 +289,14 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
assert odt.errData == []
assert xmlToText(odt._xText) == (
'<office:text>'
'<text:h text:style-name="P1" text:outline-level="1">Title</text:h>'
'<text:h text:style-name="P1" text:outline-level="1">Part'
'<text:line-break />Title</text:h>'
'</office:text>'
)
# Header 2
odt._text = "## Chapter\n"
odt._text = "## Title\n"
odt.setChapterFormat(f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
odt.tokenizeText()
odt.initDocument()
odt.doConvert()
@@ -301,12 +304,14 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
assert odt.errData == []
assert xmlToText(odt._xText) == (
'<office:text>'
'<text:h text:style-name="P2" text:outline-level="2">Chapter</text:h>'
'<text:h text:style-name="P2" text:outline-level="2">Chapter 1'
'<text:line-break />Title</text:h>'
'</office:text>'
)
# Header 3
odt._text = "### Scene\n"
odt._text = "### Title\n"
odt.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
odt.tokenizeText()
odt.initDocument()
odt.doConvert()
@@ -314,12 +319,13 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
assert odt.errData == []
assert xmlToText(odt._xText) == (
'<office:text>'
'<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene</text:h>'
'<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene 1'
'<text:line-break />Title</text:h>'
'</office:text>'
)
# Header 4
odt._text = "#### Section\n"
odt._text = "#### Title\n"
odt.tokenizeText()
odt.initDocument()
odt.doConvert()
@@ -327,7 +333,7 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
assert odt.errData == []
assert xmlToText(odt._xText) == (
'<office:text>'
'<text:h text:style-name="Heading_20_4" text:outline-level="4">Section</text:h>'
'<text:h text:style-name="Heading_20_4" text:outline-level="4">Title</text:h>'
'</office:text>'
)
+7 -2
View File
@@ -522,13 +522,18 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd
nwGUI.mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%Short: \n"
# Insert Break or Space
# =====================
# Breaks and Vertical Space
# =========================
nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "[newpage]\n### Stuff\n"
nwGUI.docEditor.setPlainText("Line OneLine Two\n")
nwGUI.docEditor.setCursorPosition(8)
nwGUI.mainMenu.aInsLineBreak.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Line One[br]Line Two\n"
nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n"
+3 -2
View File
@@ -47,6 +47,7 @@ def testTextCounting_preProcessText():
"[vspace:3]\n\n"
"[New Page]\n\n"
"[footnote:abcd]\n\n"
"[br]\n\n"
"Dashes\u2013and even longer\u2014dashes.\n\n"
)
@@ -60,7 +61,7 @@ def testTextCounting_preProcessText():
"#### Heading Four",
"", "", "",
"A paragraph.", "",
"", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"Dashes and even longer dashes.", ""
]
@@ -68,7 +69,7 @@ def testTextCounting_preProcessText():
assert preProcessText(text, keepHeaders=False) == [
"", "", "",
"A paragraph.", "",
"", "", "", "", "", "",
"", "", "", "", "", "", "", "",
"Dashes and even longer dashes.", ""
]
+11
View File
@@ -198,6 +198,17 @@ def testTextPatterns_ShortcodesPlain():
assert allMatches(regEx, "one [x]two[/x] three") == []
# Line Break Substitution
# =======================
regEx = REGEX_PATTERNS.lineBreak
assert regEx.sub("\n", "one[br]two") == "one\ntwo"
assert regEx.sub("\n", "one[br]\ntwo") == "one\ntwo"
assert regEx.sub("\n", "one[br]\n\ntwo") == "one\n\ntwo"
assert regEx.sub("\n", "one[BR]two") == "one\ntwo"
assert regEx.sub("\n", "one[BR]\ntwo") == "one\ntwo"
assert regEx.sub("\n", "one[BR]\n\ntwo") == "one\n\ntwo"
@pytest.mark.core
def testTextPatterns_ShortcodesValue():