unicode-shaper
Version:
Shape unicode text so that renderers like WebGL and WebGPU can properly display the glyphs.
176 lines (167 loc) • 5.18 kB
text/typescript
import { Type, findDominantType, getType, mirrorAdjustString } from './internal.js';
export * from './internal.js';
/** Basic Line tracker */
interface Line {
start: number;
end: number;
}
/** Chunk of unicode text to parse */
class Chunk {
/**
* @param start - begin section
* @param end - end section
* @param type - the type
*/
constructor(
public start: number,
public end: number,
public type: Type,
) {}
/**
* Check if an input type is the same as this Chunk
* @param t - input type
* @returns - true if both types are the same
*/
isType(t: Type): boolean {
return this.type === t;
}
/**
* Set the current type
* @param t - type to set
*/
setType(t: Type): void {
this.type = t;
}
/**
* Set the type given a chunk if it exists
* @param c - input chunk
*/
setChunkType(c?: Chunk): void {
if (c !== undefined) this.type = c.type;
}
}
/**
* Process a string of text and return a new string with the correct bidi ordering.
* Follows https://www.unicode.org/reports/tr9/#Basic_Display_Algorithm as closely as possible.
* Some things are not implemented, such as:
* - Explicit embedding levels
* - Explicit bracket control
* @param input - input unicode buffer
* @returns - output unicode buffer that's been reordered
*/
export function processBidiText(input: number[]): number[] {
const inputLen = input.length;
// print input:
const result: number[] = [];
// lines are storing [start, end] positions
const lines: Line[] = [];
// step 1: split by lines
let start = 0;
let end = 0;
// for (idx, c) in input.iter().enumerate() {
for (let idx = 0; idx < inputLen; idx++) {
const c = input[idx];
if (c === 0x000a || c === 0x000d) {
if (start === end) {
start = idx + 1;
} else {
lines.push({ start, end: idx });
start = idx + 1;
}
}
end = idx + 1;
}
// store the last line
if (start < inputLen) {
lines.push({ start, end: inputLen });
}
// step 2: iterate lines
// for (lineIDX, line) in lines.iter().enumerate() {
for (let lineIDX = 0; lineIDX < lines.length; lineIDX++) {
const line = lines[lineIDX];
const lineStr = input.slice(line.start, line.end);
const wordChunks: Chunk[] = [];
// s2.1: define dominant type
let curType: Type = getType(lineStr[0]);
const dominantRTL: boolean =
curType !== Type.Neutral && curType !== Type.Weak
? curType === Type.Rtl
: findDominantType(lineStr) === Type.Rtl;
if (curType === Type.Neutral || curType === Type.Weak) {
if (dominantRTL) {
curType = Type.Rtl;
} else {
curType = Type.Ltr;
}
}
// s2.2: group by RTL and LTR chunks
start = 0;
for (let idx = 0; idx < lineStr.length; idx++) {
const uCode = lineStr[idx];
const uType = getType(uCode);
if (uType !== curType) {
wordChunks.push(new Chunk(start, idx, curType));
start = idx;
curType = uType;
}
}
// store the last chunk
if (start !== lineStr.length) {
wordChunks.push(new Chunk(start, lineStr.length, curType));
}
// update neutral and weak chunks
const wordChunksLen = wordChunks.length;
// for idx in 0..wordChunksLen {
for (let idx = 0; idx < wordChunksLen; idx++) {
const prev: Chunk | undefined = idx === 0 ? undefined : wordChunks[idx - 1];
const next: Chunk | undefined = idx === wordChunksLen - 1 ? undefined : wordChunks[idx + 1];
const chunk = wordChunks[idx];
if (chunk.isType(Type.Neutral)) {
if (idx === 0) {
chunk.setChunkType(next);
} else if (idx === wordChunksLen - 1) {
continue;
} else if (prev?.type === next?.type) {
chunk.setChunkType(prev);
} else {
chunk.setType(dominantRTL ? Type.Rtl : Type.Ltr);
}
} else if (
chunk.isType(Type.Weak) &&
idx !== 0 &&
(prev?.isType(Type.Rtl) ?? false) &&
!dominantRTL
) {
// swap chunks
const tmp = wordChunks[idx - 1];
wordChunks[idx - 1] = wordChunks[idx];
wordChunks[idx] = tmp;
}
}
// merge chunks that are the same
let i = 0;
while (i < wordChunks.length - 1) {
if (wordChunks[i].type === wordChunks[i + 1].type) {
wordChunks[i].end = wordChunks[i + 1].end;
wordChunks.splice(i + 1, 1);
i -= 1;
}
i += 1;
}
// s2.3: If RTL dominant, then reverse the whole sentence (all chunks)
if (dominantRTL) wordChunks.reverse();
// s2.4: Store each part, reversing each chunk as needed
for (const chunk of wordChunks) {
const chunkVec = [...lineStr.slice(chunk.start, chunk.end)];
if (chunk.isType(Type.Rtl)) {
chunkVec.reverse();
// run through the chunkVec and check for any mirrored characters (e.g. parentheses)
mirrorAdjustString(chunkVec);
}
result.push(...chunkVec);
}
// TODO: use the original return (\n or \r) not just \n
if (lineIDX !== lines.length - 1) result.push(0x000a); // \n
}
return result;
}