Separate spell checking and link scanning, and add ODT support for links

This commit is contained in:
Veronica Berglyd Olsen
2024-10-25 19:30:22 +02:00
parent b13b91c8e8
commit fe56c88309
4 changed files with 84 additions and 35 deletions
+33 -10
View File
@@ -49,14 +49,15 @@ logger = logging.getLogger(__name__)
# Main XML NameSpaces
XML_NS = {
"dc": "http://purl.org/dc/elements/1.1/",
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
"loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
"style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
"loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
"text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
"dc": "http://purl.org/dc/elements/1.1/",
"xlink": "http://www.w3.org/1999/xlink",
}
for ns, uri in XML_NS.items():
ET.register_namespace(ns, uri)
@@ -80,7 +81,6 @@ TAG_SPC = _mkTag("text", "s")
TAG_NSPC = _mkTag("text", "c")
TAG_TAB = _mkTag("text", "tab")
TAG_SPAN = _mkTag("text", "span")
TAG_STNM = _mkTag("text", "style-name")
# Formatting Codes
X_BLD = 0x001 # Bold format
@@ -91,6 +91,7 @@ X_MRK = 0x010 # Marked format
X_SUP = 0x020 # Superscript
X_SUB = 0x040 # Subscript
X_COL = 0x080 # Coloured text
X_HRF = 0x100 # Link
# Formatting Masks
M_BLD = ~X_BLD
@@ -101,6 +102,7 @@ M_MRK = ~X_MRK
M_SUP = ~X_SUP
M_SUB = ~X_SUB
M_COL = ~X_COL
M_HRF = ~X_HRF
# ODT Styles
S_TITLE = "Title"
@@ -570,6 +572,7 @@ class ToOdt(Tokenizer):
fLast = 0
xNode = None
fClass = ""
fLink = ""
for fPos, fFmt, fData in tFmt or []:
# Add any extra nodes
@@ -582,7 +585,7 @@ class ToOdt(Tokenizer):
if xFmt == 0x00:
parProc.appendText(tFrag)
else:
parProc.appendSpan(tFrag, self._textStyle(xFmt, fClass))
parProc.appendSpan(tFrag, self._textStyle(xFmt, fClass), fLink)
# Calculate the change of format
if fFmt == TextFmt.B_B:
@@ -619,6 +622,12 @@ class ToOdt(Tokenizer):
elif fFmt == TextFmt.COL_E:
xFmt &= M_COL
fClass = ""
elif fFmt == TextFmt.HRF_B:
xFmt |= X_HRF
fLink = fData
elif fFmt == TextFmt.HRF_E:
xFmt &= M_HRF
fLink = ""
elif fFmt == TextFmt.FNOTE:
xNode = self._generateFootnote(fData)
elif fFmt == TextFmt.STRIP:
@@ -633,7 +642,7 @@ class ToOdt(Tokenizer):
if xFmt == 0x00:
parProc.appendText(tFrag)
else:
parProc.appendSpan(tFrag, self._textStyle(xFmt, fClass))
parProc.appendSpan(tFrag, self._textStyle(xFmt, fClass), fLink)
nErr, errMsg = parProc.checkError()
if nErr > 0: # pragma: no cover
@@ -692,6 +701,11 @@ class ToOdt(Tokenizer):
style.setTextPosition("sub")
if hFmt & X_COL and color:
style.setColor(color)
if hFmt & X_HRF:
style.setColor(self._theme.link)
style.setUnderlineStyle("solid")
style.setUnderlineWidth("auto")
style.setUnderlineColor("font-color")
self._autoText[tKey] = style
return style.name
@@ -1497,13 +1511,22 @@ class XMLParagraph:
return
def appendSpan(self, text: str, fmt: str) -> None:
def appendSpan(self, text: str, style: str, link: str) -> None:
"""Append a text span to the XML element. The span is always
closed since we do not allow nested spans (like Libre Office).
closed since we do not produce nested spans (like Libre Office).
Therefore we return to the root element level when we're done
processing the text of the span.
"""
self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={TAG_STNM: fmt})
if link:
self._xTail = ET.SubElement(self._xRoot, _mkTag("text", "a"), attrib={
_mkTag("xlink", "type"): "simple",
_mkTag("xlink", "href"): link,
_mkTag("text", "style-name"): style,
})
else:
self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={
_mkTag("text", "style-name"): style,
})
self._xTail.text = "" # Defaults to None
self._xTail.tail = "" # Defaults to None
self._nState = X_SPAN_TEXT
+11 -3
View File
@@ -330,12 +330,20 @@ class ToQTextDocument(Tokenizer):
cFmt.setAnchorNames([data])
elif fmt == TextFmt.ANM_E:
cFmt.setAnchor(False)
elif fmt in (TextFmt.HRF_B, TextFmt.ARF_B):
cFmt.setForeground(primary or self._theme.link)
elif fmt == TextFmt.ARF_B:
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
cFmt.setAnchorHref(data)
elif fmt in (TextFmt.HRF_E, TextFmt.ARF_E):
elif fmt == TextFmt.ARF_E:
cFmt.setFontUnderline(False)
cFmt.setAnchor(False)
cFmt.setAnchorHref("")
elif fmt == TextFmt.HRF_B:
cFmt.setForeground(self._theme.link)
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
cFmt.setAnchorHref(data)
elif fmt == TextFmt.HRF_E:
cFmt.setForeground(primary or self._theme.text)
cFmt.setFontUnderline(False)
cFmt.setAnchor(False)
+18 -13
View File
@@ -428,6 +428,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
data = TextBlockData()
self.setCurrentBlockUserData(data)
data.extractMetaData(text, xOff)
if self._spellCheck:
for xPos, xEnd in data.spellCheck(text, xOff):
for x in range(xPos, xEnd):
@@ -496,20 +497,9 @@ class TextBlockData(QTextBlockUserData):
"""Return spell error data from last check."""
return self._spellErrors
def spellCheck(self, text: str, offset: int) -> list[tuple[int, int]]:
"""Run the spell checker and cache the result, and return the
list of spell check errors.
"""
def extractMetaData(self, text: str, offset: int) -> None:
"""Extract meta data from the text."""
self._metaData = []
self._spellErrors = []
if "[" in text:
# Strip shortcodes
for regEx in [RX_FMT_SC, RX_FMT_SV]:
for res in regEx.finditer(text, offset):
if (s := res.start(0)) >= 0 and (e := res.end(0)) >= 0:
pad = " "*(e - s)
text = f"{text[:s]}{pad}{text[e:]}"
if "http" in text:
# Strip URLs
for res in RX_URL.finditer(text, offset):
@@ -518,6 +508,21 @@ class TextBlockData(QTextBlockUserData):
text = f"{text[:s]}{pad}{text[e:]}"
self._metaData.append((s, e, res.group(0), "url"))
return
def spellCheck(self, text: str, offset: int) -> list[tuple[int, int]]:
"""Run the spell checker and cache the result, and return the
list of spell check errors.
"""
if "[" in text:
# Strip shortcodes
for regEx in [RX_FMT_SC, RX_FMT_SV]:
for res in regEx.finditer(text, offset):
if (s := res.start(0)) >= 0 and (e := res.end(0)) >= 0:
pad = " "*(e - s)
text = f"{text[:s]}{pad}{text[e:]}"
self._spellErrors = []
checker = SHARED.spelling
for res in RX_WORDS.finditer(text.replace("_", " "), offset):
if (
+22 -9
View File
@@ -38,13 +38,14 @@ from novelwriter.formats.toodt import ODTParagraphStyle, ODTTextStyle, ToOdt, XM
from tests.tools import ODT_IGNORE, cmpFiles
XML_NS = [
' 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:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"',
' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"',
' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"',
' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"',
' xmlns:dc="http://purl.org/dc/elements/1.1/"'
' xmlns:xlink="http://www.w3.org/1999/xlink"',
]
@@ -1301,7 +1302,7 @@ def testFmtToOdt_XMLParagraph():
)
# Text Span
xmlPar.appendSpan("spanned text", "T1")
xmlPar.appendSpan("spanned text", "T1", "")
assert xmlToText(xRoot) == (
'<root>'
'<text:p>Hello World'
@@ -1338,7 +1339,7 @@ def testFmtToOdt_XMLParagraph():
)
# Text Span w/Line Break
xmlPar.appendSpan("spanned\ntext", "T1")
xmlPar.appendSpan("spanned\ntext", "T1", "")
assert xmlToText(xRoot) == (
'<root>'
'<text:p>Hello<text:line-break />World<text:line-break />!!'
@@ -1374,7 +1375,7 @@ def testFmtToOdt_XMLParagraph():
)
# Text Span w/Line Break
xmlPar.appendSpan("spanned\ttext", "T1")
xmlPar.appendSpan("spanned\ttext", "T1", "")
assert xmlToText(xRoot) == (
'<root>'
'<text:p>Hello<text:tab />World<text:tab />!!'
@@ -1392,6 +1393,18 @@ def testFmtToOdt_XMLParagraph():
'</root>'
)
# Tail Text w/Link
xmlPar.appendSpan("Example", "T1", "http://www.example.com")
assert xmlToText(xRoot) == (
'<root>'
'<text:p>Hello<text:tab />World<text:tab />!!'
'<text:span text:style-name="T1">spanned<text:tab />text</text:span>'
'more<text:tab />text'
'<text:a xlink:type="simple" xlink:href="http://www.example.com" text:style-name="T1">'
'Example</text:a></text:p>'
'</root>'
)
assert xmlPar.checkError() == (0, "")
# Stage 4 : Spaces
@@ -1410,7 +1423,7 @@ def testFmtToOdt_XMLParagraph():
)
# Text Span w/Spaces
xmlPar.appendSpan("spanned text", "T1")
xmlPar.appendSpan("spanned text", "T1", "")
assert xmlToText(xRoot) == (
'<root>'
'<text:p>Hello <text:s />World <text:s text:c="2" />!!'
@@ -1446,7 +1459,7 @@ def testFmtToOdt_XMLParagraph():
)
# Text Span w/Many Spaces
xmlPar.appendSpan(" C \t D \n E ", "T1")
xmlPar.appendSpan(" C \t D \n E ", "T1", "")
assert xmlToText(xRoot) == (
'<root>'
'<text:p><text:s text:c="2" /><text:tab /> A <text:line-break /> <text:s />B '