UNPKG

unicode-shaper

Version:

Shape unicode text so that renderers like WebGL and WebGPU can properly display the glyphs.

573 lines (532 loc) 16.9 kB
import { ALEFTYPE, APRESENT, ARA_LINK, COMBINE, IRRELEVANT, IRRELEVANT_POS, LAMALEF_SPACE_SUB, LAMTYPE, LAM_CHAR, LINKL, LINKR, NEW_TAIL_CHAR, PRES_ALINK, PRES_BLINK, SHADDA06_CHAR, SHADDA_CHAR, SHADDA_TATWEEL_CHAR, SHAPE_TABLE, SPACE_CHAR, TASHKEEL_MEDIAL, TASHKEEL_SPACE_SUB, TATWEEL_CHAR, U_SHAPE_AGGREGATE_TASHKEEL, U_SHAPE_AGGREGATE_TASHKEEL_MASK, U_SHAPE_LAMALEF_MASK, U_SHAPE_LAMALEF_RESIZE, U_SHAPE_LETTERS_MASK, U_SHAPE_LETTERS_SHAPE, U_SHAPE_LETTERS_SHAPE_TASHKEEL_ISOLATED, U_SHAPE_LETTERS_UNSHAPE, U_SHAPE_TASHKEEL_MASK, U_SHAPE_TASHKEEL_REPLACE_BY_TATWEEL, U_SHAPE_TASHKEEL_RESIZE, U_SHAPE_TEXT_DIRECTION_LOGICAL, U_SHAPE_TEXT_DIRECTION_MASK, U_SHAPE_TEXT_DIRECTION_VISUAL_LTR, } from './internal.js'; /** * Check if a character is Arabic * @param unicode - input unicode character * @returns - True if Arabic */ export function isArabic(unicode: number): boolean { // Arabic (0600–06FF, 256 characters) if (unicode >= 0x0600 && unicode <= 0x06ff) return true; // Arabic Supplement (0750–077F, 48 characters) if (unicode >= 0x0750 && unicode <= 0x077f) return true; // Arabic Extended-B (0870–089F, 42 characters) if (unicode >= 0x0870 && unicode <= 0x089f) return true; // Arabic Extended-A (08A0–08FF, 96 characters) if (unicode >= 0x08a0 && unicode <= 0x08ff) return true; // Arabic Presentation Forms-A (FB50–FDFF, 631 characters) if (unicode >= 0xfb50 && unicode <= 0xfdff) return true; // Arabic Presentation Forms-B (FE70–FEFF, 141 characters) if (unicode >= 0xfe70 && unicode <= 0xfeff) return true; return false; } /** * Converts the Alef characters into an equivalent * LamAlef location in the 0x06xx Range, this is an * intermediate stage in the operation of the program * later it'll be converted into the 0xFExx LamAlefs * in the shaping function. * @param ch - Alef character * @returns - LamAlef character */ function changeLamAlef(ch: number): number { if (ch === 0x0622) return 0x065c; if (ch === 0x0623) return 0x065d; if (ch === 0x0625) return 0x065e; if (ch === 0x0627) return 0x065f; return 0; } /** * Checks if a character is Tashkeel * @param ch - Tashkeel unicode character * @returns - true for Tashkeel characters in 06 range else return false */ function isTashkeelChar(ch: number): boolean { return ch >= 0x064b && ch <= 0x0652; } /** * Checks if a character is Tashkeel Fe char * @param ch - Tashkeel unicode character * @returns - true for Tashkeel characters in FE range else return false */ function isTashkeelCharFe(ch: number): boolean { return ch >= 0xfe70 && ch <= 0xfe7f; } /** * Checks if a character is Alef * @param ch - Alef unicode character * @returns - true for Alef characters in 06 range else return false */ function isAlefChar(ch: number): boolean { return ch === 0x0622 || ch === 0x0623 || ch === 0x0625 || ch === 0x0627; } /** * Checks if a character is LamAlef * @param ch - LamAlef unicode character * @returns - true for LamAlef characters in 06 range else return false */ function isLamAlefChar(ch: number): boolean { return ch >= 0xfef5 && ch <= 0xfefc; } /** * Resolves the link between the characters as * Arabic characters have four forms : * Isolated, Initial, Middle and Final Form * @param ch - Unicode character * @returns - Link */ function getLink(ch: number): number { if (ch >= 0x0622 && ch <= 0x06d3) { return ARA_LINK[ch - 0x0622]; } else if (ch === 0x200d) { return 3; } else if (ch >= 0x206d && ch <= 0x206f) { return 4; } else if (ch >= 0xfb50 && ch <= 0xfc62) { return PRES_ALINK[ch - 0xfb50]; } else if (ch >= 0xfe70 && ch <= 0xfefc) { return PRES_BLINK[ch - 0xfe70]; } return 0; } /** * Checks if the Tashkeel Character is on Tatweel or not,if the * Tashkeel on tatweel (FE range), it returns 1 else if the * Tashkeel with shadda on tatweel (FC range)return 2 otherwise * returns 0 * @param ch - Tashkeel character * @returns - 1 for Tashkeel on Tatweel, 2 for Tashkeel with Shadda on Tatweel else return 0 */ function isTashkeelOnTatweelChar(ch: number): number { if ( ch >= 0xfe70 && ch <= 0xfe7f && ch !== NEW_TAIL_CHAR && ch !== 0xfe75 && ch !== SHADDA_TATWEEL_CHAR ) { return TASHKEEL_MEDIAL[ch - 0xfe70]; } else if ((ch >= 0xfcf2 && ch <= 0xfcf4) || ch === SHADDA_TATWEEL_CHAR) { return 2; } return 0; } /** * Checks if the Tashkeel Character is in the isolated form * (i.e. Unicode FE range) returns 1 else if the Tashkeel * with shadda is in the isolated form (i.e. Unicode FC range) * returns 2 otherwise returns 0 * @param ch - Tashkeel character * @returns - 1 for Tashkeel in isolated form, 2 for Tashkeel with Shadda in isolated form else return 0 */ function isIsolatedTashkeelChar(ch: number): number { if (ch >= 0xfe70 && ch <= 0xfe7f && ch !== NEW_TAIL_CHAR && ch !== 0xfe75) { return 1 - TASHKEEL_MEDIAL[ch - 0xfe70]; } else if (ch >= 0xfc5e && ch <= 0xfc63) { return 1; } return 0; } /** * Replaces Tashkeel as following: * Case 1: if the Tashkeel on tatweel, replace it with Tatweel. * Case 2: if the Tashkeel aggregated with Shadda on Tatweel, replace * it with Shadda on Tatweel. * Case 3: if the Tashkeel is isolated replace it with Space. * @param dest - Tashkeel array */ function handleTashkeelWithTatweel(dest: number[]): void { let i = 0; const destLen = dest.length; while (i < destLen) { if (isTashkeelOnTatweelChar(dest[i]) === 1) { dest[i] = TATWEEL_CHAR; } else if (isTashkeelOnTatweelChar(dest[i]) === 2) { dest[i] = SHADDA_TATWEEL_CHAR; } else if (isIsolatedTashkeelChar(dest[i]) !== 0 && dest[i] !== SHADDA_CHAR) { dest[i] = SPACE_CHAR; } i++; } } /** * Counts the number of spaces at each end of the logical buffer * @param dest - Logical buffer * @returns - [spacesCountl, spacesCountr] */ function countSpaces(dest: number[]): [spacesCountl: number, spacesCountr: number] { let s = dest.length; let i = 0; let countl = 0; let countr = 0; while (dest[i] === SPACE_CHAR && countl < s) { countl++; i++; } if (countl < s) { // the entire buffer is not all space while (dest[s - 1] === SPACE_CHAR) { countr++; s--; } } return [countl, countr]; } /** * This function inverts the buffer, it's used * in case the user specifies the buffer to be * U_SHAPE_TEXT_DIRECTION_LOGICAL * @param buffer - input unicode buffer * @param lowlimit - lowlimit * @param highlimit - highlimit */ function invertBuffer(buffer: number[], lowlimit: number, highlimit: number): void { // let mut tmp: u16 = 0; let i = lowlimit; let j = buffer.length - highlimit - 1; while (i < j) { [buffer[i], buffer[j]] = [buffer[j], buffer[i]]; i++; j--; } } /** * Calculates the size of the output buffer * @param source - input unicode buffer * @param options - shaping options * @returns - output buffer size */ function calculateSize(source: number[], options: number): number { let destSize = source.length; let i: number; let lamAlefOption = false; let tashkeelOption = false; if ( ((options & U_SHAPE_LETTERS_MASK) === U_SHAPE_LETTERS_SHAPE || (options & U_SHAPE_LETTERS_MASK) === U_SHAPE_LETTERS_SHAPE_TASHKEEL_ISOLATED) && (options & U_SHAPE_LAMALEF_MASK) === U_SHAPE_LAMALEF_RESIZE ) { lamAlefOption = true; } if ( (options & U_SHAPE_LETTERS_MASK) === U_SHAPE_LETTERS_SHAPE && (options & U_SHAPE_TASHKEEL_MASK) === U_SHAPE_TASHKEEL_RESIZE ) { tashkeelOption = true; } if (lamAlefOption || tashkeelOption) { if ((options & U_SHAPE_TEXT_DIRECTION_MASK) === U_SHAPE_TEXT_DIRECTION_VISUAL_LTR) { i = 0; while (i < source.length) { if ( ((isAlefChar(source[i]) && i < source.length - 1 && source[i + 1] === LAM_CHAR) || isTashkeelCharFe(source[i])) && destSize > 0 ) { destSize--; } i++; } } else if ((options & U_SHAPE_TEXT_DIRECTION_MASK) === U_SHAPE_TEXT_DIRECTION_LOGICAL) { i = 0; while (i < source.length) { if ( ((source[i] === LAM_CHAR && i < source.length - 1 && isAlefChar(source[i + 1])) || isTashkeelCharFe(source[i])) && destSize > 0 ) { destSize--; } i++; } } } if ( (options & U_SHAPE_LETTERS_MASK) === U_SHAPE_LETTERS_UNSHAPE && (options & U_SHAPE_LAMALEF_MASK) === U_SHAPE_LAMALEF_RESIZE ) { i = 0; while (i < source.length) { if (isLamAlefChar(source[i]) && destSize > 0) { destSize++; } i++; } } return destSize; } /** * Converts an Arabic Unicode buffer in 06xx Range into a shaped * arabic Unicode buffer in FExx Range * @param dest - destination unicode buffer to modify * @param tashkeelFlag - tashkeel flag */ function _shapeArabic( dest: number[], // options: u32, tashkeelFlag: number, // shapeVars: UShapeVariables ) { let shape: number; let i: number; let ii: number; const I_END: number = -1; let lastPos: number; let nx = -2; let nw: number; let prevLink = 0; let lastLink = 0; let currLink: number; let nextLink = 0; let wLamalef: number; // sets the index to the end of the buffer i = dest.length - 1; lastPos = i; // This function resolves the link between the characters . // Arabic characters have four forms : // Isolated Form, Initial Form, Middle Form and Final Form currLink = getLink(dest[i]); while (true) { // If high byte of currLink > 0 then more than one shape if ((currLink & 0xff00) > 0 || (getLink(dest[i]) & IRRELEVANT) !== 0) { nw = i - 1; while (nx < 0) { // we need to know about next char if (nw === I_END) { nextLink = 0; nx = 3000; } else { nextLink = getLink(dest[nw]); if ((nextLink & IRRELEVANT) === 0) { nx = nw; } else { nw--; } } } if ((currLink & ALEFTYPE) > 0 && (lastLink & LAMTYPE) > 0) { // lamalef_found = true; wLamalef = changeLamAlef(dest[i]); // get from 0x065C-0x065f if (wLamalef !== 0) { dest[i] = LAMALEF_SPACE_SUB; // The default case is to drop the Alef and replace dest[lastPos] = wLamalef; // it by LAMALEF_SPACE_SUB which is the last character in the i = lastPos; // unicode private use area, this is done to make } // sure that removeLamAlefSpaces() handles only the lastLink = prevLink; // spaces generated during lamalef generation. currLink = getLink(wLamalef); // LAMALEF_SPACE_SUB is added here and is replaced by spaces } // in removeLamAlefSpaces() // get the proper shape according to link ability of neighbors // and of character; depends on the order of the shapes // (isolated, initial, middle, final) in the compatibility area const si = nextLink & (LINKR + LINKL); const sj = lastLink & (LINKR + LINKL); const sk = currLink & (LINKR + LINKL); shape = SHAPE_TABLE[si][sj][sk]; if ((currLink & (LINKR + LINKL)) === 1) { shape &= 1; } else if (isTashkeelChar(dest[i])) { if ( (lastLink & LINKL) > 0 && (nextLink & LINKR) > 0 && tashkeelFlag === 1 && dest[i] !== 0x064c && dest[i] !== 0x064d ) { shape = 1; if ((nextLink & ALEFTYPE) === ALEFTYPE && (lastLink & LAMTYPE) === LAMTYPE) { shape = 0; } } else if (tashkeelFlag === 2 && dest[i] === SHADDA06_CHAR) { shape = 1; } else { shape = 0; } } if ((dest[i] ^ 0x0600) < 0x100) { if (isTashkeelChar(dest[i])) { if (tashkeelFlag === 2 && dest[i] !== SHADDA06_CHAR) { dest[i] = TASHKEEL_SPACE_SUB; // tashkeel_found = true; } else { const ind = dest[i] - 0x064b; // ensure the array index is within the range if (dest[i] < 0x064b || ind >= IRRELEVANT_POS.length) { // unreachable!(); } dest[i] = 0xfe70 + IRRELEVANT_POS[ind] + shape; } } else if ((currLink & APRESENT) > 0) { dest[i] = 0xfb50 + (currLink >> 8) + shape; } else if (currLink >> 8 > 0 && (currLink & IRRELEVANT) === 0) { dest[i] = 0xfe70 + (currLink >> 8) + shape; } } } // move one notch forward if ((currLink & IRRELEVANT) === 0) { prevLink = lastLink; lastLink = currLink; lastPos = i; } ii = i - 1; // safety check if (ii >= 0) i--; if (ii === nx) { currLink = nextLink; nx = -2; } else if (ii !== I_END) { currLink = getLink(dest[i]); } if (ii === I_END) { break; } } } /** * Converts Arabic Unicode buffer into a shaped Arabic Unicode buffer * @param input - input unicode buffer * @param options - shaping options * @returns - shaped unicode buffer */ export function shapeArabic(input: number[], options: number): number[] { let sourcePtr = input; let tempsource: number[] = []; if ((options & U_SHAPE_AGGREGATE_TASHKEEL_MASK) !== 0) { tempsource = new Array(input.length * 2); const logicalOrder = (options & U_SHAPE_TEXT_DIRECTION_MASK) === U_SHAPE_TEXT_DIRECTION_LOGICAL; const aggregateTashkeel = (options & (U_SHAPE_AGGREGATE_TASHKEEL_MASK + U_SHAPE_LETTERS_SHAPE_TASHKEEL_ISOLATED)) === U_SHAPE_AGGREGATE_TASHKEEL + U_SHAPE_LETTERS_SHAPE_TASHKEEL_ISOLATED; let j = 2 * input.length; if (logicalOrder) { j = 0; } let i = input.length; if (logicalOrder) { i = 0; } let end = input.length - 1; if (logicalOrder) { end = input.length; } let aggregation_possible = true; let prev = 0; let prevLink: number; let currLink = 0; let new_source_length = 0; while (i !== end) { prevLink = currLink; currLink = getLink(input[i]); if ( aggregateTashkeel && ((prevLink | currLink) & COMBINE) === COMBINE && aggregation_possible ) { aggregation_possible = false; if (prev < input[i]) { tempsource[j] = prev - 0x064c + 0xfc5e; } else { tempsource[j] = input[i] - 0x064c + 0xfc5e; } currLink = getLink(tempsource[j]); } else { new_source_length++; aggregation_possible = true; tempsource[j] = input[i]; if (logicalOrder) { j++; } else { j--; } prev = input[i]; } // move one notch forward if (logicalOrder) { i++; } else { i--; } } if (logicalOrder) { sourcePtr = tempsource.slice(0, new_source_length); } else { sourcePtr = tempsource.slice(j, new_source_length); } } // prep output const outputSize = calculateSize(sourcePtr, options); const output: number[] = new Array(Math.max(outputSize, sourcePtr.length)); output.push(...sourcePtr); if ((options & U_SHAPE_TEXT_DIRECTION_MASK) === U_SHAPE_TEXT_DIRECTION_LOGICAL) { const [spacesCountl, spacesCountr] = countSpaces(output); invertBuffer(output, spacesCountl, spacesCountr); } // Arabic shaping if ((options & U_SHAPE_LETTERS_MASK) === U_SHAPE_LETTERS_SHAPE) { if ( (options & U_SHAPE_TASHKEEL_MASK) > 0 && (options & U_SHAPE_TASHKEEL_MASK) !== U_SHAPE_TASHKEEL_REPLACE_BY_TATWEEL ) { // Call the shaping function with tashkeel flag == 2 for removal of tashkeel _shapeArabic(output, 2); } else { // default Call the shaping function with tashkeel flag == 1 _shapeArabic(output, 1); // After shaping text check if user wants to remove tashkeel and replace it with tatweel if ((options & U_SHAPE_TASHKEEL_MASK) === U_SHAPE_TASHKEEL_REPLACE_BY_TATWEEL) { handleTashkeelWithTatweel(output); } } } else if ((options & U_SHAPE_LETTERS_MASK) === U_SHAPE_LETTERS_SHAPE_TASHKEEL_ISOLATED) { // Call the shaping function with tashkeel flag == 0 _shapeArabic(output, 0); } if ((options & U_SHAPE_TEXT_DIRECTION_MASK) === U_SHAPE_TEXT_DIRECTION_LOGICAL) { const [spacesCountl, spacesCountr] = countSpaces(output); invertBuffer(output, spacesCountl, spacesCountr); } // End of Arabic letter shaping part // copy a slice to a new slice "arabicOutput" of outputSize // and run through output, skip every LAMALEF_SPACE_SUB and TASHKEEL_SPACE_SUB const arabicOutput: number[] = new Array(outputSize); for (const ch of output) { if (ch !== LAMALEF_SPACE_SUB && ch !== TASHKEEL_SPACE_SUB) arabicOutput.push(ch); } return arabicOutput; }