Merge branch 'main' into merge_2.0.5
This commit is contained in:
@@ -6,6 +6,7 @@ A collection of tools to create and manipulate documents
|
||||
File History:
|
||||
Created: 2022-10-02 [2.0rc1] DocMerger
|
||||
Created: 2022-10-11 [2.0rc1] DocSplitter
|
||||
Created: 2022-11-03 [2.0rc2] ProjectBuilder
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2023, Veronica Berglyd Olsen
|
||||
|
||||
+22
-13
@@ -49,15 +49,24 @@ class NWIndex:
|
||||
contains the data that isn't stored in the project items themselves.
|
||||
The content of the index is updated every time a file item is saved.
|
||||
|
||||
The primary index data is contained in the ItemIndex class, which
|
||||
contains an IndexItem representing each NWItem. Each IndexItem holds
|
||||
an IndexHeading object for each heading of the item's text.
|
||||
Some information needed by the NWItem object of a project item can
|
||||
only be known after a text file has been scanned by the indexer, so
|
||||
this data is set directly by the indexer class in the NWItem object.
|
||||
|
||||
A reverse index of all tags is contained in the TagsIndex class.
|
||||
This is duplicate information used for quicker lookups from the tags
|
||||
and back to items where they are defined.
|
||||
The primary index data is contained in a single instance of the
|
||||
ItemIndex class. This object contains an IndexItem representing each
|
||||
NWItem of the project. Each IndexItem holds an IndexHeading object
|
||||
for each heading of the item's text.
|
||||
|
||||
The index data is cached in a JSON file between writing sessions.
|
||||
A reverse index of all tags is contained in a single instance of the
|
||||
TagsIndex class. This is duplicate information used for quicker
|
||||
lookups from the tags and back to items where they are defined.
|
||||
|
||||
The index data is cached in a JSON file between writing sessions in
|
||||
order to save startup time. The cached index is validated on input,
|
||||
and a broken flag set if it is not valid. If it is invalid, the
|
||||
loaded data is cleared and it is up to the calling code to initiate
|
||||
a rebuild of the index data.
|
||||
"""
|
||||
|
||||
def __init__(self, project):
|
||||
@@ -283,10 +292,10 @@ class NWIndex:
|
||||
def _scanActive(self, tHandle, theItem, theText, itemTags):
|
||||
"""Scan an active document for meta data.
|
||||
"""
|
||||
nTitle = 0 # Line Number of the previous title
|
||||
cTitle = TT_NONE # Tag of the current title
|
||||
pTitle = TT_NONE # Tag of the previous title
|
||||
firstHeader = True # First header has been seen
|
||||
nTitle = 0 # Line Number of the previous title
|
||||
cTitle = TT_NONE # Tag of the current title
|
||||
pTitle = TT_NONE # Tag of the previous title
|
||||
canSetHeader = True # First header has not yet been set
|
||||
|
||||
theLines = theText.splitlines()
|
||||
for nLine, aLine in enumerate(theLines, start=1):
|
||||
@@ -299,9 +308,9 @@ class NWIndex:
|
||||
if hDepth == "H0":
|
||||
continue
|
||||
|
||||
if firstHeader:
|
||||
if canSetHeader:
|
||||
theItem.setMainHeading(hDepth)
|
||||
firstHeader = False
|
||||
canSetHeader = False
|
||||
|
||||
cTitle = self._itemIndex.addItemHeading(tHandle, nLine, hDepth, hText)
|
||||
if cTitle != TT_NONE:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
novelWriter – Manuscript Build Init File
|
||||
"""
|
||||
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
novelWriter – Build Document Tool
|
||||
=================================
|
||||
A class to build one or more novelWriter files to a single document
|
||||
|
||||
File History:
|
||||
Created: 2022-12-01 [2.1b1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2023, Veronica Berglyd Olsen
|
||||
|
||||
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/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from PyQt5.QtGui import QFont, QFontInfo
|
||||
|
||||
from novelwriter.error import formatException
|
||||
from novelwriter.core.tomd import ToMarkdown
|
||||
from novelwriter.core.toodt import ToOdt
|
||||
from novelwriter.core.tohtml import ToHtml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWBuildDocument:
|
||||
|
||||
def __init__(self, project):
|
||||
|
||||
self._conf = novelwriter.CONFIG
|
||||
self._project = project
|
||||
self._build = {}
|
||||
self._documents = []
|
||||
self._error = None
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
return self._error
|
||||
|
||||
@property
|
||||
def buildLength(self):
|
||||
return len(self._documents)
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setBuildConfig(self, config):
|
||||
"""Set the build config dictionary.
|
||||
"""
|
||||
self._build = config
|
||||
return
|
||||
|
||||
def addDocument(self, tHandle):
|
||||
"""Add a document to the build queue.
|
||||
"""
|
||||
self._documents.append(tHandle)
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def iterBuildOpenDocument(self, savePath, isFlat):
|
||||
"""Build an Open Document file.
|
||||
"""
|
||||
makeOdt = ToOdt(self._project, isFlat=isFlat)
|
||||
self._setupBuild(makeOdt)
|
||||
makeOdt.initDocument()
|
||||
|
||||
for i, tHandle in enumerate(self._documents):
|
||||
yield i, self._doBuild(makeOdt, tHandle)
|
||||
|
||||
makeOdt.closeDocument()
|
||||
|
||||
self._error = None
|
||||
try:
|
||||
if isFlat:
|
||||
makeOdt.saveFlatXML(savePath)
|
||||
else:
|
||||
makeOdt.saveOpenDocText(savePath)
|
||||
except Exception as exc:
|
||||
self._error = formatException(exc)
|
||||
|
||||
return
|
||||
|
||||
def iterBuildHTML(self, savePath):
|
||||
"""Build an HTML file.
|
||||
"""
|
||||
makeHtml = ToHtml(self._project)
|
||||
self._setupBuild(makeHtml)
|
||||
|
||||
if self._build.get("process.replaceTabs", False):
|
||||
makeHtml.replaceTabs()
|
||||
|
||||
for i, tHandle in enumerate(self._documents):
|
||||
yield i, self._doBuild(makeHtml, tHandle)
|
||||
|
||||
self._error = None
|
||||
try:
|
||||
makeHtml.saveHTML5(savePath)
|
||||
except Exception as exc:
|
||||
self._error = formatException(exc)
|
||||
|
||||
return
|
||||
|
||||
def iterBuildMarkdown(self, savePath, extendedMd):
|
||||
"""Build a Markdown file.
|
||||
"""
|
||||
makeMd = ToMarkdown(self._project)
|
||||
self._setupBuild(makeMd)
|
||||
|
||||
if extendedMd:
|
||||
makeMd.setGitHubMarkdown()
|
||||
else:
|
||||
makeMd.setStandardMarkdown()
|
||||
|
||||
if self._build.get("process.replaceTabs", False):
|
||||
makeMd.replaceTabs(nSpaces=4, spaceChar=" ")
|
||||
|
||||
for i, tHandle in enumerate(self._documents):
|
||||
yield i, self._doBuild(makeMd, tHandle)
|
||||
|
||||
self._error = None
|
||||
try:
|
||||
makeMd.saveMarkdown(savePath)
|
||||
except Exception as exc:
|
||||
self._error = formatException(exc)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _setupBuild(self, bldObj):
|
||||
"""Configure the build object.
|
||||
"""
|
||||
# Get Settings
|
||||
fmtTitle = self._build.get("format.fmtTitle", "%title%")
|
||||
fmtChapter = self._build.get("format.fmtChapter", "%title%")
|
||||
fmtUnnumbered = self._build.get("format.fmtUnnumbered", "%title%")
|
||||
fmtScene = self._build.get("format.fmtScene", "%title%")
|
||||
fmtSection = self._build.get("format.fmtSection", "%title%")
|
||||
buildLang = self._build.get("format.buildLang", "en_GB")
|
||||
hideScene = self._build.get("format.hideScene", False)
|
||||
hideSection = self._build.get("format.hideSection", False)
|
||||
textFont = self._build.get("format.textFont", self._conf.textFont)
|
||||
textSize = self._build.get("format.textSize", self._conf.textSize)
|
||||
lineHeight = self._build.get("format.lineHeight", 1.15)
|
||||
justifyText = self._build.get("format.justifyText", False)
|
||||
noStyling = self._build.get("format.noStyling", False)
|
||||
replaceUCode = self._build.get("format.replaceUCode", False)
|
||||
incSynopsis = self._build.get("filter.includeSynopsis", False)
|
||||
incComments = self._build.get("filter.includeComments", False)
|
||||
incKeywords = self._build.get("filter.includeKeywords", False)
|
||||
includeBody = self._build.get("filter.includeBody", True)
|
||||
|
||||
# The language lookup dict is reloaded if needed
|
||||
self._project.setProjectLang(buildLang)
|
||||
|
||||
# Get font information
|
||||
fontInfo = QFontInfo(QFont(textFont, textSize))
|
||||
textFixed = fontInfo.fixedPitch()
|
||||
|
||||
bldObj.setTitleFormat(fmtTitle)
|
||||
bldObj.setChapterFormat(fmtChapter)
|
||||
bldObj.setUnNumberedFormat(fmtUnnumbered)
|
||||
bldObj.setSceneFormat(fmtScene, hideScene)
|
||||
bldObj.setSectionFormat(fmtSection, hideSection)
|
||||
|
||||
bldObj.setFont(textFont, textSize, textFixed)
|
||||
bldObj.setJustify(justifyText)
|
||||
bldObj.setLineHeight(lineHeight)
|
||||
|
||||
bldObj.setSynopsis(incSynopsis)
|
||||
bldObj.setComments(incComments)
|
||||
bldObj.setKeywords(incKeywords)
|
||||
bldObj.setBodyText(includeBody)
|
||||
|
||||
if isinstance(bldObj, ToHtml):
|
||||
bldObj.setStyles(not noStyling)
|
||||
bldObj.setReplaceUnicode(replaceUCode)
|
||||
|
||||
if isinstance(bldObj, ToOdt):
|
||||
bldObj.setColourHeaders(not noStyling)
|
||||
bldObj.setLanguage(buildLang)
|
||||
|
||||
return
|
||||
|
||||
def _doBuild(self, bldObj, tHandle):
|
||||
"""Build a single document and add it to the build object.
|
||||
"""
|
||||
self._error = None
|
||||
tItem = self._project.tree[tHandle]
|
||||
if tItem is None:
|
||||
self._error = f"Build: Unknown item '{tHandle}'"
|
||||
logger.error(self._error)
|
||||
return False
|
||||
|
||||
try:
|
||||
if tItem.isRootType() and not tItem.isNovelLike():
|
||||
bldObj.addRootHeading(tItem.itemHandle)
|
||||
bldObj.doConvert()
|
||||
elif tItem.isFileType():
|
||||
bldObj.setText(tHandle)
|
||||
bldObj.doPreProcessing()
|
||||
bldObj.tokenizeText()
|
||||
bldObj.doHeaders()
|
||||
bldObj.doConvert()
|
||||
bldObj.doPostProcessing()
|
||||
else:
|
||||
logger.info(f"Build: Skipping '{tHandle}'")
|
||||
|
||||
except Exception:
|
||||
self._error = f"Build: Failed to build '{tHandle}'"
|
||||
logger.error(self._error)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# END Class NWBuildDocument
|
||||
@@ -682,7 +682,7 @@ class GuiBuildNovel(QDialog):
|
||||
return
|
||||
|
||||
def _doBuild(self, bldObj, isPreview=False, doConvert=True):
|
||||
"""Rund the build with a specific build object.
|
||||
"""Run the build with a specific build object.
|
||||
"""
|
||||
tStart = int(time())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user