def unpackXML(self, xParent): """Unpack an XML tree and set the class values. """ theLabels = [] theColours = [] for xChild in xParent: theLabels.append(xChild.text) cR = checkInt(xChild.attrib.get("red", 0), 0, False) cG = checkInt(xChild.attrib.get("green", 0), 0, False) cB = checkInt(xChild.attrib.get("blue", 0), 0, False) theColours.append((cR, cG, cB)) if len(theLabels) > 0: self._theLabels = [] self._theColours = [] self._theCounts = [] self._theIcons = [] self._theMap = {} self._theLength = 0 self._theIndex = 0 for n in range(len(theLabels)): self.addEntry(theLabels[n], theColours[n]) return True
def setCounts(self, charCount, wordCount, paraCount): """Set the character, word and paragraph count. Make sure the value is an integer and is not smaller than 0. """ self._charCount = max(0, checkInt(charCount, 0)) self._wordCount = max(0, checkInt(wordCount, 0)) self._paraCount = max(0, checkInt(paraCount, 0)) return
def showItem(self, tHandle, sTitle): """Update the content of the tree with the given handle and line number pointing to a header. """ pIndex = self.theProject.index nwItem = self.theProject.tree[tHandle] novIdx = pIndex.getNovelData(tHandle, sTitle) theRefs = pIndex.getReferences(tHandle, sTitle) if nwItem is None or novIdx is None: return False if novIdx.level in self.LVL_MAP: self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP[novIdx.level])) else: self.titleLabel.setText("<b>%s</b>" % self.tr("Title")) self.titleValue.setText(novIdx.title) itemStatus, _ = nwItem.getImportStatus() self.fileValue.setText(nwItem.itemName) self.itemValue.setText(itemStatus) cC = checkInt(novIdx.charCount, 0) wC = checkInt(novIdx.wordCount, 0) pC = checkInt(novIdx.paraCount, 0) self.cCValue.setText(f"{cC:n}") self.wCValue.setText(f"{wC:n}") self.pCValue.setText(f"{pC:n}") self.synopValue.setText(novIdx.synopsis) self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY)) self.focKeyValue.setText( self._formatTags(theRefs, nwKeyWords.FOCUS_KEY)) self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY)) self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY)) self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY)) self.wldKeyValue.setText( self._formatTags(theRefs, nwKeyWords.WORLD_KEY)) self.objKeyValue.setText( self._formatTags(theRefs, nwKeyWords.OBJECT_KEY)) self.entKeyValue.setText( self._formatTags(theRefs, nwKeyWords.ENTITY_KEY)) self.cstKeyValue.setText( self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY)) return True
def setOrder(self, order): """Set the item order, and ensure that it is valid. This value is purely a meta value, and not actually used by novelWriter at the moment. """ self._order = checkInt(order, 0) return
def getInt(self, group, name, default): """Return the value as an int, if it exists. Otherwise, return the default value. """ if group in self._theState: return checkInt(self._theState[group].get(name, default), default) return default
def unpackXML(self, xParent): """Unpack an XML tree and set the class values. """ self._store = {} self._reverse = {} self._default = None for xChild in xParent: key = xChild.attrib.get("key", None) name = xChild.text.strip() count = max(checkInt(xChild.attrib.get("count", 0), 0), 0) red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255) green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255) blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255) self.write(key, name, (red, green, blue), count) return True
def getSelectedHandle(self): """Get the currently selected handle. If multiple items are selected, return the first. """ selItem = self.selectedItems() tHandle = None tLine = 0 if selItem: tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE) sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE) tLine = checkInt(sTitle[1:], 1) - 1 return tHandle, tLine
def getSelectedHandle(self): """Get the currently selected handle. If multiple items are selected, return the first. """ selItem = self.selectedItems() tHandle = None tLine = 0 if selItem: tHandle = selItem[0].data(self.C_TITLE, Qt.UserRole)[0] tLine = checkInt(selItem[0].data(self.C_TITLE, Qt.UserRole)[1], 1) - 1 return tHandle, tLine
def getSelectedHandle(self): """Get the currently selected handle. If multiple items are selected, return the first. """ selItem = self.selectedItems() tHandle = None tLine = 0 if selItem: tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1 return tHandle, tLine
def testBaseCommon_CheckInt(): """Test the checkInt function. """ assert checkInt(None, 3, True) is None assert checkInt("None", 3, True) is None assert checkInt(None, 3, False) == 3 assert checkInt(1, 3, False) == 1 assert checkInt(1.0, 3, False) == 1 assert checkInt(True, 3, False) == 1
def setParaCount(self, count): """Set the paragraph count, and ensure that it is an integer. """ self._paraCount = max(0, checkInt(count, 0)) return
def setCursorPos(self, position): """Set the cursor position, and ensure that it is an integer. """ self._cursorPos = max(0, checkInt(position, 0)) return
def setWordCount(self, count): """Set the word count, and ensure that it is an integer. """ self._wordCount = max(0, checkInt(count, 0)) return
def setCharCount(self, count): """Set the character count, and ensure that it is an integer. """ self._charCount = max(0, checkInt(count, 0)) return
def tokenizeText(self): """Scan the text for either lines starting with specific characters that indicate headers, comments, commands etc, or 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 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 the file where this block occurred 3: The text content of the block, without leading tags 4: The internal formatting map of the text, self.FMT_* 5: The style of the block, self.A_* """ # RegExes for adding formatting tags within text lines rxFormats = [ (QRegularExpression(nwRegEx.FMT_EI), [None, self.FMT_I_B, None, self.FMT_I_E]), (QRegularExpression(nwRegEx.FMT_EB), [None, self.FMT_B_B, None, self.FMT_B_E]), (QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]), ] self._theTokens = [] tmpMarkdown = [] nLine = 0 breakNext = False for aLine in self._theText.splitlines(): nLine += 1 sLine = aLine.strip() # Check for blank lines if len(sLine) == 0: self._theTokens.append(( 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, sAlign )) if self._doSynopsis and self._keepMarkdown: tmpMarkdown.append("%s\n" % aLine) else: self._theTokens.append(( 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, sAlign )) if self._doKeywords and self._keepMarkdown: tmpMarkdown.append("%s\n" % aLine) elif aLine[:2] == "# ": if self._isNovel: sAlign |= self.A_CENTRE sAlign |= self.A_PBB self._theTokens.append(( self.T_HEAD1, nLine, aLine[2:].strip(), None, sAlign )) if self._keepMarkdown: tmpMarkdown.append("%s\n" % aLine) elif aLine[:3] == "## ": if self._isNovel: sAlign |= self.A_PBB self._theTokens.append(( 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, sAlign )) if self._keepMarkdown: tmpMarkdown.append("%s\n" % aLine) elif aLine[:5] == "#### ": self._theTokens.append(( 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 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 sAlign |= self.A_PBB else: tStyle = self.T_HEAD2 self._theTokens.append(( tStyle, nLine, aLine[4:].strip(), None, sAlign )) if self._keepMarkdown: tmpMarkdown.append("%s\n" % aLine) else: if not self._doBodyText: # Skip all body text continue # Check Alignment and Indentation alnLeft = False alnRight = False indLeft = False indRight = False if aLine.startswith(">>"): alnRight = True aLine = aLine[2:].lstrip(" ") elif aLine.startswith(">"): indLeft = True aLine = aLine[1:].lstrip(" ") if aLine.endswith("<<"): alnLeft = True aLine = aLine[:-2].rstrip(" ") elif aLine.endswith("<"): indRight = True aLine = aLine[:-1].rstrip(" ") if alnLeft and alnRight: sAlign |= self.A_CENTRE elif alnLeft: sAlign |= self.A_LEFT elif alnRight: sAlign |= self.A_RIGHT if indLeft: sAlign |= self.A_IND_L if indRight: sAlign |= self.A_IND_R # Otherwise we use RegEx to find formatting tags within a line of text fmtPos = [] for theRX, theKeys in rxFormats: rxThis = theRX.globalMatch(aLine, 0) while rxThis.hasNext(): rxMatch = rxThis.next() for n in range(1, len(theKeys)): if theKeys[n] is not None: xPos = rxMatch.capturedStart(n) xLen = rxMatch.capturedLength(n) fmtPos.append([xPos, xLen, theKeys[n]]) # Save the line as is, but append the array of formatting locations # sorted by position fmtPos = sorted(fmtPos, key=itemgetter(0)) self._theTokens.append(( self.T_TEXT, nLine, aLine, fmtPos, sAlign )) if self._keepMarkdown: tmpMarkdown.append("%s\n" % aLine) # If we have content, turn off the first page flag if self._isFirst and self._theTokens: self._isFirst = False # Make sure the token array doesn't start with a page break # on the very first page, adding a blank first page. if self._theTokens[0][4] & self.A_PBB: tToken = self._theTokens[0] self._theTokens[0] = ( tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] & ~self.A_PBB ) # Always add an empty line at the end of the file self._theTokens.append(( self.T_EMPTY, nLine, "", None, self.A_NONE )) if self._keepMarkdown: tmpMarkdown.append("\n") if self._keepMarkdown: self._theMarkdown.append("".join(tmpMarkdown)) # Second Pass # =========== # Some items need a second pass 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): if n > 0: pToken = self._theTokens[n-1] if n < tCount - 1: nToken = self._theTokens[n+1] if tToken[0] == self.T_KEYWORD: aStyle = tToken[4] if pToken[0] == self.T_KEYWORD: aStyle |= self.A_Z_TOPMRG if nToken[0] == self.T_KEYWORD: aStyle |= self.A_Z_BTMMRG self._theTokens[n] = ( tToken[0], tToken[1], tToken[2], tToken[3], aStyle ) return
def highlightBlock(self, theText): """Highlight a single block. Prefer to check first character for all formats that are defined by their initial characters. This is significantly faster than running the regex checks used for text paragraphs. """ self.setCurrentBlockState(self.BLOCK_NONE) if self.theHandle is None or not theText: return if theText.startswith("@"): # Keywords and commands self.setCurrentBlockState(self.BLOCK_META) pIndex = self.theProject.index tItem = self.mainGui.theProject.tree[self.theHandle] isValid, theBits, thePos = pIndex.scanThis(theText) isGood = pIndex.checkThese(theBits, tItem) if isValid: for n, theBit in enumerate(theBits): xPos = thePos[n] xLen = len(theBit) if isGood[n]: if n == 0: self.setFormat(xPos, xLen, self.hStyles["keyword"]) else: self.setFormat(xPos, xLen, self.hStyles["value"]) else: kwFmt = self.format(xPos) kwFmt.setUnderlineColor(self.colError) kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(xPos, xLen, kwFmt) # We never want to run the spell checker on keyword/values, # so we force a return here return elif theText.startswith(("# ", "#! ", "## ", "##! ", "### ", "#### ")): self.setCurrentBlockState(self.BLOCK_TITLE) if theText.startswith("# "): # Header 1 self.setFormat(0, 1, self.hStyles["header1h"]) self.setFormat(1, len(theText), self.hStyles["header1"]) elif theText.startswith("## "): # Header 2 self.setFormat(0, 2, self.hStyles["header2h"]) self.setFormat(2, len(theText), self.hStyles["header2"]) 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) toCheck = theText[1:].lstrip() synTag = toCheck[:9].lower() tLen = len(theText) cLen = len(toCheck) cOff = tLen - cLen if synTag == "synopsis:": self.setFormat(0, cOff+9, self.hStyles["modifier"]) self.setFormat(cOff+9, tLen, self.hStyles["hidden"]) else: 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) while rxItt.hasNext(): rxMatch = rxItt.next() for xM in xFmt: xPos = rxMatch.capturedStart(xM) xLen = rxMatch.capturedLength(xM) for x in range(xPos, xPos+xLen): spFmt = self.format(x) if spFmt != self.hStyles["hidden"]: spFmt.merge(xFmt[xM]) self.setFormat(x, 1, spFmt) if not self.spellCheck: return rxSpell = self.spellRx.globalMatch(theText, 0) while rxSpell.hasNext(): rxMatch = rxSpell.next() if not self.spEnchant.checkWord(rxMatch.captured(0)): if rxMatch.captured(0).isupper() or rxMatch.captured(0).isnumeric(): continue xPos = rxMatch.capturedStart(0) xLen = rxMatch.capturedLength(0) for x in range(xPos, xPos+xLen): spFmt = self.format(x) spFmt.setUnderlineColor(self.colSpell) spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(x, 1, spFmt) return
def _loadLogFile(self): """Load the content of the log file into a buffer. """ logger.debug("Loading session log file") self.logData = [] self.wordOffset = 0 ttNovel = 0 ttNotes = 0 ttTime = 0 ttIdle = 0 logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS) if not os.path.isfile(logFile): logger.info("This project has no writing stats logfile") return False try: with open(logFile, mode="r", encoding="utf-8") as inFile: for inLine in inFile: if inLine.startswith("#"): if inLine.startswith("# Offset"): self.wordOffset = checkInt(inLine[9:].strip(), 0) logger.verbose( "Initial word count when log was started is %d" % self.wordOffset) continue inData = inLine.split() if len(inData) < 6: continue dStart = datetime.fromisoformat(" ".join(inData[0:2])) dEnd = datetime.fromisoformat(" ".join(inData[2:4])) sIdle = 0 if len(inData) > 6: sIdle = checkInt(inData[6], 0) tDiff = dEnd - dStart sDiff = tDiff.total_seconds() ttTime += sDiff ttIdle += sIdle wcNovel = int(inData[4]) wcNotes = int(inData[5]) ttNovel = wcNovel ttNotes = wcNotes self.logData.append( (dStart, sDiff, wcNovel, wcNotes, sIdle)) except Exception as exc: self.theParent.makeAlert( self.tr("Failed to read session log file."), nwAlert.ERROR, exception=exc) return False ttWords = ttNovel + ttNotes self.labelTotal.setText(formatTime(round(ttTime))) self.labelIdleT.setText(formatTime(round(ttIdle))) self.novelWords.setText(f"{ttNovel:n}") self.notesWords.setText(f"{ttNotes:n}") self.totalWords.setText(f"{ttWords:n}") return True