clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

main
airwaves / frontend / dist / vendor / pretext / bidi.js
bidi.js 6.5K
1// Simplified bidi metadata helper for the rich prepareWithSegments() path,2// forked from pdf.js via Sebastian's text-layout. It classifies characters3// into bidi types, computes embedding levels, and maps them onto prepared4// segments for custom rendering. The line-breaking engine does not consume5// these levels.6import { latin1BidiTypes, nonLatin1BidiRanges, } from './generated/bidi-data.js';7function classifyCodePoint(codePoint) {8 if (codePoint <= 0x00FF)9 return latin1BidiTypes[codePoint];10 let lo = 0;11 let hi = nonLatin1BidiRanges.length - 1;12 while (lo <= hi) {13 const mid = (lo + hi) >> 1;14 const range = nonLatin1BidiRanges[mid];15 if (codePoint < range[0]) {16 hi = mid - 1;17 continue;18 }19 if (codePoint > range[1]) {20 lo = mid + 1;21 continue;22 }23 return range[2];24 }25 return 'L';26}27function computeBidiLevels(str) {28 const len = str.length;29 if (len === 0)30 return null;31 // eslint-disable-next-line unicorn/no-new-array32 const types = new Array(len);33 let paragraphHasBidi = false;34 let paragraphStart = 0;35 let levels = null;36 // Keep the resolved bidi classes aligned to UTF-16 code-unit offsets,37 // because the rich prepared segments index back into the normalized string38 // with JavaScript string offsets.39 for (let i = 0; i < len;) {40 const first = str.charCodeAt(i);41 let codePoint = first;42 let codeUnitLength = 1;43 if (first >= 0xD800 && first <= 0xDBFF && i + 1 < len) {44 const second = str.charCodeAt(i + 1);45 if (second >= 0xDC00 && second <= 0xDFFF) {46 codePoint = ((first - 0xD800) << 10) + (second - 0xDC00) + 0x10000;47 codeUnitLength = 2;48 }49 }50 const t = classifyCodePoint(codePoint);51 if (t === 'R' || t === 'AL' || t === 'AN')52 paragraphHasBidi = true;53 for (let j = 0; j < codeUnitLength; j++) {54 types[i + j] = t;55 }56 // Classification has all of this paragraph's types when B arrives.57 // Resolve that range now; S (tabs) and U+2028 do not end a paragraph.58 if (t === 'B') {59 if (paragraphHasBidi) {60 levels ??= new Int8Array(len);61 // B belongs to the preceding paragraph and inherits its base level.62 levels[i] = resolveParagraphLevels(types, levels, paragraphStart, i);63 }64 paragraphStart = i + codeUnitLength;65 paragraphHasBidi = false;66 }67 i += codeUnitLength;68 }69 if (paragraphHasBidi) {70 levels ??= new Int8Array(len);71 resolveParagraphLevels(types, levels, paragraphStart, len);72 }73 // Paragraphs without bidi classes have level zero. Leave their cells at74 // the typed array's initial value, or keep the all-LTR result null.75 return levels;76}77function resolveParagraphLevels(types, levels, start, end) {78 // Use the first strong character to pick the paragraph base direction.79 // Rich-path bidi metadata is only an approximation, but this keeps mixed80 // LTR/RTL text aligned with the common UBA paragraph rule.81 let startLevel = 0;82 for (let i = start; i < end; i++) {83 const t = types[i];84 if (t === 'L') {85 startLevel = 0;86 break;87 }88 if (t === 'R' || t === 'AL') {89 startLevel = 1;90 break;91 }92 }93 const e = (startLevel & 1) ? 'R' : 'L';94 const sor = e;95 // W1-W796 let lastType = sor;97 for (let i = start; i < end; i++) {98 if (types[i] === 'NSM')99 types[i] = lastType;100 else101 lastType = types[i];102 }103 lastType = sor;104 for (let i = start; i < end; i++) {105 const t = types[i];106 if (t === 'EN')107 types[i] = lastType === 'AL' ? 'AN' : 'EN';108 else if (t === 'R' || t === 'L' || t === 'AL')109 lastType = t;110 }111 for (let i = start; i < end; i++) {112 if (types[i] === 'AL')113 types[i] = 'R';114 }115 for (let i = start + 1; i < end - 1; i++) {116 if (types[i] === 'ES' && types[i - 1] === 'EN' && types[i + 1] === 'EN') {117 types[i] = 'EN';118 }119 if (types[i] === 'CS' &&120 (types[i - 1] === 'EN' || types[i - 1] === 'AN') &&121 types[i + 1] === types[i - 1]) {122 types[i] = types[i - 1];123 }124 }125 for (let i = start; i < end; i++) {126 if (types[i] !== 'EN')127 continue;128 let j;129 for (j = i - 1; j >= start && types[j] === 'ET'; j--)130 types[j] = 'EN';131 for (j = i + 1; j < end && types[j] === 'ET'; j++)132 types[j] = 'EN';133 }134 for (let i = start; i < end; i++) {135 const t = types[i];136 if (t === 'WS' || t === 'ES' || t === 'ET' || t === 'CS')137 types[i] = 'ON';138 }139 lastType = sor;140 for (let i = start; i < end; i++) {141 const t = types[i];142 if (t === 'EN')143 types[i] = lastType === 'L' ? 'L' : 'EN';144 else if (t === 'R' || t === 'L')145 lastType = t;146 }147 // N1-N2148 for (let i = start; i < end; i++) {149 if (types[i] !== 'ON')150 continue;151 let runEnd = i + 1;152 while (runEnd < end && types[runEnd] === 'ON')153 runEnd++;154 const before = i > start ? types[i - 1] : sor;155 const after = runEnd < end ? types[runEnd] : sor;156 const bDir = before !== 'L' ? 'R' : 'L';157 const aDir = after !== 'L' ? 'R' : 'L';158 if (bDir === aDir) {159 for (let j = i; j < runEnd; j++)160 types[j] = bDir;161 }162 i = runEnd - 1;163 }164 for (let i = start; i < end; i++) {165 if (types[i] === 'ON')166 types[i] = e;167 }168 // I1-I2169 for (let i = start; i < end; i++) {170 const t = types[i];171 let level = startLevel;172 if ((startLevel & 1) === 0) {173 if (t === 'R')174 level++;175 else if (t === 'AN' || t === 'EN')176 level += 2;177 }178 else if (t === 'L' || t === 'AN' || t === 'EN') {179 level++;180 }181 levels[i] = level;182 }183 return startLevel;184}185export function computeSegmentLevels(normalized, segStarts) {186 const bidiLevels = computeBidiLevels(normalized);187 if (bidiLevels === null)188 return null;189 const segLevels = new Int8Array(segStarts.length);190 for (let i = 0; i < segStarts.length; i++) {191 segLevels[i] = bidiLevels[segStarts[i]];192 }193 return segLevels;194}