"""
novelWriter – Tokenizer Class Tester
====================================

This file is a part of novelWriter
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations

import pytest

from PyQt6.QtGui import QFont

from novelwriter import CONFIG
from novelwriter.constants import nwHeadFmt, nwStyles
from novelwriter.core.project import NWProject
from novelwriter.enum import nwComment
from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape
from novelwriter.formats.tokenizer import COMMENT_STYLE, HeadingFormatter, Tokenizer
from novelwriter.formats.tomarkdown import ToMarkdown

from tests.tools import C, buildTestProject

TMH = "0123456789abc"
TM1 = f"{TMH}:T0001"
TM2 = f"{TMH}:T0002"


class BareTokenizer(Tokenizer):
    def doConvert(self):
        super().doConvert()  # type: ignore (deliberate check)

    def closeDocument(self):
        super().closeDocument()  # type: ignore (deliberate check)

    def saveDocument(self, path) -> None:
        super().saveDocument(path)  # type: ignore (deliberate check)


@pytest.mark.core
def testFmtToken_Abstracts(mockGUI, tstPaths):
    """Test all the abstract methods of the Tokenizer class."""
    project = NWProject()
    tokens = BareTokenizer(project)

    with pytest.raises(NotImplementedError):
        tokens.doConvert()

    with pytest.raises(NotImplementedError):
        tokens.closeDocument()

    with pytest.raises(NotImplementedError):
        tokens.saveDocument(tstPaths)


@pytest.mark.core
def testFmtToken_Setters(mockGUI):
    """Test all the setters for the Tokenizer class."""
    project = NWProject()
    tokens = BareTokenizer(project)

    # Verify defaults
    assert tokens._fmtPart == nwHeadFmt.TITLE
    assert tokens._fmtChapter == nwHeadFmt.TITLE
    assert tokens._fmtUnNum == nwHeadFmt.TITLE
    assert tokens._fmtScene == nwHeadFmt.TITLE
    assert tokens._fmtHScene == nwHeadFmt.TITLE
    assert tokens._fmtSection == nwHeadFmt.TITLE
    assert tokens._textFont == QFont("Serif", 11)
    assert tokens._lineHeight == 1.15
    assert tokens._blockIndent == 4.0
    assert tokens._doJustify is False
    assert tokens._marginTitle == nwStyles.T_MARGIN["H0"]
    assert tokens._marginHead1 == nwStyles.T_MARGIN["H1"]
    assert tokens._marginHead2 == nwStyles.T_MARGIN["H2"]
    assert tokens._marginHead3 == nwStyles.T_MARGIN["H3"]
    assert tokens._marginHead4 == nwStyles.T_MARGIN["H4"]
    assert tokens._marginSep == nwStyles.T_MARGIN["SP"]
    assert tokens._marginText == nwStyles.T_MARGIN["TT"]
    assert tokens._marginMeta == nwStyles.T_MARGIN["MT"]
    assert tokens._hidePart is False
    assert tokens._hideChapter is False
    assert tokens._hideUnNum is False
    assert tokens._hideScene is False
    assert tokens._hideHScene is False
    assert tokens._hideSection is False
    assert tokens._linkHeadings is False
    assert tokens._doBodyText is True
    assert tokens._doComments == {nwComment.FOOTNOTE}
    assert tokens._doKeywords is False

    # Set new values
    tokens.setPartitionFormat(f"T: {nwHeadFmt.TITLE}", True)
    tokens.setChapterFormat(f"C: {nwHeadFmt.TITLE}", True)
    tokens.setUnNumberedFormat(f"U: {nwHeadFmt.TITLE}", True)
    tokens.setSceneFormat(f"S: {nwHeadFmt.TITLE}", True)
    tokens.setHardSceneFormat(f"H: {nwHeadFmt.TITLE}", True)
    tokens.setSectionFormat(f"X: {nwHeadFmt.TITLE}", True)
    tokens.setTextFont(QFont("Monospace", 10))
    tokens.setLineHeight(2.0)
    tokens.setBlockIndent(6.0)
    tokens.setJustify(True)
    tokens.setTitleMargins(2.0, 2.0)
    tokens.setHead1Margins(2.0, 2.0)
    tokens.setHead2Margins(2.0, 2.0)
    tokens.setHead3Margins(2.0, 2.0)
    tokens.setHead4Margins(2.0, 2.0)
    tokens.setTextMargins(2.0, 2.0)
    tokens.setMetaMargins(2.0, 2.0)
    tokens.setSeparatorMargins(2.0, 2.0)
    tokens.setLinkHeadings(True)
    tokens.setBodyText(False)
    tokens.setCommentType(nwComment.PLAIN, True)
    tokens.setCommentType(nwComment.SHORT, True)
    tokens.setCommentType(nwComment.SYNOPSIS, True)
    tokens.setKeywords(True)

    # Check new values
    assert tokens._fmtPart == f"T: {nwHeadFmt.TITLE}"
    assert tokens._fmtChapter == f"C: {nwHeadFmt.TITLE}"
    assert tokens._fmtUnNum == f"U: {nwHeadFmt.TITLE}"
    assert tokens._fmtScene == f"S: {nwHeadFmt.TITLE}"
    assert tokens._fmtHScene == f"H: {nwHeadFmt.TITLE}"
    assert tokens._fmtSection == f"X: {nwHeadFmt.TITLE}"
    assert tokens._textFont == QFont("Monospace", 10)
    assert tokens._lineHeight == 2.0
    assert tokens._blockIndent == 6.0
    assert tokens._doJustify is True
    assert tokens._marginTitle == (2.0, 2.0)
    assert tokens._marginHead1 == (2.0, 2.0)
    assert tokens._marginHead2 == (2.0, 2.0)
    assert tokens._marginHead3 == (2.0, 2.0)
    assert tokens._marginHead4 == (2.0, 2.0)
    assert tokens._marginText == (2.0, 2.0)
    assert tokens._marginMeta == (2.0, 2.0)
    assert tokens._marginSep == (2.0, 2.0)
    assert tokens._hidePart is True
    assert tokens._hideChapter is True
    assert tokens._hideUnNum is True
    assert tokens._hideScene is True
    assert tokens._hideHScene is True
    assert tokens._hideSection is True
    assert tokens._linkHeadings is True
    assert tokens._doBodyText is False
    assert tokens._doComments == {
        nwComment.FOOTNOTE, nwComment.PLAIN, nwComment.SYNOPSIS, nwComment.SHORT,
    }
    assert tokens._doKeywords is True

    # Properties
    assert tokens.textStats == {}
    assert tokens.errData == []

    # Check Limits
    tokens.setLineHeight(0.0)
    assert tokens._lineHeight == 0.5
    tokens.setLineHeight(10.0)
    assert tokens._lineHeight == 5.0

    tokens.setBlockIndent(-6.0)
    assert tokens._blockIndent == 0.0
    tokens.setBlockIndent(60.0)
    assert tokens._blockIndent == 10.0


@pytest.mark.core
def testFmtToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath):
    """Test handling files and text in the Tokenizer class."""
    project = NWProject()
    mockRnd.reset()
    buildTestProject(project, fncPath)

    project.data.setLanguage("en")
    project._loadProjectLocalisation()

    tokens = BareTokenizer(project)
    tokens._keepRaw = True

    # Set some content to work with
    docText = (
        "### Scene Six\n\n"
        "This is text with _italic text_, some **bold text**, some ~~deleted text~~, "
        "and some **_mixed text_** and **some _nested_ text**.\n\n"
        "#### Replace\n\n"
        "Also, replace <A> and <B>.\n\n"
    )
    docTextR = docText.replace("<A>", "this").replace("<B>", "that")

    nDoc = project.storage.getDocument(C.hSceneDoc)
    assert nDoc.writeDocument(docText)

    project.data.setAutoReplace({"A": "this", "B": "that"})

    assert project.saveProject()

    # Root Heading
    assert len(tokens._blocks) == 0
    tokens.addRootHeading("stuff")
    tokens.addRootHeading(C.hSceneDoc)
    assert len(tokens._blocks) == 0

    # First Page
    tokens.addRootHeading(C.hPlotRoot)
    assert tokens._raw[-1] == "#! Notes: Plot\n\n"
    assert tokens._blocks[-1] == (
        BlockTyp.TITLE, "0000000000009:T0001", "Notes: Plot", [], BlockFmt.CENTRE
    )

    # Not First Page
    tokens.addRootHeading(C.hPlotRoot)
    assert tokens._raw[-1] == "#! Notes: Plot\n\n"
    assert tokens._blocks[-1] == (
        BlockTyp.TITLE, "0000000000009:T0001", "Notes: Plot", [], BlockFmt.CENTRE | BlockFmt.PBB
    )

    # Set Text
    tokens.setText("stuff")
    assert tokens._text == ""

    tokens.setText(C.hSceneDoc)
    assert tokens._text == docText

    tokens.setText(C.hSceneDoc, docText)
    assert tokens._text == docText

    assert tokens._isNovel is True

    # Pre Processing
    tokens.doPreProcessing()
    assert tokens._text == docTextR


@pytest.mark.core
def testFmtToken_StripEscape():
    """Test the stripEscape helper function."""
    text1 = "This is text with escapes: \\** \\~~ \\__"
    text2 = "This is text with escapes: ** ~~ __"
    assert stripEscape(text1) == "This is text with escapes: ** ~~ __"
    assert stripEscape(text2) == "This is text with escapes: ** ~~ __"


@pytest.mark.core
def testFmtToken_HeaderFormat(mockGUI):
    """Test the tokenization of header formats in the Tokenizer class."""
    project = NWProject()
    tokens = BareTokenizer(project)
    tokens._handle = TMH

    # Title
    # =====

    # Story File
    tokens._isNovel = True
    tokens._isFirst = True
    tokens._text = "#! Novel Title\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TITLE, TM1, "Novel Title", [], BlockFmt.CENTRE),
    ]

    # Note File
    tokens._isNovel = False
    tokens._isFirst = True
    tokens._text = "#! Note Title\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TITLE, TM1, "Note Title", [], BlockFmt.CENTRE),
    ]

    # Header 1
    # ========

    # Story File
    tokens._isNovel = True
    tokens._isFirst = True
    tokens._text = "# Novel Title\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "Novel Title", [], BlockFmt.CENTRE),
    ]

    # Note File
    tokens._isNovel = False
    tokens._isFirst = True
    tokens._text = "# Note Title\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "Note Title", [], BlockFmt.NONE),
    ]

    # Header 2
    # ========

    # Story File
    tokens._isNovel = True
    tokens._text = "## Chapter One\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "Chapter One", [], BlockFmt.PBB),
    ]

    # Note File
    tokens._isNovel = False
    tokens._text = "## Heading 2\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD2, TM1, "Heading 2", [], BlockFmt.NONE),
    ]

    # Header 3
    # ========

    # Story File
    tokens._isNovel = True
    tokens._text = "### Scene One\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD2, TM1, "Scene One", [], BlockFmt.NONE),
    ]

    # Note File
    tokens._isNovel = False
    tokens._text = "### Heading 3\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD3, TM1, "Heading 3", [], BlockFmt.NONE),
    ]

    # Header 4
    # ========

    # Story File
    tokens._isNovel = True
    tokens._text = "#### A Section\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD3, TM1, "A Section", [], BlockFmt.NONE),
    ]

    # Note File
    tokens._isNovel = False
    tokens._text = "#### Heading 4\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD4, TM1, "Heading 4", [], BlockFmt.NONE),
    ]

    # Title
    # =====

    # Story File
    tokens._isNovel = True
    tokens._isFirst = False
    tokens._text = "#! Title\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TITLE, TM1, "Title", [], BlockFmt.PBB | BlockFmt.CENTRE),
    ]

    # Note File
    tokens._isNovel = False
    tokens._isFirst = False
    tokens._text = "#! Title\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TITLE, TM1, "Title", [], BlockFmt.PBB | BlockFmt.CENTRE),
    ]

    # Unnumbered
    # ==========

    # Story File
    tokens._isNovel = True
    tokens._text = "##! Prologue\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "Prologue", [], BlockFmt.PBB),
    ]

    # Note File
    tokens._isNovel = False
    tokens._text = "##! Prologue\n"

    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD2, TM1, "Prologue", [], BlockFmt.NONE),
    ]


@pytest.mark.core
def testFmtToken_HeaderStyleNone(mockGUI):
    """Test header styling disabled."""
    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, 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

    # Novel Docs
    tokens._isNovel = True

    # First Document is False
    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.NONE
    assert processStyle("##! Prologue\n", False) == BlockFmt.NONE

    # First Document is True
    assert processStyle("# Title\n", True) == BlockFmt.NONE
    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.NONE
    assert processStyle("##! Prologue\n", True) == BlockFmt.NONE

    # Note Docs
    tokens._isNovel = False

    # First Document is False
    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.NONE
    assert processStyle("##! Prologue\n", False) == BlockFmt.NONE

    # First Document is True
    assert processStyle("# Title\n", True) == BlockFmt.NONE
    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.NONE
    assert processStyle("##! Prologue\n", True) == BlockFmt.NONE


@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

    # Novel Docs
    tokens._isNovel = True

    # First Document is False
    assert processStyle("# Title\n", False) == BlockFmt.CENTRE
    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
    assert processStyle("##! Prologue\n", False) == BlockFmt.CENTRE

    # First Document is True
    assert processStyle("# Title\n", True) == BlockFmt.CENTRE
    assert processStyle("## Chapter\n", True) == BlockFmt.CENTRE
    assert processStyle("### Scene\n", True) == BlockFmt.CENTRE
    assert processStyle("#### Section\n", True) == BlockFmt.NONE
    assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE
    assert processStyle("##! Prologue\n", True) == BlockFmt.CENTRE

    # Note Docs
    tokens._isNovel = False

    # First Document is False
    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
    assert processStyle("##! Prologue\n", False) == BlockFmt.NONE

    # First Document is True
    assert processStyle("# Title\n", True) == BlockFmt.NONE
    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("##! Prologue\n", True) == BlockFmt.NONE


@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

    # Novel Docs
    tokens._isNovel = True

    # First Document is False
    assert processStyle("# Title\n", False) == BlockFmt.PBB
    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.PBB
    assert processStyle("##! Prologue\n", False) == BlockFmt.PBB

    # First Document is True
    assert processStyle("# Title\n", True) == BlockFmt.NONE
    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.NONE
    assert processStyle("##! Prologue\n", True) == BlockFmt.NONE

    # Note Docs
    tokens._isNovel = False

    # First Document is False
    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.PBB
    assert processStyle("##! Prologue\n", False) == BlockFmt.NONE

    # First Document is True
    assert processStyle("# Title\n", True) == BlockFmt.NONE
    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.NONE
    assert processStyle("##! Prologue\n", True) == BlockFmt.NONE


@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

    # Novel Docs
    tokens._isNovel = True

    # First Document is False
    assert processStyle("# Title\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
    assert processStyle("## Chapter\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
    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("##! Prologue\n", False) == BlockFmt.CENTRE | BlockFmt.PBB

    # First Document is True
    assert processStyle("# Title\n", True) == BlockFmt.CENTRE
    assert processStyle("## Chapter\n", True) == BlockFmt.CENTRE
    assert processStyle("### Scene\n", True) == BlockFmt.CENTRE
    assert processStyle("#### Section\n", True) == BlockFmt.NONE
    assert processStyle("#! My Novel\n", True) == BlockFmt.CENTRE
    assert processStyle("##! Prologue\n", True) == BlockFmt.CENTRE

    # Note Docs
    tokens._isNovel = False

    # First Document is False
    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

    # First Document is True
    assert processStyle("# Title\n", True) == BlockFmt.NONE
    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("##! Prologue\n", True) == BlockFmt.NONE


@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

    assert processStyle("# Title\n", False) == BlockFmt.CENTRE | BlockFmt.PBB
    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.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

    assert processStyle("# Title\n", False) == BlockFmt.NONE
    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.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

    assert processStyle("# Title\n", False) == BlockFmt.NONE
    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.NONE
    assert processStyle("##! Prologue\n", False) == BlockFmt.NONE


@pytest.mark.core
def testFmtToken_MetaFormat(mockGUI):
    """Test the tokenization of meta formats in the Tokenizer class."""
    project = NWProject()
    tokens = BareTokenizer(project)
    tokens._handle = TMH

    # Ignore Text
    tokens._text = "%~ Some text\n"
    tokens.tokenizeText()
    assert tokens._blocks == []

    # Comment
    tokens.setCommentType(nwComment.PLAIN, False)
    tokens._text = "% A comment\n"
    tokens.tokenizeText()
    assert tokens._blocks == []

    tokens.setCommentType(nwComment.PLAIN, True)
    tokens._text = "% A comment\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.COMMENT, "", "Comment: A comment", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "comment"),
            (8, TextFmt.COL_E, ""), (8, TextFmt.B_E, ""),
            (9, TextFmt.COL_B, "comment"), (18, TextFmt.COL_E, ""),
        ], BlockFmt.NONE
    )]

    # Synopsis
    tokens.setCommentType(nwComment.SYNOPSIS, False)
    tokens._text = "%synopsis: The synopsis\n"
    tokens.tokenizeText()
    assert tokens._blocks == []

    tokens.setCommentType(nwComment.SYNOPSIS, True)
    tokens._text = "% synopsis: The synopsis\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.SUMMARY, "", "Synopsis: The synopsis", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
            (9, TextFmt.COL_E, ""), (9, TextFmt.B_E, ""),
            (10, TextFmt.COL_B, "note"), (22, TextFmt.COL_E, "")
        ], BlockFmt.NONE
    )]

    # Short
    tokens.setCommentType(nwComment.SHORT, False)
    tokens._text = "% short: A short description\n"
    tokens.tokenizeText()
    assert tokens._blocks == []

    tokens.setCommentType(nwComment.SHORT, True)
    tokens._text = "% short: A short description\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.SUMMARY, "", "Short Description: A short description", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
            (18, TextFmt.COL_E, ""), (18, TextFmt.B_E, ""),
            (19, TextFmt.COL_B, "note"), (38, TextFmt.COL_E, ""),
        ], BlockFmt.NONE
    )]

    # Story Comments
    tokens.setCommentType(nwComment.STORY, False)
    tokens.setCommentType(nwComment.NOTE, False)
    tokens._text = (
        "%Story.Stuff: Stuff happens\n"
        "%Note.More: That stuff that happened\n"
        "%Note.Other: That other stuff that happened\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == []

    tokens.setCommentType(nwComment.STORY, True)
    tokens.setCommentType(nwComment.NOTE, True)
    tokens._text = (
        "%Story.Stuff: Stuff happens\n"
        "%Note.More: That stuff that happened\n"
        "%Note.Other: That other stuff that happened\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.NOTE, "", "Story Structure (Stuff): Stuff happens", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
            (24, TextFmt.COL_E, ""), (24, TextFmt.B_E, ""),
            (25, TextFmt.COL_B, "note"), (38, TextFmt.COL_E, "")
        ], BlockFmt.Z_BTM
    ), (
        BlockTyp.NOTE, "", "Note (More): That stuff that happened", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
            (12, TextFmt.COL_E, ""), (12, TextFmt.B_E, ""),
            (13, TextFmt.COL_B, "note"), (37, TextFmt.COL_E, "")
        ], BlockFmt.Z_TOP | BlockFmt.Z_BTM
    ), (
        BlockTyp.NOTE, "", "Note (Other): That other stuff that happened", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
            (13, TextFmt.COL_E, ""), (13, TextFmt.B_E, ""),
            (14, TextFmt.COL_B, "note"), (44, TextFmt.COL_E, "")
        ], BlockFmt.Z_TOP
    )]

    # Keyword
    tokens.setKeywords(False)
    tokens._text = "@char: Bod\n"
    tokens.tokenizeText()
    assert tokens._blocks == []

    tokens.setKeywords(True)
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.KEYWORD, "char", "Characters: Bod", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "keyword"),
            (11, TextFmt.COL_E, ""), (11, TextFmt.B_E, ""),
            (12, TextFmt.COL_B, "tag"), (12, TextFmt.ARF_B, "#tag_bod"),
            (15, TextFmt.ARF_E, ""), (15, TextFmt.COL_E, ""),
        ], BlockFmt.NONE
    )]

    tokens._text = "@pov: Bod\n@plot: Main\n@location: Europe\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.KEYWORD, "pov", "Point of View: Bod", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "keyword"),
            (14, TextFmt.COL_E, ""), (14, TextFmt.B_E, ""),
            (15, TextFmt.COL_B, "tag"), (15, TextFmt.ARF_B, "#tag_bod"),
            (18, TextFmt.ARF_E, ""), (18, TextFmt.COL_E, ""),
        ], BlockFmt.Z_BTM
    ), (
        BlockTyp.KEYWORD, "plot", "Plot: Main", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "keyword"),
            (5, TextFmt.COL_E, ""), (5, TextFmt.B_E, ""),
            (6, TextFmt.COL_B, "tag"), (6, TextFmt.ARF_B, "#tag_main"),
            (10, TextFmt.ARF_E, ""), (10, TextFmt.COL_E, ""),
        ], BlockFmt.Z_TOP | BlockFmt.Z_BTM
    ), (
        BlockTyp.KEYWORD, "location", "Locations: Europe", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "keyword"),
            (10, TextFmt.COL_E, ""), (10, TextFmt.B_E, ""),
            (11, TextFmt.COL_B, "tag"), (11, TextFmt.ARF_B, "#tag_europe"),
            (17, TextFmt.ARF_E, ""), (17, TextFmt.COL_E, ""),
        ], BlockFmt.Z_TOP
    )]

    # Ignored keywords
    tokens._text = "@pov: Bod\n@plot: Main\n@location: Europe\n"
    tokens.setIgnoredKeywords("@plot, @location")
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.KEYWORD, "pov", "Point of View: Bod", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "keyword"),
            (14, TextFmt.COL_E, ""), (14, TextFmt.B_E, ""),
            (15, TextFmt.COL_B, "tag"), (15, TextFmt.ARF_B, "#tag_bod"),
            (18, TextFmt.ARF_E, ""), (18, TextFmt.COL_E, ""),
        ], BlockFmt.NONE
    )]


@pytest.mark.core
def testFmtToken_MarginFormat(mockGUI):
    """Test the tokenization of margin formats in the Tokenizer class."""
    project = NWProject()
    tokens = BareTokenizer(project)

    # Alignment and Indentation
    dblIndent = BlockFmt.IND_L | BlockFmt.IND_R
    rIndAlign = BlockFmt.RIGHT | BlockFmt.IND_R
    tokens._text = (
        "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"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TEXT, "", "Some regular text", [], BlockFmt.NONE),
        (BlockTyp.TEXT, "", "Some left-aligned text", [], BlockFmt.LEFT),
        (BlockTyp.TEXT, "", "Some right-aligned text", [], BlockFmt.RIGHT),
        (BlockTyp.TEXT, "", "Some centered text", [], BlockFmt.CENTRE),
        (BlockTyp.TEXT, "", "Left-indented block", [], BlockFmt.IND_L),
        (BlockTyp.TEXT, "", "Right-indented block", [], BlockFmt.IND_R),
        (BlockTyp.TEXT, "", "Double-indented block", [], dblIndent),
        (BlockTyp.TEXT, "", "Right-indent, right-aligned", [], rIndAlign),
    ]


@pytest.mark.core
def testFmtToken_ExtractFormats(mockGUI):
    """Test the extraction of formats in the Tokenizer class."""
    project = NWProject()
    tokens = BareTokenizer(project)

    # Markdown
    # ========

    # Plain bold
    text, fmt = tokens._extractFormats("Text with **bold** in it.")
    assert text == "Text with bold in it."
    assert fmt == [(10, TextFmt.B_B, ""), (14, TextFmt.B_E, "")]

    # Plain italics
    text, fmt = tokens._extractFormats("Text with _italics_ in it.")
    assert text == "Text with italics in it."
    assert fmt == [(10, TextFmt.I_B, ""), (17, TextFmt.I_E, "")]

    # Plain strikethrough
    text, fmt = tokens._extractFormats("Text with ~~strikethrough~~ in it.")
    assert text == "Text with strikethrough in it."
    assert fmt == [(10, TextFmt.D_B, ""), (23, TextFmt.D_E, "")]

    # Plain mark
    text, fmt = tokens._extractFormats("Text with ==a highlight== in it.")
    assert text == "Text with a highlight in it."
    assert fmt == [(10, TextFmt.M_B, ""), (21, TextFmt.M_E, "")]

    # Nested bold/italics
    text, fmt = tokens._extractFormats("Text with **bold and _italics_** in it.")
    assert text == "Text with bold and italics in it."
    assert fmt == [
        (10, TextFmt.B_B, ""), (19, TextFmt.I_B, ""),
        (26, TextFmt.I_E, ""), (26, TextFmt.B_E, ""),
    ]

    # Bold with overlapping italics
    # Here, bold is ignored because it is not on word boundary
    text, fmt = tokens._extractFormats("Text with **bold and overlapping _italics**_ in it.")
    assert text == "Text with **bold and overlapping italics** in it."
    assert fmt == [(33, TextFmt.I_B, ""), (42, TextFmt.I_E, "")]

    # Shortcodes
    # ==========

    # Plain bold
    text, fmt = tokens._extractFormats("Text with [b]bold[/b] in it.")
    assert text == "Text with bold in it."
    assert fmt == [(10, TextFmt.B_B, ""), (14, TextFmt.B_E, "")]

    # Plain italics
    text, fmt = tokens._extractFormats("Text with [i]italics[/i] in it.")
    assert text == "Text with italics in it."
    assert fmt == [(10, TextFmt.I_B, ""), (17, TextFmt.I_E, "")]

    # Plain strikethrough
    text, fmt = tokens._extractFormats("Text with [s]strikethrough[/s] in it.")
    assert text == "Text with strikethrough in it."
    assert fmt == [(10, TextFmt.D_B, ""), (23, TextFmt.D_E, "")]

    # Plain underline
    text, fmt = tokens._extractFormats("Text with [u]underline[/u] in it.")
    assert text == "Text with underline in it."
    assert fmt == [(10, TextFmt.U_B, ""), (19, TextFmt.U_E, "")]

    # Plain mark
    text, fmt = tokens._extractFormats("Text with [m]highlight[/m] in it.")
    assert text == "Text with highlight in it."
    assert fmt == [(10, TextFmt.M_B, ""), (19, TextFmt.M_E, "")]

    # Plain superscript
    text, fmt = tokens._extractFormats("Text with super[sup]script[/sup] in it.")
    assert text == "Text with superscript in it."
    assert fmt == [(15, TextFmt.SUP_B, ""), (21, TextFmt.SUP_E, "")]

    # Plain subscript
    text, fmt = tokens._extractFormats("Text with sub[sub]script[/sub] in it.")
    assert text == "Text with subscript in it."
    assert fmt == [(13, TextFmt.SUB_B, ""), (19, TextFmt.SUB_E, "")]

    # Nested bold/italics
    text, fmt = tokens._extractFormats("Text with [b]bold and [i]italics[/i][/b] in it.")
    assert text == "Text with bold and italics in it."
    assert fmt == [
        (10, TextFmt.B_B, ""), (19, TextFmt.I_B, ""),
        (26, TextFmt.I_E, ""), (26, TextFmt.B_E, ""),
    ]

    # Bold with overlapping italics
    # With shortcodes, this works
    text, fmt = tokens._extractFormats(
        "Text with [b]bold and overlapping [i]italics[/b][/i] in it."
    )
    assert text == "Text with bold and overlapping italics in it."
    assert fmt == [
        (10, TextFmt.B_B, ""), (31, TextFmt.I_B, ""),
        (38, TextFmt.B_E, ""), (38, TextFmt.I_E, ""),
    ]

    # So does this
    text, fmt = tokens._extractFormats(
        "Text with [b]bold and overlapping [i]italics[/b] in[/i] it."
    )
    assert text == "Text with bold and overlapping italics in it."
    assert fmt == [
        (10, TextFmt.B_B, ""), (31, TextFmt.I_B, ""),
        (38, TextFmt.B_E, ""), (41, TextFmt.I_E, ""),
    ]


@pytest.mark.core
def testFmtToken_Paragraphs(mockGUI):
    """Test the splitting of paragraphs."""
    project = NWProject()
    tokens = BareTokenizer(project)
    tokens._handle = TMH

    # Collapse empty lines
    tokens._text = "First paragraph\n\n\nSecond paragraph\n\n\n"
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TEXT, "", "First paragraph", [], BlockFmt.NONE),
        (BlockTyp.TEXT, "", "Second paragraph", [], BlockFmt.NONE),
    ]

    # Combine multi-line paragraphs, keep breaks
    tokens._text = "This is text\nspanning multiple\nlines"
    tokens.setKeepLineBreaks(True)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.NONE),
    ]

    # Combine multi-line paragraphs, remove breaks
    tokens._text = "This is text\nspanning multiple\nlines"
    tokens.setKeepLineBreaks(False)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TEXT, "", "This is text spanning multiple lines", [], BlockFmt.NONE),
    ]

    # Combine multi-line paragraphs, remove breaks, with formatting
    tokens._text = "This **is text**\nspanning _multiple_\nlines"
    tokens.setKeepLineBreaks(False)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (
            BlockTyp.TEXT, "",
            "This is text spanning multiple lines",
            [
                (5,  TextFmt.B_B, ""),
                (12, TextFmt.B_E, ""),
                (22, TextFmt.I_B, ""),
                (30, TextFmt.I_E, ""),
            ],
            BlockFmt.NONE
        ),
    ]

    # Make sure titles break a paragraph
    tokens._text = "# Title\nText _on_\ntwo lines.\n## Chapter\nMore **text**\n_here_.\n\n\n"
    tokens.setKeepLineBreaks(False)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "Title", [], BlockFmt.NONE),
        (
            BlockTyp.TEXT, "", "Text on two lines.", [
                (5, TextFmt.I_B, ""),
                (7, TextFmt.I_E, ""),
            ], BlockFmt.NONE
        ),
        (BlockTyp.HEAD2, TM2, "Chapter", [], BlockFmt.NONE),
        (
            BlockTyp.TEXT, "", "More text here.", [
                (5,  TextFmt.B_B, ""),
                (9,  TextFmt.B_E, ""),
                (10, TextFmt.I_B, ""),
                (14, TextFmt.I_E, ""),
            ], BlockFmt.NONE
        ),
    ]


@pytest.mark.core
def testFmtToken_BreakAlignIndent(mockGUI):
    """Test the splitting of paragraphs with alignment."""
    project = NWProject()
    tokens = BareTokenizer(project)
    tokens._handle = TMH

    for text in [
        "This is text <<\nspanning multiple\nlines",
        "This is text\nspanning multiple <<\nlines",
        "This is text\nspanning multiple\nlines <<",
    ]:
        # Preserve Breaks
        tokens.setKeepLineBreaks(True)
        tokens._text = text
        tokens.tokenizeText()
        assert tokens._blocks == [
            (BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.LEFT),
        ]

        # Don't Preserve Breaks
        tokens.setKeepLineBreaks(False)
        tokens._text = text
        tokens.tokenizeText()
        assert tokens._blocks == [
            (BlockTyp.TEXT, "", "This is text spanning multiple lines", [], BlockFmt.LEFT),
        ]

        # With Justify
        # This should disable justify
        tokens.setKeepLineBreaks(True)
        tokens.setJustify(True)
        tokens._text = text
        tokens.tokenizeText()
        assert tokens._blocks == [
            (BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.LEFT),
        ]

        # With Indent
        # This should disable indent
        tokens.setKeepLineBreaks(True)
        tokens.setFirstLineIndent(True, 1.0, False)
        tokens._text = text
        tokens.tokenizeText()
        assert tokens._blocks == [
            (BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.LEFT),
        ]


@pytest.mark.core
def testFmtToken_BreakJustify(mockGUI):
    """Test the of processing of justify with breaks."""
    project = NWProject()
    tokens = BareTokenizer(project)
    tokens._handle = TMH
    tokens.setJustify(True)

    # Applied to all lines when breaks are preserved
    tokens._text = "This is text\nspanning multiple\nlines"
    tokens.setKeepLineBreaks(True)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.JUSTIFY),
    ]

    # Turning off breaks should make no difference (see issue #2426)
    tokens._text = "This is text\nspanning multiple\nlines"
    tokens.setKeepLineBreaks(False)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TEXT, "", "This is text spanning multiple lines", [], BlockFmt.JUSTIFY),
    ]


@pytest.mark.core
def testFmtToken_TextFormat(mockGUI):
    """Test the tokenization of text formats in the Tokenizer class."""
    project = NWProject()
    tokens = BareTokenizer(project)
    tokens._handle = TMH

    # Text
    tokens._text = "Some plain text\non two lines\n\n\n"
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TEXT, "", "Some plain text\non two lines", [], BlockFmt.NONE),
    ]

    tokens.setBodyText(False)
    tokens.tokenizeText()
    assert tokens._blocks == []
    tokens.setBodyText(True)

    # Text Emphasis
    tokens._text = "Some **bolded text** on this lines\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "", "Some bolded text on this lines", [
            (5,  TextFmt.B_B, ""),
            (16, TextFmt.B_E, ""),
        ], BlockFmt.NONE
    )]

    tokens._text = "Some _italic text_ on this lines\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "", "Some italic text on this lines", [
            (5,  TextFmt.I_B, ""),
            (16, TextFmt.I_E, ""),
        ], BlockFmt.NONE
    )]

    tokens._text = "Some **_bold italic text_** on this lines\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        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
    )]

    tokens._text = "Some ~~strikethrough text~~ on this lines\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "", "Some strikethrough text on this lines", [
            (5,  TextFmt.D_B, ""),
            (23, TextFmt.D_E, ""),
        ], BlockFmt.NONE
    )]

    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", [
            (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
    )]


@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.setCommentType(nwComment.PLAIN, 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_ShortcodeValue(mockGUI):
    """Test processing of shortcodes with values."""
    project = NWProject()
    tokens = BareTokenizer(project)
    tokens._handle = TMH

    # Footnote
    tokens._text = "Hello World[footnote:abcd] to you!"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "", "Hello World to you!", [
            (11, TextFmt.FNOTE, f"{TMH}:abcd"),
        ], BlockFmt.NONE
    )]

    # Field
    tokens._text = "Hello World: [field:abcd] times!"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "", "Hello World:  times!", [
            (13, TextFmt.FIELD, f"{TMH}:abcd"),
        ], BlockFmt.NONE
    )]


@pytest.mark.core
def testFmtToken_Dialogue(mockGUI):
    """Test the tokenization of dialogue in the Tokenizer class."""
    CONFIG.fmtDQuoteOpen  = "\u201c"
    CONFIG.fmtDQuoteClose = "\u201d"
    CONFIG.fmtSQuoteOpen  = "\u2018"
    CONFIG.fmtSQuoteClose = "\u2019"
    CONFIG.dialogStyle    = 3
    CONFIG.altDialogOpen  = "::"
    CONFIG.altDialogClose = "::"

    project = NWProject()
    tokens = BareTokenizer(project)
    tokens.setDialogHighlight(True)
    tokens._handle = TMH
    tokens._isNovel = True

    # Single quotes
    tokens._text = "Text with \u2018dialogue one,\u2019 and \u2018dialogue two.\u2019\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "",
        "Text with \u2018dialogue one,\u2019 and \u2018dialogue two.\u2019",
        [
            (10, TextFmt.COL_B, "dialog"),
            (25, TextFmt.COL_E, ""),
            (30, TextFmt.COL_B, "dialog"),
            (45, TextFmt.COL_E, ""),
        ],
        BlockFmt.NONE
    )]

    # Double quotes
    tokens._text = "Text with \u201cdialogue one,\u201d and \u201cdialogue two.\u201d\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "",
        "Text with \u201cdialogue one,\u201d and \u201cdialogue two.\u201d",
        [
            (10, TextFmt.COL_B, "dialog"),
            (25, TextFmt.COL_E, ""),
            (30, TextFmt.COL_B, "dialog"),
            (45, TextFmt.COL_E, ""),
        ],
        BlockFmt.NONE
    )]

    # Alt quotes
    tokens._text = "Text with ::dialogue one,:: and ::dialogue two.::\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "",
        "Text with ::dialogue one,:: and ::dialogue two.::",
        [
            (10, TextFmt.COL_B, "altdialog"),
            (27, TextFmt.COL_E, ""),
            (32, TextFmt.COL_B, "altdialog"),
            (49, TextFmt.COL_E, ""),
        ],
        BlockFmt.NONE
    )]

    # Dialogue line
    CONFIG.dialogLine = "\u2013"
    tokens = BareTokenizer(project)
    tokens.setDialogHighlight(True)
    tokens._handle = TMH
    tokens._isNovel = True
    tokens._text = "\u2013 Dialogue line without narrator break.\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "",
        "\u2013 Dialogue line without narrator break.",
        [
            (0,  TextFmt.COL_B, "dialog"),
            (39, TextFmt.COL_E, ""),
        ],
        BlockFmt.NONE
    )]

    # Dialogue line with narrator break
    CONFIG.narratorBreak = "\u2013"
    tokens = BareTokenizer(project)
    tokens.setDialogHighlight(True)
    tokens._handle = TMH
    tokens._isNovel = True
    tokens._text = "\u2013 Dialogue with a narrator break, \u2013he said\u2013, see?\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "",
        "\u2013 Dialogue with a narrator break, \u2013he said\u2013, see?",
        [
            (0,  TextFmt.COL_B, "dialog"),
            (34, TextFmt.COL_E, ""),
            (44, TextFmt.COL_B, "dialog"),
            (49, TextFmt.COL_E, ""),
        ],
        BlockFmt.NONE
    )]

    # Special Cases
    # =============

    # Dialogue + formatting on same index (Issue #2012)
    tokens._text = "[i]\u201cDialogue text.\u201d[/i]\n"
    tokens.tokenizeText()
    assert tokens._blocks == [(
        BlockTyp.TEXT, "",
        "\u201cDialogue text.\u201d",
        [
            (0,  TextFmt.I_B, ""),
            (0,  TextFmt.COL_B, "dialog"),
            (16, TextFmt.I_E, ""),
            (16, TextFmt.COL_E, ""),
        ],
        BlockFmt.NONE
    )]


@pytest.mark.core
def testFmtToken_SpecialFormat(mockGUI):
    """Test the tokenization of special formats in the Tokenizer class."""
    project = NWProject()
    tokens = BareTokenizer(project)
    tokens._handle = TMH
    tokens._isNovel = True

    # New Page
    # ========

    correctResp = [
        (BlockTyp.PART, TM1, "Title One", [], BlockFmt.CENTRE),
        (BlockTyp.PART, TM2, "Title Two", [], BlockFmt.CENTRE | BlockFmt.PBB),
    ]

    # Command wo/Space
    tokens._isFirst = True
    tokens._text = (
        "# Title One\n\n"
        "[newpage]\n\n"
        "# Title Two\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == correctResp

    # Command w/Space
    tokens._isFirst = True
    tokens._text = (
        "# Title One\n\n"
        "[new page]\n\n"
        "# Title Two\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == correctResp

    # Trailing Spaces
    tokens._isFirst = True
    tokens._text = (
        "# Title One\n\n"
        "[new page]   \t\n\n"
        "# Title Two\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == correctResp

    # Single Empty Paragraph
    # ======================

    tokens._text = (
        "# Title One\n\n"
        "[vspace] \n\n"
        "Some text to go here ...\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "Title One", [], BlockFmt.PBB | BlockFmt.CENTRE),
        (BlockTyp.SKIP, "",  "", [], BlockFmt.NONE),
        (BlockTyp.TEXT, "",  "Some text to go here ...", [], BlockFmt.NONE),
    ]

    # Multiple Empty Paragraphs
    # =========================

    # One Skip
    tokens._text = (
        "# Title One\n\n"
        "[vspace:1] \n\n"
        "Some text to go here ...\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "Title One", [], BlockFmt.PBB | BlockFmt.CENTRE),
        (BlockTyp.SKIP, "",  "", [], BlockFmt.NONE),
        (BlockTyp.TEXT, "",  "Some text to go here ...", [], BlockFmt.NONE),
    ]

    # Three Skips
    tokens._text = (
        "# Title One\n\n"
        "[vspace:3] \n\n"
        "Some text to go here ...\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "Title One", [], BlockFmt.PBB | BlockFmt.CENTRE),
        (BlockTyp.SKIP, "",  "", [], BlockFmt.NONE),
        (BlockTyp.SKIP, "",  "", [], BlockFmt.NONE),
        (BlockTyp.SKIP, "",  "", [], BlockFmt.NONE),
        (BlockTyp.TEXT, "",  "Some text to go here ...", [], BlockFmt.NONE),
    ]

    # Malformed Command, Case 1
    tokens._text = (
        "# Title One\n\n"
        "[vspace:3xa] \n\n"
        "Some text to go here ...\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "Title One", [], BlockFmt.PBB | BlockFmt.CENTRE),
        (BlockTyp.TEXT, "",  "Some text to go here ...", [], BlockFmt.NONE),
    ]

    # Malformed Command, Case 2
    tokens._text = (
        "# Title One\n\n"
        "[vspace:3.5]\n\n"
        "Some text to go here ...\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "Title One", [], BlockFmt.PBB | BlockFmt.CENTRE),
        (BlockTyp.TEXT, "",  "Some text to go here ...", [], BlockFmt.NONE),
    ]

    # Malformed Command, Case 3
    tokens._text = (
        "# Title One\n\n"
        "[vspace:-1]\n\n"
        "Some text to go here ...\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "Title One", [], BlockFmt.PBB | BlockFmt.CENTRE),
        (BlockTyp.TEXT, "",  "Some text to go here ...", [], BlockFmt.NONE),
    ]

    # Empty Paragraph and Page Break
    # ==============================

    # Single Skip
    tokens._text = (
        "# Title One\n\n"
        "[new page]\n\n"
        "[vspace]\n\n"
        "Some text to go here ...\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "Title One", [], BlockFmt.PBB | BlockFmt.CENTRE),
        (BlockTyp.SKIP, "",  "", [], BlockFmt.PBB),
        (BlockTyp.TEXT, "",  "Some text to go here ...", [], BlockFmt.NONE),
    ]

    # Multiple Skip
    tokens._text = (
        "# Title One\n\n"
        "[new page]\n\n"
        "[vspace:3]\n\n"
        "Some text to go here ...\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "Title One", [], BlockFmt.PBB | BlockFmt.CENTRE),
        (BlockTyp.SKIP, "",  "", [], BlockFmt.PBB),
        (BlockTyp.SKIP, "",  "", [], BlockFmt.NONE),
        (BlockTyp.SKIP, "",  "", [], BlockFmt.NONE),
        (BlockTyp.TEXT, "",  "Some text to go here ...", [], BlockFmt.NONE),
    ]


@pytest.mark.core
def testFmtToken_TextIndent(mockGUI):
    """Test the handling of text indent in the Tokenizer class."""
    project = NWProject()
    tokens = BareTokenizer(project)
    tokens.setCommentType(nwComment.SYNOPSIS, True)
    tokens._handle = TMH

    # No First Indent
    tokens.setFirstLineIndent(True, 1.0, False)

    assert tokens._noIndent is False
    assert tokens._firstIndent is True
    assert tokens._firstWidth == 1.0
    assert tokens._indentFirst is False

    # Page One
    # Two paragraphs in the same scene
    tokens._text = (
        "# Title One\n\n"
        "### Scene One\n\n"
        "First paragraph.\n\n"
        "Second paragraph.\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "Title One", [], BlockFmt.NONE),
        (BlockTyp.HEAD3, TM2, "Scene One", [], BlockFmt.NONE),
        (BlockTyp.TEXT,  "",  "First paragraph.", [], BlockFmt.NONE),
        (BlockTyp.TEXT,  "",  "Second paragraph.", [], BlockFmt.IND_T),
    ]
    assert tokens._noIndent is False

    # Page Two
    # New scene with only a synopsis
    tokens._text = (
        "### Scene Two\n\n"
        "%Synopsis: Stuff happens.\n\n"
    )
    tokens.tokenizeText()
    tFmt = [
        (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"), (9, TextFmt.COL_E, ""),
        (9, TextFmt.B_E, ""), (10, TextFmt.COL_B, "note"), (24, TextFmt.COL_E, ""),
    ]
    assert tokens._blocks == [
        (BlockTyp.HEAD3,   TM1, "Scene Two", [], BlockFmt.NONE),
        (BlockTyp.SUMMARY, "",  "Synopsis: Stuff happens.", tFmt, BlockFmt.NONE),
    ]
    assert tokens._noIndent is True

    # Page Three
    # Two paragraphs for the scene on the previous page
    tokens._text = (
        "First paragraph.\n\n"
        "Second paragraph.\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.TEXT, "", "First paragraph.", [], BlockFmt.NONE),
        (BlockTyp.TEXT, "", "Second paragraph.", [], BlockFmt.IND_T),
    ]
    assert tokens._noIndent is False

    # First Indent
    tokens.setFirstLineIndent(True, 1.0, True)

    assert tokens._noIndent is False
    assert tokens._firstIndent is True
    assert tokens._firstWidth == 1.0
    assert tokens._indentFirst is True

    # Page Four
    # Two paragraphs in the same scene
    tokens._text = (
        "# Title One\n\n"
        "### Scene One\n\n"
        "First paragraph.\n\n"
        "Second paragraph.\n\n"
    )
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "Title One", [], BlockFmt.NONE),
        (BlockTyp.HEAD3, TM2, "Scene One", [], BlockFmt.NONE),
        (BlockTyp.TEXT,  "",  "First paragraph.", [], BlockFmt.IND_T),
        (BlockTyp.TEXT,  "",  "Second paragraph.", [], BlockFmt.IND_T),
    ]
    assert tokens._noIndent is False


@pytest.mark.core
def testFmtToken_ProcessHeaders(mockGUI):
    """Test the header and page parser of the Tokenizer class."""
    project = NWProject()
    project.data.setLanguage("en")
    project._loadProjectLocalisation()
    tokens = BareTokenizer(project)
    tokens._isNovel = True
    tokens._handle = TMH

    # Titles
    # ======

    # H1: Title, First Page
    assert tokens._isFirst is True
    tokens._text = "# Part One\n"
    tokens.setPartitionFormat(f"T: {nwHeadFmt.TITLE}")
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "T: Part One", [], BlockFmt.CENTRE),
    ]

    # H1: Title, Not First Page
    assert tokens._isFirst is False
    tokens._text = "# Part One\n"
    tokens.setPartitionFormat(f"T: {nwHeadFmt.TITLE}")
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.PART, TM1, "T: Part One", [], BlockFmt.PBB | BlockFmt.CENTRE),
    ]

    # Chapters
    # ========

    # H2: Chapter
    tokens._text = "## Chapter One\n"
    tokens.setChapterFormat(f"C: {nwHeadFmt.TITLE}")
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "C: Chapter One", [], BlockFmt.PBB),
    ]

    # H2: Unnumbered Chapter
    tokens._text = "##! Prologue\n"
    tokens.setUnNumberedFormat(f"U: {nwHeadFmt.TITLE}")
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "U: Prologue", [], BlockFmt.PBB),
    ]

    # H2: Chapter Word Number
    tokens._text = "## Chapter\n"
    tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_WORD}")
    tokens._hFormatter._chapter = 0
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "Chapter One", [], BlockFmt.PBB),
    ]

    # H2: Chapter Roman Number Upper Case
    tokens._text = "## Chapter\n"
    tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_ROMU}")
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "Chapter II", [], BlockFmt.PBB),
    ]

    # H2: Chapter Roman Number Lower Case
    tokens._text = "## Chapter\n"
    tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_ROML}")
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD1, TM1, "Chapter iii", [], BlockFmt.PBB),
    ]

    # Scenes
    # ======

    # H3: Scene w/Title
    tokens._text = "### Scene One\n"
    tokens.setSceneFormat(f"S: {nwHeadFmt.TITLE}", False)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD2, TM1, "S: Scene One", [], BlockFmt.NONE),
    ]

    # H3: Scene Hidden wo/Format
    tokens._text = "### Scene One\n"
    tokens.setSceneFormat("", True)
    tokens.tokenizeText()
    assert tokens._blocks == []

    # H3: Scene wo/Format, first
    tokens._text = "### Scene One\n"
    tokens.setSceneFormat("", False)
    tokens._noSep = True
    tokens.tokenizeText()
    assert tokens._blocks == []

    # H3: Scene wo/Format, not first
    tokens._text = "### Scene One\n"
    tokens.setSceneFormat("", False)
    tokens._noSep = False
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.SKIP, TM1, "", [], BlockFmt.NONE),
    ]

    # H3: Scene Separator, first
    tokens._text = "### Scene One\n"
    tokens.setSceneFormat("* * *", False)
    tokens._noSep = True
    tokens.tokenizeText()
    assert tokens._blocks == []

    # H3: Scene Separator, not first
    tokens._text = "### Scene One\n"
    tokens.setSceneFormat("* * *", False)
    tokens._noSep = False
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.SEP, TM1, "* * *", [], BlockFmt.CENTRE),
    ]

    # H3: Scene w/Absolute Number
    tokens._text = "### A Scene\n"
    tokens.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}", False)
    tokens._hFormatter._scene = 0
    tokens._hFormatter._absolute = 0
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD2, TM1, "Scene 1", [], BlockFmt.NONE),
    ]

    # H3: Scene w/Chapter Number
    tokens._text = "### A Scene\n"
    tokens.setSceneFormat(f"Scene {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM}", False)
    tokens._hFormatter._scene = 1
    tokens._hFormatter._absolute = 0
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD2, TM1, "Scene 3.2", [], BlockFmt.NONE),
    ]

    # Sections
    # ========

    # H4: Section Hidden wo/Format
    tokens._text = "#### A Section\n"
    tokens.setSectionFormat("", True)
    tokens.tokenizeText()
    assert tokens._blocks == []

    # H4: Section Visible wo/Format
    tokens._text = "#### A Section\n"
    tokens.setSectionFormat("", False)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.SKIP, TM1, "", [], BlockFmt.NONE),
    ]

    # H4: Section w/Format
    tokens._text = "#### A Section\n"
    tokens.setSectionFormat(f"X: {nwHeadFmt.TITLE}", False)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.HEAD3, TM1, "X: A Section", [], BlockFmt.NONE),
    ]

    # H4: Section Separator
    tokens._text = "#### A Section\n"
    tokens.setSectionFormat("* * *", False)
    tokens.tokenizeText()
    assert tokens._blocks == [
        (BlockTyp.SEP, TM1, "* * *", [], BlockFmt.CENTRE),
    ]

    # Check the first scene detector, plain text
    tokens._noSep = True
    tokens._text = "Some text ...\n"
    tokens.tokenizeText()
    assert tokens._noSep is True

    # Check the first scene detector, text plus scene
    tokens._noSep = True
    tokens._text = "Some text ...\n\n### Scene\n\nText"
    tokens.tokenizeText()
    assert tokens._noSep is False


@pytest.mark.core
def testFmtToken_FormatComment(mockGUI):
    """Test note and comment formatting."""
    project = NWProject()
    project.data.setLanguage("en")
    project._loadProjectLocalisation()
    tokens = BareTokenizer(project)

    # Comment, No Formatting
    style = COMMENT_STYLE[nwComment.PLAIN]
    assert tokens._formatComment(style, "", "Hello world!") == (
        "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"), (21, TextFmt.COL_E, ""),
        ]
    )

    # Synopsis, No Formatting
    style = COMMENT_STYLE[nwComment.SYNOPSIS]
    assert tokens._formatComment(style, "", "Hello world!") == (
        "Synopsis: Hello world!", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
            (9, TextFmt.COL_E, ""), (9, TextFmt.B_E, ""),
            (10, TextFmt.COL_B, "note"), (22, TextFmt.COL_E, ""),
        ]
    )


@pytest.mark.core
def testFmtToken_FormatMeta(mockGUI):
    """Test meta formatting."""
    project = NWProject()
    project.data.setLanguage("en")
    project._loadProjectLocalisation()
    tokens = BareTokenizer(project)

    assert tokens._formatMeta("@tag: Jane | Jane Smith") == (
        "@tag", "Tag: Jane | Jane Smith", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "keyword"),
            (4, TextFmt.COL_E, ""), (4, TextFmt.B_E, ""),
            (5, TextFmt.COL_B, "tag"), (5, TextFmt.ANM_B, "tag_jane"),
            (9, TextFmt.ANM_E, ""), (9, TextFmt.COL_E, ""),
            (12, TextFmt.COL_B, "optional"), (22, TextFmt.COL_E, ""),
        ]
    )

    assert tokens._formatMeta("@char: Jane, John") == (
        "@char", "Characters: Jane, John", [
            (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "keyword"),
            (11, TextFmt.COL_E, ""), (11, TextFmt.B_E, ""),
            (12, TextFmt.COL_B, "tag"), (12, TextFmt.ARF_B, "#tag_jane"),
            (16, TextFmt.ARF_E, ""), (16, TextFmt.COL_E, ""),
            (18, TextFmt.COL_B, "tag"), (18, TextFmt.ARF_B, "#tag_john"),
            (22, TextFmt.ARF_E, ""), (22, TextFmt.COL_E, ""),
        ]
    )


@pytest.mark.core
def testFmtToken_BuildOutline(mockGUI, ipsumText):
    """Test stats counter of the Tokenizer class."""
    project = NWProject()
    project.data.setLanguage("en")
    project._loadProjectLocalisation()
    tokens = BareTokenizer(project)

    # Novel
    tokens._isNovel = True
    tokens._handle = "0000000000000"
    tokens._text = (
        "#! My Novel\n\n"
        "# Part One\n\n"
        "## Chapter One\n\n"
        "### Scene One\n\n"
        "Text\n\n"
        "### Scene Two\n\n"
        "Text\n\n"
        "## Chapter Two\n\n"
        "### Scene Three\n\n"
        "Text\n\n"
        "###! Scene Four\n\n"
        "Text\n\n"
        "#### Section\n\n"
        "Text\n\n"
    )
    tokens.tokenizeText()
    tokens.buildOutline()

    # Note
    tokens._isNovel = False
    tokens._handle = "0000000000001"
    tokens._text = (
        "#! My Notes\n\n"
        "# Header 1\n\n"
        "Text\n\n"
        "## Header 2\n\n"
        "Text\n\n"
        "### Header 3\n\n"
        "Text\n\n"
        "#### Header 4\n\n"
        "Text\n\n"
    )
    tokens.tokenizeText()
    tokens.buildOutline()

    # Check Outline
    assert tokens.textOutline == {
        "0000000000000:T0001": "TT|My Novel",
        "0000000000000:T0002": "PT|Part One",
        "0000000000000:T0003": "CH|Chapter One",
        "0000000000000:T0004": "SC|Scene One",
        "0000000000000:T0005": "SC|Scene Two",
        "0000000000000:T0006": "CH|Chapter Two",
        "0000000000000:T0007": "SC|Scene Three",
        "0000000000000:T0008": "SC|Scene Four",
        "0000000000001:T0001": "TT|My Notes",
        "0000000000001:T0002": "H1|Header 1",
        "0000000000001:T0003": "H2|Header 2",
        "0000000000001:T0004": "H3|Header 3",
    }


@pytest.mark.core
def testFmtToken_CountStats(mockGUI, ipsumText):
    """Test stats counter of the Tokenizer class."""
    project = NWProject()
    project.data.setLanguage("en")
    project._loadProjectLocalisation()
    tokens = BareTokenizer(project)
    tokens._isNovel = True

    # Short Text
    # ==========

    # Header wo/Format
    tokens._text = "## A Chapter Title\n\n"
    tokens._counts = {}
    tokens.tokenizeText()
    tokens.countStats()
    assert tokens._blocks[0][2] == "A Chapter Title"
    assert tokens.textStats == {
        "titleCount": 1, "paragraphCount": 0,
        "allWords": 3, "textWords": 0, "titleWords": 3,
        "allChars": 15, "textChars": 0, "titleChars": 15,
        "allWordChars": 13, "textWordChars": 0, "titleWordChars": 13
    }

    # Header w/Format
    tokens._text = "## A Chapter Title\n\n"
    tokens._counts = {}
    tokens.setChapterFormat(f"C {nwHeadFmt.CH_NUM}: {nwHeadFmt.TITLE}")
    tokens._hFormatter.resetAll()
    tokens.tokenizeText()
    tokens.countStats()
    assert tokens._blocks[0][2] == "C 1: A Chapter Title"
    assert tokens.textStats == {
        "titleCount": 1, "paragraphCount": 0,
        "allWords": 5, "textWords": 0, "titleWords": 5,
        "allChars": 20, "textChars": 0, "titleChars": 20,
        "allWordChars": 16, "textWordChars": 0, "titleWordChars": 16
    }

    # Two Paragraphs
    # First break should be counted, the double breaks not.
    tokens._text = "Some text\non two lines.\n\nWith a second paragraph.\n\n"
    tokens._counts = {}
    tokens.tokenizeText()
    tokens.countStats()
    assert tokens.textStats == {
        "titleCount": 0, "paragraphCount": 2,
        "allWords": 9, "textWords": 9, "titleWords": 0,
        "allChars": 47, "textChars": 47, "titleChars": 0,
        "allWordChars": 40, "textWordChars": 40, "titleWordChars": 0
    }

    # Two Scenes w/Separator
    tokens._text = "## Chapter\n\n### Scene\n\nText\n\n### Scene\n\nText"
    tokens._counts = {}
    tokens.setChapterFormat(nwHeadFmt.TITLE)
    tokens.setSceneFormat("* * *", False)
    tokens.tokenizeText()
    tokens.countStats()
    assert [t[2] for t in tokens._blocks] == ["Chapter", "Text", "* * *", "Text"]
    assert tokens.textStats == {
        "titleCount": 1, "paragraphCount": 2,
        "allWords": 6, "textWords": 2, "titleWords": 1,
        "allChars": 20, "textChars": 8, "titleChars": 7,
        "allWordChars": 18, "textWordChars": 8, "titleWordChars": 7
    }

    # Scene w/Synopsis
    # Synopsis does not count as a paragraph, and counts as "Synopsis: Stuff"
    tokens._text = "## Chapter\n\n### Scene\n\n%Synopsis: Stuff\n\nText"
    tokens._counts = {}
    tokens.setChapterFormat(nwHeadFmt.TITLE)
    tokens.setSceneFormat("* * *", False)
    tokens.setCommentType(nwComment.SYNOPSIS, True)
    tokens.tokenizeText()
    tokens.countStats()
    assert [t[2] for t in tokens._blocks] == ["Chapter", "Synopsis: Stuff", "Text"]
    assert tokens.textStats == {
        "titleCount": 1, "paragraphCount": 1,
        "allWords": 4, "textWords": 1, "titleWords": 1,
        "allChars": 26, "textChars": 4, "titleChars": 7,
        "allWordChars": 25, "textWordChars": 4, "titleWordChars": 7
    }

    # Scene w/Short
    # Short does not count as a paragraph, and counts as "Short Description: Stuff"
    tokens._text = "## Chapter\n\n### Scene\n\n%Short: Stuff\n\nText"
    tokens._counts = {}
    tokens.setChapterFormat(nwHeadFmt.TITLE)
    tokens.setSceneFormat("* * *", False)
    tokens.setCommentType(nwComment.SHORT, True)
    tokens.tokenizeText()
    tokens.countStats()
    assert [t[2] for t in tokens._blocks] == ["Chapter", "Short Description: Stuff", "Text"]
    assert tokens.textStats == {
        "titleCount": 1, "paragraphCount": 1,
        "allWords": 5, "textWords": 1, "titleWords": 1,
        "allChars": 35, "textChars": 4, "titleChars": 7,
        "allWordChars": 33, "textWordChars": 4, "titleWordChars": 7
    }

    # Scene w/Comment
    # Comment does not count as a paragraph, and counts as "Comment: Stuff"
    tokens._text = "## Chapter\n\n### Scene\n\n% Stuff\n\nText"
    tokens._counts = {}
    tokens.setChapterFormat(nwHeadFmt.TITLE)
    tokens.setSceneFormat("* * *", False)
    tokens.setCommentType(nwComment.PLAIN, True)
    tokens.tokenizeText()
    tokens.countStats()
    assert [t[2] for t in tokens._blocks] == ["Chapter", "Comment: Stuff", "Text"]
    assert tokens.textStats == {
        "titleCount": 1, "paragraphCount": 1,
        "allWords": 4, "textWords": 1, "titleWords": 1,
        "allChars": 25, "textChars": 4, "titleChars": 7,
        "allWordChars": 24, "textWordChars": 4, "titleWordChars": 7
    }

    # Scene w/Keyword
    # Keyword does not count as a paragraph, and counts as "Point of View: Jane"
    tokens._text = "## Chapter\n\n### Scene\n\n@pov: Jane\n\nText"
    tokens._counts = {}
    tokens.setChapterFormat(nwHeadFmt.TITLE)
    tokens.setSceneFormat("* * *", False)
    tokens.setKeywords(True)
    tokens.tokenizeText()
    tokens.countStats()
    assert [t[2] for t in tokens._blocks] == ["Chapter", "Point of View: Jane", "Text"]
    assert tokens.textStats == {
        "titleCount": 1, "paragraphCount": 1,
        "allWords": 6, "textWords": 1, "titleWords": 1,
        "allChars": 30, "textChars": 4, "titleChars": 7,
        "allWordChars": 27, "textWordChars": 4, "titleWordChars": 7
    }

    # Long Text
    # =========
    tokens._text = (
        "# Act One\n\n"

        "## Chapter\n\n"

        "### Scene\n\n"
        "@pov: Jane\n"
        "@char: Jane, John\n\n"
        "%Synopsis: A scene\n\n"
        f"{ipsumText[0]}.\n\n"

        "### Scene\n\n"
        "@pov: Jane\n"
        "@char: Jane, John\n\n"
        "%Synopsis: A scene\n\n"
        f"{ipsumText[1]}.\n\n"

        "## Chapter\n\n"

        "### Scene\n\n"
        "@pov: Jane\n"
        "@char: Jane, John\n\n"
        "%Synopsis: A scene\n\n"
        f"{ipsumText[2]}.\n\n"

        "### Scene\n\n"
        "@pov: Jane\n"
        "@char: Jane, John\n\n"
        "%Synopsis: A scene\n\n"
        f"{ipsumText[3]}.\n\n"

        "## Chapter\n\n"

        "### Scene\n\n"
        "@pov: Jane\n"
        "@char: Jane, John\n\n"
        "%Synopsis: A scene\n\n"
        f"{ipsumText[4]}.\n\n"
    )
    tokens._counts = {}

    tokens.setPartitionFormat(f"T: {nwHeadFmt.TITLE}")
    tokens.setChapterFormat(f"C {nwHeadFmt.CH_NUM}: {nwHeadFmt.TITLE}")
    tokens.setSceneFormat("* * *", False)
    tokens.setCommentType(nwComment.SYNOPSIS, True)
    tokens.setCommentType(nwComment.PLAIN, True)
    tokens.setKeywords(True)

    tokens.tokenizeText()
    tokens.countStats()
    assert tokens.textStats == {
        "titleCount": 4, "paragraphCount": 5,
        "allWords": 596, "textWords": 528, "titleWords": 12,
        "allChars": 3859, "textChars": 3513, "titleChars": 46,
        "allWordChars": 3289, "textWordChars": 2990, "titleWordChars": 38
    }


@pytest.mark.core
def testFmtToken_SceneSeparators(mockGUI):
    """Test the section and scene separators of the Tokenizer class."""
    project = NWProject()
    project.data.setLanguage("en")
    project._loadProjectLocalisation()
    md = ToMarkdown(project, False)
    md._isNovel = True

    # Separator Handling, Titles
    # ==========================

    md._text = (
        "# Title One\n\n"
        "### Scene One\n\n"
        "Text\n\n"
        "### Scene Two\n\n"
        "Text\n\n"
        "# Title Two\n\n"
        "### Scene Three\n\n"
        "Text\n\n"
        "###! Scene Four\n\n"
        "Text\n\n"
    )
    md.setPartitionFormat(f"T: {nwHeadFmt.TITLE}")
    md.setChapterFormat(f"C: {nwHeadFmt.TITLE}")
    md.setSectionFormat("", True)

    # Static Separator
    md.setSceneFormat("~", False)
    md.setHardSceneFormat("* * *", False)
    md.tokenizeText()
    md.doConvert()
    assert md._pages[-1] == (
        "T: Title One\n"
        "============\n\n"
        "Text\n\n"
        "~\n\n"
        "Text\n\n"
        "T: Title Two\n"
        "============\n\n"
        "Text\n\n"
        "* * *\n\n"
        "Text\n\n"
    )

    # Scene Title Formatted
    md.setSceneFormat(f"S: {nwHeadFmt.TITLE}", False)
    md.setHardSceneFormat(f"H: {nwHeadFmt.TITLE}", False)
    md.tokenizeText()
    md.doConvert()
    assert md._pages[-1] == (
        "T: Title One\n"
        "============\n\n"
        "## S: Scene One\n\n"
        "Text\n\n"
        "## S: Scene Two\n\n"
        "Text\n\n"
        "T: Title Two\n"
        "============\n\n"
        "## S: Scene Three\n\n"
        "Text\n\n"
        "## H: Scene Four\n\n"
        "Text\n\n"
    )

    # Separator Handling, Chapters
    # ============================

    md._text = (
        "# Title One\n\n"
        "## Chapter One\n\n"
        "### Scene One\n\n"
        "Text\n\n"
        "### Scene Two\n\n"
        "Text\n\n"
        "## Chapter Two\n\n"
        "### Scene Three\n\n"
        "Text\n\n"
        "###! Scene Four\n\n"
        "Text\n\n"
    )
    md.setPartitionFormat(f"T: {nwHeadFmt.TITLE}")
    md.setChapterFormat(f"C: {nwHeadFmt.TITLE}")
    md.setSectionFormat("", True)

    # Static Separator
    md.setSceneFormat("* * *", False)
    md.setHardSceneFormat("* * *", False)
    md.tokenizeText()
    md.doConvert()
    assert md._pages[-1] == (
        "T: Title One\n"
        "============\n\n"
        "# C: Chapter One\n\n"
        "Text\n\n"
        "* * *\n\n"
        "Text\n\n"
        "# C: Chapter Two\n\n"
        "Text\n\n"
        "* * *\n\n"
        "Text\n\n"
    )

    # Scene Title Formatted
    md.setSceneFormat(f"S: {nwHeadFmt.TITLE}", False)
    md.setHardSceneFormat(f"H: {nwHeadFmt.TITLE}", False)
    md.tokenizeText()
    md.doConvert()
    assert md._pages[-1] == (
        "T: Title One\n"
        "============\n\n"
        "# C: Chapter One\n\n"
        "## S: Scene One\n\n"
        "Text\n\n"
        "## S: Scene Two\n\n"
        "Text\n\n"
        "# C: Chapter Two\n\n"
        "## S: Scene Three\n\n"
        "Text\n\n"
        "## H: Scene Four\n\n"
        "Text\n\n"
    )

    # Separators with Scenes Only
    # ===========================
    # Requires a fresh builder class
    md = ToMarkdown(project, True)
    md._isNovel = True

    md._text = (
        "### Scene One\n\n"
        "Text\n\n"
        "### Scene Two\n\n"
        "Text\n\n"
        "### Scene Three\n\n"
        "Text\n\n"
        "###! Scene Four\n\n"
        "Text\n\n"
    )

    md.setSceneFormat("", False)
    md.setHardSceneFormat("* * *", False)
    md.tokenizeText()
    md.doConvert()
    assert md._pages[-1] == (
        "Text\n\n"
        "\u205f\n\n"
        "Text\n\n"
        "\u205f\n\n"
        "Text\n\n"
        "* * *\n\n"
        "Text\n\n"
    )


@pytest.mark.core
def testFmtToken_HeaderVisibility(mockGUI):
    """Test the heading visibility settings of the Tokenizer class."""
    project = NWProject()
    project.data.setLanguage("en")
    project._loadProjectLocalisation()
    md = ToMarkdown(project, False)

    md._text = (
        "#! Novel\n\n"
        "# Title One\n\n"
        "##! Prologue\n\n"
        "Text\n\n"
        "## Chapter One\n\n"
        "### Scene One\n\n"
        "Text\n\n"
        "### Scene Two\n\n"
        "#### Section Two\n\n"
        "Text\n\n"
        "## Chapter Two\n\n"
        "### Scene Three\n\n"
        "Text\n\n"
        "###! Scene Four\n\n"
        "Text\n\n"
    )

    # Novel Files
    # ===========
    # Headers are promoted one level

    md._isNovel = True

    # Show All
    md.setPartitionFormat(nwHeadFmt.TITLE, False)
    md.setChapterFormat(nwHeadFmt.TITLE, False)
    md.setUnNumberedFormat(nwHeadFmt.TITLE, False)
    md.setSceneFormat(nwHeadFmt.TITLE, False)
    md.setHardSceneFormat(nwHeadFmt.TITLE, False)
    md.setSectionFormat(nwHeadFmt.TITLE, False)

    md.tokenizeText()
    md.doConvert()
    assert md._pages[-1] == (
        "Novel\n"
        "=====\n\n"
        "Title One\n"
        "=========\n\n"
        "# Prologue\n\n"
        "Text\n\n"
        "# Chapter One\n\n"
        "## Scene One\n\n"
        "Text\n\n"
        "## Scene Two\n\n"
        "### Section Two\n\n"
        "Text\n\n"
        "# Chapter Two\n\n"
        "## Scene Three\n\n"
        "Text\n\n"
        "## Scene Four\n\n"
        "Text\n\n"
    )

    # Hide All
    md.setPartitionFormat(nwHeadFmt.TITLE, True)
    md.setChapterFormat(nwHeadFmt.TITLE, True)
    md.setUnNumberedFormat(nwHeadFmt.TITLE, True)
    md.setSceneFormat(nwHeadFmt.TITLE, True)
    md.setHardSceneFormat(nwHeadFmt.TITLE, True)
    md.setSectionFormat(nwHeadFmt.TITLE, True)

    md.tokenizeText()
    md.doConvert()
    assert md._pages[-1] == (
        "Novel\n"
        "=====\n\n"
        "Text\n\n"
        "Text\n\n"
        "Text\n\n"
        "Text\n\n"
        "Text\n\n"
    )

    # Note Files
    # ==========
    # Headers are exported as-is

    md._isNovel = False

    # Hide All
    md.setPartitionFormat(nwHeadFmt.TITLE, True)
    md.setChapterFormat(nwHeadFmt.TITLE, True)
    md.setUnNumberedFormat(nwHeadFmt.TITLE, True)
    md.setSceneFormat(nwHeadFmt.TITLE, True)
    md.setHardSceneFormat(nwHeadFmt.TITLE, True)
    md.setSectionFormat(nwHeadFmt.TITLE, True)

    md.tokenizeText()
    md.doConvert()
    assert md._pages[-1] == (
        "Novel\n"
        "=====\n\n"
        "# Title One\n\n"
        "## Prologue\n\n"
        "Text\n\n"
        "## Chapter One\n\n"
        "### Scene One\n\n"
        "Text\n\n"
        "### Scene Two\n\n"
        "#### Section Two\n\n"
        "Text\n\n"
        "## Chapter Two\n\n"
        "### Scene Three\n\n"
        "Text\n\n"
        "### Scene Four\n\n"
        "Text\n\n"
    )


@pytest.mark.core
def testFmtToken_CounterHandling(mockGUI):
    """Test the heading counter of the Tokenizer class."""
    project = NWProject()
    project.data.setLanguage("en")
    project._loadProjectLocalisation()
    md = ToMarkdown(project, False)
    md._isNovel = True

    # Counter Handling, Novel Titles
    # ==============================
    # This also checks that only numbered chapters bump the counter

    md._text = (
        "#! Novel One\n\n"
        "##! Prologue\n\n"
        "Text\n\n"
        "## Chapter One\n\n"
        "### Scene One\n\n"
        "Text\n\n"
        "### Scene Two\n\n"
        "Text\n\n"
        "## Chapter Two\n\n"
        "### Scene Three\n\n"
        "Text\n\n"
        "###! Scene Four\n\n"
        "Text\n\n"
        "#! Novel Two\n\n"
        "##! Prologue\n\n"
        "Text\n\n"
        "## Chapter One\n\n"
        "### Scene One\n\n"
        "Text\n\n"
        "### Scene Two\n\n"
        "Text\n\n"
        "## Chapter Two\n\n"
        "### Scene Three\n\n"
        "Text\n\n"
        "###! Scene Four\n\n"
        "Text\n\n"
    )
    md.setPartitionFormat(f"T: {nwHeadFmt.TITLE}")
    md.setChapterFormat(f"C {nwHeadFmt.CH_NUM}: {nwHeadFmt.TITLE}")
    md.setUnNumberedFormat(f"U: {nwHeadFmt.TITLE}")
    md.setSceneFormat(
        f"S {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM} ({nwHeadFmt.SC_ABS}): {nwHeadFmt.TITLE}", False
    )
    md.setHardSceneFormat(
        f"H {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM} ({nwHeadFmt.SC_ABS}): {nwHeadFmt.TITLE}", False
    )
    md.setSectionFormat("", True)

    # Two Novel Format
    md.tokenizeText()
    md.doConvert()
    assert md._pages[-1] == (
        "Novel One\n"
        "=========\n\n"
        "# U: Prologue\n\n"
        "Text\n\n"
        "# C 1: Chapter One\n\n"
        "## S 1.1 (1): Scene One\n\n"
        "Text\n\n"
        "## S 1.2 (2): Scene Two\n\n"
        "Text\n\n"
        "# C 2: Chapter Two\n\n"
        "## S 2.1 (3): Scene Three\n\n"
        "Text\n\n"
        "## H 2.2 (4): Scene Four\n\n"
        "Text\n\n"
        "Novel Two\n"
        "=========\n\n"
        "# U: Prologue\n\n"
        "Text\n\n"
        "# C 1: Chapter One\n\n"
        "## S 1.1 (1): Scene One\n\n"
        "Text\n\n"
        "## S 1.2 (2): Scene Two\n\n"
        "Text\n\n"
        "# C 2: Chapter Two\n\n"
        "## S 2.1 (3): Scene Three\n\n"
        "Text\n\n"
        "## H 2.2 (4): Scene Four\n\n"
        "Text\n\n"
    )


@pytest.mark.core
def testFmtToken_HeadingFormatter(fncPath, mockGUI, mockRnd):
    """Check the HeadingFormatter class."""
    project = NWProject()
    project.setProjectLang("en_GB")
    mockRnd.reset()
    buildTestProject(project, fncPath)

    nHandle = project.newFile("Hello", C.hNovelRoot)
    cHandle = project.newFile("Jane",  C.hCharRoot)
    dHandle = project.newFile("John",  C.hCharRoot)

    assert isinstance(nHandle, str)
    assert isinstance(cHandle, str)
    assert isinstance(dHandle, str)

    assert project.index.scanText(cHandle, (
        "# Jane Smith\n"
        "@tag: Jane | Jane Smith\n"
    ))
    assert project.index.scanText(dHandle, (
        "# John Smith\n"
        "@tag: John | John Smith\n"
    ))
    assert project.index.scanText(nHandle, (
        "## Hi Bob\n"
        "@pov: Jane\n"
        "@focus: John\n"
        "@char: Jane, John\n\n"
        "This is a story about Jane Smith.\n\n"
        "Well, not really.\n"
    ))

    formatter = HeadingFormatter(project)
    formatter.setHandle(nHandle)

    # Counters
    # ========
    cFormat = (
        f"Chapter {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM} - "
        f"Scene {nwHeadFmt.SC_ABS} - {nwHeadFmt.TITLE}"
    )

    # Initial
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 0.0 - Scene 0 - Hi Bob"

    # First Chapter and Scene
    formatter.incChapter()
    formatter.incScene()
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 1.1 - Scene 1 - Hi Bob"

    # Next Scenes
    formatter.incScene()
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 1.2 - Scene 2 - Hi Bob"
    formatter.incScene()
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 1.3 - Scene 3 - Hi Bob"
    formatter.incScene()
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 1.4 - Scene 4 - Hi Bob"

    # Next Chapter and Scene
    formatter.incChapter()
    formatter.resetScene()
    formatter.incScene()
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 2.1 - Scene 5 - Hi Bob"

    # New Main Title
    formatter.resetAll()
    formatter.incChapter()
    formatter.incScene()
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 1.1 - Scene 1 - Hi Bob"

    # Special Formats
    # ===============
    formatter._chapter = 2
    formatter._scene = 3
    formatter._absolute = 8

    # Chapter Number Word
    cFormat = f"Chapter {nwHeadFmt.CH_WORD}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.TITLE}"
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter Two, Scene 3 - Hi Bob"

    # Chapter Upper Case Roman
    cFormat = f"Chapter {nwHeadFmt.CH_ROMU}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.TITLE}"
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter II, Scene 3 - Hi Bob"

    # Chapter Lower Case Roman
    cFormat = f"Chapter {nwHeadFmt.CH_ROML}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.TITLE}"
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter ii, Scene 3 - Hi Bob"

    # Chapter w/PoV Character
    cFormat = f"Chapter {nwHeadFmt.CH_NUM}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.CHAR_POV}"
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 2, Scene 3 - Jane Smith"

    # Chapter w/Focus Character
    cFormat = f"Chapter {nwHeadFmt.CH_NUM}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.CHAR_FOCUS}"
    assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 2, Scene 3 - John Smith"

    # Chapter w/Fallback PoV
    cFormat = f"Chapter {nwHeadFmt.CH_NUM}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.CHAR_POV}"
    assert formatter.apply(cFormat, "Hi Bob", 0) == "Chapter 2, Scene 3 - Point of View"

    # Chapter w/Fallback Focus
    cFormat = f"Chapter {nwHeadFmt.CH_NUM}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.CHAR_FOCUS}"
    assert formatter.apply(cFormat, "Hi Bob", 0) == "Chapter 2, Scene 3 - Focus"
