Export template source from e33714a5ca38429d1bdb77a961a14461697944f5:examples/local-first/packages/language-reader-presentation

This commit is contained in:
Quixos Template Publisher
2000-01-01 00:00:00 +00:00
commit 8f327c9ac8
63 changed files with 10474 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
import type { Range } from "./types.js";
// All ranges refer to the original UTF-16 source. Never trim paragraph text:
// Fluxtapose's trimmed paragraphs broke selection and annotations after spaces.
const graphemeSegmenter = new Intl.Segmenter("zh", { granularity: "grapheme" });
const wordSegmenter = new Intl.Segmenter("zh", { granularity: "word" });
let lastWords: { text: string; ranges: Range[]; wordStarts: Set<number> } | null = null;
const segmented = new Map<string, (Range & { text: string })[]>();
let cachedUnits = 0;
export function graphemes(text: string): (Range & { text: string })[] {
const cached = segmented.get(text);
if (cached) return cached;
const units = Array.from(graphemeSegmenter.segment(text), ({ segment, index }) => ({
text: segment,
start: index,
end: index + segment.length,
}));
// Retain documents across layout changes, without filling the cache with
// every small annotation label. Bound both object count and retained units.
if (text.length >= 256 && units.length <= 120000) {
while (segmented.size >= 4 || cachedUnits + units.length > 120000) {
const oldest = segmented.keys().next().value!;
cachedUnits -= segmented.get(oldest)!.length;
segmented.delete(oldest);
}
segmented.set(text, units);
cachedUnits += units.length;
}
return units;
}
export function wordAt(text: string, offset: number): Range {
if (lastWords?.text !== text) {
const words = Array.from(wordSegmenter.segment(text));
lastWords = {
text,
ranges: words.map(({ index, segment }) => ({
start: index,
end: index + segment.length,
})),
wordStarts: new Set(words.filter((word) => word.isWordLike).map((word) => word.index)),
};
}
const ranges = lastWords.ranges;
let low = 0,
high = ranges.length;
while (low < high) {
const mid = (low + high) >>> 1;
if (ranges[mid].end <= offset) low = mid + 1;
else high = mid;
}
if (ranges[low] && offset >= ranges[low].start) return { start: ranges[low].start, end: ranges[low].end };
return { start: text.length, end: text.length };
}
/** Segmenter classifies word-like spans using Unicode data, including CJK.
* Selection still uses wordAt, where selecting punctuation is legitimate. */
export function hoverWordAt(text: string, offset: number): Range | null {
const range = wordAt(text, offset);
return lastWords?.wordStarts.has(range.start) ? range : null;
}
export function snapOffset(text: string, offset: number, direction = 1): number {
const bounded = Math.max(0, Math.min(text.length, offset));
for (const unit of graphemes(text)) {
if (bounded > unit.start && bounded < unit.end) return direction < 0 ? unit.start : unit.end;
}
return bounded;
}