Merge Novel Layouts (#837)

* Merge novel layouts and simplify the Tokenizer class
* Fix core tests
* Delete auto-layout code
* Remove no longer needed item layouts
* Remove references to deleted item layouts in test
* Make some minor changes to Tokenizer class and update tests
* Update test reference files
* Update sample project
* Fix a few issues with the Tokenizer
* Centre some text in the sample documents
* Make some minor changes to how new projects are generated
* Add support for New Page and VSpace commands to the Tokenizer
* Remove Title and Page layouts and add codes for page break and vertical space
* Add document layout and fix some issues in index class with new title formats
* Update tests and reference files
* Bump the project file version and fix a minor issue in items class
* Update tests and test coverage
* Clean up some warnings and issues in tests
This commit is contained in:
Veronica Berglyd Olsen
2021-08-02 23:44:35 +02:00
committed by GitHub
parent c6973043e6
commit bb1a778277
92 changed files with 1574 additions and 1121 deletions
+9 -18
View File
@@ -65,6 +65,9 @@ class nwLists():
# Item classes which do not require items to have same class
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
# Deprecated nwItemLayout entries
DEP_LAYOUT = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE", "STORY")
# END Class nwLists
@@ -169,26 +172,14 @@ class nwLabels():
nwItemClass.TRASH: "cls_trash",
}
LAYOUT_NAME = {
nwItemLayout.NO_LAYOUT: QT_TRANSLATE_NOOP("Constant", "None"),
nwItemLayout.TITLE: QT_TRANSLATE_NOOP("Constant", "Title Page"),
nwItemLayout.BOOK: QT_TRANSLATE_NOOP("Constant", "Book"),
nwItemLayout.PAGE: QT_TRANSLATE_NOOP("Constant", "Plain Page"),
nwItemLayout.PARTITION: QT_TRANSLATE_NOOP("Constant", "Partition"),
nwItemLayout.UNNUMBERED: QT_TRANSLATE_NOOP("Constant", "Unnumbered"),
nwItemLayout.CHAPTER: QT_TRANSLATE_NOOP("Constant", "Chapter"),
nwItemLayout.SCENE: QT_TRANSLATE_NOOP("Constant", "Scene"),
nwItemLayout.NOTE: QT_TRANSLATE_NOOP("Constant", "Note"),
nwItemLayout.NO_LAYOUT: QT_TRANSLATE_NOOP("Constant", "None"),
nwItemLayout.DOCUMENT: QT_TRANSLATE_NOOP("Constant", "Novel Document"),
nwItemLayout.NOTE: QT_TRANSLATE_NOOP("Constant", "Project Note"),
}
LAYOUT_FLAG = {
nwItemLayout.NO_LAYOUT: "Xo",
nwItemLayout.TITLE: "Tt",
nwItemLayout.BOOK: "Bk",
nwItemLayout.PAGE: "Pg",
nwItemLayout.PARTITION: "Pt",
nwItemLayout.UNNUMBERED: "Un",
nwItemLayout.CHAPTER: "Ch",
nwItemLayout.SCENE: "Sc",
nwItemLayout.NOTE: "Nt",
nwItemLayout.NO_LAYOUT: "Xo",
nwItemLayout.DOCUMENT: "Dc",
nwItemLayout.NOTE: "Nt",
}
KEY_NAME = {
nwKeyWords.TAG_KEY: QT_TRANSLATE_NOOP("Constant", "Tag"),
+20 -1
View File
@@ -354,6 +354,12 @@ class NWIndex():
elif aLine.startswith("#### "):
hDepth = "H4"
hText = aLine[5:].strip()
elif aLine.startswith("#! "):
hDepth = "H1"
hText = aLine[2:].strip()
elif aLine.startswith("##! "):
hDepth = "H2"
hText = aLine[4:].strip()
else:
return False
@@ -861,7 +867,13 @@ def countWords(theText):
if aLine[0] == "@" or aLine[0] == "%":
continue
if aLine[0] == "#":
if aLine[0] == "[":
if aLine.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")):
continue
elif aLine.startswith("[VSPACE:") and aLine.endswith("]"):
continue
elif aLine[0] == "#":
if aLine[:5] == "#### ":
aLine = aLine[5:]
countPara = False
@@ -874,6 +886,13 @@ def countWords(theText):
elif aLine[:2] == "# ":
aLine = aLine[2:]
countPara = False
elif aLine[:3] == "#! ":
aLine = aLine[3:]
countPara = False
elif aLine[:4] == "##! ":
aLine = aLine[4:]
countPara = False
elif aLine[0] == ">" or aLine[-1] == "<":
if aLine[:2] == ">>":
aLine = aLine[2:].lstrip(" ")
+4 -1
View File
@@ -29,6 +29,7 @@ from lxml import etree
from nw.enum import nwItemType, nwItemClass, nwItemLayout
from nw.common import checkInt, isHandle, isItemClass, isItemLayout, isItemType
from nw.constants import nwLists
logger = logging.getLogger(__name__)
@@ -230,6 +231,8 @@ class NWItem():
self.itemLayout = theLayout
elif isItemLayout(theLayout):
self.itemLayout = nwItemLayout[theLayout]
elif theLayout in nwLists.DEP_LAYOUT:
self.itemLayout = nwItemLayout.DOCUMENT
else:
logger.error("Unrecognised item layout '%s'", theLayout)
self.itemLayout = nwItemLayout.NO_LAYOUT
@@ -239,7 +242,7 @@ class NWItem():
"""Set the item status by looking it up in the valid status
items of the current project.
"""
if self.itemClass == nwItemClass.NOVEL:
if self.itemClass in nwLists.CLS_NOVEL:
self.itemStatus = self.theProject.statusItems.checkEntry(theStatus)
else:
self.itemStatus = self.theProject.importItems.checkEntry(theStatus)
+35 -20
View File
@@ -52,6 +52,8 @@ logger = logging.getLogger(__name__)
class NWProject():
FILE_VERSION = "1.3"
def __init__(self, theParent):
# Internal
@@ -142,14 +144,14 @@ class NWProject():
return newItem.itemHandle
def newFile(self, fileName, fileClass, pHandle):
"""Add a new file with a given name and class, and set a default
layout based on the class. SCENE for NOVEL, and otherwise NOTE.
"""Add a new file with a given name and class, and set a layout
based on the class. DOCUMENT for NOVEL, otherwise NOTE.
"""
newItem = NWItem(self)
newItem.setName(fileName)
newItem.setType(nwItemType.FILE)
if fileClass == nwItemClass.NOVEL:
newItem.setLayout(nwItemLayout.SCENE)
newItem.setLayout(nwItemLayout.DOCUMENT)
else:
newItem.setLayout(nwItemLayout.NOTE)
newItem.setClass(fileClass)
@@ -204,7 +206,7 @@ class NWProject():
self.autoReplace = {}
self.titleFormat = {
"title": "%title%",
"chapter": self.tr("Chapter")+" %ch%: %title%",
"chapter": "%title%",
"unnumbered": "%title%",
"scene": "* * *",
"section": "",
@@ -261,9 +263,9 @@ class NWProject():
self.setBookTitle(projTitle)
self.setBookAuthors(projAuthors)
titlePage = "# %s\n\n" % (self.bookTitle if self.bookTitle else self.projName)
titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self.projName)
if self.bookAuthors:
titlePage = "%s%s %s\n" % (titlePage, self.tr("By"), self.getAuthors())
titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors())
if popMinimal:
# Creating a minimal project with a few root folders and a
@@ -278,9 +280,6 @@ class NWProject():
xHandle[7] = self.newFile(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[6])
xHandle[8] = self.newFile(self.tr("New Scene"), nwItemClass.NOVEL, xHandle[6])
self.projTree.setFileItemLayout(xHandle[5], nwItemLayout.TITLE)
self.projTree.setFileItemLayout(xHandle[7], nwItemLayout.CHAPTER)
aDoc = NWDoc(self, xHandle[5])
aDoc.writeDocument(titlePage)
@@ -303,7 +302,7 @@ class NWProject():
# Create a title page
tHandle = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, nHandle)
self.projTree.setFileItemLayout(tHandle, nwItemLayout.TITLE)
self.projTree.setFileItemLayout(tHandle, nwItemLayout.DOCUMENT)
aDoc = NWDoc(self, tHandle)
aDoc.writeDocument(titlePage)
@@ -322,7 +321,7 @@ class NWProject():
pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle)
cHandle = self.newFile(chTitle, nwItemClass.NOVEL, pHandle)
self.projTree.setFileItemLayout(cHandle, nwItemLayout.CHAPTER)
self.projTree.setFileItemLayout(cHandle, nwItemLayout.DOCUMENT)
aDoc = NWDoc(self, cHandle)
aDoc.writeDocument("## %s\n\n" % chTitle)
@@ -466,8 +465,11 @@ class NWProject():
# 1.2 : Changes the way autoReplace entries are stored. The 1.1
# parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10.
# 1.3 : Reduces the number of layouts to onlye two. One for
# novel documents and one for notes. Introduced in version
# 1.5.
if fileVersion not in ("1.0", "1.1", "1.2"):
if fileVersion not in ("1.0", "1.1", "1.2", "1.3"):
self.makeAlert(self.tr(
"Unknown or unsupported novelWriter project file format. "
"The project cannot be opened by this version of novelWriter. "
@@ -476,6 +478,19 @@ class NWProject():
self.clearProject()
return False
if fileVersion != self.FILE_VERSION:
msgYes = self.theParent.askQuestion(
self.tr("File Version"),
self.tr(
"The file format of your project is about to be updated. "
"If you proceed, this project can no longer be opened by "
"an older version of novelWriter. Continue?"
).format(appVersion, nw.__version__)
)
if not msgYes:
self.clearProject()
return False
# Check novelWriter Version
# =========================
@@ -621,10 +636,10 @@ class NWProject():
# Root element and project details
logger.debug("Writing project meta")
nwXML = etree.Element("novelWriterXML", attrib={
"appVersion": str(nw.__version__),
"hexVersion": str(nw.__hexversion__),
"fileVersion": "1.2",
"timeStamp": formatTimeStamp(saveTime),
"appVersion": str(nw.__version__),
"hexVersion": str(nw.__hexversion__),
"fileVersion": self.FILE_VERSION,
"timeStamp": formatTimeStamp(saveTime),
})
editTime = int(self.editTime + saveTime - self.projOpened)
@@ -728,14 +743,14 @@ class NWProject():
if self.projPath is None or self.projPath == "":
return False
if self.projPath == os.path.expanduser("~"):
# Don't make a mess in the user's home folder
return False
self.projMeta = os.path.join(self.projPath, "meta")
self.projCache = os.path.join(self.projPath, "cache")
self.projContent = os.path.join(self.projPath, "content")
if self.projPath == os.path.expanduser("~"):
# Don't make a mess in the user's home folder
return False
if not self._checkFolder(self.projMeta):
return False
if not self._checkFolder(self.projCache):
+1 -1
View File
@@ -143,7 +143,7 @@ class NWSpellEnchant(NWSpellCheck):
try:
import enchant
if self.theBroker is not None:
logger.verbose("Deleting old pyenchant broker")
logger.debug("Deleting old pyenchant broker")
del self.theBroker
self.theBroker = enchant.Broker()
+6 -8
View File
@@ -128,9 +128,7 @@ class ToHtml(Tokenizer):
}
if self.isNovel and self.genMode != self.M_PREVIEW:
# For novel files for export, we bump the titles one level
# up as this is more useful for printing and word processor
# imports.
# For story files, we bump the titles one level up
h1Cl = " class='title'"
h1 = "h1"
h2 = "h1"
@@ -168,13 +166,9 @@ class ToHtml(Tokenizer):
if tStyle & self.A_PBB:
aStyle.append("page-break-before: always;")
elif tStyle & self.A_PBB_AUT:
aStyle.append("page-break-before: auto;")
if tStyle & self.A_PBA:
aStyle.append("page-break-after: always;")
elif tStyle & self.A_PBA_AUT:
aStyle.append("page-break-after: auto;")
if tStyle & self.A_Z_BTMMRG:
aStyle.append("margin-bottom: 0;")
@@ -206,7 +200,7 @@ class ToHtml(Tokenizer):
parClass = ""
if len(thisPar) > 0:
tTemp = "<br/>".join(thisPar)
tmpResult.append("<p%s%s>%s</p>\n" % (parStyle, parClass, tTemp.rstrip()))
tmpResult.append("<p%s%s>%s</p>\n" % (parClass, parStyle, tTemp.rstrip()))
thisPar = []
parStyle = None
@@ -214,6 +208,10 @@ class ToHtml(Tokenizer):
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h1 class='title'%s>%s%s</h1>\n" % (hStyle, aNm, tHead))
elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h2, hStyle, aNm, tHead, h2))
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s%s>%s%s</%s>\n" % (h1, h1Cl, hStyle, aNm, tHead, h1))
+219 -197
View File
@@ -33,7 +33,7 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression
from nw.enum import nwItemLayout, nwItemType
from nw.common import numberToRoman
from nw.common import numberToRoman, checkInt
from nw.constants import nwConst, nwRegEx, nwUnicode
from nw.core.document import NWDoc
@@ -56,13 +56,14 @@ class Tokenizer():
T_COMMENT = 3 # Comment line
T_KEYWORD = 4 # Command line
T_TITLE = 5 # Title
T_HEAD1 = 6 # Header 1
T_HEAD2 = 7 # Header 2
T_HEAD3 = 8 # Header 3
T_HEAD4 = 9 # Header 4
T_TEXT = 10 # Text line
T_SEP = 11 # Scene separator
T_SKIP = 12 # Paragraph break
T_UNNUM = 6 # Unnumbered
T_HEAD1 = 7 # Header 1
T_HEAD2 = 8 # Header 2
T_HEAD3 = 9 # Header 3
T_HEAD4 = 10 # Header 4
T_TEXT = 11 # Text line
T_SEP = 12 # Scene separator
T_SKIP = 13 # Paragraph break
# Block Style
A_NONE = 0x0000 # No special style
@@ -70,14 +71,12 @@ class Tokenizer():
A_RIGHT = 0x0002 # Right aligned
A_CENTRE = 0x0004 # Centred
A_JUSTIFY = 0x0008 # Justified
A_PBB = 0x0010 # Page break before always
A_PBB_AUT = 0x0020 # Page break before auto
A_PBA = 0x0040 # Page break after always
A_PBA_AUT = 0x0080 # Page break after auto
A_Z_TOPMRG = 0x0100 # Zero top margin
A_Z_BTMMRG = 0x0200 # Zero bottom margin
A_IND_L = 0x0400 # Left indentation
A_IND_R = 0x0800 # Right indentation
A_PBB = 0x0010 # Page break before
A_PBA = 0x0020 # Page break after
A_Z_TOPMRG = 0x0040 # Zero top margin
A_Z_BTMMRG = 0x0080 # Zero bottom margin
A_IND_L = 0x0100 # Left indentation
A_IND_R = 0x0200 # Right indentation
def __init__(self, theProject):
@@ -136,15 +135,9 @@ class Tokenizer():
# This File
self.isNone = False
self.isTitle = False
self.isBook = False
self.isPage = False
self.isPart = False
self.isUnNum = False
self.isChap = False
self.isScene = False
self.isNote = False
self.isNovel = False
self.isNote = False
self.isFirst = True
# Error Handling
self.errData = []
@@ -268,10 +261,16 @@ class Tokenizer():
if theItem.itemType != nwItemType.ROOT:
return False
if self.isFirst:
textAlign = self.A_CENTRE
self.isFirst = False
else:
textAlign = self.A_PBB | self.A_CENTRE
theTitle = "%s: %s" % (self._localLookup("Notes"), theItem.itemName)
self.theTokens = []
self.theTokens.append((
self.T_TITLE, 0, theTitle, None, self.A_PBB | self.A_CENTRE
self.T_TITLE, 0, theTitle, None, textAlign
))
if self.keepMarkdown:
self.theMarkdown.append("# %s\n\n" % theTitle)
@@ -307,15 +306,8 @@ class Tokenizer():
self.errData.append(errVal)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
self.isTitle = self.theItem.itemLayout == nwItemLayout.TITLE
self.isBook = self.theItem.itemLayout == nwItemLayout.BOOK
self.isPage = self.theItem.itemLayout == nwItemLayout.PAGE
self.isPart = self.theItem.itemLayout == nwItemLayout.PARTITION
self.isUnNum = self.theItem.itemLayout == nwItemLayout.UNNUMBERED
self.isChap = self.theItem.itemLayout == nwItemLayout.CHAPTER
self.isScene = self.theItem.itemLayout == nwItemLayout.SCENE
self.isNovel = self.theItem.itemLayout == nwItemLayout.DOCUMENT
self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE
self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene
return True
@@ -356,11 +348,11 @@ class Tokenizer():
def tokenizeText(self):
"""Scan the text for either lines starting with specific
characters that indicate headers, comments, commands etc, or
just contains plain text. in the case of plain text, apply the
just contain plain text. In the case of plain text, apply the
same RegExes that the syntax highlighter uses and save the
locations of these formatting tags into the token array.
The format of the token list is an entry with a four-tuple for
The format of the token list is an entry with a five-tuple for
each line in the file. The tuple is as follows:
1: The type of the block, self.T_*
2: The line in file where this block occurred
@@ -375,73 +367,144 @@ class Tokenizer():
(QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]),
]
# Determine default text alignment
if self.isTitle or self.isPart:
defAlign = self.A_CENTRE
else:
defAlign = self.A_NONE
self.theTokens = []
tmpMarkdown = []
nLine = 0
breakNext = False
for aLine in self.theText.splitlines():
nLine += 1
sLine = aLine.strip()
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
# Check for blank lines
if len(sLine) == 0:
self.theTokens.append((
self.T_EMPTY, nLine, "", None, defAlign
self.T_EMPTY, nLine, "", None, self.A_NONE
))
if self.keepMarkdown:
tmpMarkdown.append("\n")
continue
if breakNext:
sAlign = self.A_PBB
breakNext = False
else:
sAlign = self.A_NONE
# Check Line Format
# =================
if aLine[0] == "[":
# Parse special formatting line
if sLine in ("[NEWPAGE]", "[NEW PAGE]"):
breakNext = True
continue
elif sLine == "[VSPACE]":
self.theTokens.append(
(self.T_SKIP, nLine, "", None, sAlign)
)
continue
elif sLine.startswith("[VSPACE:") and sLine.endswith("]"):
nSkip = checkInt(sLine[8:-1], 0)
if nSkip >= 1:
self.theTokens.append(
(self.T_SKIP, nLine, "", None, sAlign)
)
if nSkip > 1:
self.theTokens += (nSkip - 1) * [
(self.T_SKIP, nLine, "", None, self.A_NONE)
]
continue
elif aLine[0] == "%":
cLine = aLine[1:].lstrip()
synTag = cLine[:9].lower()
if synTag == "synopsis:":
self.theTokens.append((
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, defAlign
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, sAlign
))
if self.doSynopsis and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
self.T_COMMENT, nLine, aLine[1:].strip(), None, defAlign
self.T_COMMENT, nLine, aLine[1:].strip(), None, sAlign
))
if self.doComments and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
self.T_KEYWORD, nLine, aLine[1:].strip(), None, defAlign
self.T_KEYWORD, nLine, aLine[1:].strip(), None, sAlign
))
if self.doKeywords and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
if self.isNovel:
sAlign |= self.A_CENTRE
if self.isNovel and not self.isFirst:
sAlign |= self.A_PBB
self.theTokens.append((
self.T_HEAD1, nLine, aLine[2:].strip(), None, defAlign
self.T_HEAD1, nLine, aLine[2:].strip(), None, sAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
if self.isNovel and not self.isFirst:
sAlign |= self.A_PBB
self.theTokens.append((
self.T_HEAD2, nLine, aLine[3:].strip(), None, defAlign
self.T_HEAD2, nLine, aLine[3:].strip(), None, sAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
self.T_HEAD3, nLine, aLine[4:].strip(), None, defAlign
self.T_HEAD3, nLine, aLine[4:].strip(), None, sAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
self.T_HEAD4, nLine, aLine[5:].strip(), None, defAlign
self.T_HEAD4, nLine, aLine[5:].strip(), None, sAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "#! ":
if self.isNovel:
tStyle = self.T_TITLE
else:
tStyle = self.T_HEAD1
if self.isNovel and not self.isFirst:
sAlign |= self.A_PBB
self.theTokens.append((
tStyle, nLine, aLine[3:].strip(), None, sAlign | self.A_CENTRE
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "##! ":
if self.isNovel:
tStyle = self.T_UNNUM
else:
tStyle = self.T_HEAD2
if self.isNovel and not self.isFirst:
sAlign |= self.A_PBB
self.theTokens.append((
tStyle, nLine, aLine[4:].strip(), None, sAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -452,36 +515,35 @@ class Tokenizer():
continue
# Check Alignment and Indentation
tagLeft = False
tagRight = False
alnLeft = False
alnRight = False
indLeft = False
indRight = False
if aLine.startswith(">>"):
tagRight = True
alnRight = True
aLine = aLine[2:].lstrip(" ")
elif aLine.startswith(">"):
indLeft = True
aLine = aLine[1:].lstrip(" ")
if aLine.endswith("<<"):
tagLeft = True
alnLeft = True
aLine = aLine[:-2].rstrip(" ")
elif aLine.endswith("<"):
indRight = True
aLine = aLine[:-1].rstrip(" ")
textAlign = defAlign
if tagLeft and tagRight:
textAlign = self.A_CENTRE
elif tagLeft:
textAlign = self.A_LEFT
elif tagRight:
textAlign = self.A_RIGHT
if alnLeft and alnRight:
sAlign |= self.A_CENTRE
elif alnLeft:
sAlign |= self.A_LEFT
elif alnRight:
sAlign |= self.A_RIGHT
if indLeft:
textAlign |= self.A_IND_L
sAlign |= self.A_IND_L
if indRight:
textAlign |= self.A_IND_R
sAlign |= self.A_IND_R
# Otherwise we use RegEx to find formatting tags within a line of text
fmtPos = []
@@ -499,14 +561,18 @@ class Tokenizer():
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append((
self.T_TEXT, nLine, aLine, fmtPos, textAlign
self.T_TEXT, nLine, aLine, fmtPos, sAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end
# If we have content, turn off the first page flag
if self.isFirst and self.theTokens:
self.isFirst = False
# Always add an empty line at the end of the file
self.theTokens.append((
self.T_EMPTY, nLine, "", None, defAlign
self.T_EMPTY, nLine, "", None, self.A_NONE
))
if self.keepMarkdown:
tmpMarkdown.append("\n")
@@ -518,8 +584,8 @@ class Tokenizer():
# ===========
# Some items need a second pass
pToken = (self.T_EMPTY, 0, "", None, defAlign)
nToken = (self.T_EMPTY, 0, "", None, defAlign)
pToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
nToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
tCount = len(self.theTokens)
for n, tToken in enumerate(self.theTokens):
@@ -541,148 +607,104 @@ class Tokenizer():
return
def doHeaders(self):
"""Apply formatting to the text headers according to document
layout and user settings.
"""Apply formatting to the text headers for novel files. This
also applies chapter and scene numbering.
"""
# No special header formatting for notes and no-layout files
if self.isNone or self.isNote:
if not self.isNovel:
return False
# For novel files, we need to handle chapter numbering, scene
# numbering, and scene breaks
if self.isNovel:
for n, tToken in enumerate(self.theTokens):
for n, tToken in enumerate(self.theTokens):
# In case we see text before a scene, we reset the flag
if tToken[0] == self.T_TEXT:
self.firstScene = False
# In case we see text before a scene, we reset the flag
if tToken[0] == self.T_TEXT:
self.firstScene = False
elif tToken[0] == self.T_HEAD1:
# Main Title
# ==========
elif tToken[0] == self.T_HEAD1:
# Partition
tTemp = self._formatHeading(self.fmtTitle, tToken[2])
tTemp = self._formatHeading(self.fmtTitle, tToken[2])
self.theTokens[n] = (
tToken[0], tToken[1], tTemp, None, tToken[4]
)
elif tToken[0] in (self.T_HEAD2, self.T_UNNUM):
# Chapter
# Numbered or Unnumbered
if tToken[2].startswith("*"):
tTemp = self._formatHeading(self.fmtUnNum, tToken[2][1:].lstrip())
elif tToken[0] == self.T_UNNUM:
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
else:
self.numChapter += 1
tTemp = self._formatHeading(self.fmtChapter, tToken[2])
# Format the chapter header
self.theTokens[n] = (
tToken[0], tToken[1], tTemp, None, tToken[4]
)
# Set scene variables
self.firstScene = True
self.numChScene = 0
elif tToken[0] == self.T_HEAD3:
# Scene
self.numChScene += 1
self.numAbsScene += 1
tTemp = self._formatHeading(self.fmtScene, tToken[2])
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
self.T_SKIP, tToken[1], "", None, self.A_NONE
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tToken[0], tToken[1], tTemp, None, self.A_NONE
)
elif tToken[0] == self.T_HEAD2:
# Novel Chapter
# =============
# Definitely no longer the first scene
self.firstScene = False
# Numbered or Unnumbered
if self.isUnNum:
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
elif tToken[2].startswith("*"):
tTemp = self._formatHeading(self.fmtUnNum, tToken[2][1:].lstrip())
else:
self.numChapter += 1
tTemp = self._formatHeading(self.fmtChapter, tToken[2])
elif tToken[0] == self.T_HEAD4:
# Section
# Format the chapter header
tTemp = self._formatHeading(self.fmtSection, tToken[2])
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
tToken[0], tToken[1], tTemp, None, self.A_PBB
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
self.T_SKIP, tToken[1], "", None, self.A_NONE
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tToken[0], tToken[1], tTemp, None, self.A_NONE
)
# Set scene variables
self.firstScene = True
self.numChScene = 0
elif tToken[0] == self.T_HEAD3:
# Novel Scene
# ===========
self.numChScene += 1
self.numAbsScene += 1
tTemp = self._formatHeading(self.fmtScene, tToken[2])
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
self.T_SKIP, tToken[1], "", None, self.A_NONE
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tToken[0], tToken[1], tTemp, None, self.A_NONE
)
# Definitely no longer the first scene
self.firstScene = False
elif tToken[0] == self.T_HEAD4:
# Novel Section
# =============
tTemp = self._formatHeading(self.fmtSection, tToken[2])
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
self.T_SKIP, tToken[1], "", None, self.A_NONE
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tToken[0], tToken[1], tTemp, None, self.A_NONE
)
# For title page we use a different title class, and we will set
# an automatic page break before (i.e. added if needed). For
# partitions we always need a page break before.
if self.isTitle or self.isPart:
for n, tToken in enumerate(self.theTokens):
aStyle = tToken[4]
if n == 0:
if self.isTitle:
aStyle |= self.A_PBB_AUT
else:
aStyle |= self.A_PBB
if tToken[0] == self.T_HEAD1:
if self.isTitle:
self.theTokens[n] = (
self.T_TITLE, tToken[1], tToken[2], tToken[3], aStyle
)
else:
self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
)
# Add a page break after the last entry
if len(self.theTokens) > 0:
tToken = self.theTokens[-1]
self.theTokens[-1] = (
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA
)
# A single page always starts on a fresh page, unless it's empty.
if self.isPage and len(self.theTokens) > 0:
tToken = self.theTokens[0]
self.theTokens[0] = (
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBB
)
return True
+4
View File
@@ -108,6 +108,10 @@ class ToMarkdown(Tokenizer):
tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead)
elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("## %s\n\n" % tHead)
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead)
+4 -4
View File
@@ -345,13 +345,9 @@ class ToOdt(Tokenizer):
if tStyle & self.A_PBB:
oStyle.setBreakBefore("page")
elif tStyle & self.A_PBB_AUT:
oStyle.setBreakBefore("auto")
if tStyle & self.A_PBA:
oStyle.setBreakAfter("page")
elif tStyle & self.A_PBA_AUT:
oStyle.setBreakAfter("auto")
if tStyle & self.A_Z_BTMMRG:
oStyle.setMarginBottom("0.000cm")
@@ -384,6 +380,10 @@ class ToOdt(Tokenizer):
tHead = tText.replace(r"\\", "\n")
self._addTextPar("Title", oStyle, tHead, isHead=True)
elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "\n")
self._addTextPar("Heading_2", oStyle, tHead, isHead=True, oLevel="2")
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "\n")
self._addTextPar("Heading_1", oStyle, tHead, isHead=True, oLevel="1")
+1 -48
View File
@@ -33,34 +33,11 @@ from hashlib import sha256
from nw.enum import nwItemType, nwItemClass, nwItemLayout
from nw.common import checkHandle
from nw.constants import nwConst, nwLists, nwFiles
from nw.constants import nwConst, nwFiles
from nw.core.item import NWItem
logger = logging.getLogger(__name__)
# Layout Translation Map
LAYOUT_MAP = {
nwItemLayout.SCENE: {
"H1": nwItemLayout.BOOK,
"H2": nwItemLayout.CHAPTER,
},
nwItemLayout.CHAPTER: {
"H1": nwItemLayout.BOOK,
"H3": nwItemLayout.SCENE,
"H4": nwItemLayout.SCENE,
},
nwItemLayout.UNNUMBERED: {
"H1": nwItemLayout.BOOK,
"H3": nwItemLayout.SCENE,
"H4": nwItemLayout.SCENE,
},
nwItemLayout.PARTITION: {
"H2": nwItemLayout.CHAPTER,
"H3": nwItemLayout.SCENE,
"H4": nwItemLayout.SCENE,
},
}
class NWTree():
@@ -229,30 +206,6 @@ class NWTree():
novelWords += tItem.wordCount
return novelWords, noteWords
def updateItemLayout(self, tHandle, hLevel):
"""Check if the item layout needs updating based on the header
given level.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return False
if tItem.itemClass not in nwLists.CLS_NOVEL:
return False
if hLevel not in ("H1", "H2", "H3", "H4"):
return False
iLayout = tItem.itemLayout
if iLayout in LAYOUT_MAP:
if hLevel in LAYOUT_MAP[iLayout]:
tItem.itemLayout = LAYOUT_MAP[iLayout][hLevel]
logger.debug(
"Changed layout for %s from %s to %s",
tHandle, iLayout.name, tItem.itemLayout.name
)
return True
return False
##
# Tree Structure Methods
##
+3 -9
View File
@@ -196,16 +196,10 @@ class GuiDocSplit(QDialog):
# Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder:
itemLayout = nwItemLayout.NOTE
if srcItem.itemClass == nwItemClass.NOVEL:
if wTitle.startswith("# "):
itemLayout = nwItemLayout.PARTITION
elif wTitle.startswith("## "):
itemLayout = nwItemLayout.CHAPTER
elif wTitle.startswith("### "):
itemLayout = nwItemLayout.SCENE
elif wTitle.startswith("#### "):
itemLayout = nwItemLayout.SCENE
itemLayout = nwItemLayout.DOCUMENT
else:
itemLayout = nwItemLayout.NOTE
wTitle = wTitle.lstrip("#")
wTitle = wTitle.strip()
+1 -7
View File
@@ -90,13 +90,7 @@ class GuiItemEditor(QDialog):
validLayouts = []
if self.theItem.itemType == nwItemType.FILE:
if self.theItem.itemClass in nwLists.CLS_NOVEL:
validLayouts.append(nwItemLayout.TITLE)
validLayouts.append(nwItemLayout.BOOK)
validLayouts.append(nwItemLayout.PAGE)
validLayouts.append(nwItemLayout.PARTITION)
validLayouts.append(nwItemLayout.UNNUMBERED)
validLayouts.append(nwItemLayout.CHAPTER)
validLayouts.append(nwItemLayout.SCENE)
validLayouts.append(nwItemLayout.DOCUMENT)
validLayouts.append(nwItemLayout.NOTE)
else:
validLayouts.append(nwItemLayout.NO_LAYOUT)
+3 -9
View File
@@ -56,15 +56,9 @@ class nwItemClass(Enum):
class nwItemLayout(Enum):
NO_LAYOUT = 0
TITLE = 1
BOOK = 2
PAGE = 3
PARTITION = 4
UNNUMBERED = 5
CHAPTER = 6
SCENE = 7
NOTE = 8
NO_LAYOUT = 0
DOCUMENT = 1
NOTE = 2
# END Enum nwItemLayout
+7 -7
View File
@@ -468,14 +468,14 @@ class GuiDocEditor(QTextEdit):
else:
self.theParent.novelView.updateWordCounts(tHandle)
hLevel = "H0"
if self._docHeaders:
hLevel = self._docHeaders[0][1]
# hLevel = "H0"
# if self._docHeaders:
# hLevel = self._docHeaders[0][1]
if self.theProject.projTree.updateItemLayout(tHandle, hLevel):
self.theParent.treeView.setTreeItemValues(tHandle)
self._nwDocument.writeDocument(docText)
self.docFooter.updateInfo()
# if self.theProject.projTree.updateItemLayout(tHandle, hLevel):
# self.theParent.treeView.setTreeItemValues(tHandle)
# self._nwDocument.writeDocument(docText)
# self.docFooter.updateInfo()
# Update the status bar
self.theParent.setStatus(
+44 -15
View File
@@ -34,6 +34,7 @@ from PyQt5.QtGui import (
)
from nw.constants import nwRegEx, nwUnicode
from nw.common import checkInt
logger = logging.getLogger(__name__)
@@ -127,6 +128,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"keyword": self._makeFormat(self.colKey),
"modifier": self._makeFormat(self.colMod),
"value": self._makeFormat(self.colVal, "underline"),
"codevalue": self._makeFormat(self.colVal),
"codeinval": self._makeFormat(None, "errline"),
}
self.hRules = []
@@ -312,25 +315,32 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# so we force a return here
return
elif theText.startswith("# "): # Header 1
elif theText.startswith(("# ", "#! ", "## ", "##! ", "### ", "#### ")):
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 1, self.hStyles["header1h"])
self.setFormat(1, len(theText), self.hStyles["header1"])
elif theText.startswith("## "): # Header 2
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 2, self.hStyles["header2h"])
self.setFormat(2, len(theText), self.hStyles["header2"])
if theText.startswith("# "): # Header 1
self.setFormat(0, 1, self.hStyles["header1h"])
self.setFormat(1, len(theText), self.hStyles["header1"])
elif theText.startswith("### "): # Header 3
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 3, self.hStyles["header3h"])
self.setFormat(3, len(theText), self.hStyles["header3"])
elif theText.startswith("## "): # Header 2
self.setFormat(0, 2, self.hStyles["header2h"])
self.setFormat(2, len(theText), self.hStyles["header2"])
elif theText.startswith("#### "): # Header 4
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 4, self.hStyles["header4h"])
self.setFormat(4, len(theText), self.hStyles["header4"])
elif theText.startswith("### "): # Header 3
self.setFormat(0, 3, self.hStyles["header3h"])
self.setFormat(3, len(theText), self.hStyles["header3"])
elif theText.startswith("#### "): # Header 4
self.setFormat(0, 4, self.hStyles["header4h"])
self.setFormat(4, len(theText), self.hStyles["header4"])
if theText.startswith("#! "): # Title
self.setFormat(0, 2, self.hStyles["header1h"])
self.setFormat(2, len(theText), self.hStyles["header1"])
elif theText.startswith("##! "): # Unnumbered
self.setFormat(0, 3, self.hStyles["header2h"])
self.setFormat(3, len(theText), self.hStyles["header2"])
elif theText.startswith("%"): # Comments
self.setCurrentBlockState(self.BLOCK_TEXT)
@@ -346,6 +356,25 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(0, tLen, self.hStyles["hidden"])
else: # Text Paragraph
if theText.startswith("["): # Special Command
sText = theText.rstrip()
if sText in ("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]"):
self.setFormat(0, len(theText), self.hStyles["keyword"])
return
elif sText.startswith("[VSPACE:") and sText.endswith("]"):
tLen = len(sText)
tVal = checkInt(sText[8:-1], 0)
self.setFormat(0, 8, self.hStyles["keyword"])
if tVal > 0:
self.setFormat(8, tLen-9, self.hStyles["codevalue"])
else:
self.setFormat(8, tLen-9, self.hStyles["codeinval"])
self.setFormat(tLen-1, tLen, self.hStyles["keyword"])
return
# Regular text
self.setCurrentBlockState(self.BLOCK_TEXT)
for rX, xFmt in self.rxRules:
rxItt = rX.globalMatch(theText, 0)
+6
View File
@@ -187,6 +187,7 @@ class GuiDocViewer(QTextBrowser):
logger.error("Failed to generate preview for document with handle '%s'", tHandle)
nw.logException()
self.setText(self.tr("An error occurred while generating the preview."))
return False
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
@@ -208,7 +209,9 @@ class GuiDocViewer(QTextBrowser):
theCursor.insertText("\t")
if self._docHandle == tHandle:
# This is a refresh, so we set the scrollbar back to where it was
self.verticalScrollBar().setValue(sPos)
self._docHandle = tHandle
self.theProject.setLastViewed(tHandle)
self.docHeader.setTitleFromHandle(self._docHandle)
@@ -539,6 +542,9 @@ class GuiDocViewer(QTextBrowser):
".synopsis {{"
" color: rgb({mColR}, {mColG}, {mColB});"
"}}\n"
".title {{"
" text-align: center;"
"}}\n"
).format(
tColR=self.theTheme.colText[0],
tColG=self.theTheme.colText[1],
+1 -5
View File
@@ -292,11 +292,7 @@ class GuiProjectTree(QTreeWidget):
curTxt = ""
if curTxt == "":
if nwItem.itemLayout == nwItemLayout.CHAPTER:
newText = f"## {nwItem.itemName}\n\n"
elif nwItem.itemLayout == nwItemLayout.UNNUMBERED:
newText = f"## {nwItem.itemName}\n\n"
elif nwItem.itemLayout == nwItemLayout.SCENE:
if nwItem.itemLayout == nwItemLayout.DOCUMENT:
newText = f"### {nwItem.itemName}\n\n"
else:
newText = f"# {nwItem.itemName}\n\n"