From bfa82f6bb3bebb13e961ac79e6e6ae19cbde3273 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 23 Oct 2024 21:12:25 +0200 Subject: [PATCH 1/6] Add test coverage of title line breaks, and don't break markdown titles --- novelwriter/formats/tomarkdown.py | 10 +- tests/test_formats/test_fmt_todocx.py | 228 ++++++++++++++-------- tests/test_formats/test_fmt_tohtml.py | 20 +- tests/test_formats/test_fmt_tomarkdown.py | 16 +- tests/test_formats/test_fmt_toodt.py | 20 +- 5 files changed, 185 insertions(+), 109 deletions(-) diff --git a/novelwriter/formats/tomarkdown.py b/novelwriter/formats/tomarkdown.py index 198b7e7a..db1627be 100644 --- a/novelwriter/formats/tomarkdown.py +++ b/novelwriter/formats/tomarkdown.py @@ -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(nwHeadFmt.BR, " - ") lines.append(f"# {tHead}\n\n") elif tType == BlockTyp.HEAD1: - tHead = tText.replace(nwHeadFmt.BR, "\n") + tHead = tText.replace(nwHeadFmt.BR, " - ") lines.append(f"# {tHead}\n\n") elif tType == BlockTyp.HEAD2: - tHead = tText.replace(nwHeadFmt.BR, "\n") + tHead = tText.replace(nwHeadFmt.BR, " - ") lines.append(f"## {tHead}\n\n") elif tType == BlockTyp.HEAD3: - tHead = tText.replace(nwHeadFmt.BR, "\n") + tHead = tText.replace(nwHeadFmt.BR, " - ") lines.append(f"### {tHead}\n\n") elif tType == BlockTyp.HEAD4: - tHead = tText.replace(nwHeadFmt.BR, "\n") + tHead = tText.replace(nwHeadFmt.BR, " - ") lines.append(f"#### {tHead}\n\n") elif tType == BlockTyp.SEP: diff --git a/tests/test_formats/test_fmt_todocx.py b/tests/test_formats/test_fmt_todocx.py index a9214db9..d395eaee 100644 --- a/tests/test_formats/test_fmt_todocx.py +++ b/tests/test_formats/test_fmt_todocx.py @@ -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) == ( + '' + 'Hello World' + ) + + # 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) == ( + '' + '' + 'Hello World' + ) + + # 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) == ( + '' + '' + 'PartHello World' + ) + + # 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) == ( + '' + '' + 'Hello World' + ) + + # 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) == ( + '' + '' + 'Chapter 2Hello World' + ) + + # 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) == ( + '' + 'Hello World' + ) + + # 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) == ( + '' + 'Scene 2Hello World' + ) + + # Heading Level 4 + # =============== + doc._text = "#### Hello World" + + xTest = ET.Element(_wTag("body")) + doc.tokenizeText() + doc.doConvert() + doc._pars[-1].toXml(xTest) + assert xmlToText(xTest) == ( + '' + 'Hello World' + ) + + @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'' - 'Hello World' - ) - - # 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'' - 'Hello World' - ) - - # 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'' - 'Hello World' - ) - - # 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'' - 'Hello World' - ) - - # 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'' - 'Hello World' - ) - - # 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'' + '' 'Hello World' ) @@ -135,7 +197,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' '* * *' ) @@ -145,7 +207,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' ) # Synopsis @@ -155,7 +217,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Synopsis:' ' ' 'Hello World' @@ -169,7 +231,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Short Description:' ' ' 'Hello World' @@ -183,7 +245,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Comment:' ' ' 'Hello World' @@ -197,7 +259,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Tag:' ' ' 'Stuff' @@ -211,7 +273,7 @@ def testFmtToDocX_ParagraphStyles(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Characters:' ' ' 'Jane' @@ -245,7 +307,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Hello World' ) @@ -255,7 +317,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Hello World' ) @@ -265,7 +327,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Hello World' ) @@ -275,7 +337,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Hello World' ) @@ -285,7 +347,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' '' 'Hello World' '' @@ -297,7 +359,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Hello World' '' '' @@ -309,7 +371,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' '' 'Hello World' ) @@ -320,7 +382,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' '' 'Hello World' ) @@ -331,7 +393,7 @@ def testFmtToDocX_ParagraphFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' '' 'Hello World' ) @@ -351,7 +413,7 @@ def testFmtToDocX_TextFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Text ' 'bold' ', ' @@ -369,7 +431,7 @@ def testFmtToDocX_TextFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Some ' 'nested ' 'bold' @@ -389,7 +451,7 @@ def testFmtToDocX_TextFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Some super' 'script' ' and sub' @@ -405,7 +467,7 @@ def testFmtToDocX_TextFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Some ' '' 'underlined and ' @@ -422,7 +484,7 @@ def testFmtToDocX_TextFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Some text.Next line' '' ) @@ -434,7 +496,7 @@ def testFmtToDocX_TextFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Item 1Item 2' '' ) @@ -446,7 +508,7 @@ def testFmtToDocX_TextFormatting(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Some ' 'boldtext' '' @@ -474,7 +536,7 @@ def testFmtToDocX_Footnotes(mockGUI): doc.doConvert() doc._pars[-1].toXml(xTest) assert xmlToText(xTest) == ( - f'' + '' 'Text with one' '' '' @@ -493,11 +555,11 @@ def testFmtToDocX_Footnotes(mockGUI): doc._footnotesXml() assert xmlToText(doc._files["footnotes.xml"].xml) == ( '' - f'' + '' 'Footnote text A.' - f'' + '' 'Another footnote.' - f'' + '' 'Again?' '' ) diff --git a/tests/test_formats/test_fmt_tohtml.py b/tests/test_formats/test_fmt_tohtml.py index 68db421c..ac5abbec 100644 --- a/tests/test_formats/test_fmt_tohtml.py +++ b/tests/test_formats/test_fmt_tohtml.py @@ -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] == ( - "

Partition

\n" + "

Part
Title

\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] == ( - "

Chapter Title

\n" + "

Chapter 1
Title

\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] == "

