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", "Short Description": "Short Description",
"Footnotes": "Footnotes", "Footnotes": "Footnotes",
"Comment": "Comment", "Comment": "Comment",
"Note": "Note", "Notes": "Notes",
"Tag": "Tag", "Tag": "Tag",
"Point of View": "Point of View", "Point of View": "Point of View",
"Focus": "Focus", "Focus": "Focus",
+3 -1
View File
@@ -61,10 +61,11 @@ class nwConst:
class nwRegEx: class nwRegEx:
WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b" WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b"
BREAK = r"(?i)(?<!\\)(\[br\]\n?)"
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = 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):)(.+?)(?<!\\)(\])" FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
@@ -84,6 +85,7 @@ class nwShortcode:
SUP_C = "[/sup]" SUP_C = "[/sup]"
SUB_O = "[sub]" SUB_O = "[sub]"
SUB_C = "[/sub]" SUB_C = "[/sub]"
BREAK = "[br]"
FOOTNOTE_B = "[footnote:" 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.centerPart": (bool, True),
"headings.centerChapter": (bool, False), "headings.centerChapter": (bool, False),
"headings.centerScene": (bool, False), "headings.centerScene": (bool, False),
"headings.breakTitle": (bool, True), "headings.breakTitle": (bool, False),
"headings.breakPart": (bool, True), "headings.breakPart": (bool, True),
"headings.breakChapter": (bool, True), "headings.breakChapter": (bool, True),
"headings.breakScene": (bool, False), "headings.breakScene": (bool, False),
+2 -2
View File
@@ -267,8 +267,8 @@ class NWBuildDocument:
self._build.getBool("headings.hideSection") self._build.getBool("headings.hideSection")
) )
bldObj.setTitleStyle( bldObj.setTitleStyle(
self._build.getBool("headings.centerPart"), self._build.getBool("headings.centerTitle"),
self._build.getBool("headings.breakPart") self._build.getBool("headings.breakTitle")
) )
bldObj.setPartitionStyle( bldObj.setPartitionStyle(
self._build.getBool("headings.centerPart"), self._build.getBool("headings.centerPart"),
+1
View File
@@ -137,6 +137,7 @@ class nwDocInsert(Enum):
VSPACE_M = 9 VSPACE_M = 9
LIPSUM = 10 LIPSUM = 10
FOOTNOTE = 11 FOOTNOTE = 11
LINE_BRK = 12
class nwView(Enum): class nwView(Enum):
+5 -10
View File
@@ -255,24 +255,19 @@ class ToDocX(Tokenizer):
self._processFragments(par, S_NORM, tText, tFormat) self._processFragments(par, S_NORM, tText, tFormat)
elif tType == BlockTyp.TITLE: elif tType == BlockTyp.TITLE:
tHead = tText.replace(nwHeadFmt.BR, "\n") self._processFragments(par, S_TITLE, tText, tFormat)
self._processFragments(par, S_TITLE, tHead, tFormat)
elif tType == BlockTyp.HEAD1: elif tType == BlockTyp.HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "\n") self._processFragments(par, S_HEAD1, tText, tFormat)
self._processFragments(par, S_HEAD1, tHead, tFormat)
elif tType == BlockTyp.HEAD2: elif tType == BlockTyp.HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "\n") self._processFragments(par, S_HEAD2, tText, tFormat)
self._processFragments(par, S_HEAD2, tHead, tFormat)
elif tType == BlockTyp.HEAD3: elif tType == BlockTyp.HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "\n") self._processFragments(par, S_HEAD3, tText, tFormat)
self._processFragments(par, S_HEAD3, tHead, tFormat)
elif tType == BlockTyp.HEAD4: elif tType == BlockTyp.HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "\n") self._processFragments(par, S_HEAD4, tText, tFormat)
self._processFragments(par, S_HEAD4, tHead, tFormat)
elif tType == BlockTyp.SEP: elif tType == BlockTyp.SEP:
self._processFragments(par, S_SEP, tText) self._processFragments(par, S_SEP, tText)
+6 -6
View File
@@ -30,7 +30,7 @@ from pathlib import Path
from time import time from time import time
from novelwriter.common import formatTimeStamp from novelwriter.common import formatTimeStamp
from novelwriter.constants import nwHeadFmt, nwHtmlUnicode from novelwriter.constants import nwHtmlUnicode
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
from novelwriter.formats.tokenizer import Tokenizer from novelwriter.formats.tokenizer import Tokenizer
@@ -211,23 +211,23 @@ class ToHtml(Tokenizer):
lines.append(f"<p{hStyle}>{self._formatText(tText, tFmt)}</p>\n") lines.append(f"<p{hStyle}>{self._formatText(tText, tFmt)}</p>\n")
elif tType == BlockTyp.TITLE: 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") lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
elif tType == BlockTyp.HEAD1: 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") lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
elif tType == BlockTyp.HEAD2: elif tType == BlockTyp.HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "<br>") tHead = tText.replace("\n", "<br>")
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n") lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
elif tType == BlockTyp.HEAD3: elif tType == BlockTyp.HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "<br>") tHead = tText.replace("\n", "<br>")
lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n") lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
elif tType == BlockTyp.HEAD4: elif tType == BlockTyp.HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "<br>") tHead = tText.replace("\n", "<br>")
lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n") lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
elif tType == BlockTyp.SEP: elif tType == BlockTyp.SEP:
+52 -42
View File
@@ -56,7 +56,6 @@ class ComStyle(NamedTuple):
textClass: str = "" textClass: str = ""
B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE)
COMMENT_STYLE = { COMMENT_STYLE = {
nwComment.PLAIN: ComStyle("Comment", "comment", "comment"), nwComment.PLAIN: ComStyle("Comment", "comment", "comment"),
nwComment.IGNORE: ComStyle(), nwComment.IGNORE: ComStyle(),
@@ -67,13 +66,12 @@ COMMENT_STYLE = {
nwComment.COMMENT: ComStyle(), nwComment.COMMENT: ComStyle(),
nwComment.STORY: ComStyle("", "modifier", "note"), nwComment.STORY: ComStyle("", "modifier", "note"),
} }
# Lookups
HEADINGS = [BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.HEAD4] HEADINGS = [BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.HEAD4]
SKIP_INDENT = [ SKIP_INDENT = [
BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD2, BlockTyp.HEAD3,
BlockTyp.HEAD4, BlockTyp.SEP, BlockTyp.SKIP, BlockTyp.HEAD4, BlockTyp.SEP, BlockTyp.SKIP,
] ]
B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE)
class Tokenizer(ABC): class Tokenizer(ABC):
@@ -182,8 +180,6 @@ class Tokenizer(ABC):
(REGEX_PATTERNS.markdownBold, [0, TextFmt.B_B, 0, TextFmt.B_E]), (REGEX_PATTERNS.markdownBold, [0, TextFmt.B_B, 0, TextFmt.B_E]),
(REGEX_PATTERNS.markdownStrike, [0, TextFmt.D_B, 0, TextFmt.D_E]), (REGEX_PATTERNS.markdownStrike, [0, TextFmt.D_B, 0, TextFmt.D_E]),
] ]
self._rxShortCodes = REGEX_PATTERNS.shortcodePlain
self._rxShortCodeVals = REGEX_PATTERNS.shortcodeValue
self._shortCodeFmt = { self._shortCodeFmt = {
nwShortcode.ITALIC_O: TextFmt.I_B, nwShortcode.ITALIC_C: TextFmt.I_E, nwShortcode.ITALIC_O: TextFmt.I_B, nwShortcode.ITALIC_C: TextFmt.I_E,
@@ -456,20 +452,22 @@ class Tokenizer(ABC):
self._text = "" self._text = ""
self._handle = None self._handle = None
if (tItem := self._project.tree[tHandle]) and tItem.isRootType(): if (item := self._project.tree[tHandle]) and item.isRootType():
self._handle = tHandle self._handle = tHandle
style = BlockFmt.CENTRE
if self._isFirst: if self._isFirst:
textAlign = BlockFmt.CENTRE
self._isFirst = False self._isFirst = False
else: else:
textAlign = BlockFmt.PBB | BlockFmt.CENTRE style |= BlockFmt.PBB
trNotes = self._localLookup("Notes") title = item.itemName
title = f"{trNotes}: {tItem.itemName}" if not item.isNovelLike():
self._blocks = [] notes = self._localLookup("Notes")
self._blocks.append(( title = f"{notes}: {title}"
BlockTyp.TITLE, f"{self._handle}:T0001", title, [], textAlign
)) self._blocks = [(
BlockTyp.TITLE, f"{self._handle}:T0001", title, [], style
)]
if self._keepRaw: if self._keepRaw:
self._raw.append(f"#! {title}\n\n") self._raw.append(f"#! {title}\n\n")
@@ -523,25 +521,30 @@ class Tokenizer(ABC):
isNovel = self._isNovel isNovel = self._isNovel
keepRaw = self._keepRaw keepRaw = self._keepRaw
doJustify = self._doJustify doJustify = self._doJustify
keepBreaks = self._keepBreaks
indentFirst = self._indentFirst indentFirst = self._indentFirst
firstIndent = self._firstIndent firstIndent = self._firstIndent
if self._isNovel: if self._isNovel:
self._hFormatter.setHandle(self._handle) self._hFormatter.setHandle(self._handle)
# Replace all instances of [br] with a placeholder character
text = REGEX_PATTERNS.lineBreak.sub("\uffff", self._text)
nHead = 0 nHead = 0
breakNext = False breakNext = False
tmpMarkdown = [] rawText = []
tHandle = self._handle or "" tHandle = self._handle or ""
tBlocks: list[T_Block] = [B_EMPTY] 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() sLine = aLine.strip().lower()
# Check for blank lines # Check for blank lines
if not sLine: if not sLine:
tBlocks.append(B_EMPTY) tBlocks.append(B_EMPTY)
if keepRaw: if keepRaw:
tmpMarkdown.append("\n") rawText.append("\n")
continue continue
if breakNext: if breakNext:
@@ -607,13 +610,13 @@ class Tokenizer(ABC):
BlockTyp.COMMENT, "", tLine, tFmt, sAlign BlockTyp.COMMENT, "", tLine, tFmt, sAlign
)) ))
if keepRaw: if keepRaw:
tmpMarkdown.append(f"{aLine}\n") rawText.append(f"{aLine}\n")
elif cStyle == nwComment.FOOTNOTE: elif cStyle == nwComment.FOOTNOTE:
tLine, tFmt = self._extractFormats(cText, skip=TextFmt.FNOTE) tLine, tFmt = self._extractFormats(cText, skip=TextFmt.FNOTE)
self._footnotes[f"{tHandle}:{cKey}"] = (tLine, tFmt) self._footnotes[f"{tHandle}:{cKey}"] = (tLine, tFmt)
if keepRaw: if keepRaw:
tmpMarkdown.append(f"{aLine}\n") rawText.append(f"{aLine}\n")
elif aLine.startswith("@"): elif aLine.startswith("@"):
# Keywords # Keywords
@@ -628,7 +631,7 @@ class Tokenizer(ABC):
BlockTyp.KEYWORD, tTag[1:], tLine, tFmt, sAlign BlockTyp.KEYWORD, tTag[1:], tLine, tFmt, sAlign
)) ))
if keepRaw: if keepRaw:
tmpMarkdown.append(f"{aLine}\n") rawText.append(f"{aLine}\n")
elif aLine.startswith(("# ", "#! ")): elif aLine.startswith(("# ", "#! ")):
# Title or Partition Headings # Title or Partition Headings
@@ -664,7 +667,7 @@ class Tokenizer(ABC):
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
)) ))
if keepRaw: if keepRaw:
tmpMarkdown.append(f"{aLine}\n") rawText.append(f"{aLine}\n")
elif aLine.startswith(("## ", "##! ")): elif aLine.startswith(("## ", "##! ")):
# (Unnumbered) Chapter Headings # (Unnumbered) Chapter Headings
@@ -699,7 +702,7 @@ class Tokenizer(ABC):
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
)) ))
if keepRaw: if keepRaw:
tmpMarkdown.append(f"{aLine}\n") rawText.append(f"{aLine}\n")
elif aLine.startswith(("### ", "###! ")): elif aLine.startswith(("### ", "###! ")):
# (Alternative) Scene Headings # (Alternative) Scene Headings
@@ -740,7 +743,7 @@ class Tokenizer(ABC):
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
)) ))
if keepRaw: if keepRaw:
tmpMarkdown.append(f"{aLine}\n") rawText.append(f"{aLine}\n")
elif aLine.startswith("#### "): elif aLine.startswith("#### "):
# Section Headings # Section Headings
@@ -770,7 +773,7 @@ class Tokenizer(ABC):
tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle tType, f"{tHandle}:T{nHead:04d}", tText, [], tStyle
)) ))
if keepRaw: if keepRaw:
tmpMarkdown.append(f"{aLine}\n") rawText.append(f"{aLine}\n")
else: else:
# Text Lines # Text Lines
@@ -786,19 +789,19 @@ class Tokenizer(ABC):
alnRight = False alnRight = False
indLeft = False indLeft = False
indRight = False indRight = False
if aLine.startswith(">>"): if bLine.startswith(">>"):
alnRight = True alnRight = True
aLine = aLine[2:].lstrip(" ") bLine = bLine[2:].lstrip(" ")
elif aLine.startswith(">"): elif bLine.startswith(">"):
indLeft = True indLeft = True
aLine = aLine[1:].lstrip(" ") bLine = bLine[1:].lstrip(" ")
if aLine.endswith("<<"): if bLine.endswith("<<"):
alnLeft = True alnLeft = True
aLine = aLine[:-2].rstrip(" ") bLine = bLine[:-2].rstrip(" ")
elif aLine.endswith("<"): elif bLine.endswith("<"):
indRight = True indRight = True
aLine = aLine[:-1].rstrip(" ") bLine = bLine[:-1].rstrip(" ")
if alnLeft and alnRight: if alnLeft and alnRight:
sAlign |= BlockFmt.CENTRE sAlign |= BlockFmt.CENTRE
@@ -813,12 +816,12 @@ class Tokenizer(ABC):
sAlign |= BlockFmt.IND_R sAlign |= BlockFmt.IND_R
# Process formats # Process formats
tLine, tFmt = self._extractFormats(aLine, hDialog=isNovel) tLine, tFmt = self._extractFormats(bLine, hDialog=isNovel)
tBlocks.append(( tBlocks.append((
BlockTyp.TEXT, "", tLine, tFmt, sAlign BlockTyp.TEXT, "", tLine, tFmt, sAlign
)) ))
if keepRaw: if keepRaw:
tmpMarkdown.append(f"{aLine}\n") rawText.append(f"{aLine}\n")
# If we have content, turn off the first page flag # If we have content, turn off the first page flag
if self._isFirst and len(tBlocks) > 1: 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 # Always add an empty line at the end of the file
tBlocks.append(B_EMPTY) tBlocks.append(B_EMPTY)
if keepRaw: if keepRaw:
tmpMarkdown.append("\n") rawText.append("\n")
self._raw.append("".join(tmpMarkdown)) self._raw.append("".join(rawText))
# Second Pass # Second Pass
# =========== # ===========
@@ -844,7 +847,7 @@ class Tokenizer(ABC):
# It also ensures that there isn't paragraph spacing between # It also ensures that there isn't paragraph spacing between
# meta data lines for formats that have spacing. # meta data lines for formats that have spacing.
lineSep = "\n" if self._keepBreaks else " " lineSep = "\n" if keepBreaks else " "
pLines: list[T_Block] = [] pLines: list[T_Block] = []
sBlocks: list[T_Block] = [] sBlocks: list[T_Block] = []
@@ -894,9 +897,12 @@ class Tokenizer(ABC):
# enabled, and there is no alignment, we apply it. # enabled, and there is no alignment, we apply it.
if doJustify and not cStyle & BlockFmt.ALIGNED: if doJustify and not cStyle & BlockFmt.ALIGNED:
cStyle |= BlockFmt.JUSTIFY cStyle |= BlockFmt.JUSTIFY
pTxt = pLines[0][2].replace("\uffff", "\n")
sBlocks.append(( 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: elif nLines > 1:
# The paragraph contains multiple lines, so we need to # The paragraph contains multiple lines, so we need to
# join them according to the line break policy, and # join them according to the line break policy, and
@@ -907,8 +913,11 @@ class Tokenizer(ABC):
tLen = len(tTxt) tLen = len(tTxt)
tTxt += f"{aBlock[2]}{lineSep}" tTxt += f"{aBlock[2]}{lineSep}"
tFmt.extend((p+tLen, fmt, key) for p, fmt, key in aBlock[3]) tFmt.extend((p+tLen, fmt, key) for p, fmt, key in aBlock[3])
cStyle |= aBlock[4]
pTxt = tTxt[:-1].replace("\uffff", "\n")
sBlocks.append(( 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 # Reset buffer and make sure text indent is on for next pass
@@ -1136,12 +1145,12 @@ class Tokenizer(ABC):
# Post-process text and format # Post-process text and format
result = text result = text
formats = [] 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 fmt > 0:
if end > pos: if end > pos:
result = result[:pos] + result[end:] result = result[:pos] + result[end:]
formats = [(p+pos-end if p > pos else p, f, k) for p, f, k in formats] formats = [(p+pos-end if p > pos else p, f, m) for p, f, m in formats]
formats.insert(0, (pos, fmt, key)) formats.insert(0, (pos, fmt, meta))
return result, formats return result, formats
@@ -1187,6 +1196,7 @@ class HeadingFormatter:
def apply(self, hFormat: str, text: str, nHead: int) -> str: def apply(self, hFormat: str, text: str, nHead: int) -> str:
"""Apply formatting to a specific heading.""" """Apply formatting to a specific heading."""
hFormat = hFormat.replace(nwHeadFmt.TITLE, text) 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.CH_NUM, str(self._chCount))
hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount)) hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount))
hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount)) hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount))
+6 -6
View File
@@ -27,7 +27,7 @@ import logging
from pathlib import Path from pathlib import Path
from novelwriter.constants import nwHeadFmt, nwUnicode from novelwriter.constants import nwUnicode
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
from novelwriter.formats.tokenizer import Tokenizer from novelwriter.formats.tokenizer import Tokenizer
@@ -113,23 +113,23 @@ class ToMarkdown(Tokenizer):
lines.append(f"{tTemp}\n\n") lines.append(f"{tTemp}\n\n")
elif tType == BlockTyp.TITLE: elif tType == BlockTyp.TITLE:
tHead = tText.replace(nwHeadFmt.BR, "\n") tHead = tText.replace("\n", " - ")
lines.append(f"# {tHead}\n\n") lines.append(f"# {tHead}\n\n")
elif tType == BlockTyp.HEAD1: elif tType == BlockTyp.HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "\n") tHead = tText.replace("\n", " - ")
lines.append(f"# {tHead}\n\n") lines.append(f"# {tHead}\n\n")
elif tType == BlockTyp.HEAD2: elif tType == BlockTyp.HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "\n") tHead = tText.replace("\n", " - ")
lines.append(f"## {tHead}\n\n") lines.append(f"## {tHead}\n\n")
elif tType == BlockTyp.HEAD3: elif tType == BlockTyp.HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "\n") tHead = tText.replace("\n", " - ")
lines.append(f"### {tHead}\n\n") lines.append(f"### {tHead}\n\n")
elif tType == BlockTyp.HEAD4: elif tType == BlockTyp.HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "\n") tHead = tText.replace("\n", " - ")
lines.append(f"#### {tHead}\n\n") lines.append(f"#### {tHead}\n\n")
elif tType == BlockTyp.SEP: elif tType == BlockTyp.SEP:
+5 -10
View File
@@ -444,24 +444,19 @@ class ToOdt(Tokenizer):
elif tType == BlockTyp.TITLE: elif tType == BlockTyp.TITLE:
# Title must be text:p # Title must be text:p
tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar(xText, S_TITLE, oStyle, tText, isHead=False)
self._addTextPar(xText, S_TITLE, oStyle, tHead, isHead=False)
elif tType == BlockTyp.HEAD1: elif tType == BlockTyp.HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar(xText, S_HEAD1, oStyle, tText, isHead=True, oLevel="1")
self._addTextPar(xText, S_HEAD1, oStyle, tHead, isHead=True, oLevel="1")
elif tType == BlockTyp.HEAD2: elif tType == BlockTyp.HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar(xText, S_HEAD2, oStyle, tText, isHead=True, oLevel="2")
self._addTextPar(xText, S_HEAD2, oStyle, tHead, isHead=True, oLevel="2")
elif tType == BlockTyp.HEAD3: elif tType == BlockTyp.HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar(xText, S_HEAD3, oStyle, tText, isHead=True, oLevel="3")
self._addTextPar(xText, S_HEAD3, oStyle, tHead, isHead=True, oLevel="3")
elif tType == BlockTyp.HEAD4: elif tType == BlockTyp.HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar(xText, S_HEAD4, oStyle, tText, isHead=True, oLevel="4")
self._addTextPar(xText, S_HEAD4, oStyle, tHead, isHead=True, oLevel="4")
elif tType == BlockTyp.SEP: elif tType == BlockTyp.SEP:
self._addTextPar(xText, S_SEP, oStyle, tText) 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 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.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
from novelwriter.formats.tokenizer import HEADINGS, Tokenizer from novelwriter.formats.tokenizer import HEADINGS, Tokenizer
@@ -217,7 +217,7 @@ class ToQTextDocument(Tokenizer):
elif tType in HEADINGS: elif tType in HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt) bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt)
newBlock(cursor, bFmt) newBlock(cursor, bFmt)
cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt) cursor.insertText(tText, cFmt)
elif tType == BlockTyp.SEP: elif tType == BlockTyp.SEP:
newBlock(cursor, bFmt) newBlock(cursor, bFmt)
+2
View File
@@ -877,6 +877,8 @@ class GuiDocEditor(QPlainTextEdit):
after = False after = False
elif insert == nwDocInsert.FOOTNOTE: elif insert == nwDocInsert.FOOTNOTE:
self._insertCommentStructure(nwComment.FOOTNOTE) self._insertCommentStructure(nwComment.FOOTNOTE)
elif insert == nwDocInsert.LINE_BRK:
text = nwShortcode.BREAK
if text: if text:
if block: if block:
+8 -2
View File
@@ -570,8 +570,8 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocInsert.emit(nwDocInsert.SHORT) lambda: self.requestDocInsert.emit(nwDocInsert.SHORT)
) )
# Insert > Symbols # Insert > Breaks and Vertical Space
self.mInsBreaks = self.insMenu.addMenu(self.tr("Page Break and Space")) self.mInsBreaks = self.insMenu.addMenu(self.tr("Breaks and Vertical Space"))
# Insert > New Page # Insert > New Page
self.aInsNewPage = self.mInsBreaks.addAction(self.tr("Page Break")) self.aInsNewPage = self.mInsBreaks.addAction(self.tr("Page Break"))
@@ -579,6 +579,12 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocInsert.emit(nwDocInsert.NEW_PAGE) 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) # Insert > Vertical Space (Single)
self.aInsVSpaceS = self.mInsBreaks.addAction(self.tr("Vertical Space (Single)")) self.aInsVSpaceS = self.mInsBreaks.addAction(self.tr("Vertical Space (Single)"))
self.aInsVSpaceS.triggered.connect( self.aInsVSpaceS.triggered.connect(
+6
View File
@@ -33,6 +33,7 @@ class RegExPatterns:
# Static RegExes # Static RegExes
_rxWords = re.compile(nwRegEx.WORDS, re.UNICODE) _rxWords = re.compile(nwRegEx.WORDS, re.UNICODE)
_rxBreak = re.compile(nwRegEx.BREAK, re.UNICODE)
_rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE) _rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE)
_rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE) _rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE)
_rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE) _rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE)
@@ -44,6 +45,11 @@ class RegExPatterns:
"""Split text into words.""" """Split text into words."""
return self._rxWords return self._rxWords
@property
def lineBreak(self) -> re.Pattern:
"""Find forced line break."""
return self._rxBreak
@property @property
def markdownItalic(self) -> re.Pattern: def markdownItalic(self) -> re.Pattern:
"""Markdown italic style.""" """Markdown italic style."""
+10 -3
View File
@@ -1,11 +1,18 @@
%%~name: Title Page %%~name: Title Page
%%~path: 7031beac91f75/53b69b83cdafc %%~path: 7031beac91f75/53b69b83cdafc
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
%%~hash: c5dc35d18ecb074a9e41a1410d1bff8021cf0a5b %%~hash: 4072adb6d21ff877577f033f19714d9bd01396f3
%%~date: Unknown/2023-08-25 16:51:52 %%~date: Unknown/2024-10-24 16:25:44
Jane Smith[br]
42 Main Street[br]
1234 Capital City <<
[vspace:5]
#! My Novel #! My Novel
>> **By Jane Smith** << >> **By Jane Smith** <<
>> This is the title page. << >> 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 %%~name: Interlude
%%~path: 7031beac91f75/ba8a28a246524 %%~path: 7031beac91f75/ba8a28a246524
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
%%~hash: 721d3d15e0233186354ac6fa61f27db10f38e6bc %%~hash: 5c8f68d48573b576dcaacf6d6928c496ec361b9d
%%~date: Unknown/2024-03-14 22:55:04 %%~date: Unknown/2024-10-24 01:22:15
##! Interlude ##! 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. % 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 am the very model of a modern Major-General[br]
I've information vegetable, animal, and mineral I've information vegetable, animal, and mineral[br]
I know the kings of England, and I quote the fights historical I know the kings of England, and I quote the fights historical[br]
From Marathon to Waterloo, in order categorical From Marathon to Waterloo, in order categorical <<
I'm very well acquainted, too, with matters mathematical << I'm very well acquainted, too, with matters mathematical[br]
I understand equations, both the simple and quadratical I understand equations, both the simple and quadratical[br]
About binomial theorem I'm teeming with a lot o news 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 hypotenuse[br]
With many cheerful facts about the square of the hypotenuse With many cheerful facts about the square of the hypotenuse[br]
With many cheerful facts about the square of the hypotepotenuse 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. % 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'?> <?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"> <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="2074" autoCount="277" editTime="93039"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2077" autoCount="279" editTime="93511">
<name>Sample Project</name> <name>Sample Project</name>
<author>Jane Smith</author> <author>Jane Smith</author>
</project> </project>
@@ -36,13 +36,13 @@
<entry key="i56be10" count="1" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry> <entry key="i56be10" count="1" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance> </importance>
</settings> </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"> <item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" /> <meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name> <name status="sc24b8f" import="ia857f0">Novel</name>
</item> </item>
<item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <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> <name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item> </item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <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> <name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item> </item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <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> <name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item> </item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <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> <name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
</item> </item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT"> <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> <name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
</item> </item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE"> <item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
@@ -2,19 +2,19 @@
"meta": { "meta": {
"projectName": "Lorem Ipsum", "projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com", "novelAuthor": "lipsum.com",
"buildTime": 1729029144, "buildTime": 1729725334,
"buildTimeStr": "2024-10-15 23:52:24" "buildTimeStr": "2024-10-24 01:15:34"
}, },
"text": { "text": {
"nwd": [ "nwd": [
[ [
"#! Lorem Ipsum", "#! 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", "# Act One",
"", "",
"\u201cFusce maximus felis libero\u201d" ">> \u201cFusce maximus felis libero\u201d <<"
], ],
[ [
"## Chapter One", "## Chapter One",
@@ -1,10 +1,10 @@
#! Lorem Ipsum #! 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. % 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 # Act One
“Fusce maximus felis libero” >> “Fusce maximus felis libero” <<
## Chapter One ## Chapter One
+3 -3
View File
@@ -412,7 +412,7 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
assert error == [] assert error == []
assert docFile.read_text(encoding="utf-8") == ( assert docFile.read_text(encoding="utf-8") == (
"#! New Novel\n\n" "#! New Novel\n\n"
"By Jane Doe\n\n" ">> By Jane Doe <<\n\n"
"## New Chapter\n\n\n" "## New Chapter\n\n\n"
"### New Scene\n\n\n" "### New Scene\n\n\n"
) )
@@ -442,7 +442,7 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
assert error == [] assert error == []
assert docFile.read_text(encoding="utf-8") == ( assert docFile.read_text(encoding="utf-8") == (
"#! New Novel\n\n" "#! New Novel\n\n"
"By Jane Doe\n\n" ">> By Jane Doe <<\n\n"
"## New Chapter\n\n\n" "## New Chapter\n\n\n"
"### New Scene\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 isinstance(docBuild.lastBuild, ToRaw)
assert docFile.read_text(encoding="utf-8") == ( assert docFile.read_text(encoding="utf-8") == (
"#! New Novel\n\n" "#! New Novel\n\n"
"By Jane Doe\n\n" ">> By Jane Doe <<\n\n"
"## New Chapter\n\n\n" "## New Chapter\n\n\n"
"### New Scene\n\n\n" "### New Scene\n\n\n"
"#! Notes: Plot\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.core.project import NWProject
from novelwriter.enum import nwBuildFmt from novelwriter.enum import nwBuildFmt
from novelwriter.formats.shared import BlockFmt, BlockTyp from novelwriter.formats.shared import BlockFmt, BlockTyp
from novelwriter.formats.todocx import ( from novelwriter.formats.todocx import ToDocX, _mkTag, _wTag
S_FNOTE, S_HEAD1, S_HEAD2, S_HEAD3, S_HEAD4, S_META, S_NORM, S_SEP,
S_TITLE, ToDocX, _mkTag, _wTag
)
from tests.tools import DOCX_IGNORE, cmpFiles from tests.tools import DOCX_IGNORE, cmpFiles
@@ -59,6 +56,120 @@ def xmlToText(xElem):
return rTxt 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 @pytest.mark.core
def testFmtToDocX_ParagraphStyles(mockGUI): def testFmtToDocX_ParagraphStyles(mockGUI):
"""Test formatting of paragraphs.""" """Test formatting of paragraphs."""
@@ -71,61 +182,12 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
# Normal Text # Normal Text
xTest = ET.Element(_wTag("body")) xTest = ET.Element(_wTag("body"))
doc._blocks = [(BlockTyp.TEXT, "", "Hello World", [], BlockFmt.NONE)] doc._text = "Hello World"
doc.tokenizeText()
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(xTest) == ( assert xmlToText(xTest) == (
f'<w:body><w:p><w:pPr><w:pStyle w:val="{S_NORM}" /></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>'
)
# 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:t>Hello World</w:t></w:r></w:p></w:body>' '<w:t>Hello World</w:t></w:r></w:p></w:body>'
) )
@@ -135,7 +197,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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>' '<w:t>* * *</w:t></w:r></w:p></w:body>'
) )
@@ -145,7 +207,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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 # Synopsis
@@ -155,7 +217,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI):
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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: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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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: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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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: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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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: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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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: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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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:br w:type="page" /></w:r>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r>' '<w:r><w:rPr /><w:t>Hello World</w:t></w:r>'
'</w:p></w:body>' '</w:p></w:body>'
@@ -297,7 +359,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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:rPr /><w:t>Hello World</w:t></w:r>'
'<w:r><w:br w:type="page" /></w:r>' '<w:r><w:br w:type="page" /></w:r>'
'</w:p></w:body>' '</w:p></w:body>'
@@ -309,7 +371,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI):
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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:ind w:firstLine="308" /></w:pPr>'
'<w:r><w:rPr /><w:t>Hello World</w:t></w:r></w:p></w:body>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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:b /></w:rPr><w:t>bold</w:t></w:r>'
'<w:r><w:rPr /><w:t xml:space="preserve">, </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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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: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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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: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>' '<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.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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:t xml:space="preserve">Some </w:t></w:r>'
'<w:r><w:rPr><w:u w:val="single" /></w:rPr>' '<w:r><w:rPr><w:u w:val="single" /></w:rPr>'
'<w:t xml:space="preserve">underlined and </w:t></w:r>' '<w:t xml:space="preserve">underlined and </w:t></w:r>'
@@ -422,7 +484,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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:r><w:rPr /><w:t>Some text.</w:t><w:br /><w:t>Next line</w:t></w:r>'
'</w:p></w:body>' '</w:p></w:body>'
) )
@@ -434,7 +496,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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>' '</w:p></w:body>'
) )
@@ -446,7 +508,7 @@ def testFmtToDocX_TextFormatting(mockGUI):
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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: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: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>' '</w:p></w:body>'
@@ -474,7 +536,7 @@ def testFmtToDocX_Footnotes(mockGUI):
doc.doConvert() doc.doConvert()
doc._pars[-1].toXml(xTest) doc._pars[-1].toXml(xTest)
assert xmlToText(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:t>Text with one</w:t></w:r>'
'<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr>' '<w:r><w:rPr><w:vertAlign w:val="superscript" /></w:rPr>'
'<w:footnoteReference w:id="1" /></w:r>' '<w:footnoteReference w:id="1" /></w:r>'
@@ -493,11 +555,11 @@ def testFmtToDocX_Footnotes(mockGUI):
doc._footnotesXml() doc._footnotesXml()
assert xmlToText(doc._files["footnotes.xml"].xml) == ( assert xmlToText(doc._files["footnotes.xml"].xml) == (
'<w:footnotes>' '<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>' '<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>' '<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:r><w:rPr /><w:t>Again?</w:t></w:r></w:p></w:footnote>'
'</w:footnotes>' '</w:footnotes>'
) )
+12 -8
View File
@@ -25,6 +25,7 @@ import json
import pytest import pytest
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.constants import nwHeadFmt
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp from novelwriter.formats.shared import BlockFmt, BlockTyp
from novelwriter.formats.tohtml import ToHtml from novelwriter.formats.tohtml import ToHtml
@@ -44,32 +45,35 @@ def testFmtToHtml_ConvertHeaders(mockGUI):
html._isFirst = True html._isFirst = True
# Header 1 # Header 1
html._text = "# Partition\n" html._text = "# Title\n"
html.setPartitionFormat(f"Part{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
html.tokenizeText() html.tokenizeText()
html.doConvert() html.doConvert()
assert html._pages[-1] == ( 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 # 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.tokenizeText()
html.doConvert() html.doConvert()
assert html._pages[-1] == ( 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 # 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.tokenizeText()
html.doConvert() html.doConvert()
assert html._pages[-1] == "<h2>Scene Title</h2>\n" assert html._pages[-1] == "<h2>Scene 1<br>Title</h2>\n"
# Header 4 # Header 4
html._text = "#### Section Title\n" html._text = "#### Title\n"
html.tokenizeText() html.tokenizeText()
html.doConvert() html.doConvert()
assert html._pages[-1] == "<h3>Section Title</h3>\n" assert html._pages[-1] == "<h3>Title</h3>\n"
# Title # Title
html._text = "#! Title\n" html._text = "#! Title\n"
+172 -53
View File
@@ -421,8 +421,8 @@ def testFmtToken_HeaderFormat(mockGUI):
@pytest.mark.core @pytest.mark.core
def testFmtToken_HeaderStyle(mockGUI): def testFmtToken_HeaderStyleNone(mockGUI):
"""Test the styling of headers in the Tokenizer class.""" """Test header styling disabled."""
project = NWProject() project = NWProject()
tokens = BareTokenizer(project) tokens = BareTokenizer(project)
@@ -432,13 +432,12 @@ def testFmtToken_HeaderStyle(mockGUI):
tokens.tokenizeText() tokens.tokenizeText()
return tokens._blocks[0][4] return tokens._blocks[0][4]
# No Styles tokens.setTitleStyle(False, False)
# =========
tokens.setPartitionStyle(False, False) tokens.setPartitionStyle(False, False)
tokens.setChapterStyle(False, False) tokens.setChapterStyle(False, False)
tokens.setSceneStyle(False, False) tokens.setSceneStyle(False, False)
assert tokens._titleStyle == BlockFmt.NONE
assert tokens._partStyle == BlockFmt.NONE assert tokens._partStyle == BlockFmt.NONE
assert tokens._chapterStyle == BlockFmt.NONE assert tokens._chapterStyle == BlockFmt.NONE
assert tokens._sceneStyle == BlockFmt.NONE assert tokens._sceneStyle == BlockFmt.NONE
@@ -451,7 +450,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\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 assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# First Document is True # First Document is True
@@ -459,7 +458,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", True) == BlockFmt.NONE assert processStyle("## Chapter\n", True) == BlockFmt.NONE
assert processStyle("### Scene\n", True) == BlockFmt.NONE assert processStyle("### Scene\n", True) == BlockFmt.NONE
assert processStyle("#### Section\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 assert processStyle("##! Prologue\n", True) == BlockFmt.NONE
# Note Docs # Note Docs
@@ -470,7 +469,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\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 assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# First Document is True # First Document is True
@@ -478,16 +477,28 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", True) == BlockFmt.NONE assert processStyle("## Chapter\n", True) == BlockFmt.NONE
assert processStyle("### Scene\n", True) == BlockFmt.NONE assert processStyle("### Scene\n", True) == BlockFmt.NONE
assert processStyle("#### Section\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 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.setPartitionStyle(True, False)
tokens.setChapterStyle(True, False) tokens.setChapterStyle(True, False)
tokens.setSceneStyle(True, False) tokens.setSceneStyle(True, False)
assert tokens._titleStyle == BlockFmt.CENTRE
assert tokens._partStyle == BlockFmt.CENTRE assert tokens._partStyle == BlockFmt.CENTRE
assert tokens._chapterStyle == BlockFmt.CENTRE assert tokens._chapterStyle == BlockFmt.CENTRE
assert tokens._sceneStyle == BlockFmt.CENTRE assert tokens._sceneStyle == BlockFmt.CENTRE
@@ -500,7 +511,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.CENTRE assert processStyle("## Chapter\n", False) == BlockFmt.CENTRE
assert processStyle("### Scene\n", False) == BlockFmt.CENTRE assert processStyle("### Scene\n", False) == BlockFmt.CENTRE
assert processStyle("#### Section\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.CENTRE assert processStyle("##! Prologue\n", False) == BlockFmt.CENTRE
# First Document is True # First Document is True
@@ -519,7 +530,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\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 assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# First Document is True # First Document is True
@@ -530,13 +541,25 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE
assert processStyle("##! Prologue\n", True) == BlockFmt.NONE 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.setPartitionStyle(False, True)
tokens.setChapterStyle(False, True) tokens.setChapterStyle(False, True)
tokens.setSceneStyle(False, True) tokens.setSceneStyle(False, True)
assert tokens._titleStyle == BlockFmt.PBB
assert tokens._partStyle == BlockFmt.PBB assert tokens._partStyle == BlockFmt.PBB
assert tokens._chapterStyle == BlockFmt.PBB assert tokens._chapterStyle == BlockFmt.PBB
assert tokens._sceneStyle == BlockFmt.PBB assert tokens._sceneStyle == BlockFmt.PBB
@@ -549,7 +572,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.PBB assert processStyle("## Chapter\n", False) == BlockFmt.PBB
assert processStyle("### Scene\n", False) == BlockFmt.PBB assert processStyle("### Scene\n", False) == BlockFmt.PBB
assert processStyle("#### Section\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.PBB assert processStyle("##! Prologue\n", False) == BlockFmt.PBB
# First Document is True # First Document is True
@@ -557,7 +580,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", True) == BlockFmt.NONE assert processStyle("## Chapter\n", True) == BlockFmt.NONE
assert processStyle("### Scene\n", True) == BlockFmt.NONE assert processStyle("### Scene\n", True) == BlockFmt.NONE
assert processStyle("#### Section\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 assert processStyle("##! Prologue\n", True) == BlockFmt.NONE
# Note Docs # Note Docs
@@ -568,7 +591,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\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 assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# First Document is True # First Document is True
@@ -576,16 +599,28 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", True) == BlockFmt.NONE assert processStyle("## Chapter\n", True) == BlockFmt.NONE
assert processStyle("### Scene\n", True) == BlockFmt.NONE assert processStyle("### Scene\n", True) == BlockFmt.NONE
assert processStyle("#### Section\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 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.setPartitionStyle(True, True)
tokens.setChapterStyle(True, True) tokens.setChapterStyle(True, True)
tokens.setSceneStyle(True, True) tokens.setSceneStyle(True, True)
assert tokens._titleStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._partStyle == BlockFmt.CENTRE | BlockFmt.PBB assert tokens._partStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._chapterStyle == BlockFmt.CENTRE | BlockFmt.PBB assert tokens._chapterStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._sceneStyle == 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("#! My Novel\n", True) == BlockFmt.CENTRE
assert processStyle("##! Prologue\n", True) == BlockFmt.NONE 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 tokens._isNovel = True
# Title Styles # 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.setPartitionStyle(True, True)
tokens.setChapterStyle(False, False) tokens.setChapterStyle(False, False)
tokens.setSceneStyle(False, False) tokens.setSceneStyle(False, False)
assert tokens._titleStyle == BlockFmt.NONE
assert tokens._partStyle == BlockFmt.CENTRE | BlockFmt.PBB assert tokens._partStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._chapterStyle == BlockFmt.NONE assert tokens._chapterStyle == BlockFmt.NONE
assert tokens._sceneStyle == BlockFmt.NONE assert tokens._sceneStyle == BlockFmt.NONE
@@ -645,14 +711,16 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.NONE assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\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 assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
# Chapter Styles # Chapter Styles
tokens.setTitleStyle(False, False)
tokens.setPartitionStyle(False, False) tokens.setPartitionStyle(False, False)
tokens.setChapterStyle(True, True) tokens.setChapterStyle(True, True)
tokens.setSceneStyle(False, False) tokens.setSceneStyle(False, False)
assert tokens._titleStyle == BlockFmt.NONE
assert tokens._partStyle == BlockFmt.NONE assert tokens._partStyle == BlockFmt.NONE
assert tokens._chapterStyle == BlockFmt.CENTRE | BlockFmt.PBB assert tokens._chapterStyle == BlockFmt.CENTRE | BlockFmt.PBB
assert tokens._sceneStyle == BlockFmt.NONE assert tokens._sceneStyle == BlockFmt.NONE
@@ -661,14 +729,16 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.CENTRE | BlockFmt.PBB assert processStyle("## Chapter\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("### Scene\n", False) == BlockFmt.NONE assert processStyle("### Scene\n", False) == BlockFmt.NONE
assert processStyle("#### Section\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 assert processStyle("##! Prologue\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
# Scene Styles # Scene Styles
tokens.setTitleStyle(False, False)
tokens.setPartitionStyle(False, False) tokens.setPartitionStyle(False, False)
tokens.setChapterStyle(False, False) tokens.setChapterStyle(False, False)
tokens.setSceneStyle(True, True) tokens.setSceneStyle(True, True)
assert tokens._titleStyle == BlockFmt.NONE
assert tokens._partStyle == BlockFmt.NONE assert tokens._partStyle == BlockFmt.NONE
assert tokens._chapterStyle == BlockFmt.NONE assert tokens._chapterStyle == BlockFmt.NONE
assert tokens._sceneStyle == BlockFmt.CENTRE | BlockFmt.PBB assert tokens._sceneStyle == BlockFmt.CENTRE | BlockFmt.PBB
@@ -677,7 +747,7 @@ def testFmtToken_HeaderStyle(mockGUI):
assert processStyle("## Chapter\n", False) == BlockFmt.NONE assert processStyle("## Chapter\n", False) == BlockFmt.NONE
assert processStyle("### Scene\n", False) == BlockFmt.CENTRE | BlockFmt.PBB assert processStyle("### Scene\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
assert processStyle("#### Section\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 assert processStyle("##! Prologue\n", False) == BlockFmt.NONE
@@ -842,13 +912,13 @@ def testFmtToken_MarginFormat(mockGUI):
] ]
assert tokens._raw[-1] == ( assert tokens._raw[-1] == (
"Some regular text\n\n" "Some regular text\n\n"
"Some left-aligned text\n\n" "Some left-aligned text <<\n\n"
"Some right-aligned text\n\n" ">> Some right-aligned text\n\n"
"Some centered text\n\n" ">> Some centered text <<\n\n"
"Left-indented block\n\n" "> Left-indented block\n\n"
"Right-indented block\n\n" "Right-indented block <\n\n"
"Double-indented block\n\n" "> Double-indented block <\n\n"
"Right-indent, right-aligned\n\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._text = "Some **bolded text** on this lines\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._blocks == [( assert tokens._blocks == [(
BlockTyp.TEXT, "", "Some bolded text on this lines", BlockTyp.TEXT, "", "Some bolded text on this lines", [
[
(5, TextFmt.B_B, ""), (5, TextFmt.B_B, ""),
(16, TextFmt.B_E, ""), (16, TextFmt.B_E, ""),
], ], BlockFmt.NONE
BlockFmt.NONE
)] )]
assert tokens._raw[-1] == "Some **bolded text** on this lines\n\n" assert tokens._raw[-1] == "Some **bolded text** on this lines\n\n"
tokens._text = "Some _italic text_ on this lines\n" tokens._text = "Some _italic text_ on this lines\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._blocks == [( assert tokens._blocks == [(
BlockTyp.TEXT, "", "Some italic text on this lines", BlockTyp.TEXT, "", "Some italic text on this lines", [
[
(5, TextFmt.I_B, ""), (5, TextFmt.I_B, ""),
(16, TextFmt.I_E, ""), (16, TextFmt.I_E, ""),
], ], BlockFmt.NONE
BlockFmt.NONE
)] )]
assert tokens._raw[-1] == "Some _italic text_ on this lines\n\n" assert tokens._raw[-1] == "Some _italic text_ on this lines\n\n"
tokens._text = "Some **_bold italic text_** on this lines\n" tokens._text = "Some **_bold italic text_** on this lines\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._blocks == [( 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.B_B, ""),
(5, TextFmt.I_B, ""), (5, TextFmt.I_B, ""),
(21, TextFmt.I_E, ""), (21, TextFmt.I_E, ""),
(21, TextFmt.B_E, ""), (21, TextFmt.B_E, ""),
], ], BlockFmt.NONE
BlockFmt.NONE
)] )]
assert tokens._raw[-1] == "Some **_bold italic text_** on this lines\n\n" assert tokens._raw[-1] == "Some **_bold italic text_** on this lines\n\n"
tokens._text = "Some ~~strikethrough text~~ on this lines\n" tokens._text = "Some ~~strikethrough text~~ on this lines\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._blocks == [( assert tokens._blocks == [(
BlockTyp.TEXT, "", "Some strikethrough text on this lines", BlockTyp.TEXT, "", "Some strikethrough text on this lines", [
[
(5, TextFmt.D_B, ""), (5, TextFmt.D_B, ""),
(23, TextFmt.D_E, ""), (23, TextFmt.D_E, ""),
], ], BlockFmt.NONE
BlockFmt.NONE
)] )]
assert tokens._raw[-1] == "Some ~~strikethrough text~~ on this lines\n\n" 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._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
tokens.tokenizeText() tokens.tokenizeText()
assert tokens._blocks == [( 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, ""), (5, TextFmt.B_B, ""),
(21, TextFmt.I_B, ""), (21, TextFmt.I_B, ""),
(27, TextFmt.I_E, ""), (27, TextFmt.I_E, ""),
(32, TextFmt.D_B, ""), (32, TextFmt.D_B, ""),
(45, TextFmt.D_E, ""), (45, TextFmt.D_E, ""),
(50, TextFmt.B_E, ""), (50, TextFmt.B_E, ""),
], ], BlockFmt.NONE
BlockFmt.NONE
)] )]
assert tokens._raw[-1] == ( assert tokens._raw[-1] == (
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" "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 @pytest.mark.core
def testFmtToken_Dialogue(mockGUI): def testFmtToken_Dialogue(mockGUI):
"""Test the tokenization of dialogue in the Tokenizer class.""" """Test the tokenization of dialogue in the Tokenizer class."""
+10 -6
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import pytest import pytest
from novelwriter.constants import nwHeadFmt
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp from novelwriter.formats.shared import BlockFmt, BlockTyp
from novelwriter.formats.tomarkdown import ToMarkdown from novelwriter.formats.tomarkdown import ToMarkdown
@@ -37,22 +38,25 @@ def testFmtToMarkdown_ConvertHeaders(mockGUI):
md._isFirst = True md._isFirst = True
# Header 1 # Header 1
md._text = "# Partition\n" md._text = "# Title\n"
md.setPartitionFormat(f"Part{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
md.tokenizeText() md.tokenizeText()
md.doConvert() md.doConvert()
assert md._pages[-1] == "# Partition\n\n" assert md._pages[-1] == "# Part - Title\n\n"
# Header 2 # 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.tokenizeText()
md.doConvert() md.doConvert()
assert md._pages[-1] == "## Chapter Title\n\n" assert md._pages[-1] == "## Chapter 1 - Title\n\n"
# Header 3 # 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.tokenizeText()
md.doConvert() md.doConvert()
assert md._pages[-1] == "### Scene Title\n\n" assert md._pages[-1] == "### Scene 1 - Title\n\n"
# Header 4 # Header 4
md._text = "#### Section Title\n" md._text = "#### Section Title\n"
+13 -7
View File
@@ -281,6 +281,7 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
# Header 1 # Header 1
odt._text = "# Title\n" odt._text = "# Title\n"
odt.setPartitionFormat(f"Part{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
odt.tokenizeText() odt.tokenizeText()
odt.initDocument() odt.initDocument()
odt.doConvert() odt.doConvert()
@@ -288,12 +289,14 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
assert odt.errData == [] assert odt.errData == []
assert xmlToText(odt._xText) == ( assert xmlToText(odt._xText) == (
'<office:text>' '<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>' '</office:text>'
) )
# Header 2 # Header 2
odt._text = "## Chapter\n" odt._text = "## Title\n"
odt.setChapterFormat(f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
odt.tokenizeText() odt.tokenizeText()
odt.initDocument() odt.initDocument()
odt.doConvert() odt.doConvert()
@@ -301,12 +304,14 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
assert odt.errData == [] assert odt.errData == []
assert xmlToText(odt._xText) == ( assert xmlToText(odt._xText) == (
'<office:text>' '<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>' '</office:text>'
) )
# Header 3 # Header 3
odt._text = "### Scene\n" odt._text = "### Title\n"
odt.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
odt.tokenizeText() odt.tokenizeText()
odt.initDocument() odt.initDocument()
odt.doConvert() odt.doConvert()
@@ -314,12 +319,13 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
assert odt.errData == [] assert odt.errData == []
assert xmlToText(odt._xText) == ( assert xmlToText(odt._xText) == (
'<office:text>' '<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>' '</office:text>'
) )
# Header 4 # Header 4
odt._text = "#### Section\n" odt._text = "#### Title\n"
odt.tokenizeText() odt.tokenizeText()
odt.initDocument() odt.initDocument()
odt.doConvert() odt.doConvert()
@@ -327,7 +333,7 @@ def testFmtToOdt_ConvertHeaders(mockGUI):
assert odt.errData == [] assert odt.errData == []
assert xmlToText(odt._xText) == ( assert xmlToText(odt._xText) == (
'<office:text>' '<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>' '</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) nwGUI.mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%Short: \n" assert nwGUI.docEditor.getText() == "Stuff\n%Short: \n"
# Insert Break or Space # Breaks and Vertical Space
# ===================== # =========================
nwGUI.docEditor.setPlainText("### Stuff\n") nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger) nwGUI.mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "[newpage]\n### Stuff\n" 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.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger) nwGUI.mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n" assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n"
+3 -2
View File
@@ -47,6 +47,7 @@ def testTextCounting_preProcessText():
"[vspace:3]\n\n" "[vspace:3]\n\n"
"[New Page]\n\n" "[New Page]\n\n"
"[footnote:abcd]\n\n" "[footnote:abcd]\n\n"
"[br]\n\n"
"Dashes\u2013and even longer\u2014dashes.\n\n" "Dashes\u2013and even longer\u2014dashes.\n\n"
) )
@@ -60,7 +61,7 @@ def testTextCounting_preProcessText():
"#### Heading Four", "#### Heading Four",
"", "", "", "", "", "",
"A paragraph.", "", "A paragraph.", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "",
"Dashes and even longer dashes.", "" "Dashes and even longer dashes.", ""
] ]
@@ -68,7 +69,7 @@ def testTextCounting_preProcessText():
assert preProcessText(text, keepHeaders=False) == [ assert preProcessText(text, keepHeaders=False) == [
"", "", "", "", "", "",
"A paragraph.", "", "A paragraph.", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "",
"Dashes and even longer dashes.", "" "Dashes and even longer dashes.", ""
] ]
+11
View File
@@ -198,6 +198,17 @@ def testTextPatterns_ShortcodesPlain():
assert allMatches(regEx, "one [x]two[/x] three") == [] 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 @pytest.mark.core
def testTextPatterns_ShortcodesValue(): def testTextPatterns_ShortcodesValue():