""" novelWriter – Text Counting Functions ===================================== File History: Created: 2019-04-22 [0.0.1] standardCounter Rewritten: 2024-02-27 [2.4b1] preProcessText, standardCounter Created: 2024-02-27 [2.4b1] bodyTextCounter This file is a part of novelWriter Copyright (C) 2024 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 . """ # noqa from __future__ import annotations import re from novelwriter.constants import nwRegEx, nwUnicode RX_SC = re.compile(nwRegEx.FMT_SC) RX_SV = re.compile(nwRegEx.FMT_SV) RX_LO = re.compile(r"(?i)(? list[str]: """Strip formatting codes from the text and split into lines.""" if not isinstance(text, str): return [] # We need to treat dashes as word separators for counting words. # The check+replace approach is much faster than direct replace for # large texts, and a bit slower for small texts, but in the latter # case it doesn't really matter. if nwUnicode.U_ENDASH in text: text = text.replace(nwUnicode.U_ENDASH, " ") if nwUnicode.U_EMDASH in text: text = text.replace(nwUnicode.U_EMDASH, " ") ignore = "%@" if keepHeaders else "%@#" result = [] for line in text.splitlines(): line = line.rstrip() if line: if line[0] in ignore: continue if line[0] == ">": line = line.lstrip(">").lstrip(" ") if line: # Above block can return empty line (Issue #1816) if line[-1] == "<": line = line.rstrip("<").rstrip(" ") if "[" in line: # Strip shortcodes and special formatting # RegEx is slow, so we do this only when necessary line = RX_SC.sub("", line) line = RX_SV.sub("", line) line = RX_LO.sub("", line) result.append(line) return result def standardCounter(text: str) -> tuple[int, int, int]: """Return a standard count. A counter that counts paragraphs, words and characters. This is the standard counter that includes headings in the word and character counts. """ cCount = 0 wCount = 0 pCount = 0 prevEmpty = True for line in preProcessText(text): countPara = True if not line: prevEmpty = True continue if line[0] == "#": if line[:5] == "#### ": line = line[5:] countPara = False elif line[:4] == "### ": line = line[4:] countPara = False elif line[:3] == "## ": line = line[3:] countPara = False elif line[:2] == "# ": line = line[2:] countPara = False elif line[:3] == "#! ": line = line[3:] countPara = False elif line[:4] == "##! ": line = line[4:] countPara = False elif line[:5] == "###! ": line = line[5:] countPara = False wCount += len(line.split()) cCount += len(line) if countPara and prevEmpty: pCount += 1 prevEmpty = not countPara return cCount, wCount, pCount def bodyTextCounter(text: str) -> tuple[int, int, int]: """Return a body text count. A counter that counts body text words, characters, and characters without white spaces. """ wCount = 0 cCount = 0 sCount = 0 for line in preProcessText(text, keepHeaders=False): words = line.split() wCount += len(words) cCount += len(line) sCount += len("".join(words)) return wCount, cCount, sCount