import { LookupTree, LookupTreeEntry } from './types'; /** * Merges the provided trees into a single lookup tree. When conflicting lookups * are encountered between two trees, the one with the lower index, then the * lower subindex is chosen. * * @param trees Array of trees to merge. Entries in earlier trees are favored * over those in later trees when there is a choice. */ export default function mergeTrees(trees: LookupTree[]): LookupTree { const result: LookupTree = { individual: {}, range: [] }; for (const tree of trees) { mergeSubtree(result, tree); } return result; } /** * Recursively merges the data for the mergeTree into the mainTree. * * @param mainTree The tree where the values should be merged * @param mergeTree The tree to be merged into the mainTree */ function mergeSubtree(mainTree: LookupTree, mergeTree: LookupTree): void { // Need to fix this recursively (and handle lookups) for (const [glyphId, value] of Object.entries(mergeTree.individual)) { // The main tree is guaranteed to have no overlaps between the // individual and range values, so if we match an invididual, there // must not be a range if (mainTree.individual[glyphId]) { mergeTreeEntry(mainTree.individual[glyphId], value); } else { let matched = false; for (const [index, { range, entry }] of mainTree.range.entries()) { const overlap = getIndividualOverlap(Number(glyphId), range); // Don't overlap if (overlap.both === null) { continue; } matched = true; // If they overlap, we have to split the range and then // merge the overlap mainTree.individual[glyphId] = value; mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry)); // When there's an overlap, we also have to fix up the range // that we had already processed mainTree.range.splice(index, 1); for (const glyph of overlap.second) { if (Array.isArray(glyph)) { mainTree.range.push({ range: glyph, entry: cloneEntry(entry) }); } else { mainTree.individual[glyph] = cloneEntry(entry); } } } if (!matched) { mainTree.individual[glyphId] = value; } } } for (const { range, entry } of mergeTree.range) { // Ranges are more complicated, because they can overlap with // multiple things, individual and range alike. We start by // eliminating ranges that are already present in another range let remainingRanges: (number | [number, number])[] = [range]; for (let index = 0; index < mainTree.range.length; index++) { const { range, entry: resultEntry } = mainTree.range[index]; for (const [remainingIndex, remainingRange] of remainingRanges.entries()) { if (Array.isArray(remainingRange)) { const overlap = getRangeOverlap(remainingRange, range); if (overlap.both === null) { continue; } mainTree.range.splice(index, 1); index--; const entryToMerge: LookupTreeEntry = cloneEntry(resultEntry); if (Array.isArray(overlap.both)) { mainTree.range.push({ range: overlap.both, entry: entryToMerge }); } else { mainTree.individual[overlap.both] = entryToMerge; } mergeTreeEntry(entryToMerge, cloneEntry(entry)); for (const second of overlap.second) { if (Array.isArray(second)) { mainTree.range.push({ range: second, entry: cloneEntry(resultEntry) }); } else { mainTree.individual[second] = cloneEntry(resultEntry); } } remainingRanges = overlap.first; } else { const overlap = getIndividualOverlap(remainingRange, range); if (overlap.both === null) { continue; } // If they overlap, we have to split the range and then // merge the overlap mainTree.individual[remainingRange] = cloneEntry(entry); mergeTreeEntry(mainTree.individual[remainingRange], cloneEntry(resultEntry)); // When there's an overlap, we also have to fix up the range // that we had already processed mainTree.range.splice(index, 1); index--; for (const glyph of overlap.second) { if (Array.isArray(glyph)) { mainTree.range.push({ range: glyph, entry: cloneEntry(resultEntry) }); } else { mainTree.individual[glyph] = cloneEntry(resultEntry); } } remainingRanges.splice(remainingIndex, 1, ...overlap.first); break; } } } // Next, we run the same against any individual glyphs for (const glyphId of Object.keys(mainTree.individual)) { for (const [remainingIndex, remainingRange] of remainingRanges.entries()) { if (Array.isArray(remainingRange)) { const overlap = getIndividualOverlap(Number(glyphId), remainingRange); if (overlap.both === null) { continue; } // If they overlap, we have to merge the overlap mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry)); // Update the remaining ranges remainingRanges.splice(remainingIndex, 1, ...overlap.second); break; } else { if (Number(glyphId) === remainingRange) { mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry)); break; } } } } // Any remaining ranges should just be added directly for (const remainingRange of remainingRanges) { if (Array.isArray(remainingRange)) { mainTree.range.push({ range: remainingRange, entry: cloneEntry(entry) }); } else { mainTree.individual[remainingRange] = cloneEntry(entry); } } } } /** * Recursively merges the entry forr the mergeTree into the mainTree * * @param mainTree The entry where the values should be merged * @param mergeTree The entry to merge into the mainTree */ function mergeTreeEntry(mainTree: LookupTreeEntry, mergeTree: LookupTreeEntry): void { if ( mergeTree.lookup && ( !mainTree.lookup || mainTree.lookup.index > mergeTree.lookup.index || (mainTree.lookup.index === mergeTree.lookup.index && mainTree.lookup.subIndex > mergeTree.lookup.subIndex) ) ) { mainTree.lookup = mergeTree.lookup; } if (mergeTree.forward) { if (!mainTree.forward) { mainTree.forward = mergeTree.forward; } else { mergeSubtree(mainTree.forward, mergeTree.forward); } } if (mergeTree.reverse) { if (!mainTree.reverse) { mainTree.reverse = mergeTree.reverse; } else { mergeSubtree(mainTree.reverse, mergeTree.reverse); } } } interface Overlap { first: (number | [number, number])[]; second: (number | [number, number])[]; both: number | [number, number] | null; } /** * Determines the overlap (if any) between two ranges. Returns the distinct * ranges for each range and the overlap (if any). * * @param first First range * @param second Second range */ function getRangeOverlap(first: [number, number], second: [number, number]): Overlap { const result: Overlap = { first: [], second: [], both: null }; // Both if (first[0] < second[1] && second[0] < first[1]) { const start = Math.max(first[0], second[0]); const end = Math.min(first[1], second[1]); result.both = rangeOrIndividual(start, end); } // Before if (first[0] < second[0]) { const start = first[0]; const end = Math.min(second[0], first[1]); result.first.push(rangeOrIndividual(start, end)); } else if (second[0] < first[0]) { const start = second[0]; const end = Math.min(second[1], first[0]); result.second.push(rangeOrIndividual(start, end)); } // After if (first[1] > second[1]) { const start = Math.max(first[0], second[1]); const end = first[1]; result.first.push(rangeOrIndividual(start, end)); } else if (second[1] > first[1]) { const start = Math.max(first[1], second[0]); const end = second[1]; result.second.push(rangeOrIndividual(start, end)); } return result; } /** * Determines the overlap (if any) between the individual glyph and the range * provided. Returns the glyphs and/or ranges that are unique to each provided * and the overlap (if any). * * @param first Individual glyph * @param second Range */ function getIndividualOverlap(first: number, second: [number, number]): Overlap { // Disjoint if (first < second[0] || first > second[1]) { return { first: [first], second: [second], both: null }; } const result: Overlap = { first: [], second: [], both: first }; if (second[0] < first) { result.second.push(rangeOrIndividual(second[0], first)); } if (second[1] > first) { result.second.push(rangeOrIndividual(first + 1, second[1])); } return result; } /** * Returns an individual glyph if the range is of size one or a range if it is * larger. * * @param start Beginning of the range (inclusive) * @param end End of the range (exclusive) */ function rangeOrIndividual(start: number, end: number): number | [number, number] { if (end - start === 1) { return start; } else { return [start, end]; } } /** * Clones an individual lookup tree entry. * * @param entry Lookup tree entry to clone */ function cloneEntry(entry: LookupTreeEntry): LookupTreeEntry { const result: LookupTreeEntry = {}; if (entry.forward) { result.forward = cloneTree(entry.forward); } if (entry.reverse) { result.reverse = cloneTree(entry.reverse); } if (entry.lookup) { result.lookup = { contextRange: entry.lookup.contextRange.slice() as [number, number], index: entry.lookup.index, length: entry.lookup.length, subIndex: entry.lookup.subIndex, substitutions: entry.lookup.substitutions.slice() }; } return result; } /** * Clones a lookup tree. * * @param tree Lookup tree to clone */ function cloneTree(tree: LookupTree): LookupTree { const individual: { [glyphId: number]: LookupTreeEntry; } = {}; for (const [glyphId, entry] of Object.entries(tree.individual)) { individual[glyphId] = cloneEntry(entry); } return { individual, range: tree.range.map(({ range, entry }) => ({ range: range.slice() as [number, number], entry: cloneEntry(entry) })) }; }