Add sub/superscript and partial word emphasis (#1540)
This commit is contained in:
@@ -159,6 +159,7 @@ A syntax theme ``.conf`` file consists of the follwing settings:
|
|||||||
doublequotes = 0, 0, 0
|
doublequotes = 0, 0, 0
|
||||||
singlequotes = 0, 0, 0
|
singlequotes = 0, 0, 0
|
||||||
hidden = 0, 0, 0
|
hidden = 0, 0, 0
|
||||||
|
shortcode = 0, 0, 0
|
||||||
keyword = 0, 0, 0
|
keyword = 0, 0, 0
|
||||||
value = 0, 0, 0
|
value = 0, 0, 0
|
||||||
spellcheckline = 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
|
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
|
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,
|
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
|
doublequotes = 184, 200, 0
|
||||||
singlequotes = 136, 200, 0
|
singlequotes = 136, 200, 0
|
||||||
hidden = 150, 150, 150
|
hidden = 150, 150, 150
|
||||||
|
shortcode = 0, 155, 200
|
||||||
keyword = 200, 46, 0
|
keyword = 200, 46, 0
|
||||||
value = 184, 200, 0
|
value = 184, 200, 0
|
||||||
spellcheckline = 200, 46, 0
|
spellcheckline = 200, 46, 0
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ straightquotes = 200, 0, 0
|
|||||||
doublequotes = 0, 0, 200
|
doublequotes = 0, 0, 200
|
||||||
singlequotes = 0, 0, 200
|
singlequotes = 0, 0, 200
|
||||||
hidden = 100, 100, 100
|
hidden = 100, 100, 100
|
||||||
|
shortcode = 0, 100, 0
|
||||||
keyword = 200, 50, 50
|
keyword = 200, 50, 50
|
||||||
value = 50, 150, 50
|
value = 50, 150, 50
|
||||||
spellcheckline = 200, 0, 0
|
spellcheckline = 200, 0, 0
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ straightquotes = 200, 200, 200
|
|||||||
doublequotes = 200, 200, 200
|
doublequotes = 200, 200, 200
|
||||||
singlequotes = 200, 200, 200
|
singlequotes = 200, 200, 200
|
||||||
hidden = 150, 150, 150
|
hidden = 150, 150, 150
|
||||||
|
shortcode = 225, 225, 225
|
||||||
keyword = 225, 225, 225
|
keyword = 225, 225, 225
|
||||||
value = 200, 200, 200
|
value = 200, 200, 200
|
||||||
spellcheckline = 200, 46, 0
|
spellcheckline = 200, 46, 0
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ straightquotes = 20, 20, 20
|
|||||||
doublequotes = 20, 20, 20
|
doublequotes = 20, 20, 20
|
||||||
singlequotes = 20, 20, 20
|
singlequotes = 20, 20, 20
|
||||||
hidden = 100, 100, 100
|
hidden = 100, 100, 100
|
||||||
|
shortcode = 0, 0, 0
|
||||||
keyword = 0, 0, 0
|
keyword = 0, 0, 0
|
||||||
value = 20, 20, 20
|
value = 20, 20, 20
|
||||||
spellcheckline = 200, 0, 0
|
spellcheckline = 200, 0, 0
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ straightquotes = 222, 61, 58
|
|||||||
doublequotes = 8, 145, 106
|
doublequotes = 8, 145, 106
|
||||||
singlequotes = 255, 235, 149
|
singlequotes = 255, 235, 149
|
||||||
hidden = 152, 159, 177
|
hidden = 152, 159, 177
|
||||||
|
shortcode = 40, 142, 215
|
||||||
keyword = 222, 61, 58
|
keyword = 222, 61, 58
|
||||||
value = 150, 74, 193
|
value = 150, 74, 193
|
||||||
spellcheckline = 222, 61, 58
|
spellcheckline = 222, 61, 58
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ straightquotes = 247, 140, 108
|
|||||||
doublequotes = 173, 219, 103
|
doublequotes = 173, 219, 103
|
||||||
singlequotes = 255, 235, 149
|
singlequotes = 255, 235, 149
|
||||||
hidden = 99, 119, 119
|
hidden = 99, 119, 119
|
||||||
|
shortcode = 130, 170, 255
|
||||||
keyword = 247, 140, 108
|
keyword = 247, 140, 108
|
||||||
value = 199, 146, 234
|
value = 199, 146, 234
|
||||||
spellcheckline = 247, 140, 108
|
spellcheckline = 247, 140, 108
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ straightquotes = 211, 54, 130
|
|||||||
doublequotes = 42, 161, 152
|
doublequotes = 42, 161, 152
|
||||||
singlequotes = 42, 161, 152
|
singlequotes = 42, 161, 152
|
||||||
hidden = 147, 161, 161
|
hidden = 147, 161, 161
|
||||||
|
shortcode = 147, 161, 161
|
||||||
keyword = 133, 153, 0
|
keyword = 133, 153, 0
|
||||||
value = 203, 75, 22
|
value = 203, 75, 22
|
||||||
spellcheckline = 203, 75, 22
|
spellcheckline = 203, 75, 22
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ straightquotes = 211, 54, 130
|
|||||||
doublequotes = 42, 161, 152
|
doublequotes = 42, 161, 152
|
||||||
singlequotes = 42, 161, 152
|
singlequotes = 42, 161, 152
|
||||||
hidden = 88, 110, 117
|
hidden = 88, 110, 117
|
||||||
|
shortcode = 88, 110, 117
|
||||||
keyword = 133, 153, 0
|
keyword = 133, 153, 0
|
||||||
value = 203, 75, 22
|
value = 203, 75, 22
|
||||||
spellcheckline = 203, 75, 22
|
spellcheckline = 203, 75, 22
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ straightquotes = 240, 40, 41
|
|||||||
doublequotes = 113, 140, 0
|
doublequotes = 113, 140, 0
|
||||||
singlequotes = 234, 183, 0
|
singlequotes = 234, 183, 0
|
||||||
hidden = 142, 144, 140
|
hidden = 142, 144, 140
|
||||||
|
shortcode = 66, 113, 174
|
||||||
keyword = 240, 40, 41
|
keyword = 240, 40, 41
|
||||||
value = 137, 89, 168
|
value = 137, 89, 168
|
||||||
spellcheckline = 240, 40, 41
|
spellcheckline = 240, 40, 41
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ straightquotes = 204, 102, 102
|
|||||||
doublequotes = 181, 189, 104
|
doublequotes = 181, 189, 104
|
||||||
singlequotes = 240, 198, 116
|
singlequotes = 240, 198, 116
|
||||||
hidden = 150, 152, 150
|
hidden = 150, 152, 150
|
||||||
|
shortcode = 129, 162, 190
|
||||||
keyword = 204, 102, 102
|
keyword = 204, 102, 102
|
||||||
value = 178, 148, 187
|
value = 178, 148, 187
|
||||||
spellcheckline = 204, 102, 102
|
spellcheckline = 204, 102, 102
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ straightquotes = 255, 157, 164
|
|||||||
doublequotes = 209, 241, 169
|
doublequotes = 209, 241, 169
|
||||||
singlequotes = 255, 238, 173
|
singlequotes = 255, 238, 173
|
||||||
hidden = 114, 133, 183
|
hidden = 114, 133, 183
|
||||||
|
shortcode = 187, 218, 255
|
||||||
keyword = 255, 157, 164
|
keyword = 255, 157, 164
|
||||||
value = 235, 187, 255
|
value = 235, 187, 255
|
||||||
spellcheckline = 255, 157, 164
|
spellcheckline = 255, 157, 164
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ straightquotes = 213, 78, 83
|
|||||||
doublequotes = 185, 202, 74
|
doublequotes = 185, 202, 74
|
||||||
singlequotes = 231, 197, 71
|
singlequotes = 231, 197, 71
|
||||||
hidden = 150, 152, 150
|
hidden = 150, 152, 150
|
||||||
|
shortcode = 122, 166, 218
|
||||||
keyword = 213, 78, 83
|
keyword = 213, 78, 83
|
||||||
value = 195, 151, 216
|
value = 195, 151, 216
|
||||||
spellcheckline = 213, 78, 83
|
spellcheckline = 213, 78, 83
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ straightquotes = 242, 119, 122
|
|||||||
doublequotes = 153, 204, 153
|
doublequotes = 153, 204, 153
|
||||||
singlequotes = 255, 204, 102
|
singlequotes = 255, 204, 102
|
||||||
hidden = 153, 153, 153
|
hidden = 153, 153, 153
|
||||||
|
shortcode = 102, 153, 204
|
||||||
keyword = 242, 119, 122
|
keyword = 242, 119, 122
|
||||||
value = 204, 153, 204
|
value = 204, 153, 204
|
||||||
spellcheckline = 242, 119, 122
|
spellcheckline = 242, 119, 122
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
|
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
|
||||||
|
|
||||||
from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline
|
from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline
|
||||||
@@ -59,6 +61,11 @@ class nwRegEx:
|
|||||||
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
|
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||||
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
|
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||||
FMT_ST = 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
|
# END Class nwRegEx
|
||||||
|
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ class NWBuildDocument:
|
|||||||
filtered = self._setupBuild(makeObj)
|
filtered = self._setupBuild(makeObj)
|
||||||
|
|
||||||
if extendedMd:
|
if extendedMd:
|
||||||
makeObj.setGitHubMarkdown()
|
makeObj.setExtendedMarkdown()
|
||||||
else:
|
else:
|
||||||
makeObj.setStandardMarkdown()
|
makeObj.setStandardMarkdown()
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ from pathlib import Path
|
|||||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
|
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
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
@@ -1229,6 +1229,10 @@ def countWords(text: str) -> tuple[int, int, int]:
|
|||||||
if nwUnicode.U_EMDASH in text:
|
if nwUnicode.U_EMDASH in text:
|
||||||
text = text.replace(nwUnicode.U_EMDASH, " ")
|
text = text.replace(nwUnicode.U_EMDASH, " ")
|
||||||
|
|
||||||
|
# Strip shortcodes
|
||||||
|
if "[" in text:
|
||||||
|
text = nwRegEx.RX_SC.sub("", text)
|
||||||
|
|
||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
|
|
||||||
countPara = True
|
countPara = True
|
||||||
@@ -1241,9 +1245,10 @@ def countWords(text: str) -> tuple[int, int, int]:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if line[0] == "[":
|
if line[0] == "[":
|
||||||
if line.startswith(("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]")):
|
check = line.lower()
|
||||||
|
if check.startswith(("[newpage]", "[new page]", "[vspace]")):
|
||||||
continue
|
continue
|
||||||
elif line.startswith("[VSPACE:") and line.endswith("]"):
|
elif check.startswith("[vspace:") and line.endswith("]"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif line[0] == "#":
|
elif line[0] == "#":
|
||||||
|
|||||||
+23
-23
@@ -31,7 +31,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG
|
||||||
from novelwriter.common import formatTimeStamp
|
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.project import NWProject
|
||||||
from novelwriter.core.tokenizer import Tokenizer, stripEscape
|
from novelwriter.core.tokenizer import Tokenizer, stripEscape
|
||||||
|
|
||||||
@@ -131,6 +131,8 @@ class ToHtml(Tokenizer):
|
|||||||
self.FMT_I_E: "</i>",
|
self.FMT_I_E: "</i>",
|
||||||
self.FMT_D_B: "<span style='text-decoration: line-through;'>",
|
self.FMT_D_B: "<span style='text-decoration: line-through;'>",
|
||||||
self.FMT_D_E: "</span>",
|
self.FMT_D_E: "</span>",
|
||||||
|
self.FMT_U_B: "<u>",
|
||||||
|
self.FMT_U_E: "</u>",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
htmlTags = { # HTML5 (for export)
|
htmlTags = { # HTML5 (for export)
|
||||||
@@ -140,8 +142,15 @@ class ToHtml(Tokenizer):
|
|||||||
self.FMT_I_E: "</em>",
|
self.FMT_I_E: "</em>",
|
||||||
self.FMT_D_B: "<del>",
|
self.FMT_D_B: "<del>",
|
||||||
self.FMT_D_E: "</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:
|
if self._isNovel and self._genMode != self.M_PREVIEW:
|
||||||
# For story files, we bump the titles one level up
|
# For story files, we bump the titles one level up
|
||||||
h1Cl = " class='title'"
|
h1Cl = " class='title'"
|
||||||
@@ -172,18 +181,16 @@ class ToHtml(Tokenizer):
|
|||||||
for c in tText:
|
for c in tText:
|
||||||
if c == "<":
|
if c == "<":
|
||||||
cText.append("<")
|
cText.append("<")
|
||||||
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
|
i += 4
|
||||||
elif c == ">":
|
elif c == ">":
|
||||||
cText.append(">")
|
cText.append(">")
|
||||||
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
|
i += 4
|
||||||
else:
|
else:
|
||||||
cText.append(c)
|
cText.append(c)
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
tText = "".join(cText)
|
tText = "".join(cText)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# If we don't have formatting, we can do a plain replace
|
# If we don't have formatting, we can do a plain replace
|
||||||
tText = tText.replace("<", "<").replace(">", ">")
|
tText = tText.replace("<", "<").replace(">", ">")
|
||||||
@@ -275,8 +282,8 @@ class ToHtml(Tokenizer):
|
|||||||
tTemp = tText
|
tTemp = tText
|
||||||
if pStyle is None:
|
if pStyle is None:
|
||||||
pStyle = hStyle
|
pStyle = hStyle
|
||||||
for xPos, xLen, xFmt in reversed(tFormat):
|
for pos, fmt in reversed(tFormat):
|
||||||
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
|
tTemp = f"{tTemp[:pos]}{htmlTags[fmt]}{tTemp[pos:]}"
|
||||||
para.append(stripEscape(tTemp.rstrip()))
|
para.append(stripEscape(tTemp.rstrip()))
|
||||||
|
|
||||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||||
@@ -467,25 +474,18 @@ class ToHtml(Tokenizer):
|
|||||||
def _formatKeywords(self, text: str) -> str:
|
def _formatKeywords(self, text: str) -> str:
|
||||||
"""Apply HTML formatting to keywords."""
|
"""Apply HTML formatting to keywords."""
|
||||||
valid, bits, _ = self._project.index.scanThis("@"+text)
|
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 ""
|
return ""
|
||||||
|
|
||||||
result = ""
|
result = f"<span class='tags'>{trConst(nwLabels.KEY_NAME[bits[0]])}:</span> "
|
||||||
tags = []
|
if len(bits) > 1:
|
||||||
if bits[0] in nwLabels.KEY_NAME:
|
if bits[0] == nwKeyWords.TAG_KEY:
|
||||||
result += f"<span class='tags'>{nwLabels.KEY_NAME[bits[0]]}:</span> "
|
result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>"
|
||||||
if len(bits) > 1:
|
else:
|
||||||
if bits[0] == nwKeyWords.TAG_KEY:
|
if self._genMode == self.M_PREVIEW:
|
||||||
result += f"<a name='tag_{bits[1]}'>{bits[1]}</a>"
|
result += ", ".join(f"<a href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:])
|
||||||
else:
|
else:
|
||||||
if self._genMode == self.M_PREVIEW:
|
result += ", ".join(f"<a href='#tag_{t}'>{t}</a>" for t in bits[1:])
|
||||||
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)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import logging
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from time import time
|
from time import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from operator import itemgetter
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
||||||
@@ -43,15 +42,14 @@ from novelwriter.core.project import NWProject
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
def stripEscape(text) -> str:
|
||||||
"""Helper function to strip escaped Markdown characters from
|
"""Strip escaped Markdown characters from paragraph text."""
|
||||||
paragraph text.
|
|
||||||
"""
|
|
||||||
if "\\" in text:
|
if "\\" in text:
|
||||||
# Checking first is slightly slower when there are escaped
|
return RX_ESC.sub(lambda x: ESCAPES[x.group(0)], text)
|
||||||
# characters in the text, but significantly faster when not
|
|
||||||
return text.replace(r"\*", "*").replace(r"\~", "~").replace(r"\_", "_")
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
@@ -65,12 +63,18 @@ class Tokenizer(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# In-Text Format
|
# In-Text Format
|
||||||
FMT_B_B = 1 # Begin bold
|
FMT_B_B = 1 # Begin bold
|
||||||
FMT_B_E = 2 # End bold
|
FMT_B_E = 2 # End bold
|
||||||
FMT_I_B = 3 # Begin italics
|
FMT_I_B = 3 # Begin italics
|
||||||
FMT_I_E = 4 # End italics
|
FMT_I_E = 4 # End italics
|
||||||
FMT_D_B = 5 # Begin strikeout
|
FMT_D_B = 5 # Begin strikeout
|
||||||
FMT_D_E = 6 # End 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
|
# Block Type
|
||||||
T_EMPTY = 1 # Empty line (new paragraph)
|
T_EMPTY = 1 # Empty line (new paragraph)
|
||||||
@@ -166,6 +170,24 @@ class Tokenizer(ABC):
|
|||||||
# Cached Translations
|
# Cached Translations
|
||||||
self._trSynopsis = self.tr("Synopsis")
|
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
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -387,19 +409,12 @@ class Tokenizer(ABC):
|
|||||||
4: The internal formatting map of the text, self.FMT_*
|
4: The internal formatting map of the text, self.FMT_*
|
||||||
5: The style of the block, self.A_*
|
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 = []
|
self._tokens = []
|
||||||
tmpMarkdown = []
|
tmpMarkdown = []
|
||||||
nHead = 0
|
nHead = 0
|
||||||
breakNext = False
|
breakNext = False
|
||||||
for aLine in self._text.splitlines():
|
for aLine in self._text.splitlines():
|
||||||
sLine = aLine.strip()
|
sLine = aLine.strip().lower()
|
||||||
|
|
||||||
# Check for blank lines
|
# Check for blank lines
|
||||||
if len(sLine) == 0:
|
if len(sLine) == 0:
|
||||||
@@ -422,18 +437,21 @@ class Tokenizer(ABC):
|
|||||||
|
|
||||||
if aLine[0] == "[":
|
if aLine[0] == "[":
|
||||||
# Parse special formatting line
|
# 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
|
breakNext = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif sLine == "[VSPACE]":
|
elif sLine == "[vspace]":
|
||||||
self._tokens.append(
|
self._tokens.append(
|
||||||
(self.T_SKIP, nHead, "", None, sAlign)
|
(self.T_SKIP, nHead, "", None, sAlign)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif sLine.startswith("[VSPACE:") and sLine.endswith("]"):
|
elif sLine.startswith("[vspace:") and sLine.endswith("]"):
|
||||||
nSkip = checkInt(sLine[8:-1], 0)
|
nSkip = checkInt(sLine[8:-1], 0)
|
||||||
if nSkip >= 1:
|
if nSkip >= 1:
|
||||||
self._tokens.append(
|
self._tokens.append(
|
||||||
@@ -445,7 +463,7 @@ class Tokenizer(ABC):
|
|||||||
]
|
]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif aLine[0] == "%":
|
if aLine[0] == "%":
|
||||||
cLine = aLine[1:].lstrip()
|
cLine = aLine[1:].lstrip()
|
||||||
synTag = cLine[:9].lower()
|
synTag = cLine[:9].lower()
|
||||||
if synTag == "synopsis:":
|
if synTag == "synopsis:":
|
||||||
@@ -570,23 +588,10 @@ class Tokenizer(ABC):
|
|||||||
if indRight:
|
if indRight:
|
||||||
sAlign |= self.A_IND_R
|
sAlign |= self.A_IND_R
|
||||||
|
|
||||||
# Otherwise we use RegEx to find formatting tags within a line of text
|
# Process formats
|
||||||
fmtPos = []
|
tLine, fmtPos = self._extractFormats(aLine)
|
||||||
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._tokens.append((
|
self._tokens.append((
|
||||||
self.T_TEXT, nHead, aLine, fmtPos, sAlign
|
self.T_TEXT, nHead, tLine, fmtPos, sAlign
|
||||||
))
|
))
|
||||||
if self._keepMarkdown:
|
if self._keepMarkdown:
|
||||||
tmpMarkdown.append("%s\n" % aLine)
|
tmpMarkdown.append("%s\n" % aLine)
|
||||||
@@ -763,6 +768,45 @@ class Tokenizer(ABC):
|
|||||||
json.dump(data, fObj, indent=2)
|
json.dump(data, fObj, indent=2)
|
||||||
return
|
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
|
# END Class Tokenizer
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+24
-12
@@ -27,7 +27,7 @@ import logging
|
|||||||
|
|
||||||
from pathlib import Path
|
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.project import NWProject
|
||||||
from novelwriter.core.tokenizer import Tokenizer
|
from novelwriter.core.tokenizer import Tokenizer
|
||||||
|
|
||||||
@@ -38,12 +38,12 @@ class ToMarkdown(Tokenizer):
|
|||||||
"""Core: Markdown Document Writer
|
"""Core: Markdown Document Writer
|
||||||
|
|
||||||
Extend the Tokenizer class to writer Markdown output. It supports
|
Extend the Tokenizer class to writer Markdown output. It supports
|
||||||
both Standard Markdown and GitHub Flavour Markdown (Extended). The
|
both Standard Markdown and Extended Markdown. The class also
|
||||||
class also supports concatenating novelWriter markup files.
|
supports concatenating novelWriter markup files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
M_STD = 0 # Standard Markdown
|
M_STD = 0 # Standard Markdown
|
||||||
M_GH = 1 # GitHub Markdown
|
M_EXT = 1 # Extended Markdown
|
||||||
|
|
||||||
def __init__(self, project: NWProject) -> None:
|
def __init__(self, project: NWProject) -> None:
|
||||||
super().__init__(project)
|
super().__init__(project)
|
||||||
@@ -69,9 +69,9 @@ class ToMarkdown(Tokenizer):
|
|||||||
self._genMode = self.M_STD
|
self._genMode = self.M_STD
|
||||||
return
|
return
|
||||||
|
|
||||||
def setGitHubMarkdown(self) -> None:
|
def setExtendedMarkdown(self) -> None:
|
||||||
"""Set the converter to use GitHub Markdown formatting."""
|
"""Set the converter to use Extended Markdown formatting."""
|
||||||
self._genMode = self.M_GH
|
self._genMode = self.M_EXT
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -85,7 +85,7 @@ class ToMarkdown(Tokenizer):
|
|||||||
def doConvert(self) -> None:
|
def doConvert(self) -> None:
|
||||||
"""Convert the list of text tokens into a Markdown document."""
|
"""Convert the list of text tokens into a Markdown document."""
|
||||||
if self._genMode == self.M_STD:
|
if self._genMode == self.M_STD:
|
||||||
# Standard
|
# Standard Markdown
|
||||||
mdTags = {
|
mdTags = {
|
||||||
self.FMT_B_B: "**",
|
self.FMT_B_B: "**",
|
||||||
self.FMT_B_E: "**",
|
self.FMT_B_E: "**",
|
||||||
@@ -93,9 +93,15 @@ class ToMarkdown(Tokenizer):
|
|||||||
self.FMT_I_E: "_",
|
self.FMT_I_E: "_",
|
||||||
self.FMT_D_B: "",
|
self.FMT_D_B: "",
|
||||||
self.FMT_D_E: "",
|
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:
|
else:
|
||||||
# GitHub
|
# Extended Markdown
|
||||||
mdTags = {
|
mdTags = {
|
||||||
self.FMT_B_B: "**",
|
self.FMT_B_B: "**",
|
||||||
self.FMT_B_E: "**",
|
self.FMT_B_E: "**",
|
||||||
@@ -103,6 +109,12 @@ class ToMarkdown(Tokenizer):
|
|||||||
self.FMT_I_E: "_",
|
self.FMT_I_E: "_",
|
||||||
self.FMT_D_B: "~~",
|
self.FMT_D_B: "~~",
|
||||||
self.FMT_D_E: "~~",
|
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 = ""
|
self._result = ""
|
||||||
@@ -150,8 +162,8 @@ class ToMarkdown(Tokenizer):
|
|||||||
|
|
||||||
elif tType == self.T_TEXT:
|
elif tType == self.T_TEXT:
|
||||||
tTemp = tText
|
tTemp = tText
|
||||||
for xPos, xLen, xFmt in reversed(tFormat):
|
for pos, fmt in reversed(tFormat):
|
||||||
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
|
tTemp = f"{tTemp[:pos]}{mdTags[fmt]}{tTemp[pos:]}"
|
||||||
para.append(tTemp.rstrip())
|
para.append(tTemp.rstrip())
|
||||||
|
|
||||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||||
@@ -197,7 +209,7 @@ class ToMarkdown(Tokenizer):
|
|||||||
|
|
||||||
result = ""
|
result = ""
|
||||||
if bits[0] in nwLabels.KEY_NAME:
|
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:
|
if len(bits) > 1:
|
||||||
result += ", ".join(bits[1:])
|
result += ", ".join(bits[1:])
|
||||||
|
|
||||||
|
|||||||
+126
-96
@@ -29,6 +29,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from typing import Sequence
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@@ -36,7 +37,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from novelwriter import __version__
|
from novelwriter import __version__
|
||||||
from novelwriter.common import xmlIndent
|
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.project import NWProject
|
||||||
from novelwriter.core.tokenizer import Tokenizer, stripEscape
|
from novelwriter.core.tokenizer import Tokenizer, stripEscape
|
||||||
|
|
||||||
@@ -82,11 +83,17 @@ TAG_STNM = _mkTag("text", "style-name")
|
|||||||
X_BLD = 0x01 # Bold format
|
X_BLD = 0x01 # Bold format
|
||||||
X_ITA = 0x02 # Italic format
|
X_ITA = 0x02 # Italic format
|
||||||
X_DEL = 0x04 # Strikethrough format
|
X_DEL = 0x04 # Strikethrough format
|
||||||
|
X_UND = 0x08 # Underline format
|
||||||
|
X_SUP = 0x10 # Superscript
|
||||||
|
X_SUB = 0x20 # Subscript
|
||||||
|
|
||||||
# Formatting Masks
|
# Formatting Masks
|
||||||
M_BLD = ~X_BLD
|
M_BLD = ~X_BLD
|
||||||
M_ITA = ~X_ITA
|
M_ITA = ~X_ITA
|
||||||
M_DEL = ~X_DEL
|
M_DEL = ~X_DEL
|
||||||
|
M_UND = ~X_UND
|
||||||
|
M_SUP = ~X_SUP
|
||||||
|
M_SUB = ~X_SUB
|
||||||
|
|
||||||
|
|
||||||
class ToOdt(Tokenizer):
|
class ToOdt(Tokenizer):
|
||||||
@@ -385,17 +392,8 @@ class ToOdt(Tokenizer):
|
|||||||
"""Convert the list of text tokens into XML elements."""
|
"""Convert the list of text tokens into XML elements."""
|
||||||
self._result = "" # Not used, but cleared just in case
|
self._result = "" # Not used, but cleared just in case
|
||||||
|
|
||||||
odtTags = {
|
pFmt = []
|
||||||
self.FMT_B_B: "_B", # Bold open format
|
pText = []
|
||||||
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 = []
|
|
||||||
pStyle = None
|
pStyle = None
|
||||||
for tType, _, tText, tFormat, tStyle in self._tokens:
|
for tType, _, tText, tFormat, tStyle in self._tokens:
|
||||||
|
|
||||||
@@ -429,19 +427,21 @@ class ToOdt(Tokenizer):
|
|||||||
|
|
||||||
# Process Text Types
|
# Process Text Types
|
||||||
if tType == self.T_EMPTY:
|
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:
|
if self._doJustify:
|
||||||
pStyle.setTextAlign("left")
|
pStyle.setTextAlign("left")
|
||||||
|
|
||||||
if len(para) > 0 and pStyle is not None:
|
if len(pText) > 0 and pStyle is not None:
|
||||||
tTemp = "\n".join(para)
|
tTxt = ""
|
||||||
fTemp = " ".join(fmt)
|
tFmt = []
|
||||||
tTxt = tTemp.rstrip()
|
for nText, nFmt in zip(pText, pFmt):
|
||||||
tFmt = fTemp[:len(tTxt)]
|
tLen = len(tTxt)
|
||||||
self._addTextPar("Text_20_body", pStyle, tTxt, tFmt=tFmt)
|
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 = []
|
pFmt = []
|
||||||
para = []
|
pText = []
|
||||||
pStyle = None
|
pStyle = None
|
||||||
|
|
||||||
elif tType == self.T_TITLE:
|
elif tType == self.T_TITLE:
|
||||||
@@ -477,15 +477,8 @@ class ToOdt(Tokenizer):
|
|||||||
elif tType == self.T_TEXT:
|
elif tType == self.T_TEXT:
|
||||||
if pStyle is None:
|
if pStyle is None:
|
||||||
pStyle = oStyle
|
pStyle = oStyle
|
||||||
|
pText.append(tText)
|
||||||
tFmt = " "*len(tText)
|
pFmt.append(tFormat)
|
||||||
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)
|
|
||||||
|
|
||||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||||
tTemp, fTemp = self._formatSynopsis(tText)
|
tTemp, fTemp = self._formatSynopsis(tText)
|
||||||
@@ -559,46 +552,39 @@ class ToOdt(Tokenizer):
|
|||||||
# Internal Functions
|
# 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."""
|
"""Apply formatting to synopsis lines."""
|
||||||
sSynop = self._localLookup("Synopsis")
|
name = self._localLookup("Synopsis")
|
||||||
rTxt = "**{0}:** {1}".format(sSynop, text)
|
rTxt = f"{name}: {text}"
|
||||||
rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(text))
|
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
|
||||||
return rTxt, rFmt
|
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."""
|
"""Apply formatting to comments."""
|
||||||
sComm = self._localLookup("Comment")
|
name = self._localLookup("Comment")
|
||||||
rTxt = "**{0}:** {1}".format(sComm, text)
|
rTxt = f"{name}: {text}"
|
||||||
rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(text))
|
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
|
||||||
return rTxt, rFmt
|
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."""
|
"""Apply formatting to keywords."""
|
||||||
valid, bits, _ = self._project.index.scanThis("@"+text)
|
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 "", ""
|
return "", []
|
||||||
|
|
||||||
rTxt = ""
|
rTxt = f"{trConst(nwLabels.KEY_NAME[bits[0]])}: "
|
||||||
rFmt = ""
|
rFmt = [(0, self.FMT_B_B), (len(rTxt) - 1, self.FMT_B_E)]
|
||||||
if bits[0] in nwLabels.KEY_NAME:
|
if len(bits) > 1:
|
||||||
text = nwLabels.KEY_NAME[bits[0]]
|
if bits[0] == nwKeyWords.TAG_KEY:
|
||||||
rTxt += "**{0}:** ".format(text)
|
rTxt += bits[1]
|
||||||
rFmt += "_B{0} b_ ".format(" "*len(text))
|
else:
|
||||||
if len(bits) > 1:
|
rTxt += ", ".join(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))
|
|
||||||
|
|
||||||
return rTxt, rFmt
|
return rTxt, rFmt
|
||||||
|
|
||||||
def _addTextPar(
|
def _addTextPar(
|
||||||
self, styleName: str, oStyle: ODTParagraphStyle, tText: str, tFmt: str = "",
|
self, styleName: str, oStyle: ODTParagraphStyle, tText: str,
|
||||||
isHead: bool = False, oLevel: str | None = None
|
tFmt: Sequence[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a text paragraph to the text XML element."""
|
"""Add a text paragraph to the text XML element."""
|
||||||
tAttr = {}
|
tAttr = {}
|
||||||
@@ -616,58 +602,59 @@ class ToOdt(Tokenizer):
|
|||||||
if not tText:
|
if not tText:
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
# Loop Over Fragments
|
||||||
# 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
|
|
||||||
|
|
||||||
parProc = XMLParagraph(xElem)
|
parProc = XMLParagraph(xElem)
|
||||||
|
|
||||||
for i, c in enumerate(tText):
|
pErr = 0
|
||||||
|
xFmt = 0x00
|
||||||
|
tFrag = ""
|
||||||
|
fLast = 0
|
||||||
|
for fPos, fFmt in tFmt:
|
||||||
|
|
||||||
if tFmt[i] == " ":
|
# Add the text up to the current fragment
|
||||||
tTemp += c
|
if tFrag := tText[fLast:fPos]:
|
||||||
elif tFmt[i] == "_":
|
if xFmt == 0x00:
|
||||||
continue
|
parProc.appendText(tFrag)
|
||||||
elif tFmt[i] == "B":
|
else:
|
||||||
|
parProc.appendSpan(tFrag, self._textStyle(xFmt))
|
||||||
|
|
||||||
|
# Calculate the change of format
|
||||||
|
if fFmt == self.FMT_B_B:
|
||||||
xFmt |= X_BLD
|
xFmt |= X_BLD
|
||||||
elif tFmt[i] == "b":
|
elif fFmt == self.FMT_B_E:
|
||||||
xFmt &= M_BLD
|
xFmt &= M_BLD
|
||||||
elif tFmt[i] == "I":
|
elif fFmt == self.FMT_I_B:
|
||||||
xFmt |= X_ITA
|
xFmt |= X_ITA
|
||||||
elif tFmt[i] == "i":
|
elif fFmt == self.FMT_I_E:
|
||||||
xFmt &= M_ITA
|
xFmt &= M_ITA
|
||||||
elif tFmt[i] == "S":
|
elif fFmt == self.FMT_D_B:
|
||||||
xFmt |= X_DEL
|
xFmt |= X_DEL
|
||||||
elif tFmt[i] == "s":
|
elif fFmt == self.FMT_D_E:
|
||||||
xFmt &= M_DEL
|
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:
|
else:
|
||||||
pErr += 1
|
pErr += 1
|
||||||
|
|
||||||
if xFmt != pFmt:
|
fLast = fPos
|
||||||
if pFmt == 0x00:
|
|
||||||
parProc.appendText(tTemp)
|
|
||||||
tTemp = ""
|
|
||||||
else:
|
|
||||||
parProc.appendSpan(tTemp, self._textStyle(pFmt))
|
|
||||||
tTemp = ""
|
|
||||||
|
|
||||||
pFmt = xFmt
|
if tFrag := tText[fLast:]:
|
||||||
|
if xFmt == 0x00:
|
||||||
# Save what remains in the buffer
|
parProc.appendText(tFrag)
|
||||||
if pFmt == 0x00:
|
else:
|
||||||
parProc.appendText(tTemp)
|
parProc.appendSpan(tFrag, self._textStyle(xFmt))
|
||||||
else:
|
|
||||||
parProc.appendSpan(tTemp, self._textStyle(pFmt))
|
|
||||||
|
|
||||||
if pErr > 0:
|
if pErr > 0:
|
||||||
self._errData.append("Unknown format tag encountered")
|
self._errData.append("Unknown format tag encountered")
|
||||||
@@ -713,6 +700,14 @@ class ToOdt(Tokenizer):
|
|||||||
if hFmt & X_DEL:
|
if hFmt & X_DEL:
|
||||||
newStyle.setStrikeStyle("solid")
|
newStyle.setStrikeStyle("solid")
|
||||||
newStyle.setStrikeType("single")
|
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)
|
self._autoText[hFmt] = (newName, newStyle)
|
||||||
|
|
||||||
@@ -1256,16 +1251,23 @@ class ODTTextStyle:
|
|||||||
"""
|
"""
|
||||||
VALID_WEIGHT = ["normal", "inherit", "bold"]
|
VALID_WEIGHT = ["normal", "inherit", "bold"]
|
||||||
VALID_STYLE = ["normal", "inherit", "italic"]
|
VALID_STYLE = ["normal", "inherit", "italic"]
|
||||||
|
VALID_POS = ["super", "sub"]
|
||||||
VALID_LSTYLE = ["none", "solid"]
|
VALID_LSTYLE = ["none", "solid"]
|
||||||
VALID_LTYPE = ["none", "single", "double"]
|
VALID_LTYPE = ["single", "double"]
|
||||||
|
VALID_LWIDTH = ["auto"]
|
||||||
|
VALID_LCOL = ["font-color"]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
# Text Attributes
|
# Text Attributes
|
||||||
self._tAttr = {
|
self._tAttr = {
|
||||||
"font-weight": ["fo", None],
|
"font-weight": ["fo", None],
|
||||||
"font-style": ["fo", None],
|
"font-style": ["fo", None],
|
||||||
|
"text-position": ["style", None],
|
||||||
"text-line-through-style": ["style", None],
|
"text-line-through-style": ["style", None],
|
||||||
"text-line-through-type": ["style", None],
|
"text-line-through-type": ["style", None],
|
||||||
|
"text-underline-style": ["style", None],
|
||||||
|
"text-underline-width": ["style", None],
|
||||||
|
"text-underline-color": ["style", None],
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1287,6 +1289,13 @@ class ODTTextStyle:
|
|||||||
self._tAttr["font-style"][1] = None
|
self._tAttr["font-style"][1] = None
|
||||||
return
|
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:
|
def setStrikeStyle(self, value: str | None) -> None:
|
||||||
if value in self.VALID_LSTYLE:
|
if value in self.VALID_LSTYLE:
|
||||||
self._tAttr["text-line-through-style"][1] = value
|
self._tAttr["text-line-through-style"][1] = value
|
||||||
@@ -1301,6 +1310,27 @@ class ODTTextStyle:
|
|||||||
self._tAttr["text-line-through-type"][1] = None
|
self._tAttr["text-line-through-type"][1] = None
|
||||||
return
|
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
|
# Methods
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass
|
|||||||
from novelwriter.common import minmax, transferCase
|
from novelwriter.common import minmax, transferCase
|
||||||
from novelwriter.constants import nwKeyWords, nwUnicode
|
from novelwriter.constants import nwKeyWords, nwUnicode
|
||||||
from novelwriter.core.index import countWords
|
from novelwriter.core.index import countWords
|
||||||
|
from novelwriter.core.document import NWDocument
|
||||||
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
||||||
from novelwriter.gui.editordocument import GuiTextDocument
|
from novelwriter.gui.editordocument import GuiTextDocument
|
||||||
from novelwriter.extensions.wheeleventfilter import WheelEventFilter
|
from novelwriter.extensions.wheeleventfilter import WheelEventFilter
|
||||||
@@ -408,12 +409,6 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
|
self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
|
||||||
return
|
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:
|
def replaceText(self, text: str) -> None:
|
||||||
"""Replace the text of the current document with the provided
|
"""Replace the text of the current document with the provided
|
||||||
text. This also clears undo history.
|
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
|
"""Tell the user where on the file system the file in the editor
|
||||||
is saved.
|
is saved.
|
||||||
"""
|
"""
|
||||||
if self._nwDocument is None:
|
if isinstance(self._nwDocument, NWDocument):
|
||||||
logger.error("No document open")
|
SHARED.info(
|
||||||
return
|
"<br>".join([
|
||||||
SHARED.info(
|
self.tr("Document Details"),
|
||||||
"<br>".join([
|
"–"*40,
|
||||||
self.tr("Document Details"),
|
self.tr("Created: {0}").format(self._nwDocument.createdDate),
|
||||||
"–"*40,
|
self.tr("Updated: {0}").format(self._nwDocument.updatedDate),
|
||||||
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
|
||||||
details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation),
|
)
|
||||||
log=False
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def insertText(self, insert: str | nwDocInsert) -> bool:
|
def insertText(self, insert: str | nwDocInsert) -> bool:
|
||||||
@@ -775,15 +768,15 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
newBlock = True
|
newBlock = True
|
||||||
goAfter = True
|
goAfter = True
|
||||||
elif insert == nwDocInsert.NEW_PAGE:
|
elif insert == nwDocInsert.NEW_PAGE:
|
||||||
text = "[NEW PAGE]"
|
text = "[newpage]"
|
||||||
newBlock = True
|
newBlock = True
|
||||||
goAfter = False
|
goAfter = False
|
||||||
elif insert == nwDocInsert.VSPACE_S:
|
elif insert == nwDocInsert.VSPACE_S:
|
||||||
text = "[VSPACE]"
|
text = "[vspace]"
|
||||||
newBlock = True
|
newBlock = True
|
||||||
goAfter = False
|
goAfter = False
|
||||||
elif insert == nwDocInsert.VSPACE_M:
|
elif insert == nwDocInsert.VSPACE_M:
|
||||||
text = "[VSPACE:2]"
|
text = "[vspace:2]"
|
||||||
newBlock = True
|
newBlock = True
|
||||||
goAfter = False
|
goAfter = False
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ from novelwriter.constants import nwRegEx, nwUnicode
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—]+\b")
|
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b")
|
||||||
SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
||||||
|
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
self._colDialD = QColor(0, 0, 0)
|
self._colDialD = QColor(0, 0, 0)
|
||||||
self._colDialS = QColor(0, 0, 0)
|
self._colDialS = QColor(0, 0, 0)
|
||||||
self._colHidden = QColor(0, 0, 0)
|
self._colHidden = QColor(0, 0, 0)
|
||||||
|
self._colCode = QColor(0, 0, 0)
|
||||||
self._colKey = QColor(0, 0, 0)
|
self._colKey = QColor(0, 0, 0)
|
||||||
self._colVal = QColor(0, 0, 0)
|
self._colVal = QColor(0, 0, 0)
|
||||||
self._colSpell = QColor(0, 0, 0)
|
self._colSpell = QColor(0, 0, 0)
|
||||||
@@ -95,6 +96,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
self._colDialD = QColor(*SHARED.theme.colDialD)
|
self._colDialD = QColor(*SHARED.theme.colDialD)
|
||||||
self._colDialS = QColor(*SHARED.theme.colDialS)
|
self._colDialS = QColor(*SHARED.theme.colDialS)
|
||||||
self._colHidden = QColor(*SHARED.theme.colHidden)
|
self._colHidden = QColor(*SHARED.theme.colHidden)
|
||||||
|
self._colCode = QColor(*SHARED.theme.colCode)
|
||||||
self._colKey = QColor(*SHARED.theme.colKey)
|
self._colKey = QColor(*SHARED.theme.colKey)
|
||||||
self._colVal = QColor(*SHARED.theme.colVal)
|
self._colVal = QColor(*SHARED.theme.colVal)
|
||||||
self._colSpell = QColor(*SHARED.theme.colSpell)
|
self._colSpell = QColor(*SHARED.theme.colSpell)
|
||||||
@@ -127,6 +129,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
"dialogue3": self._makeFormat(self._colDialS),
|
"dialogue3": self._makeFormat(self._colDialS),
|
||||||
"replace": self._makeFormat(self._colRepTag),
|
"replace": self._makeFormat(self._colRepTag),
|
||||||
"hidden": self._makeFormat(self._colHidden),
|
"hidden": self._makeFormat(self._colHidden),
|
||||||
|
"code": self._makeFormat(self._colCode),
|
||||||
"keyword": self._makeFormat(self._colKey),
|
"keyword": self._makeFormat(self._colKey),
|
||||||
"modifier": self._makeFormat(self._colMod),
|
"modifier": self._makeFormat(self._colMod),
|
||||||
"value": self._makeFormat(self._colVal, "underline"),
|
"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
|
# Alignment Tags
|
||||||
self._hRules.append((
|
self._hRules.append((
|
||||||
r"(^>{1,2}|<{1,2}$)", {
|
r"(^>{1,2}|<{1,2}$)", {
|
||||||
@@ -347,18 +366,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
else: # Text Paragraph
|
else: # Text Paragraph
|
||||||
|
|
||||||
if text.startswith("["): # Special Command
|
if text.startswith("["): # Special Command
|
||||||
sText = text.rstrip()
|
sText = text.rstrip().lower()
|
||||||
if sText in ("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]"):
|
if sText in ("[newpage]", "[new page]", "[vspace]"):
|
||||||
self.setFormat(0, len(text), self._hStyles["keyword"])
|
self.setFormat(0, len(text), self._hStyles["code"])
|
||||||
return
|
return
|
||||||
|
elif sText.startswith("[vspace:") and sText.endswith("]"):
|
||||||
elif sText.startswith("[VSPACE:") and sText.endswith("]"):
|
|
||||||
tLen = len(sText)
|
tLen = len(sText)
|
||||||
tVal = checkInt(sText[8:-1], 0)
|
tVal = checkInt(sText[8:-1], 0)
|
||||||
cVal = "codevalue" if tVal > 0 else "codeinval"
|
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(8, tLen-9, self._hStyles[cVal])
|
||||||
self.setFormat(tLen-1, tLen, self._hStyles["keyword"])
|
self.setFormat(tLen-1, tLen, self._hStyles["code"])
|
||||||
return
|
return
|
||||||
|
|
||||||
# Regular Text
|
# Regular Text
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ class GuiTheme:
|
|||||||
self.colDialD = [0, 0, 0]
|
self.colDialD = [0, 0, 0]
|
||||||
self.colDialS = [0, 0, 0]
|
self.colDialS = [0, 0, 0]
|
||||||
self.colHidden = [0, 0, 0]
|
self.colHidden = [0, 0, 0]
|
||||||
|
self.colCode = [0, 0, 0]
|
||||||
self.colKey = [0, 0, 0]
|
self.colKey = [0, 0, 0]
|
||||||
self.colVal = [0, 0, 0]
|
self.colVal = [0, 0, 0]
|
||||||
self.colSpell = [0, 0, 0]
|
self.colSpell = [0, 0, 0]
|
||||||
@@ -316,6 +317,7 @@ class GuiTheme:
|
|||||||
self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes")
|
self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes")
|
||||||
self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes")
|
self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes")
|
||||||
self.colHidden = self._parseColour(confParser, cnfSec, "hidden")
|
self.colHidden = self._parseColour(confParser, cnfSec, "hidden")
|
||||||
|
self.colCode = self._parseColour(confParser, cnfSec, "shortcode")
|
||||||
self.colKey = self._parseColour(confParser, cnfSec, "keyword")
|
self.colKey = self._parseColour(confParser, cnfSec, "keyword")
|
||||||
self.colVal = self._parseColour(confParser, cnfSec, "value")
|
self.colVal = self._parseColour(confParser, cnfSec, "value")
|
||||||
self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
|
self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
%%~name: Making a Scene
|
%%~name: Making a Scene
|
||||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
%%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5
|
%%~hash: d7920fe95fb48140278ae160fe7c3c1ce530072c
|
||||||
%%~date: Unknown/2023-09-07 19:00:10
|
%%~date: Unknown/2023-10-20 22:57:56
|
||||||
### Making a Scene
|
### Making a Scene
|
||||||
|
|
||||||
@pov: Jane
|
@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 doesn’t show it correctly, the export tool will not either.
|
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 doesn’t 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.”
|
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, let’s 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.
|
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, let’s 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.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
%%~name: Page
|
%%~name: Page
|
||||||
%%~path: 7031beac91f75/974e400180a99
|
%%~path: 7031beac91f75/974e400180a99
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
%%~hash: 7547809e972205eb2a55dd988595e0aa1d01e139
|
%%~hash: a0b0719e19763bf505bcf797bea0968c505c9b4a
|
||||||
%%~date: Unknown/2023-08-25 16:51:54
|
%%~date: Unknown/2023-11-06 11:19:33
|
||||||
[NEW PAGE]
|
[newpage]
|
||||||
[VSPACE:2]
|
[vspace:2]
|
||||||
|
|
||||||
This is a plain page with some text on it.
|
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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?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">
|
<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="1532" autoCount="238" editTime="75633">
|
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1609" autoCount="255" editTime="81165">
|
||||||
<name>Sample Project</name>
|
<name>Sample Project</name>
|
||||||
<title>Sample Project</title>
|
<title>Sample Project</title>
|
||||||
<author>Jane Smith</author>
|
<author>Jane Smith</author>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
|
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
|
||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</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">
|
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
|
||||||
<meta expanded="yes" />
|
<meta expanded="yes" />
|
||||||
<name status="sc24b8f" import="ia857f0">Novel</name>
|
<name status="sc24b8f" import="ia857f0">Novel</name>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
|
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<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>
|
<name status="sf12341" import="ia857f0" active="yes">Page</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<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>
|
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<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>
|
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<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'?>
|
<?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: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>
|
<office:meta>
|
||||||
<meta:creation-date>2023-07-19T23:55:29</meta:creation-date>
|
<meta:creation-date>2023-10-20T22:51:30</meta:creation-date>
|
||||||
<meta:generator>novelWriter/2.1-beta1</meta:generator>
|
<meta:generator>novelWriter/2.2-alpha1</meta:generator>
|
||||||
<meta:initial-creator>Jane Smith</meta:initial-creator>
|
<meta:initial-creator>Jane Smith</meta:initial-creator>
|
||||||
<meta:editing-cycles>1234</meta:editing-cycles>
|
<meta:editing-cycles>1234</meta:editing-cycles>
|
||||||
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
|
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
|
||||||
<dc:title>Test Project</dc:title>
|
<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>
|
<dc:creator>Jane Smith</dc:creator>
|
||||||
</office:meta>
|
</office:meta>
|
||||||
<office:font-face-decls>
|
<office:font-face-decls>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
</office:styles>
|
</office:styles>
|
||||||
<office:automatic-styles>
|
<office:automatic-styles>
|
||||||
<style:page-layout style:name="PM1">
|
<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-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-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>
|
</style:header-style>
|
||||||
|
|||||||
@@ -1281,7 +1281,7 @@ def testCoreIndex_CountWords():
|
|||||||
assert wC == 9
|
assert wC == 9
|
||||||
assert pC == 4
|
assert pC == 4
|
||||||
|
|
||||||
# Formatting Codes
|
# Formatting Codes, Upper Case (Old Implementation)
|
||||||
cC, wC, pC = countWords((
|
cC, wC, pC = countWords((
|
||||||
"Some text\n\n"
|
"Some text\n\n"
|
||||||
"[NEWPAGE]\n\n"
|
"[NEWPAGE]\n\n"
|
||||||
@@ -1297,4 +1297,29 @@ def testCoreIndex_CountWords():
|
|||||||
assert wC == 13
|
assert wC == 13
|
||||||
assert pC == 5
|
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
|
# END Test testCoreIndex_CountWords
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -81,8 +81,8 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
|
|||||||
# Paragraphs
|
# Paragraphs
|
||||||
# ==========
|
# ==========
|
||||||
|
|
||||||
# Text for GitHub Markdown
|
# Text for Extended Markdown
|
||||||
theMD.setGitHubMarkdown()
|
theMD.setExtendedMarkdown()
|
||||||
theMD._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
theMD._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||||
theMD.tokenizeText()
|
theMD.tokenizeText()
|
||||||
theMD.doConvert()
|
theMD.doConvert()
|
||||||
|
|||||||
+390
-297
File diff suppressed because it is too large
Load Diff
@@ -600,15 +600,15 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
|||||||
|
|
||||||
nwGUI.docEditor.setPlainText("### Stuff\n")
|
nwGUI.docEditor.setPlainText("### Stuff\n")
|
||||||
nwGUI.mainMenu.aInsNewPage.activate(QAction.Trigger)
|
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.docEditor.setPlainText("### Stuff\n")
|
||||||
nwGUI.mainMenu.aInsVSpaceS.activate(QAction.Trigger)
|
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.docEditor.setPlainText("### Stuff\n")
|
||||||
nwGUI.mainMenu.aInsVSpaceM.activate(QAction.Trigger)
|
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()
|
nwGUI.docEditor.clear()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user