Scene Title

\n" + assert html._pages[-1] == "

Scene 1
Title

\n" # Header 4 - html._text = "#### Section Title\n" + html._text = "#### Title\n" html.tokenizeText() html.doConvert() - assert html._pages[-1] == "

Section Title

\n" + assert html._pages[-1] == "

Title

\n" # Title html._text = "#! Title\n" diff --git a/tests/test_formats/test_fmt_tomarkdown.py b/tests/test_formats/test_fmt_tomarkdown.py index ad0633ae..e94f4b72 100644 --- a/tests/test_formats/test_fmt_tomarkdown.py +++ b/tests/test_formats/test_fmt_tomarkdown.py @@ -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" diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py index 0da42138..fedb5f2a 100644 --- a/tests/test_formats/test_fmt_toodt.py +++ b/tests/test_formats/test_fmt_toodt.py @@ -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) == ( '' - 'Title' + 'Part' + 'Title' '' ) # 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) == ( '' - 'Chapter' + 'Chapter 1' + 'Title' '' ) # 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) == ( '' - 'Scene' + 'Scene 1' + 'Title' '' ) # 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) == ( '' - 'Section' + 'Title' '' ) From 560540618c3e89f73afcf5020e5c53d93668cd67 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 23 Oct 2024 21:18:15 +0200 Subject: [PATCH 2/6] Process heading line breaks in the Tokenizer --- novelwriter/formats/todocx.py | 15 +++++---------- novelwriter/formats/tohtml.py | 12 ++++++------ novelwriter/formats/tokenizer.py | 1 + novelwriter/formats/tomarkdown.py | 12 ++++++------ novelwriter/formats/toodt.py | 15 +++++---------- novelwriter/formats/toqdoc.py | 4 ++-- 6 files changed, 25 insertions(+), 34 deletions(-) diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 7c56e29f..1ea7d6b9 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -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) diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py index d129349f..2f8be4ce 100644 --- a/novelwriter/formats/tohtml.py +++ b/novelwriter/formats/tohtml.py @@ -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"{self._formatText(tText, tFmt)}

\n") elif tType == BlockTyp.TITLE: - tHead = tText.replace(nwHeadFmt.BR, "
") + tHead = tText.replace("\n", "
") lines.append(f"

{aNm}{tHead}

