Make dialog parser and keyword lines work with 4 byte unicode

This commit is contained in:
Veronica Berglyd Olsen
2025-07-05 01:50:47 +02:00
parent f8a333c4ea
commit 57bed18ccf
3 changed files with 45 additions and 9 deletions
+15
View File
@@ -493,6 +493,21 @@ def decodeMimeHandles(mimeData: QMimeData) -> list[str]:
return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|") return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|")
def utf16CharMap(text: str) -> list[int]:
"""Compute mapping from Python string index to QString index.
Python strings are always one character per position in either
ASCII, UCS-2 or UCS-4. QStrings are in UTF-16, so wide characters
use 2 indices, and thus creates an offset.
"""
posMap = list(range(0, len(text) + 1))
offset = 0
for i, c in enumerate(text):
if ord(c) > 0xffff:
offset += 1
posMap[i + 1] = i + 1 + offset
return posMap
## ##
# Encoder Functions # Encoder Functions
## ##
+24 -7
View File
@@ -35,7 +35,7 @@ from PyQt6.QtGui import (
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import checkInt from novelwriter.common import checkInt, utf16CharMap
from novelwriter.constants import nwStyles, nwUnicode from novelwriter.constants import nwStyles, nwUnicode
from novelwriter.enum import nwComment from novelwriter.enum import nwComment
from novelwriter.text.comments import processComment from novelwriter.text.comments import processComment
@@ -301,6 +301,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return return
bLen = self.currentBlock().length() bLen = self.currentBlock().length()
isWide = bLen > len(text) + 1
xOff = 0 xOff = 0
hRules = None hRules = None
if text.startswith("@"): # Keywords and commands if text.startswith("@"): # Keywords and commands
@@ -309,17 +311,32 @@ class GuiDocHighlighter(QSyntaxHighlighter):
isValid, bits, pos = index.scanThis(text) isValid, bits, pos = index.scanThis(text)
isGood = index.checkThese(bits, self._tHandle) isGood = index.checkThese(bits, self._tHandle)
if isValid: if isValid:
posMap = []
if isWide:
posMap = utf16CharMap(text)
for n, bit in enumerate(bits): for n, bit in enumerate(bits):
xPos = pos[n] if posMap:
xLen = len(bit) xPos = posMap[pos[n]]
xLen = posMap[pos[n] + len(bit)] - xPos
else:
xPos = pos[n]
xLen = len(bit)
if n == 0 and isGood[n]: if n == 0 and isGood[n]:
self.setFormat(xPos, xLen, self._hStyles["keyword"]) self.setFormat(xPos, xLen, self._hStyles["keyword"])
elif isGood[n] and not self._isInactive: elif isGood[n] and not self._isInactive:
one, two = index.parseValue(bit) one, two = index.parseValue(bit)
self.setFormat(xPos, len(one), self._hStyles["tag"]) if posMap:
oLen = posMap[pos[n] + len(one)] - xPos
else:
oLen = len(one)
self.setFormat(xPos, oLen, self._hStyles["tag"])
if two: if two:
yPos = xPos + len(bit) - len(two) if posMap:
self.setFormat(yPos, len(two), self._hStyles["optional"]) yLen = posMap[pos[n] + len(two)] - xPos
else:
yLen = len(two)
yPos = xPos + xLen - yLen
self.setFormat(yPos, yLen, self._hStyles["optional"])
elif not self._isInactive: elif not self._isInactive:
self.setFormat(xPos, xLen, self._hStyles["invalid"]) self.setFormat(xPos, xLen, self._hStyles["invalid"])
@@ -399,7 +416,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setCurrentBlockState(BLOCK_TEXT) self.setCurrentBlockState(BLOCK_TEXT)
hRules = self._txtRules if self._isNovel else self._minRules hRules = self._txtRules if self._isNovel else self._minRules
if self._isNovel and self._dialogParser.enabled: if self._isNovel and self._dialogParser.enabled:
for pos, end in self._dialogParser(text): for pos, end in self._dialogParser(text, isWide):
length = end - pos length = end - pos
self.setFormat(pos, length, self._hStyles["dialog"]) self.setFormat(pos, length, self._hStyles["dialog"])
+6 -2
View File
@@ -27,7 +27,7 @@ from __future__ import annotations
import re import re
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.common import compact, uniqueCompact from novelwriter.common import compact, uniqueCompact, utf16CharMap
from novelwriter.constants import nwRegEx, nwUnicode from novelwriter.constants import nwRegEx, nwUnicode
@@ -170,7 +170,7 @@ class DialogParser:
return return
def __call__(self, text: str) -> list[tuple[int, int]]: def __call__(self, text: str, wideChar: bool = False) -> list[tuple[int, int]]:
"""Caller wrapper for dialogue processing.""" """Caller wrapper for dialogue processing."""
temp: list[int] = [] temp: list[int] = []
result: list[tuple[int, int]] = [] result: list[tuple[int, int]] = []
@@ -218,4 +218,8 @@ class DialogParser:
result.append((start, pos)) result.append((start, pos))
start = None start = None
if wideChar:
posMap = utf16CharMap(text)
result = [(posMap[s], posMap[p]) for s, p in result]
return result return result