Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
1014 lines
28 KiB
C++
1014 lines
28 KiB
C++
// This file is part of Desktop App Toolkit,
|
|
// a set of libraries for developing nice desktop applications.
|
|
//
|
|
// For license and copyright information please follow this link:
|
|
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
|
//
|
|
#pragma once
|
|
|
|
#include "ui/text/text.h"
|
|
|
|
#include <private/qunicodetables_p.h>
|
|
#include <private/qtextengine_p.h>
|
|
|
|
#define BIDI_DEBUG if (1) ; else qDebug
|
|
|
|
namespace Ui::Text {
|
|
|
|
enum { BidiDebugEnabled = false };
|
|
|
|
class BidiAlgorithm {
|
|
public:
|
|
template<typename T> using Vector = QVarLengthArray<T, 64>;
|
|
|
|
BidiAlgorithm(const QChar *text, QScriptAnalysis *analysis, int length, bool baseDirectionIsRtl,
|
|
Blocks::const_iterator startInBlocks,
|
|
Blocks::const_iterator endInBlocks,
|
|
int offsetInBlocks)
|
|
: text(text),
|
|
analysis(analysis),
|
|
length(length),
|
|
baseLevel(baseDirectionIsRtl ? 1 : 0),
|
|
_startInBlocks(startInBlocks),
|
|
_endInBlocks(endInBlocks),
|
|
_currentBlock(_startInBlocks),
|
|
_offsetInBlocks(offsetInBlocks)
|
|
{
|
|
|
|
}
|
|
|
|
struct IsolatePair {
|
|
int start;
|
|
int end;
|
|
};
|
|
|
|
void initScriptAnalysisAndIsolatePairs(Vector<IsolatePair> &isolatePairs)
|
|
{
|
|
int isolateStack[128];
|
|
int isolateLevel = 0;
|
|
// load directions of string, and determine isolate pairs
|
|
for (int i = 0; i < length; ++i) {
|
|
int pos = i;
|
|
const auto info = infoAt(i);
|
|
if (info.surrogate) {
|
|
++i;
|
|
analysis[i].bidiDirection = QChar::DirNSM;
|
|
}
|
|
const auto p = info.properties;
|
|
analysis[pos].bidiDirection = QChar::Direction(p->direction);
|
|
switch (QChar::Direction(p->direction)) {
|
|
case QChar::DirON:
|
|
// all mirrored chars are DirON
|
|
if (p->mirrorDiff)
|
|
analysis[pos].bidiFlags = QScriptAnalysis::BidiMirrored;
|
|
break;
|
|
case QChar::DirLRE:
|
|
case QChar::DirRLE:
|
|
case QChar::DirLRO:
|
|
case QChar::DirRLO:
|
|
case QChar::DirPDF:
|
|
case QChar::DirBN:
|
|
analysis[pos].bidiFlags = QScriptAnalysis::BidiMaybeResetToParagraphLevel|QScriptAnalysis::BidiBN;
|
|
break;
|
|
case QChar::DirLRI:
|
|
case QChar::DirRLI:
|
|
case QChar::DirFSI:
|
|
if (isolateLevel < 128) {
|
|
isolateStack[isolateLevel] = isolatePairs.size();
|
|
isolatePairs.append({ pos, length });
|
|
}
|
|
++isolateLevel;
|
|
analysis[pos].bidiFlags = QScriptAnalysis::BidiMaybeResetToParagraphLevel;
|
|
break;
|
|
case QChar::DirPDI:
|
|
if (isolateLevel > 0) {
|
|
--isolateLevel;
|
|
if (isolateLevel < 128)
|
|
isolatePairs[isolateStack[isolateLevel]].end = pos;
|
|
}
|
|
Q_FALLTHROUGH();
|
|
case QChar::DirWS:
|
|
analysis[pos].bidiFlags = QScriptAnalysis::BidiMaybeResetToParagraphLevel;
|
|
break;
|
|
case QChar::DirS:
|
|
case QChar::DirB:
|
|
analysis[pos].bidiFlags = QScriptAnalysis::BidiResetToParagraphLevel;
|
|
if (text[pos] == QChar::ParagraphSeparator) {
|
|
// close all open isolates as we start a new paragraph
|
|
while (isolateLevel > 0) {
|
|
--isolateLevel;
|
|
if (isolateLevel < 128)
|
|
isolatePairs[isolateStack[isolateLevel]].end = pos;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DirectionalRun {
|
|
int start;
|
|
int end;
|
|
int continuation;
|
|
ushort level;
|
|
bool isContinuation;
|
|
bool hasContent;
|
|
};
|
|
|
|
void generateDirectionalRuns(const Vector<IsolatePair> &isolatePairs, Vector<DirectionalRun> &runs)
|
|
{
|
|
struct DirectionalStack {
|
|
enum { MaxDepth = 125 };
|
|
struct Item {
|
|
ushort level;
|
|
bool isOverride;
|
|
bool isIsolate;
|
|
int runBeforeIsolate;
|
|
};
|
|
Item items[128];
|
|
int counter = 0;
|
|
|
|
void push(Item i) {
|
|
items[counter] = i;
|
|
++counter;
|
|
}
|
|
void pop() {
|
|
--counter;
|
|
}
|
|
int depth() const {
|
|
return counter;
|
|
}
|
|
const Item &top() const {
|
|
return items[counter - 1];
|
|
}
|
|
} stack;
|
|
int overflowIsolateCount = 0;
|
|
int overflowEmbeddingCount = 0;
|
|
int validIsolateCount = 0;
|
|
|
|
ushort level = baseLevel;
|
|
bool override = false;
|
|
stack.push({ level, false, false, -1 });
|
|
|
|
BIDI_DEBUG() << "resolving explicit levels";
|
|
int runStart = 0;
|
|
int continuationFrom = -1;
|
|
int lastRunWithContent = -1;
|
|
bool runHasContent = false;
|
|
|
|
auto appendRun = [&](int runEnd) {
|
|
if (runEnd < runStart)
|
|
return;
|
|
bool isContinuation = false;
|
|
if (continuationFrom != -1) {
|
|
runs[continuationFrom].continuation = runs.size();
|
|
isContinuation = true;
|
|
} else if (lastRunWithContent != -1 && level == runs.at(lastRunWithContent).level) {
|
|
runs[lastRunWithContent].continuation = runs.size();
|
|
isContinuation = true;
|
|
}
|
|
if (runHasContent)
|
|
lastRunWithContent = runs.size();
|
|
BIDI_DEBUG() << " appending run start/end" << runStart << runEnd << "level" << level;
|
|
runs.append({ runStart, runEnd, -1, level, isContinuation, runHasContent });
|
|
runHasContent = false;
|
|
runStart = runEnd + 1;
|
|
continuationFrom = -1;
|
|
};
|
|
|
|
int isolatePairPosition = 0;
|
|
|
|
for (int i = 0; i < length; ++i) {
|
|
QChar::Direction dir = analysis[i].bidiDirection;
|
|
|
|
|
|
auto doEmbed = [&](bool isRtl, bool isOverride, bool isIsolate) {
|
|
if (isIsolate) {
|
|
if (override)
|
|
analysis[i].bidiDirection = (level & 1) ? QChar::DirR : QChar::DirL;
|
|
runHasContent = true;
|
|
lastRunWithContent = -1;
|
|
++isolatePairPosition;
|
|
}
|
|
int runBeforeIsolate = runs.size();
|
|
ushort newLevel = isRtl ? ((stack.top().level + 1) | 1) : ((stack.top().level + 2) & ~1);
|
|
if (newLevel <= DirectionalStack::MaxDepth && !overflowEmbeddingCount && !overflowIsolateCount) {
|
|
if (isIsolate)
|
|
++validIsolateCount;
|
|
else
|
|
runBeforeIsolate = -1;
|
|
appendRun(isIsolate ? i : i - 1);
|
|
BIDI_DEBUG() << "pushing new item on stack: level" << (int)newLevel << "isOverride" << isOverride << "isIsolate" << isIsolate << runBeforeIsolate;
|
|
stack.push({ newLevel, isOverride, isIsolate, runBeforeIsolate });
|
|
override = isOverride;
|
|
level = newLevel;
|
|
} else {
|
|
if (isIsolate)
|
|
++overflowIsolateCount;
|
|
else if (!overflowIsolateCount)
|
|
++overflowEmbeddingCount;
|
|
}
|
|
if (!isIsolate) {
|
|
if (override)
|
|
analysis[i].bidiDirection = (level & 1) ? QChar::DirR : QChar::DirL;
|
|
else
|
|
analysis[i].bidiDirection = QChar::DirBN;
|
|
}
|
|
};
|
|
|
|
switch (dir) {
|
|
case QChar::DirLRE:
|
|
doEmbed(false, false, false);
|
|
break;
|
|
case QChar::DirRLE:
|
|
doEmbed(true, false, false);
|
|
break;
|
|
case QChar::DirLRO:
|
|
doEmbed(false, true, false);
|
|
break;
|
|
case QChar::DirRLO:
|
|
doEmbed(true, true, false);
|
|
break;
|
|
case QChar::DirLRI:
|
|
doEmbed(false, false, true);
|
|
break;
|
|
case QChar::DirRLI:
|
|
doEmbed(true, false, true);
|
|
break;
|
|
case QChar::DirFSI: {
|
|
bool isRtl = false;
|
|
if (isolatePairPosition < isolatePairs.size()) {
|
|
const auto &pair = isolatePairs.at(isolatePairPosition);
|
|
Q_ASSERT(pair.start == i);
|
|
isRtl = QStringView(text + pair.start + 1, pair.end - pair.start - 1).isRightToLeft();
|
|
}
|
|
doEmbed(isRtl, false, true);
|
|
break;
|
|
}
|
|
|
|
case QChar::DirPDF:
|
|
if (override)
|
|
analysis[i].bidiDirection = (level & 1) ? QChar::DirR : QChar::DirL;
|
|
else
|
|
analysis[i].bidiDirection = QChar::DirBN;
|
|
if (overflowIsolateCount) {
|
|
; // do nothing
|
|
} else if (overflowEmbeddingCount) {
|
|
--overflowEmbeddingCount;
|
|
} else if (!stack.top().isIsolate && stack.depth() >= 2) {
|
|
appendRun(i);
|
|
stack.pop();
|
|
override = stack.top().isOverride;
|
|
level = stack.top().level;
|
|
BIDI_DEBUG() << "popped PDF from stack, level now" << (int)stack.top().level;
|
|
}
|
|
break;
|
|
case QChar::DirPDI:
|
|
runHasContent = true;
|
|
if (overflowIsolateCount) {
|
|
--overflowIsolateCount;
|
|
} else if (validIsolateCount == 0) {
|
|
; // do nothing
|
|
} else {
|
|
appendRun(i - 1);
|
|
overflowEmbeddingCount = 0;
|
|
while (!stack.top().isIsolate)
|
|
stack.pop();
|
|
continuationFrom = stack.top().runBeforeIsolate;
|
|
BIDI_DEBUG() << "popped PDI from stack, level now" << (int)stack.top().level << "continuation from" << continuationFrom;
|
|
stack.pop();
|
|
override = stack.top().isOverride;
|
|
level = stack.top().level;
|
|
lastRunWithContent = -1;
|
|
--validIsolateCount;
|
|
}
|
|
if (override)
|
|
analysis[i].bidiDirection = (level & 1) ? QChar::DirR : QChar::DirL;
|
|
break;
|
|
case QChar::DirB:
|
|
// paragraph separator, go down to base direction, reset all state
|
|
if (text[i].unicode() == QChar::ParagraphSeparator) {
|
|
appendRun(i - 1);
|
|
while (stack.counter > 1) {
|
|
// there might be remaining isolates on the stack that are missing a PDI. Those need to get
|
|
// a continuation indicating to take the eos from the end of the string (ie. the paragraph level)
|
|
const auto &t = stack.top();
|
|
if (t.isIsolate) {
|
|
runs[t.runBeforeIsolate].continuation = -2;
|
|
}
|
|
--stack.counter;
|
|
}
|
|
continuationFrom = -1;
|
|
lastRunWithContent = -1;
|
|
validIsolateCount = 0;
|
|
overflowIsolateCount = 0;
|
|
overflowEmbeddingCount = 0;
|
|
level = baseLevel;
|
|
}
|
|
break;
|
|
default:
|
|
runHasContent = true;
|
|
Q_FALLTHROUGH();
|
|
case QChar::DirBN:
|
|
if (override)
|
|
analysis[i].bidiDirection = (level & 1) ? QChar::DirR : QChar::DirL;
|
|
break;
|
|
}
|
|
}
|
|
appendRun(length - 1);
|
|
while (stack.counter > 1) {
|
|
// there might be remaining isolates on the stack that are missing a PDI. Those need to get
|
|
// a continuation indicating to take the eos from the end of the string (ie. the paragraph level)
|
|
const auto &t = stack.top();
|
|
if (t.isIsolate) {
|
|
runs[t.runBeforeIsolate].continuation = -2;
|
|
}
|
|
--stack.counter;
|
|
}
|
|
}
|
|
|
|
void resolveExplicitLevels(Vector<DirectionalRun> &runs)
|
|
{
|
|
Vector<IsolatePair> isolatePairs;
|
|
|
|
initScriptAnalysisAndIsolatePairs(isolatePairs);
|
|
generateDirectionalRuns(isolatePairs, runs);
|
|
}
|
|
|
|
struct IsolatedRunSequenceIterator {
|
|
struct Position {
|
|
int current = -1;
|
|
int pos = -1;
|
|
|
|
Position() = default;
|
|
Position(int current, int pos) : current(current), pos(pos) {}
|
|
|
|
bool isValid() const { return pos != -1; }
|
|
void clear() { pos = -1; }
|
|
};
|
|
IsolatedRunSequenceIterator(const Vector<DirectionalRun> &runs, int i)
|
|
: runs(runs),
|
|
current(i)
|
|
{
|
|
pos = runs.at(current).start;
|
|
}
|
|
int operator *() const { return pos; }
|
|
bool atEnd() const { return pos < 0; }
|
|
void operator++() {
|
|
++pos;
|
|
if (pos > runs.at(current).end) {
|
|
current = runs.at(current).continuation;
|
|
if (current > -1)
|
|
pos = runs.at(current).start;
|
|
else
|
|
pos = -1;
|
|
}
|
|
}
|
|
void setPosition(Position p) {
|
|
current = p.current;
|
|
pos = p.pos;
|
|
}
|
|
Position position() const {
|
|
return Position(current, pos);
|
|
}
|
|
bool operator !=(int position) const {
|
|
return pos != position;
|
|
}
|
|
|
|
const Vector<DirectionalRun> &runs;
|
|
int current;
|
|
int pos;
|
|
};
|
|
|
|
|
|
void resolveW1W2W3(const Vector<DirectionalRun> &runs, int i, QChar::Direction sos)
|
|
{
|
|
QChar::Direction last = sos;
|
|
QChar::Direction lastStrong = sos;
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
while (!it.atEnd()) {
|
|
int pos = *it;
|
|
|
|
// Rule W1: Resolve NSM
|
|
QChar::Direction current = analysis[pos].bidiDirection;
|
|
if (current == QChar::DirNSM) {
|
|
current = last;
|
|
analysis[pos].bidiDirection = current;
|
|
} else if (current >= QChar::DirLRI) {
|
|
last = QChar::DirON;
|
|
} else if (current == QChar::DirBN) {
|
|
current = last;
|
|
} else {
|
|
// there shouldn't be any explicit embedding marks here
|
|
Q_ASSERT(current != QChar::DirLRE);
|
|
Q_ASSERT(current != QChar::DirRLE);
|
|
Q_ASSERT(current != QChar::DirLRO);
|
|
Q_ASSERT(current != QChar::DirRLO);
|
|
Q_ASSERT(current != QChar::DirPDF);
|
|
|
|
last = current;
|
|
}
|
|
|
|
// Rule W2
|
|
if (current == QChar::DirEN && lastStrong == QChar::DirAL) {
|
|
current = QChar::DirAN;
|
|
analysis[pos].bidiDirection = current;
|
|
}
|
|
|
|
// remember last strong char for rule W2
|
|
if (current == QChar::DirL || current == QChar::DirR) {
|
|
lastStrong = current;
|
|
} else if (current == QChar::DirAL) {
|
|
// Rule W3
|
|
lastStrong = current;
|
|
analysis[pos].bidiDirection = QChar::DirR;
|
|
}
|
|
last = current;
|
|
++it;
|
|
}
|
|
}
|
|
|
|
|
|
void resolveW4(const Vector<DirectionalRun> &runs, int i, QChar::Direction sos)
|
|
{
|
|
// Rule W4
|
|
QChar::Direction secondLast = sos;
|
|
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
int lastPos = *it;
|
|
QChar::Direction last = analysis[lastPos].bidiDirection;
|
|
|
|
// BIDI_DEBUG() << "Applying rule W4/W5";
|
|
++it;
|
|
while (!it.atEnd()) {
|
|
int pos = *it;
|
|
QChar::Direction current = analysis[pos].bidiDirection;
|
|
if (current == QChar::DirBN) {
|
|
++it;
|
|
continue;
|
|
}
|
|
// BIDI_DEBUG() << pos << secondLast << last << current;
|
|
if (last == QChar::DirES && current == QChar::DirEN && secondLast == QChar::DirEN) {
|
|
last = QChar::DirEN;
|
|
analysis[lastPos].bidiDirection = last;
|
|
} else if (last == QChar::DirCS) {
|
|
if (current == QChar::DirEN && secondLast == QChar::DirEN) {
|
|
last = QChar::DirEN;
|
|
analysis[lastPos].bidiDirection = last;
|
|
} else if (current == QChar::DirAN && secondLast == QChar::DirAN) {
|
|
last = QChar::DirAN;
|
|
analysis[lastPos].bidiDirection = last;
|
|
}
|
|
}
|
|
secondLast = last;
|
|
last = current;
|
|
lastPos = pos;
|
|
++it;
|
|
}
|
|
}
|
|
|
|
void resolveW5(const Vector<DirectionalRun> &runs, int i)
|
|
{
|
|
// Rule W5
|
|
IsolatedRunSequenceIterator::Position lastETPosition;
|
|
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
int lastPos = *it;
|
|
QChar::Direction last = analysis[lastPos].bidiDirection;
|
|
if (last == QChar::DirET || last == QChar::DirBN)
|
|
lastETPosition = it.position();
|
|
|
|
++it;
|
|
while (!it.atEnd()) {
|
|
int pos = *it;
|
|
QChar::Direction current = analysis[pos].bidiDirection;
|
|
if (current == QChar::DirBN) {
|
|
++it;
|
|
continue;
|
|
}
|
|
if (current == QChar::DirET) {
|
|
if (last == QChar::DirEN) {
|
|
current = QChar::DirEN;
|
|
analysis[pos].bidiDirection = current;
|
|
} else if (!lastETPosition.isValid()) {
|
|
lastETPosition = it.position();
|
|
}
|
|
} else if (lastETPosition.isValid()) {
|
|
if (current == QChar::DirEN) {
|
|
it.setPosition(lastETPosition);
|
|
while (it != pos) {
|
|
int pos = *it;
|
|
analysis[pos].bidiDirection = QChar::DirEN;
|
|
++it;
|
|
}
|
|
}
|
|
lastETPosition.clear();
|
|
}
|
|
last = current;
|
|
lastPos = pos;
|
|
++it;
|
|
}
|
|
}
|
|
|
|
void resolveW6W7(const Vector<DirectionalRun> &runs, int i, QChar::Direction sos)
|
|
{
|
|
QChar::Direction lastStrong = sos;
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
while (!it.atEnd()) {
|
|
int pos = *it;
|
|
|
|
// Rule W6
|
|
QChar::Direction current = analysis[pos].bidiDirection;
|
|
if (current == QChar::DirBN) {
|
|
++it;
|
|
continue;
|
|
}
|
|
if (current == QChar::DirET || current == QChar::DirES || current == QChar::DirCS) {
|
|
analysis[pos].bidiDirection = QChar::DirON;
|
|
}
|
|
|
|
// Rule W7
|
|
else if (current == QChar::DirL || current == QChar::DirR) {
|
|
lastStrong = current;
|
|
} else if (current == QChar::DirEN && lastStrong == QChar::DirL) {
|
|
analysis[pos].bidiDirection = lastStrong;
|
|
}
|
|
++it;
|
|
}
|
|
}
|
|
|
|
struct BracketPair {
|
|
int first;
|
|
int second;
|
|
|
|
bool isValid() const { return second > 0; }
|
|
|
|
QChar::Direction containedDirection(const QScriptAnalysis *analysis, QChar::Direction embeddingDir) const {
|
|
int isolateCounter = 0;
|
|
QChar::Direction containedDir = QChar::DirON;
|
|
for (int i = first + 1; i < second; ++i) {
|
|
QChar::Direction dir = analysis[i].bidiDirection;
|
|
if (isolateCounter) {
|
|
if (dir == QChar::DirPDI)
|
|
--isolateCounter;
|
|
continue;
|
|
}
|
|
if (dir == QChar::DirL) {
|
|
containedDir = dir;
|
|
if (embeddingDir == dir)
|
|
break;
|
|
} else if (dir == QChar::DirR || dir == QChar::DirAN || dir == QChar::DirEN) {
|
|
containedDir = QChar::DirR;
|
|
if (embeddingDir == QChar::DirR)
|
|
break;
|
|
} else if (dir == QChar::DirLRI || dir == QChar::DirRLI || dir == QChar::DirFSI)
|
|
++isolateCounter;
|
|
}
|
|
BIDI_DEBUG() << " contained dir for backet pair" << first << "/" << second << "is" << containedDir;
|
|
return containedDir;
|
|
}
|
|
};
|
|
|
|
|
|
struct BracketStack {
|
|
struct Item {
|
|
Item() = default;
|
|
Item(uint pairedBracked, int position) : pairedBracked(pairedBracked), position(position) {}
|
|
uint pairedBracked = 0;
|
|
int position = 0;
|
|
};
|
|
|
|
void push(uint closingUnicode, int pos) {
|
|
if (position < MaxDepth)
|
|
stack[position] = Item(closingUnicode, pos);
|
|
++position;
|
|
}
|
|
int match(uint unicode) {
|
|
Q_ASSERT(!overflowed());
|
|
int p = position;
|
|
while (--p >= 0) {
|
|
if (stack[p].pairedBracked == unicode ||
|
|
// U+3009 and U+2329 are canonical equivalents of each other. Fortunately it's the only pair in Unicode 10
|
|
(stack[p].pairedBracked == 0x3009 && unicode == 0x232a) ||
|
|
(stack[p].pairedBracked == 0x232a && unicode == 0x3009)) {
|
|
position = p;
|
|
return stack[p].position;
|
|
}
|
|
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
enum { MaxDepth = 63 };
|
|
Item stack[MaxDepth];
|
|
int position = 0;
|
|
|
|
bool overflowed() const { return position > MaxDepth; }
|
|
};
|
|
|
|
void resolveN0(const Vector<DirectionalRun> &runs, int i, QChar::Direction sos)
|
|
{
|
|
ushort level = runs.at(i).level;
|
|
|
|
Vector<BracketPair> bracketPairs;
|
|
{
|
|
BracketStack bracketStack;
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
while (!it.atEnd()) {
|
|
int pos = *it;
|
|
QChar::Direction dir = analysis[pos].bidiDirection;
|
|
if (dir == QChar::DirON) {
|
|
const QUnicodeTables::Properties *p = infoAt(pos).properties;
|
|
if (p->mirrorDiff) {
|
|
// either opening or closing bracket
|
|
if (p->category == QChar::Punctuation_Open) {
|
|
// opening bracked
|
|
uint closingBracked = text[pos].unicode() + p->mirrorDiff;
|
|
bracketStack.push(closingBracked, bracketPairs.size());
|
|
if (bracketStack.overflowed()) {
|
|
bracketPairs.clear();
|
|
break;
|
|
}
|
|
bracketPairs.append({ pos, -1 });
|
|
} else if (p->category == QChar::Punctuation_Close) {
|
|
int pairPos = bracketStack.match(text[pos].unicode());
|
|
if (pairPos != -1)
|
|
bracketPairs[pairPos].second = pos;
|
|
}
|
|
}
|
|
}
|
|
++it;
|
|
}
|
|
}
|
|
|
|
if (BidiDebugEnabled && bracketPairs.size()) {
|
|
BIDI_DEBUG() << "matched bracket pairs:";
|
|
for (int i = 0; i < bracketPairs.size(); ++i)
|
|
BIDI_DEBUG() << " " << bracketPairs.at(i).first << bracketPairs.at(i).second;
|
|
}
|
|
|
|
QChar::Direction lastStrong = sos;
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
QChar::Direction embeddingDir = (level & 1) ? QChar::DirR : QChar::DirL;
|
|
for (int i = 0; i < bracketPairs.size(); ++i) {
|
|
const auto &pair = bracketPairs.at(i);
|
|
if (!pair.isValid())
|
|
continue;
|
|
QChar::Direction containedDir = pair.containedDirection(analysis, embeddingDir);
|
|
if (containedDir == QChar::DirON) {
|
|
BIDI_DEBUG() << " 3: resolve bracket pair" << i << "to DirON";
|
|
continue;
|
|
} else if (containedDir == embeddingDir) {
|
|
analysis[pair.first].bidiDirection = embeddingDir;
|
|
analysis[pair.second].bidiDirection = embeddingDir;
|
|
BIDI_DEBUG() << " 1: resolve bracket pair" << i << "to" << embeddingDir;
|
|
} else {
|
|
// case c.
|
|
while (it.pos < pair.first) {
|
|
int pos = *it;
|
|
switch (analysis[pos].bidiDirection) {
|
|
case QChar::DirR:
|
|
case QChar::DirEN:
|
|
case QChar::DirAN:
|
|
lastStrong = QChar::DirR;
|
|
break;
|
|
case QChar::DirL:
|
|
lastStrong = QChar::DirL;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
++it;
|
|
}
|
|
analysis[pair.first].bidiDirection = lastStrong;
|
|
analysis[pair.second].bidiDirection = lastStrong;
|
|
BIDI_DEBUG() << " 2: resolve bracket pair" << i << "to" << lastStrong;
|
|
}
|
|
for (int i = pair.second + 1; i < length; ++i) {
|
|
if (infoAt(i).properties->direction == QChar::DirNSM)
|
|
analysis[i].bidiDirection = analysis[pair.second].bidiDirection;
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void resolveN1N2(const Vector<DirectionalRun> &runs, int i, QChar::Direction sos, QChar::Direction eos)
|
|
{
|
|
// Rule N1 & N2
|
|
QChar::Direction lastStrong = sos;
|
|
IsolatedRunSequenceIterator::Position niPos;
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
// QChar::Direction last = QChar::DirON;
|
|
while (1) {
|
|
int pos = *it;
|
|
|
|
QChar::Direction current = pos >= 0 ? analysis[pos].bidiDirection : eos;
|
|
QChar::Direction currentStrong = current;
|
|
switch (current) {
|
|
case QChar::DirEN:
|
|
case QChar::DirAN:
|
|
currentStrong = QChar::DirR;
|
|
Q_FALLTHROUGH();
|
|
case QChar::DirL:
|
|
case QChar::DirR:
|
|
if (niPos.isValid()) {
|
|
QChar::Direction dir = currentStrong;
|
|
if (lastStrong != currentStrong)
|
|
dir = (runs.at(i).level) & 1 ? QChar::DirR : QChar::DirL;
|
|
it.setPosition(niPos);
|
|
while (*it != pos) {
|
|
if (analysis[*it].bidiDirection != QChar::DirBN)
|
|
analysis[*it].bidiDirection = dir;
|
|
++it;
|
|
}
|
|
niPos.clear();
|
|
}
|
|
lastStrong = currentStrong;
|
|
break;
|
|
|
|
case QChar::DirBN:
|
|
case QChar::DirS:
|
|
case QChar::DirWS:
|
|
case QChar::DirON:
|
|
case QChar::DirFSI:
|
|
case QChar::DirLRI:
|
|
case QChar::DirRLI:
|
|
case QChar::DirPDI:
|
|
case QChar::DirB:
|
|
if (!niPos.isValid())
|
|
niPos = it.position();
|
|
break;
|
|
|
|
default:
|
|
Q_UNREACHABLE();
|
|
}
|
|
if (it.atEnd())
|
|
break;
|
|
// last = current;
|
|
++it;
|
|
}
|
|
}
|
|
|
|
void resolveImplicitLevelsForIsolatedRun(const Vector<DirectionalRun> &runs, int i)
|
|
{
|
|
// Rule X10
|
|
int level = runs.at(i).level;
|
|
int before = i - 1;
|
|
while (before >= 0 && !runs.at(before).hasContent)
|
|
--before;
|
|
int level_before = (before >= 0) ? runs.at(before).level : baseLevel;
|
|
int after = i;
|
|
while (runs.at(after).continuation >= 0)
|
|
after = runs.at(after).continuation;
|
|
if (runs.at(after).continuation == -2) {
|
|
after = runs.size();
|
|
} else {
|
|
++after;
|
|
while (after < runs.size() && !runs.at(after).hasContent)
|
|
++after;
|
|
}
|
|
int level_after = (after == runs.size()) ? baseLevel : runs.at(after).level;
|
|
QChar::Direction sos = (qMax(level_before, level) & 1) ? QChar::DirR : QChar::DirL;
|
|
QChar::Direction eos = (qMax(level_after, level) & 1) ? QChar::DirR : QChar::DirL;
|
|
|
|
if (BidiDebugEnabled) {
|
|
BIDI_DEBUG() << "Isolated run starting at" << i << "sos/eos" << sos << eos;
|
|
BIDI_DEBUG() << "before implicit level processing:";
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
while (!it.atEnd()) {
|
|
BIDI_DEBUG() << " " << *it << Qt::hex << text[*it].unicode() << analysis[*it].bidiDirection;
|
|
++it;
|
|
}
|
|
}
|
|
|
|
resolveW1W2W3(runs, i, sos);
|
|
resolveW4(runs, i, sos);
|
|
resolveW5(runs, i);
|
|
|
|
if (BidiDebugEnabled) {
|
|
BIDI_DEBUG() << "after W4/W5";
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
while (!it.atEnd()) {
|
|
BIDI_DEBUG() << " " << *it << Qt::hex << text[*it].unicode() << analysis[*it].bidiDirection;
|
|
++it;
|
|
}
|
|
}
|
|
|
|
resolveW6W7(runs, i, sos);
|
|
|
|
// Resolve neutral types
|
|
|
|
// Rule N0
|
|
resolveN0(runs, i, sos);
|
|
resolveN1N2(runs, i, sos, eos);
|
|
|
|
BIDI_DEBUG() << "setting levels (run at" << level << ")";
|
|
// Rules I1 & I2: set correct levels
|
|
{
|
|
ushort level = runs.at(i).level;
|
|
IsolatedRunSequenceIterator it(runs, i);
|
|
while (!it.atEnd()) {
|
|
int pos = *it;
|
|
|
|
QChar::Direction current = analysis[pos].bidiDirection;
|
|
switch (current) {
|
|
case QChar::DirBN:
|
|
break;
|
|
case QChar::DirL:
|
|
analysis[pos].bidiLevel = (level + 1) & ~1;
|
|
break;
|
|
case QChar::DirR:
|
|
analysis[pos].bidiLevel = level | 1;
|
|
break;
|
|
case QChar::DirAN:
|
|
case QChar::DirEN:
|
|
analysis[pos].bidiLevel = (level + 2) & ~1;
|
|
break;
|
|
default:
|
|
Q_UNREACHABLE();
|
|
}
|
|
BIDI_DEBUG() << " " << pos << current << analysis[pos].bidiLevel;
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
void resolveImplicitLevels(const Vector<DirectionalRun> &runs)
|
|
{
|
|
for (int i = 0; i < runs.size(); ++i) {
|
|
if (runs.at(i).isContinuation)
|
|
continue;
|
|
|
|
resolveImplicitLevelsForIsolatedRun(runs, i);
|
|
}
|
|
}
|
|
|
|
bool checkForBidi() const
|
|
{
|
|
if (baseLevel != 0)
|
|
return true;
|
|
for (int i = 0; i < length; ++i) {
|
|
if (text[i].unicode() >= 0x590) {
|
|
switch (infoAt(i).properties->direction) {
|
|
case QChar::DirR: case QChar::DirAN:
|
|
case QChar::DirLRE: case QChar::DirLRO: case QChar::DirAL:
|
|
case QChar::DirRLE: case QChar::DirRLO: case QChar::DirPDF:
|
|
case QChar::DirLRI: case QChar::DirRLI: case QChar::DirFSI: case QChar::DirPDI:
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool process()
|
|
{
|
|
memset(analysis, 0, length * sizeof(QScriptAnalysis));
|
|
|
|
bool hasBidi = checkForBidi();
|
|
|
|
if (!hasBidi)
|
|
return false;
|
|
|
|
if (BidiDebugEnabled) {
|
|
BIDI_DEBUG() << ">>>> start bidi, text length" << length;
|
|
for (int i = 0; i < length; ++i)
|
|
BIDI_DEBUG() << Qt::hex << " (" << i << ")" << text[i].unicode() << text[i].direction();
|
|
}
|
|
|
|
{
|
|
Vector<DirectionalRun> runs;
|
|
resolveExplicitLevels(runs);
|
|
|
|
if (BidiDebugEnabled) {
|
|
BIDI_DEBUG() << "resolved explicit levels, nruns" << runs.size();
|
|
for (int i = 0; i < runs.size(); ++i)
|
|
BIDI_DEBUG() << " " << i << "start/end" << runs.at(i).start << runs.at(i).end << "level" << (int)runs.at(i).level << "continuation" << runs.at(i).continuation;
|
|
}
|
|
|
|
// now we have a list of isolated run sequences inside the vector of runs, that can be fed
|
|
// through the implicit level resolving
|
|
|
|
resolveImplicitLevels(runs);
|
|
}
|
|
|
|
BIDI_DEBUG() << "Rule L1:";
|
|
// Rule L1:
|
|
bool resetLevel = true;
|
|
for (int i = length - 1; i >= 0; --i) {
|
|
if (analysis[i].bidiFlags & QScriptAnalysis::BidiResetToParagraphLevel) {
|
|
BIDI_DEBUG() << "resetting pos" << i << "to baselevel";
|
|
analysis[i].bidiLevel = baseLevel;
|
|
resetLevel = true;
|
|
} else if (resetLevel && analysis[i].bidiFlags & QScriptAnalysis::BidiMaybeResetToParagraphLevel) {
|
|
BIDI_DEBUG() << "resetting pos" << i << "to baselevel (maybereset flag)";
|
|
analysis[i].bidiLevel = baseLevel;
|
|
} else {
|
|
resetLevel = false;
|
|
}
|
|
}
|
|
|
|
// set directions for BN to the minimum of adjacent chars
|
|
// This makes is possible to be conformant with the Bidi algorithm even though we don't
|
|
// remove BN and explicit embedding chars from the stream of characters to reorder
|
|
int lastLevel = baseLevel;
|
|
int lastBNPos = -1;
|
|
for (int i = 0; i < length; ++i) {
|
|
if (analysis[i].bidiFlags & QScriptAnalysis::BidiBN) {
|
|
if (lastBNPos < 0)
|
|
lastBNPos = i;
|
|
analysis[i].bidiLevel = lastLevel;
|
|
} else {
|
|
int l = analysis[i].bidiLevel;
|
|
if (lastBNPos >= 0) {
|
|
if (l < lastLevel) {
|
|
while (lastBNPos < i) {
|
|
analysis[lastBNPos].bidiLevel = l;
|
|
++lastBNPos;
|
|
}
|
|
}
|
|
lastBNPos = -1;
|
|
}
|
|
lastLevel = l;
|
|
}
|
|
}
|
|
if (lastBNPos >= 0 && baseLevel < lastLevel) {
|
|
while (lastBNPos < length) {
|
|
analysis[lastBNPos].bidiLevel = baseLevel;
|
|
++lastBNPos;
|
|
}
|
|
}
|
|
|
|
if (BidiDebugEnabled) {
|
|
BIDI_DEBUG() << "final resolved levels:";
|
|
for (int i = 0; i < length; ++i)
|
|
BIDI_DEBUG() << " " << i << Qt::hex << text[i].unicode() << Qt::dec << (int)analysis[i].bidiLevel;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
const QChar *text;
|
|
QScriptAnalysis *analysis;
|
|
int length;
|
|
char baseLevel;
|
|
|
|
Blocks::const_iterator _startInBlocks;
|
|
Blocks::const_iterator _endInBlocks;
|
|
mutable Blocks::const_iterator _currentBlock;
|
|
int _offsetInBlocks;
|
|
|
|
struct Info {
|
|
const QUnicodeTables::Properties *properties = nullptr;
|
|
bool surrogate = false;
|
|
};
|
|
[[nodiscard]] Info infoAt(int i) const {
|
|
if (_currentBlock != _startInBlocks
|
|
&& (*_currentBlock)->position() > _offsetInBlocks + i) {
|
|
_currentBlock = _startInBlocks;
|
|
}
|
|
auto next = _currentBlock + 1;
|
|
while (next != _endInBlocks
|
|
&& (*next)->position() <= _offsetInBlocks + i) {
|
|
_currentBlock = next;
|
|
++next;
|
|
}
|
|
const auto type = (*_currentBlock)->type();
|
|
const auto object = (type == TextBlockType::Emoji)
|
|
|| (type == TextBlockType::CustomEmoji)
|
|
|| (type == TextBlockType::Skip);
|
|
|
|
constexpr auto kQt5 = (QT_VERSION < QT_VERSION_CHECK(6, 0, 0));
|
|
using wide = std::conditional_t<kQt5, uint, char32_t>;
|
|
using narrow = std::conditional_t<kQt5, ushort, char16_t>;
|
|
|
|
auto uc = wide(text[i].unicode());
|
|
if (QChar::isHighSurrogate(uc) && i < length - 1 && text[i + 1].isLowSurrogate()) {
|
|
uc = QChar::surrogateToUcs4(ushort(uc), text[i + 1].unicode());
|
|
|
|
return {
|
|
.properties = QUnicodeTables::properties(object
|
|
? wide(QChar::ObjectReplacementCharacter)
|
|
: uc),
|
|
.surrogate = true,
|
|
};
|
|
}
|
|
return {
|
|
.properties = QUnicodeTables::properties(object
|
|
? narrow(QChar::ObjectReplacementCharacter)
|
|
: narrow(uc)),
|
|
.surrogate = false,
|
|
};
|
|
}
|
|
};
|
|
|
|
} // namespace Ui::Text
|
|
|
|
#undef BIDI_DEBUG
|