Add sub/superscript and partial word emphasis (#1540)

This commit is contained in:
Veronica Berglyd Olsen
2023-11-06 11:54:49 +01:00
committed by GitHub
33 changed files with 1345 additions and 997 deletions
+5
View File
@@ -159,6 +159,7 @@ A syntax theme ``.conf`` file consists of the follwing settings:
doublequotes = 0, 0, 0
singlequotes = 0, 0, 0
hidden = 0, 0, 0
shortcode = 0, 0, 0
keyword = 0, 0, 0
value = 0, 0, 0
spellcheckline = 0, 0, 0
@@ -169,3 +170,7 @@ A syntax theme ``.conf`` file consists of the follwing settings:
In the Main section, you must define at least the ``name`` setting. The Syntax colour values are
RGB numbers of the format ``r, g, b`` where each is an integer from to 255. Omitted values default
to black, except ``background`` which defaults to white,
.. versionadded:: 2.2
The `shortcode` syntax colour entry was added, so you need to update your custom themes if you
made any before version 2.2.
@@ -17,6 +17,7 @@ straightquotes = 200, 46, 0
doublequotes = 184, 200, 0
singlequotes = 136, 200, 0
hidden = 150, 150, 150
shortcode = 0, 155, 200
keyword = 200, 46, 0
value = 184, 200, 0
spellcheckline = 200, 46, 0
@@ -17,6 +17,7 @@ straightquotes = 200, 0, 0
doublequotes = 0, 0, 200
singlequotes = 0, 0, 200
hidden = 100, 100, 100
shortcode = 0, 100, 0
keyword = 200, 50, 50
value = 50, 150, 50
spellcheckline = 200, 0, 0
+1
View File
@@ -17,6 +17,7 @@ straightquotes = 200, 200, 200
doublequotes = 200, 200, 200
singlequotes = 200, 200, 200
hidden = 150, 150, 150
shortcode = 225, 225, 225
keyword = 225, 225, 225
value = 200, 200, 200
spellcheckline = 200, 46, 0
@@ -17,6 +17,7 @@ straightquotes = 20, 20, 20
doublequotes = 20, 20, 20
singlequotes = 20, 20, 20
hidden = 100, 100, 100
shortcode = 0, 0, 0
keyword = 0, 0, 0
value = 20, 20, 20
spellcheckline = 200, 0, 0
+1
View File
@@ -37,6 +37,7 @@ straightquotes = 222, 61, 58
doublequotes = 8, 145, 106
singlequotes = 255, 235, 149
hidden = 152, 159, 177
shortcode = 40, 142, 215
keyword = 222, 61, 58
value = 150, 74, 193
spellcheckline = 222, 61, 58
+1
View File
@@ -37,6 +37,7 @@ straightquotes = 247, 140, 108
doublequotes = 173, 219, 103
singlequotes = 255, 235, 149
hidden = 99, 119, 119
shortcode = 130, 170, 255
keyword = 247, 140, 108
value = 199, 146, 234
spellcheckline = 247, 140, 108
@@ -17,6 +17,7 @@ straightquotes = 211, 54, 130
doublequotes = 42, 161, 152
singlequotes = 42, 161, 152
hidden = 147, 161, 161
shortcode = 147, 161, 161
keyword = 133, 153, 0
value = 203, 75, 22
spellcheckline = 203, 75, 22
@@ -17,6 +17,7 @@ straightquotes = 211, 54, 130
doublequotes = 42, 161, 152
singlequotes = 42, 161, 152
hidden = 88, 110, 117
shortcode = 88, 110, 117
keyword = 133, 153, 0
value = 203, 75, 22
spellcheckline = 203, 75, 22
+1
View File
@@ -37,6 +37,7 @@ straightquotes = 240, 40, 41
doublequotes = 113, 140, 0
singlequotes = 234, 183, 0
hidden = 142, 144, 140
shortcode = 66, 113, 174
keyword = 240, 40, 41
value = 137, 89, 168
spellcheckline = 240, 40, 41
@@ -37,6 +37,7 @@ straightquotes = 204, 102, 102
doublequotes = 181, 189, 104
singlequotes = 240, 198, 116
hidden = 150, 152, 150
shortcode = 129, 162, 190
keyword = 204, 102, 102
value = 178, 148, 187
spellcheckline = 204, 102, 102
@@ -37,6 +37,7 @@ straightquotes = 255, 157, 164
doublequotes = 209, 241, 169
singlequotes = 255, 238, 173
hidden = 114, 133, 183
shortcode = 187, 218, 255
keyword = 255, 157, 164
value = 235, 187, 255
spellcheckline = 255, 157, 164
@@ -37,6 +37,7 @@ straightquotes = 213, 78, 83
doublequotes = 185, 202, 74
singlequotes = 231, 197, 71
hidden = 150, 152, 150
shortcode = 122, 166, 218
keyword = 213, 78, 83
value = 195, 151, 216
spellcheckline = 213, 78, 83
@@ -37,6 +37,7 @@ straightquotes = 242, 119, 122
doublequotes = 153, 204, 153
singlequotes = 255, 204, 102
hidden = 153, 153, 153
shortcode = 102, 153, 204
keyword = 242, 119, 122
value = 204, 153, 204
spellcheckline = 242, 119, 122
+7
View File
@@ -23,6 +23,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import re
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline
@@ -59,6 +61,11 @@ class nwRegEx:
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:i|b|s|u|sup|sub)\])"
FMT_SV = r"(?<!\\)(\[(?i)(?:fn|footnote):)(.+?)(?<!\\)(\])"
# Pre-Compiled RegEx
RX_SC = re.compile(FMT_SC)
# END Class nwRegEx
+1 -1
View File
@@ -184,7 +184,7 @@ class NWBuildDocument:
filtered = self._setupBuild(makeObj)
if extendedMd:
makeObj.setGitHubMarkdown()
makeObj.setExtendedMarkdown()
else:
makeObj.setStandardMarkdown()
+8 -3
View File
@@ -38,7 +38,7 @@ from pathlib import Path
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders
from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.item import NWItem
@@ -1229,6 +1229,10 @@ def countWords(text: str) -> tuple[int, int, int]:
if nwUnicode.U_EMDASH in text:
text = text.replace(nwUnicode.U_EMDASH, " ")
# Strip shortcodes
if "[" in text:
text = nwRegEx.RX_SC.sub("", text)
for line in text.splitlines():
countPara = True
@@ -1241,9 +1245,10 @@ def countWords(text: str) -> tuple[int, int, int]:
continue
if line[0] == "[":
if line.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")):
check = line.lower()
if check.startswith(("[newpage]", "[new page]", "[vspace]")):
continue
elif line.startswith("[VSPACE:") and line.endswith("]"):
elif check.startswith("[vspace:") and line.endswith("]"):
continue
elif line[0] == "#":
+23 -23
View File
@@ -31,7 +31,7 @@ from pathlib import Path
from novelwriter import CONFIG
from novelwriter.common import formatTimeStamp
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode, trConst
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import Tokenizer, stripEscape
@@ -131,6 +131,8 @@ class ToHtml(Tokenizer):
self.FMT_I_E: "</i>",
self.FMT_D_B: "<span style='text-decoration: line-through;'>",
self.FMT_D_E: "</span>",
self.FMT_U_B: "<u>",
self.FMT_U_E: "</u>",
}
else:
htmlTags = { # HTML5 (for export)
@@ -140,8 +142,15 @@ class ToHtml(Tokenizer):
self.FMT_I_E: "</em>",
self.FMT_D_B: "<del>",
self.FMT_D_E: "</del>",
self.FMT_U_B: "<span style='text-decoration: underline;'>",
self.FMT_U_E: "</span>",
}
htmlTags[self.FMT_SUP_B] = "<sup>"
htmlTags[self.FMT_SUP_E] = "</sup>"
htmlTags[self.FMT_SUB_B] = "<sub>"
htmlTags[self.FMT_SUB_E] = "</sub>"
if self._isNovel and self._genMode != self.M_PREVIEW:
# For story files, we bump the titles one level up
h1Cl = " class='title'"
@@ -172,18 +181,16 @@ class ToHtml(Tokenizer):
for c in tText:
if c == "<":
cText.append("&lt;")
tFormat = [[a + 3 if a > i else a, b, c] for a, b, c in tFormat]
tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
i += 4
elif c == ">":
cText.append("&gt;")
tFormat = [[a + 3 if a > i else a, b, c] for a, b, c in tFormat]
tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
i += 4
else:
cText.append(c)
i += 1
tText = "".join(cText)
else:
# If we don't have formatting, we can do a plain replace
tText = tText.replace("<", "&lt;").replace(">", "&gt;")
@@ -275,8 +282,8 @@ class ToHtml(Tokenizer):
tTemp = tText
if pStyle is None:
pStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
for pos, fmt in reversed(tFormat):
tTemp = f"{tTemp[:pos]}{htmlTags[fmt]}{tTemp[pos:]}"
para.append(stripEscape(tTemp.rstrip()))
elif tType == self.T_SYNOPSIS and self._doSynopsis:
@@ -467,25 +474,18 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, text: str) -> str:
"""Apply HTML formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text)
if not valid or not bits:
if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
return ""
result = ""
tags = []
if bits[0] in nwLabels.KEY_NAME:
result += f"<span class='tags'>{nwLabels.KEY_NAME[bits[0]]}:</span> "
if len(bits) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>"
result = f"<span class='tags'>{trConst(nwLabels.KEY_NAME[bits[0]])}:</span> "
if len(bits) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>"
else:
if self._genMode == self.M_PREVIEW:
result += ", ".join(f"<a href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:])
else:
if self._genMode == self.M_PREVIEW:
for tTag in bits[1:]:
tags.append(f"<a href='#{bits[0][1:]}={tTag}'>{tTag}</a>")
result += ", ".join(tags)
else:
for tTag in bits[1:]:
tags.append(f"<a href='#tag_{tTag}'>{tTag}</a>")
result += ", ".join(tags)
result += ", ".join(f"<a href='#tag_{t}'>{t}</a>" for t in bits[1:])
return result
+85 -41
View File
@@ -31,7 +31,6 @@ import logging
from abc import ABC, abstractmethod
from time import time
from pathlib import Path
from operator import itemgetter
from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression
@@ -43,15 +42,14 @@ from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__)
ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""}
RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL)
def stripEscape(text) -> str:
"""Helper function to strip escaped Markdown characters from
paragraph text.
"""
"""Strip escaped Markdown characters from paragraph text."""
if "\\" in text:
# Checking first is slightly slower when there are escaped
# characters in the text, but significantly faster when not
return text.replace(r"\*", "*").replace(r"\~", "~").replace(r"\_", "_")
return RX_ESC.sub(lambda x: ESCAPES[x.group(0)], text)
return text
@@ -65,12 +63,18 @@ class Tokenizer(ABC):
"""
# In-Text Format
FMT_B_B = 1 # Begin bold
FMT_B_E = 2 # End bold
FMT_I_B = 3 # Begin italics
FMT_I_E = 4 # End italics
FMT_D_B = 5 # Begin strikeout
FMT_D_E = 6 # End strikeout
FMT_B_B = 1 # Begin bold
FMT_B_E = 2 # End bold
FMT_I_B = 3 # Begin italics
FMT_I_E = 4 # End italics
FMT_D_B = 5 # Begin strikeout
FMT_D_E = 6 # End strikeout
FMT_U_B = 7 # Begin underline
FMT_U_E = 8 # End underline
FMT_SUP_B = 9 # Begin superscript
FMT_SUP_E = 10 # End superscript
FMT_SUB_B = 11 # Begin subscript
FMT_SUB_E = 12 # End subscript
# Block Type
T_EMPTY = 1 # Empty line (new paragraph)
@@ -166,6 +170,24 @@ class Tokenizer(ABC):
# Cached Translations
self._trSynopsis = self.tr("Synopsis")
# Format RegEx
self._rxMarkdown = [
(QRegularExpression(nwRegEx.FMT_EI), [0, self.FMT_I_B, 0, self.FMT_I_E]),
(QRegularExpression(nwRegEx.FMT_EB), [0, self.FMT_B_B, 0, self.FMT_B_E]),
(QRegularExpression(nwRegEx.FMT_ST), [0, self.FMT_D_B, 0, self.FMT_D_E]),
]
self._rxShortCodes = QRegularExpression(nwRegEx.FMT_SC)
self._rxShortCodeVals = QRegularExpression(nwRegEx.FMT_SV)
self._shortCodeFmt = {
"[i]": self.FMT_I_B, "[/i]": self.FMT_I_E,
"[b]": self.FMT_B_B, "[/b]": self.FMT_B_E,
"[s]": self.FMT_D_B, "[/s]": self.FMT_D_E,
"[u]": self.FMT_U_B, "[/u]": self.FMT_U_E,
"[sup]": self.FMT_SUP_B, "[/sup]": self.FMT_SUP_E,
"[sub]": self.FMT_SUB_B, "[/sub]": self.FMT_SUB_E,
}
return
##
@@ -387,19 +409,12 @@ class Tokenizer(ABC):
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._tokens = []
tmpMarkdown = []
nHead = 0
breakNext = False
for aLine in self._text.splitlines():
sLine = aLine.strip()
sLine = aLine.strip().lower()
# Check for blank lines
if len(sLine) == 0:
@@ -422,18 +437,21 @@ class Tokenizer(ABC):
if aLine[0] == "[":
# Parse special formatting line
# This must be a separate if statement, as it may not
# reach a continue statement and must thefore proceed to
# check other formats.
if sLine in ("[NEWPAGE]", "[NEW PAGE]"):
if sLine in ("[newpage]", "[new page]"):
breakNext = True
continue
elif sLine == "[VSPACE]":
elif sLine == "[vspace]":
self._tokens.append(
(self.T_SKIP, nHead, "", None, sAlign)
)
continue
elif sLine.startswith("[VSPACE:") and sLine.endswith("]"):
elif sLine.startswith("[vspace:") and sLine.endswith("]"):
nSkip = checkInt(sLine[8:-1], 0)
if nSkip >= 1:
self._tokens.append(
@@ -445,7 +463,7 @@ class Tokenizer(ABC):
]
continue
elif aLine[0] == "%":
if aLine[0] == "%":
cLine = aLine[1:].lstrip()
synTag = cLine[:9].lower()
if synTag == "synopsis:":
@@ -570,23 +588,10 @@ class Tokenizer(ABC):
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))
# Process formats
tLine, fmtPos = self._extractFormats(aLine)
self._tokens.append((
self.T_TEXT, nHead, aLine, fmtPos, sAlign
self.T_TEXT, nHead, tLine, fmtPos, sAlign
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -763,6 +768,45 @@ class Tokenizer(ABC):
json.dump(data, fObj, indent=2)
return
##
# Internal Functions
##
def _extractFormats(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Extract format markers from a text paragraph."""
temp = []
# Match Markdown
for regEx, fmts in self._rxMarkdown:
rxItt = regEx.globalMatch(text, 0)
while rxItt.hasNext():
rxMatch = rxItt.next()
temp.extend(
[rxMatch.capturedStart(n), rxMatch.capturedLength(n), fmt]
for n, fmt in enumerate(fmts) if fmt > 0
)
# Match Shortcodes
rxItt = self._rxShortCodes.globalMatch(text, 0)
while rxItt.hasNext():
rxMatch = rxItt.next()
temp.append([
rxMatch.capturedStart(1),
rxMatch.capturedLength(1),
self._shortCodeFmt.get(rxMatch.captured(1).lower(), 0)
])
# Post-process text and format markers
result = text
formats = []
for pos, n, fmt in reversed(sorted(temp, key=lambda x: x[0])):
if fmt > 0:
result = result[:pos] + result[pos+n:]
formats = [(p-n, f) for p, f in formats]
formats.insert(0, (pos, fmt))
return result, formats
# END Class Tokenizer
+24 -12
View File
@@ -27,7 +27,7 @@ import logging
from pathlib import Path
from novelwriter.constants import nwHeadFmt, nwLabels
from novelwriter.constants import nwHeadFmt, nwLabels, trConst
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import Tokenizer
@@ -38,12 +38,12 @@ class ToMarkdown(Tokenizer):
"""Core: Markdown Document Writer
Extend the Tokenizer class to writer Markdown output. It supports
both Standard Markdown and GitHub Flavour Markdown (Extended). The
class also supports concatenating novelWriter markup files.
both Standard Markdown and Extended Markdown. The class also
supports concatenating novelWriter markup files.
"""
M_STD = 0 # Standard Markdown
M_GH = 1 # GitHub Markdown
M_EXT = 1 # Extended Markdown
def __init__(self, project: NWProject) -> None:
super().__init__(project)
@@ -69,9 +69,9 @@ class ToMarkdown(Tokenizer):
self._genMode = self.M_STD
return
def setGitHubMarkdown(self) -> None:
"""Set the converter to use GitHub Markdown formatting."""
self._genMode = self.M_GH
def setExtendedMarkdown(self) -> None:
"""Set the converter to use Extended Markdown formatting."""
self._genMode = self.M_EXT
return
##
@@ -85,7 +85,7 @@ class ToMarkdown(Tokenizer):
def doConvert(self) -> None:
"""Convert the list of text tokens into a Markdown document."""
if self._genMode == self.M_STD:
# Standard
# Standard Markdown
mdTags = {
self.FMT_B_B: "**",
self.FMT_B_E: "**",
@@ -93,9 +93,15 @@ class ToMarkdown(Tokenizer):
self.FMT_I_E: "_",
self.FMT_D_B: "",
self.FMT_D_E: "",
self.FMT_U_B: "",
self.FMT_U_E: "",
self.FMT_SUP_B: "",
self.FMT_SUP_E: "",
self.FMT_SUB_B: "",
self.FMT_SUB_E: "",
}
else:
# GitHub
# Extended Markdown
mdTags = {
self.FMT_B_B: "**",
self.FMT_B_E: "**",
@@ -103,6 +109,12 @@ class ToMarkdown(Tokenizer):
self.FMT_I_E: "_",
self.FMT_D_B: "~~",
self.FMT_D_E: "~~",
self.FMT_U_B: "",
self.FMT_U_E: "",
self.FMT_SUP_B: "^",
self.FMT_SUP_E: "^",
self.FMT_SUB_B: "~",
self.FMT_SUB_E: "~",
}
self._result = ""
@@ -150,8 +162,8 @@ class ToMarkdown(Tokenizer):
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
for pos, fmt in reversed(tFormat):
tTemp = f"{tTemp[:pos]}{mdTags[fmt]}{tTemp[pos:]}"
para.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self._doSynopsis:
@@ -197,7 +209,7 @@ class ToMarkdown(Tokenizer):
result = ""
if bits[0] in nwLabels.KEY_NAME:
result += f"**{nwLabels.KEY_NAME[bits[0]]}:** "
result += f"**{trConst(nwLabels.KEY_NAME[bits[0]])}:** "
if len(bits) > 1:
result += ", ".join(bits[1:])
+126 -96
View File
@@ -29,6 +29,7 @@ from __future__ import annotations
import logging
import xml.etree.ElementTree as ET
from typing import Sequence
from hashlib import sha256
from pathlib import Path
from zipfile import ZipFile
@@ -36,7 +37,7 @@ from datetime import datetime
from novelwriter import __version__
from novelwriter.common import xmlIndent
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, trConst
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import Tokenizer, stripEscape
@@ -82,11 +83,17 @@ TAG_STNM = _mkTag("text", "style-name")
X_BLD = 0x01 # Bold format
X_ITA = 0x02 # Italic format
X_DEL = 0x04 # Strikethrough format
X_UND = 0x08 # Underline format
X_SUP = 0x10 # Superscript
X_SUB = 0x20 # Subscript
# Formatting Masks
M_BLD = ~X_BLD
M_ITA = ~X_ITA
M_DEL = ~X_DEL
M_UND = ~X_UND
M_SUP = ~X_SUP
M_SUB = ~X_SUB
class ToOdt(Tokenizer):
@@ -385,17 +392,8 @@ class ToOdt(Tokenizer):
"""Convert the list of text tokens into XML elements."""
self._result = "" # Not used, but cleared just in case
odtTags = {
self.FMT_B_B: "_B", # Bold open format
self.FMT_B_E: "b_", # Bold close format
self.FMT_I_B: "I", # Italic open format
self.FMT_I_E: "i", # Italic close format
self.FMT_D_B: "_S", # Strikethrough open format
self.FMT_D_E: "s_", # Strikethrough close format
}
fmt = []
para = []
pFmt = []
pText = []
pStyle = None
for tType, _, tText, tFormat, tStyle in self._tokens:
@@ -429,19 +427,21 @@ class ToOdt(Tokenizer):
# Process Text Types
if tType == self.T_EMPTY:
if len(para) > 1 and pStyle is not None:
if len(pText) > 1 and pStyle is not None:
if self._doJustify:
pStyle.setTextAlign("left")
if len(para) > 0 and pStyle is not None:
tTemp = "\n".join(para)
fTemp = " ".join(fmt)
tTxt = tTemp.rstrip()
tFmt = fTemp[:len(tTxt)]
self._addTextPar("Text_20_body", pStyle, tTxt, tFmt=tFmt)
if len(pText) > 0 and pStyle is not None:
tTxt = ""
tFmt = []
for nText, nFmt in zip(pText, pFmt):
tLen = len(tTxt)
tTxt += f"{nText}\n"
tFmt.extend((p+tLen, fmt) for p, fmt in nFmt)
self._addTextPar("Text_20_body", pStyle, tTxt.rstrip(), tFmt=tFmt)
fmt = []
para = []
pFmt = []
pText = []
pStyle = None
elif tType == self.T_TITLE:
@@ -477,15 +477,8 @@ class ToOdt(Tokenizer):
elif tType == self.T_TEXT:
if pStyle is None:
pStyle = oStyle
tFmt = " "*len(tText)
for xPos, xLen, xFmt in tFormat:
tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
tTxt = tText.rstrip()
tFmt = tFmt[:len(tTxt)]
para.append(tTxt)
fmt.append(tFmt)
pText.append(tText)
pFmt.append(tFormat)
elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText)
@@ -559,46 +552,39 @@ class ToOdt(Tokenizer):
# Internal Functions
##
def _formatSynopsis(self, text: str) -> tuple[str, str]:
def _formatSynopsis(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to synopsis lines."""
sSynop = self._localLookup("Synopsis")
rTxt = "**{0}:** {1}".format(sSynop, text)
rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(text))
name = self._localLookup("Synopsis")
rTxt = f"{name}: {text}"
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
return rTxt, rFmt
def _formatComments(self, text: str) -> tuple[str, str]:
def _formatComments(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to comments."""
sComm = self._localLookup("Comment")
rTxt = "**{0}:** {1}".format(sComm, text)
rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(text))
name = self._localLookup("Comment")
rTxt = f"{name}: {text}"
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
return rTxt, rFmt
def _formatKeywords(self, text: str) -> tuple[str, str]:
def _formatKeywords(self, text: str) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text)
if not valid or not bits:
return "", ""
if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
return "", []
rTxt = ""
rFmt = ""
if bits[0] in nwLabels.KEY_NAME:
text = nwLabels.KEY_NAME[bits[0]]
rTxt += "**{0}:** ".format(text)
rFmt += "_B{0} b_ ".format(" "*len(text))
if len(bits) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
rTxt += bits[1]
rFmt += " "*len(bits[1])
else:
tTags = ", ".join(bits[1:])
rTxt += tTags
rFmt += (" "*len(tTags))
rTxt = f"{trConst(nwLabels.KEY_NAME[bits[0]])}: "
rFmt = [(0, self.FMT_B_B), (len(rTxt) - 1, self.FMT_B_E)]
if len(bits) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
rTxt += bits[1]
else:
rTxt += ", ".join(bits[1:])
return rTxt, rFmt
def _addTextPar(
self, styleName: str, oStyle: ODTParagraphStyle, tText: str, tFmt: str = "",
isHead: bool = False, oLevel: str | None = None
self, styleName: str, oStyle: ODTParagraphStyle, tText: str,
tFmt: Sequence[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None
) -> None:
"""Add a text paragraph to the text XML element."""
tAttr = {}
@@ -616,58 +602,59 @@ class ToOdt(Tokenizer):
if not tText:
return
##
# Process Formatting
##
if len(tText) != len(tFmt):
# Generate an empty format if there isn't any or it doesn't match
tFmt = " "*len(tText)
# The formatting loop
tTemp = ""
xFmt = 0x00
pFmt = 0x00
pErr = 0
# Loop Over Fragments
# ===================
parProc = XMLParagraph(xElem)
for i, c in enumerate(tText):
pErr = 0
xFmt = 0x00
tFrag = ""
fLast = 0
for fPos, fFmt in tFmt:
if tFmt[i] == " ":
tTemp += c
elif tFmt[i] == "_":
continue
elif tFmt[i] == "B":
# Add the text up to the current fragment
if tFrag := tText[fLast:fPos]:
if xFmt == 0x00:
parProc.appendText(tFrag)
else:
parProc.appendSpan(tFrag, self._textStyle(xFmt))
# Calculate the change of format
if fFmt == self.FMT_B_B:
xFmt |= X_BLD
elif tFmt[i] == "b":
elif fFmt == self.FMT_B_E:
xFmt &= M_BLD
elif tFmt[i] == "I":
elif fFmt == self.FMT_I_B:
xFmt |= X_ITA
elif tFmt[i] == "i":
elif fFmt == self.FMT_I_E:
xFmt &= M_ITA
elif tFmt[i] == "S":
elif fFmt == self.FMT_D_B:
xFmt |= X_DEL
elif tFmt[i] == "s":
elif fFmt == self.FMT_D_E:
xFmt &= M_DEL
elif fFmt == self.FMT_U_B:
xFmt |= X_UND
elif fFmt == self.FMT_U_E:
xFmt &= M_UND
elif fFmt == self.FMT_SUP_B:
xFmt |= X_SUP
elif fFmt == self.FMT_SUP_E:
xFmt &= M_SUP
elif fFmt == self.FMT_SUB_B:
xFmt |= X_SUB
elif fFmt == self.FMT_SUB_E:
xFmt &= M_SUB
else:
pErr += 1
if xFmt != pFmt:
if pFmt == 0x00:
parProc.appendText(tTemp)
tTemp = ""
else:
parProc.appendSpan(tTemp, self._textStyle(pFmt))
tTemp = ""
fLast = fPos
pFmt = xFmt
# Save what remains in the buffer
if pFmt == 0x00:
parProc.appendText(tTemp)
else:
parProc.appendSpan(tTemp, self._textStyle(pFmt))
if tFrag := tText[fLast:]:
if xFmt == 0x00:
parProc.appendText(tFrag)
else:
parProc.appendSpan(tFrag, self._textStyle(xFmt))
if pErr > 0:
self._errData.append("Unknown format tag encountered")
@@ -713,6 +700,14 @@ class ToOdt(Tokenizer):
if hFmt & X_DEL:
newStyle.setStrikeStyle("solid")
newStyle.setStrikeType("single")
if hFmt & X_UND:
newStyle.setUnderlineStyle("solid")
newStyle.setUnderlineWidth("auto")
newStyle.setUnderlineColour("font-color")
if hFmt & X_SUP:
newStyle.setTextPosition("super")
if hFmt & X_SUB:
newStyle.setTextPosition("sub")
self._autoText[hFmt] = (newName, newStyle)
@@ -1256,16 +1251,23 @@ class ODTTextStyle:
"""
VALID_WEIGHT = ["normal", "inherit", "bold"]
VALID_STYLE = ["normal", "inherit", "italic"]
VALID_POS = ["super", "sub"]
VALID_LSTYLE = ["none", "solid"]
VALID_LTYPE = ["none", "single", "double"]
VALID_LTYPE = ["single", "double"]
VALID_LWIDTH = ["auto"]
VALID_LCOL = ["font-color"]
def __init__(self) -> None:
# Text Attributes
self._tAttr = {
"font-weight": ["fo", None],
"font-style": ["fo", None],
"text-position": ["style", None],
"text-line-through-style": ["style", None],
"text-line-through-type": ["style", None],
"text-underline-style": ["style", None],
"text-underline-width": ["style", None],
"text-underline-color": ["style", None],
}
return
@@ -1287,6 +1289,13 @@ class ODTTextStyle:
self._tAttr["font-style"][1] = None
return
def setTextPosition(self, value: str | None) -> None:
if value in self.VALID_POS:
self._tAttr["text-position"][1] = f"{value} 58%"
else:
self._tAttr["text-position"][1] = None
return
def setStrikeStyle(self, value: str | None) -> None:
if value in self.VALID_LSTYLE:
self._tAttr["text-line-through-style"][1] = value
@@ -1301,6 +1310,27 @@ class ODTTextStyle:
self._tAttr["text-line-through-type"][1] = None
return
def setUnderlineStyle(self, value: str | None) -> None:
if value in self.VALID_LSTYLE:
self._tAttr["text-underline-style"][1] = value
else:
self._tAttr["text-underline-style"][1] = None
return
def setUnderlineWidth(self, value: str | None) -> None:
if value in self.VALID_LWIDTH:
self._tAttr["text-underline-width"][1] = value
else:
self._tAttr["text-underline-width"][1] = None
return
def setUnderlineColour(self, value: str | None) -> None:
if value in self.VALID_LCOL:
self._tAttr["text-underline-color"][1] = value
else:
self._tAttr["text-underline-color"][1] = None
return
##
# Methods
##
+15 -22
View File
@@ -55,6 +55,7 @@ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass
from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.core.index import countWords
from novelwriter.core.document import NWDocument
from novelwriter.gui.dochighlight import GuiDocHighlighter
from novelwriter.gui.editordocument import GuiTextDocument
from novelwriter.extensions.wheeleventfilter import WheelEventFilter
@@ -408,12 +409,6 @@ class GuiDocEditor(QPlainTextEdit):
self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
return
def redrawText(self) -> None:
"""Redraw the text by marking the document content as dirty."""
self._qDocument.markContentsDirty(0, self._qDocument.characterCount())
self.updateDocMargins()
return
def replaceText(self, text: str) -> None:
"""Replace the text of the current document with the provided
text. This also clears undo history.
@@ -735,19 +730,17 @@ class GuiDocEditor(QPlainTextEdit):
"""Tell the user where on the file system the file in the editor
is saved.
"""
if self._nwDocument is None:
logger.error("No document open")
return
SHARED.info(
"<br>".join([
self.tr("Document Details"),
""*40,
self.tr("Created: {0}").format(self._nwDocument.createdDate),
self.tr("Updated: {0}").format(self._nwDocument.updatedDate),
]),
details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation),
log=False
)
if isinstance(self._nwDocument, NWDocument):
SHARED.info(
"<br>".join([
self.tr("Document Details"),
""*40,
self.tr("Created: {0}").format(self._nwDocument.createdDate),
self.tr("Updated: {0}").format(self._nwDocument.updatedDate),
]),
details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation),
log=False
)
return
def insertText(self, insert: str | nwDocInsert) -> bool:
@@ -775,15 +768,15 @@ class GuiDocEditor(QPlainTextEdit):
newBlock = True
goAfter = True
elif insert == nwDocInsert.NEW_PAGE:
text = "[NEW PAGE]"
text = "[newpage]"
newBlock = True
goAfter = False
elif insert == nwDocInsert.VSPACE_S:
text = "[VSPACE]"
text = "[vspace]"
newBlock = True
goAfter = False
elif insert == nwDocInsert.VSPACE_M:
text = "[VSPACE:2]"
text = "[vspace:2]"
newBlock = True
goAfter = False
else:
+26 -8
View File
@@ -39,7 +39,7 @@ from novelwriter.constants import nwRegEx, nwUnicode
logger = logging.getLogger(__name__)
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—]+\b")
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b")
SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
@@ -69,6 +69,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._colDialD = QColor(0, 0, 0)
self._colDialS = QColor(0, 0, 0)
self._colHidden = QColor(0, 0, 0)
self._colCode = QColor(0, 0, 0)
self._colKey = QColor(0, 0, 0)
self._colVal = QColor(0, 0, 0)
self._colSpell = QColor(0, 0, 0)
@@ -95,6 +96,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._colDialD = QColor(*SHARED.theme.colDialD)
self._colDialS = QColor(*SHARED.theme.colDialS)
self._colHidden = QColor(*SHARED.theme.colHidden)
self._colCode = QColor(*SHARED.theme.colCode)
self._colKey = QColor(*SHARED.theme.colKey)
self._colVal = QColor(*SHARED.theme.colVal)
self._colSpell = QColor(*SHARED.theme.colSpell)
@@ -127,6 +129,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"dialogue3": self._makeFormat(self._colDialS),
"replace": self._makeFormat(self._colRepTag),
"hidden": self._makeFormat(self._colHidden),
"code": self._makeFormat(self._colCode),
"keyword": self._makeFormat(self._colKey),
"modifier": self._makeFormat(self._colMod),
"value": self._makeFormat(self._colVal, "underline"),
@@ -205,6 +208,22 @@ class GuiDocHighlighter(QSyntaxHighlighter):
}
))
# Shortcodes
self._hRules.append((
nwRegEx.FMT_SC, {
1: self._hStyles["code"],
}
))
# Shortcodes w/Value
self._hRules.append((
nwRegEx.FMT_SV, {
1: self._hStyles["code"],
2: self._hStyles["codevalue"],
3: self._hStyles["code"],
}
))
# Alignment Tags
self._hRules.append((
r"(^>{1,2}|<{1,2}$)", {
@@ -347,18 +366,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
else: # Text Paragraph
if text.startswith("["): # Special Command
sText = text.rstrip()
if sText in ("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]"):
self.setFormat(0, len(text), self._hStyles["keyword"])
sText = text.rstrip().lower()
if sText in ("[newpage]", "[new page]", "[vspace]"):
self.setFormat(0, len(text), self._hStyles["code"])
return
elif sText.startswith("[VSPACE:") and sText.endswith("]"):
elif sText.startswith("[vspace:") and sText.endswith("]"):
tLen = len(sText)
tVal = checkInt(sText[8:-1], 0)
cVal = "codevalue" if tVal > 0 else "codeinval"
self.setFormat(0, 8, self._hStyles["keyword"])
self.setFormat(0, 8, self._hStyles["code"])
self.setFormat(8, tLen-9, self._hStyles[cVal])
self.setFormat(tLen-1, tLen, self._hStyles["keyword"])
self.setFormat(tLen-1, tLen, self._hStyles["code"])
return
# Regular Text
+2
View File
@@ -97,6 +97,7 @@ class GuiTheme:
self.colDialD = [0, 0, 0]
self.colDialS = [0, 0, 0]
self.colHidden = [0, 0, 0]
self.colCode = [0, 0, 0]
self.colKey = [0, 0, 0]
self.colVal = [0, 0, 0]
self.colSpell = [0, 0, 0]
@@ -316,6 +317,7 @@ class GuiTheme:
self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes")
self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes")
self.colHidden = self._parseColour(confParser, cnfSec, "hidden")
self.colCode = self._parseColour(confParser, cnfSec, "shortcode")
self.colKey = self._parseColour(confParser, cnfSec, "keyword")
self.colVal = self._parseColour(confParser, cnfSec, "value")
self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
+4 -2
View File
@@ -1,8 +1,8 @@
%%~name: Making a Scene
%%~path: 6a2d6d5f4f401/636b6aa9b697b
%%~kind: NOVEL/DOCUMENT
%%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5
%%~date: Unknown/2023-09-07 19:00:10
%%~hash: d7920fe95fb48140278ae160fe7c3c1ce530072c
%%~date: Unknown/2023-10-20 22:57:56
### Making a Scene
@pov: Jane
@@ -13,6 +13,8 @@ A scene is defined by a level three heading, like the one at the top of this pag
Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesnt show it correctly, the export tool will not either.
For special formatting aside from standard emphasis, you can use “shortcodes”. Here we have super[sup]script[/sup] and sub[sub]script[/sub], and [b]part[/b]ial bold too, which ends in the middle of a word. The shortcodes also support [u]underline[/u].
In addition, the editor supports automatic formatting of “quotes”, both double and single. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is set in Project Settings.
+5 -5
View File
@@ -1,11 +1,11 @@
%%~name: Page
%%~path: 7031beac91f75/974e400180a99
%%~kind: NOVEL/DOCUMENT
%%~hash: 7547809e972205eb2a55dd988595e0aa1d01e139
%%~date: Unknown/2023-08-25 16:51:54
[NEW PAGE]
[VSPACE:2]
%%~hash: a0b0719e19763bf505bcf797bea0968c505c9b4a
%%~date: Unknown/2023-11-06 11:19:33
[newpage]
[vspace:2]
This is a plain page with some text on it.
If you want the text to start on a fresh page, add the [NEW PAGE] code above the text. You can also add empty paragraphs with the [VSPACE] code. The above code adds two empty paragraphs before the text starts.
If you want the text to start on a fresh page, add the [newpage] code above the text. You can also add empty paragraphs with the [vspace] code. The above code adds two empty paragraphs before the text starts.
+5 -5
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-11-05 18:54:42">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1532" autoCount="238" editTime="75633">
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-11-06 11:44:21">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1609" autoCount="255" editTime="81165">
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
@@ -36,7 +36,7 @@
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
</importance>
</settings>
<content items="27" novelWords="954" notesWords="409">
<content items="27" novelWords="989" notesWords="409">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name>
@@ -46,7 +46,7 @@
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H0" charCount="251" wordCount="50" paraCount="2" cursorPos="277" />
<meta expanded="no" heading="H0" charCount="269" wordCount="51" paraCount="3" cursorPos="275" />
<name status="sf12341" import="ia857f0" active="yes">Page</name>
</item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -58,7 +58,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2686" wordCount="479" paraCount="14" cursorPos="19" />
<meta expanded="no" heading="H3" charCount="2901" wordCount="513" paraCount="15" cursorPos="749" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta>
<meta:creation-date>2023-07-19T23:55:29</meta:creation-date>
<meta:generator>novelWriter/2.1-beta1</meta:generator>
<meta:creation-date>2023-10-20T22:51:30</meta:creation-date>
<meta:generator>novelWriter/2.2-alpha1</meta:generator>
<meta:initial-creator>Jane Smith</meta:initial-creator>
<meta:editing-cycles>1234</meta:editing-cycles>
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
<dc:title>Test Project</dc:title>
<dc:date>2023-07-19T23:55:29</dc:date>
<dc:date>2023-10-20T22:51:30</dc:date>
<dc:creator>Jane Smith</dc:creator>
</office:meta>
<office:font-face-decls>
@@ -64,7 +64,7 @@
</office:styles>
<office:automatic-styles>
<style:page-layout style:name="PM1">
<style:page-layout-properties fo:page-width="21.0cm" fo:page-height="29.7cm" fo:margin-top="2.000cm" fo:margin-bottom="2.000cm" fo:margin-left="2.000cm" fo:margin-right="2.000cm" fo:print-orientation="portrait" />
<style:page-layout-properties fo:page-width="14.800cm" fo:page-height="21.000cm" fo:margin-top="2.000cm" fo:margin-bottom="1.800cm" fo:margin-left="1.700cm" fo:margin-right="1.500cm" fo:print-orientation="portrait" />
<style:header-style>
<style:header-footer-properties fo:min-height="0.600cm" fo:margin-left="0.000cm" fo:margin-right="0.000cm" fo:margin-bottom="0.500cm" />
</style:header-style>
+26 -1
View File
@@ -1281,7 +1281,7 @@ def testCoreIndex_CountWords():
assert wC == 9
assert pC == 4
# Formatting Codes
# Formatting Codes, Upper Case (Old Implementation)
cC, wC, pC = countWords((
"Some text\n\n"
"[NEWPAGE]\n\n"
@@ -1297,4 +1297,29 @@ def testCoreIndex_CountWords():
assert wC == 13
assert pC == 5
# Formatting Codes, Lower Case (Current Implementation)
cC, wC, pC = countWords((
"Some text\n\n"
"[newpage]\n\n"
"more text\n\n"
"[new page]]\n\n"
"even more text\n\n"
"[vspace]\n\n"
"and some final text\n\n"
"[vspace:4]\n\n"
"THE END\n\n"
))
assert cC == 58
assert wC == 13
assert pC == 5
# Check ShortCodes
cC, wC, pC = countWords((
"Text with [b]bold[/b] text and padded [b] bold [/b] text.\n\n"
"Text with [b][i] nested [/i] emphasis [/b] in it.\n\n"
))
assert cC == 78
assert wC == 14
assert pC == 2
# END Test testCoreIndex_CountWords
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -81,8 +81,8 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
# Paragraphs
# ==========
# Text for GitHub Markdown
theMD.setGitHubMarkdown()
# Text for Extended Markdown
theMD.setExtendedMarkdown()
theMD._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theMD.tokenizeText()
theMD.doConvert()
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -600,15 +600,15 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsNewPage.activate(QAction.Trigger)
assert nwGUI.docEditor.getText() == "[NEW PAGE]\n### Stuff\n"
assert nwGUI.docEditor.getText() == "[newpage]\n### Stuff\n"
nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsVSpaceS.activate(QAction.Trigger)
assert nwGUI.docEditor.getText() == "[VSPACE]\n### Stuff\n"
assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n"
nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsVSpaceM.activate(QAction.Trigger)
assert nwGUI.docEditor.getText() == "[VSPACE:2]\n### Stuff\n"
assert nwGUI.docEditor.getText() == "[vspace:2]\n### Stuff\n"
nwGUI.docEditor.clear()