\n") elif tType == BlockTyp.HEAD1: - tHead = tText.replace(nwHeadFmt.BR, "
") + tHead = tText.replace("\n", "
") lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}\n") elif tType == BlockTyp.HEAD2: - tHead = tText.replace(nwHeadFmt.BR, "
") + tHead = tText.replace("\n", "
") lines.append(f"<{h2}{hStyle}>{aNm}{tHead}\n") elif tType == BlockTyp.HEAD3: - tHead = tText.replace(nwHeadFmt.BR, "
") + tHead = tText.replace("\n", "
") lines.append(f"<{h3}{hStyle}>{aNm}{tHead}\n") elif tType == BlockTyp.HEAD4: - tHead = tText.replace(nwHeadFmt.BR, "
") + tHead = tText.replace("\n", "
") lines.append(f"<{h4}{hStyle}>{aNm}{tHead}\n") elif tType == BlockTyp.SEP: diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index f01d6e85..1dd83767 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -1187,6 +1187,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)) diff --git a/novelwriter/formats/tomarkdown.py b/novelwriter/formats/tomarkdown.py index db1627be..61111091 100644 --- a/novelwriter/formats/tomarkdown.py +++ b/novelwriter/formats/tomarkdown.py @@ -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, " - ") + tHead = tText.replace("\n", " - ") lines.append(f"# {tHead}\n\n") elif tType == BlockTyp.HEAD1: - tHead = tText.replace(nwHeadFmt.BR, " - ") + tHead = tText.replace("\n", " - ") lines.append(f"# {tHead}\n\n") elif tType == BlockTyp.HEAD2: - tHead = tText.replace(nwHeadFmt.BR, " - ") + tHead = tText.replace("\n", " - ") lines.append(f"## {tHead}\n\n") elif tType == BlockTyp.HEAD3: - tHead = tText.replace(nwHeadFmt.BR, " - ") + tHead = tText.replace("\n", " - ") lines.append(f"### {tHead}\n\n") elif tType == BlockTyp.HEAD4: - tHead = tText.replace(nwHeadFmt.BR, " - ") + tHead = tText.replace("\n", " - ") lines.append(f"#### {tHead}\n\n") elif tType == BlockTyp.SEP: diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index b4b61807..4edce58b 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -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) diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 5c478276..295c9745 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -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) From ff7404d01d4f7d78cd5b362f92fe6ac4f99351d1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:07:03 +0200 Subject: [PATCH 3/6] Add line break shortcode support --- novelwriter/constants.py | 4 +++- novelwriter/enum.py | 1 + novelwriter/formats/tokenizer.py | 12 ++++-------- novelwriter/gui/doceditor.py | 2 ++ novelwriter/gui/mainmenu.py | 10 ++++++++-- novelwriter/text/patterns.py | 6 ++++++ tests/test_gui/test_gui_mainmenu.py | 9 +++++++-- tests/test_text/test_text_counting.py | 5 +++-- tests/test_text/test_text_patterns.py | 11 +++++++++++ 9 files changed, 45 insertions(+), 15 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 03886a57..9451ea7f 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -61,10 +61,11 @@ class nwConst: class nwRegEx: WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b" + BREAK = r"(?i)(? 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 diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index a29fe34b..bedb45da 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -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: diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 5205b21a..3dcdfbcf 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -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( diff --git a/novelwriter/text/patterns.py b/novelwriter/text/patterns.py index c3a7cf54..faf39679 100644 --- a/novelwriter/text/patterns.py +++ b/novelwriter/text/patterns.py @@ -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.""" diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 60947af0..5f9801f0 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -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" diff --git a/tests/test_text/test_text_counting.py b/tests/test_text/test_text_counting.py index 360070bd..228c017e 100644 --- a/tests/test_text/test_text_counting.py +++ b/tests/test_text/test_text_counting.py @@ -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.", "" ] diff --git a/tests/test_text/test_text_patterns.py b/tests/test_text/test_text_patterns.py index 34610b46..8877cc0c 100644 --- a/tests/test_text/test_text_patterns.py +++ b/tests/test_text/test_text_patterns.py @@ -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(): From e15941520f43fe79f2a08fa0c0088fe63a648efe Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:25:32 +0200 Subject: [PATCH 4/6] Add working forced line break implementation --- novelwriter/formats/tokenizer.py | 36 ++++++++++++------- sample/content/ba8a28a246524.nwd | 26 +++++++------- .../mBuildDocBuild_NWD_Lorem_Ipsum.json | 12 +++---- .../mBuildDocBuild_NWD_Lorem_Ipsum.txt | 8 ++--- tests/test_core/test_core_docbuild.py | 6 ++-- tests/test_formats/test_fmt_tokenizer.py | 14 ++++---- 6 files changed, 56 insertions(+), 46 deletions(-) diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index d93d262d..2fa617ca 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -519,18 +519,22 @@ 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) + text = REGEX_PATTERNS.lineBreak.sub("\uffff", self._text) + nHead = 0 breakNext = False tmpMarkdown = [] 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", "\n") sLine = aLine.strip().lower() # Check for blank lines @@ -782,19 +786,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 @@ -809,7 +813,7 @@ 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 )) @@ -840,7 +844,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] = [] @@ -890,9 +894,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 @@ -903,8 +910,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 diff --git a/sample/content/ba8a28a246524.nwd b/sample/content/ba8a28a246524.nwd index 3e995307..eabc9567 100644 --- a/sample/content/ba8a28a246524.nwd +++ b/sample/content/ba8a28a246524.nwd @@ -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. diff --git a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json index 630d9592..f760b711 100644 --- a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json +++ b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json @@ -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", diff --git a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt index 75476c81..b943858a 100644 --- a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt +++ b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt @@ -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 diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py index df979e74..7871f83f 100644 --- a/tests/test_core/test_core_docbuild.py +++ b/tests/test_core/test_core_docbuild.py @@ -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" diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 3750af7a..5549c14a 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -842,13 +842,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" ) From 76a9f8e3df71879ced0a41ded10929109780df6c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:32:54 +0200 Subject: [PATCH 5/6] Fix a minor issue, and add test --- novelwriter/formats/tokenizer.py | 3 +- tests/test_formats/test_fmt_tokenizer.py | 89 ++++++++++++++++++------ 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 2fa617ca..69a422c8 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -526,6 +526,7 @@ class Tokenizer(ABC): 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 @@ -534,7 +535,7 @@ class Tokenizer(ABC): tHandle = self._handle or "" tBlocks: list[T_Block] = [B_EMPTY] for bLine in text.splitlines(): - aLine = bLine.replace("\uffff", "\n") + aLine = bLine.replace("\uffff", "") # Remove placeholder characters sLine = aLine.strip().lower() # Check for blank lines diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 5549c14a..115203e3 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -1056,72 +1056,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.""" From 6b01bd7ffeb3480e016e7724ccf2ab2982f64de6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:47:14 +0200 Subject: [PATCH 6/6] Fix title page break issue and add test coverage --- novelwriter/assets/i18n/project_en_GB.json | 2 +- novelwriter/core/buildsettings.py | 2 +- novelwriter/core/docbuild.py | 4 +- novelwriter/formats/tokenizer.py | 44 ++++---- sample/content/53b69b83cdafc.nwd | 13 ++- sample/nwProject.nwx | 12 +- tests/test_formats/test_fmt_tokenizer.py | 122 ++++++++++++++++----- 7 files changed, 139 insertions(+), 60 deletions(-) diff --git a/novelwriter/assets/i18n/project_en_GB.json b/novelwriter/assets/i18n/project_en_GB.json index fe4be646..39d52e6e 100644 --- a/novelwriter/assets/i18n/project_en_GB.json +++ b/novelwriter/assets/i18n/project_en_GB.json @@ -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", diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index c26c94b1..f8cc1c40 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -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), diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index d587a2dd..a2417e8f 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -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"), diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 69a422c8..f341cb1e 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -452,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") @@ -531,7 +533,7 @@ class Tokenizer(ABC): nHead = 0 breakNext = False - tmpMarkdown = [] + rawText = [] tHandle = self._handle or "" tBlocks: list[T_Block] = [B_EMPTY] for bLine in text.splitlines(): @@ -542,7 +544,7 @@ class Tokenizer(ABC): if not sLine: tBlocks.append(B_EMPTY) if keepRaw: - tmpMarkdown.append("\n") + rawText.append("\n") continue if breakNext: @@ -608,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 @@ -629,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 @@ -665,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 @@ -700,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 @@ -741,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 @@ -771,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 @@ -819,7 +821,7 @@ class Tokenizer(ABC): 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: @@ -835,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 # =========== diff --git a/sample/content/53b69b83cdafc.nwd b/sample/content/53b69b83cdafc.nwd index 9163e10f..f793413a 100644 --- a/sample/content/53b69b83cdafc.nwd +++ b/sample/content/53b69b83cdafc.nwd @@ -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. << \ No newline at end of file +>> It should be the first document of the project. << diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 373111cc..53f3a5e7 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -36,13 +36,13 @@ Main - + Novel - + Title Page @@ -58,7 +58,7 @@ Chapter One - + Making a Scene @@ -66,7 +66,7 @@ Another Scene - + Interlude diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 115203e3..1a7691e5 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -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