blob: 002a113cf77fda1a4ed2d352524f73e9ccbd095f [file] [edit]
/*
* Copyright 2026 Rive
*/
#include "rive/text/line_break_dictionary.hpp"
#ifdef RIVE_LINE_BREAK_DICTIONARIES
#include <algorithm>
#include <vector>
#ifdef WITH_RIVE_TEXT_DICT_COMPRESSED
#include <mutex>
#endif
using namespace rive;
namespace rive
{
#ifdef WITH_RIVE_TEXT_DICT_THAI
extern const LineBreakDictionary kLineBreakDictionaryThai;
#endif
#ifdef WITH_RIVE_TEXT_DICT_LAO
extern const LineBreakDictionary kLineBreakDictionaryLao;
#endif
#ifdef WITH_RIVE_TEXT_DICT_KHMER
extern const LineBreakDictionary kLineBreakDictionaryKhmer;
#endif
#ifdef WITH_RIVE_TEXT_DICT_BURMESE
extern const LineBreakDictionary kLineBreakDictionaryBurmese;
#endif
} // namespace rive
namespace
{
const LineBreakDictionary* const kDictionaries[] = {
#ifdef WITH_RIVE_TEXT_DICT_THAI
&kLineBreakDictionaryThai,
#endif
#ifdef WITH_RIVE_TEXT_DICT_LAO
&kLineBreakDictionaryLao,
#endif
#ifdef WITH_RIVE_TEXT_DICT_KHMER
&kLineBreakDictionaryKhmer,
#endif
#ifdef WITH_RIVE_TEXT_DICT_BURMESE
&kLineBreakDictionaryBurmese,
#endif
};
bool inRanges(const LineBreakDictionary& dictionary, uint32_t codepoint)
{
Span<const uint32_t> ranges = dictionary.ranges;
if (codepoint < ranges.front() || codepoint > ranges.back())
{
return false;
}
for (size_t i = 0; i + 1 < ranges.size(); i += 2)
{
if (codepoint >= ranges[i] && codepoint <= ranges[i + 1])
{
return true;
}
}
return false;
}
bool isJoiner(uint32_t codepoint)
{
return codepoint == 0x200C || codepoint == 0x200D;
}
bool contains(Span<const uint32_t> sorted, uint32_t codepoint)
{
return std::binary_search(sorted.begin(), sorted.end(), codepoint);
}
// Combining marks and joiners, which stay with the codepoint before them.
bool isMark(uint32_t codepoint)
{
return isJoiner(codepoint) ||
lineBreakProps(codepoint).cls == LineBreakClass::CM;
}
// Whether run[i] is a consonant directly followed by a coda mark, which
// makes it end the previous syllable.
bool isFinalConsonant(
const LineBreakDictionary& dictionary,
const uint32_t* run,
size_t count,
size_t i)
{
size_t j = i + 1;
while (j < count && contains(dictionary.codaSkip, run[j]))
{
j++;
}
return j < count && contains(dictionary.codaMarks, run[j]);
}
// Whether a word boundary may sit before run[i], 0 < i < count, without
// splitting a syllable.
bool canBreak(
const LineBreakDictionary& dictionary,
const uint32_t* run,
size_t count,
size_t i)
{
uint32_t prev = run[i - 1];
uint32_t next = run[i];
return !isJoiner(prev) && !isMark(next) &&
!contains(dictionary.noBreakBefore, next) &&
!contains(dictionary.suffixes, next) &&
!contains(dictionary.noBreakAfter, prev) &&
!isFinalConsonant(dictionary, run, count, i);
}
constexpr uint32_t kUnreached = UINT32_MAX;
// One per boundary of a run, the best segmentation of the run up to it.
struct Cell
{
int32_t symbol; // of the codepoint after the boundary, -1 if none
bool canBreak; // a segment may start or end here
bool word; // the segment ending here is a dictionary word
uint32_t unknown; // codepoints no word covers, kUnreached until reached
uint32_t segments; // segments so far
uint32_t from; // where the segment ending here starts
};
// Fewest codepoints outside words wins, then fewest segments, so longer
// words beat splitting them into shorter ones.
void relax(Cell* cells, size_t from, size_t to, uint32_t unknown, bool word)
{
uint32_t total = cells[from].unknown + unknown;
uint32_t segments = cells[from].segments + 1;
Cell& cell = cells[to];
if (total < cell.unknown ||
(total == cell.unknown && segments < cell.segments))
{
cell.unknown = total;
cell.segments = segments;
cell.from = (uint32_t)from;
cell.word = word;
}
}
uint32_t edgeSymbol(uint32_t edge)
{
return edge & LineBreakDictionary::symbolMask;
}
// Returns the edge leaving the state at first for symbol, or -1. The root
// has an edge for nearly every symbol, so it gets a binary search over its
// rootCount edges.
int64_t findEdge(
Span<const uint32_t> edges,
uint32_t first,
uint32_t symbol,
uint32_t rootCount)
{
if (first == 0)
{
const uint32_t* end = edges.data() + rootCount;
const uint32_t* found = std::lower_bound(
edges.data(),
end,
symbol,
[](uint32_t edge, uint32_t symbol) {
return edgeSymbol(edge) < symbol;
});
return found != end && edgeSymbol(*found) == symbol ? found - edges.data()
: -1;
}
for (uint32_t i = first; i < edges.size(); i++)
{
uint32_t edge = edges[i];
if (edgeSymbol(edge) == symbol)
{
return i;
}
if (edgeSymbol(edge) > symbol ||
(edge & LineBreakDictionary::lastEdge) != 0)
{
break;
}
}
return -1;
}
// Where a word that ends before run[end] really ends: past the suffixes and
// killed final consonants that belong to it, or 0 when it would end mid
// syllable, which makes the match spurious.
size_t extendWordEnd(
const LineBreakDictionary& dictionary,
const uint32_t* run,
size_t count,
const Cell* cells,
size_t end)
{
while (!cells[end].canBreak)
{
if (contains(dictionary.suffixes, run[end]))
{
end++;
}
else if (!isMark(run[end]) && isFinalConsonant(dictionary, run, count, end))
{
// The consonant and every mark on it.
end++;
while (end < count && isMark(run[end]))
{
end++;
}
}
else
{
return 0;
}
}
return end;
}
// Segments run[0, count) into dictionary words, keeping any stretch no word
// covers whole, and allows a break at the start of every segment.
void segmentRun(
const LineBreakDictionary& dictionary,
Span<const uint32_t> edges,
const uint32_t* run,
size_t count,
LineBreak* out)
{
// Cells live on the stack for typical runs.
constexpr size_t kInlineCells = 128;
Cell inlineCells[kInlineCells + 1];
std::vector<Cell> heapCells;
Cell* cells = inlineCells;
if (count > kInlineCells)
{
heapCells.resize(count + 1);
cells = heapCells.data();
}
Span<const uint32_t> alphabet = dictionary.alphabet;
for (size_t i = 0; i <= count; i++)
{
Cell& cell = cells[i];
cell.symbol = -1;
if (i < count)
{
auto found = std::lower_bound(alphabet.begin(), alphabet.end(), run[i]);
if (found != alphabet.end() && *found == run[i])
{
cell.symbol = (int32_t)(found - alphabet.begin());
}
}
cell.canBreak = i == 0 || i == count || canBreak(dictionary, run, count, i);
cell.word = false;
cell.unknown = kUnreached;
cell.segments = 0;
cell.from = 0;
}
cells[0].unknown = 0;
uint32_t rootCount = 1;
while ((edges[rootCount - 1] & LineBreakDictionary::lastEdge) == 0)
{
rootCount++;
}
for (size_t i = 0; i < count; i++)
{
if (cells[i].unknown == kUnreached)
{
continue;
}
// Every dictionary word starting here.
uint32_t state = 0;
for (size_t j = i; j < count && cells[j].symbol >= 0; j++)
{
int64_t edge = findEdge(
edges,
state,
(uint32_t)cells[j].symbol,
rootCount);
if (edge < 0)
{
break;
}
uint32_t value = edges[(size_t)edge];
if ((value & LineBreakDictionary::wordEnd) != 0)
{
size_t end = extendWordEnd(dictionary, run, count, cells, j + 1);
if (end != 0)
{
// What the word took along counts as outside any word, so an
// exact word still wins.
relax(cells, i, end, (uint32_t)(end - j - 1), true);
}
}
state = value >> LineBreakDictionary::targetShift;
if (state == 0)
{
break;
}
}
// Or the syllable here, outside any word.
size_t next = i + 1;
while (!cells[next].canBreak)
{
next++;
}
relax(cells, i, next, (uint32_t)(next - i), false);
}
// Walk the segments back from the end. Neighbouring stretches outside any
// word stay joined.
for (size_t end = count; end > 0;)
{
size_t start = cells[end].from;
if (start > 0 && (cells[end].word || cells[start].word) &&
out[start] == LineBreak::none)
{
out[start] = LineBreak::allowed;
}
end = start;
}
}
} // namespace
#ifdef WITH_RIVE_TEXT_DICT_COMPRESSED
Span<const uint32_t> rive::lineBreakDictionaryEdges(
const LineBreakDictionary& dictionary)
{
// Built at most once per dictionary, and only for scripts the text uses.
struct Built
{
std::once_flag once;
std::vector<uint32_t> edges;
};
constexpr size_t kCount = sizeof(kDictionaries) / sizeof(kDictionaries[0]);
static Built built[kCount];
for (size_t i = 0; i < kCount; i++)
{
if (kDictionaries[i] == &dictionary)
{
std::call_once(built[i].once, [&] {
built[i].edges = buildLineBreakDictionaryEdges(dictionary);
});
return built[i].edges;
}
}
return {};
}
#else
Span<const uint32_t> rive::lineBreakDictionaryEdges(
const LineBreakDictionary& dictionary)
{
return dictionary.edges;
}
#endif
const LineBreakDictionary* rive::lineBreakDictionaryFor(uint32_t codepoint)
{
for (const LineBreakDictionary* dictionary : kDictionaries)
{
if (inRanges(*dictionary, codepoint))
{
return dictionary;
}
}
return nullptr;
}
void rive::applyLineBreakDictionaries(
Span<const uint32_t> text,
Span<LineBreak> out)
{
// Codepoints outside every dictionary's span skip the per dictionary
// checks, which keeps Latin text cheap.
uint32_t first = UINT32_MAX;
uint32_t last = 0;
for (const LineBreakDictionary* dictionary : kDictionaries)
{
first = std::min(first, dictionary->ranges.front());
last = std::max(last, dictionary->ranges.back());
}
size_t count = text.size();
for (size_t i = 0; i < count;)
{
if (text[i] < first || text[i] > last)
{
i++;
continue;
}
// A run never starts on a mark: one there belongs to whatever precedes
// it, like an opening bracket, and breaking after it would strand them.
const LineBreakDictionary* dictionary = lineBreakDictionaryFor(text[i]);
if (dictionary == nullptr || isMark(text[i]))
{
i++;
continue;
}
size_t end = i + 1;
while (end < count &&
(inRanges(*dictionary, text[end]) || isJoiner(text[end])))
{
end++;
}
// A single codepoint has nothing to split, so it never builds a
// compressed dictionary.
if (end - i > 1)
{
Span<const uint32_t> edges = lineBreakDictionaryEdges(*dictionary);
if (!edges.empty())
{
segmentRun(
*dictionary,
edges,
text.data() + i,
end - i,
out.data() + i);
}
}
i = end;
}
}
#endif