2 space indent
This commit is contained in:
@@ -1,35 +1,35 @@
|
||||
import { LookupTree, FlattenedLookupTree, LookupTreeEntry, FlattenedLookupTreeEntry } from './types';
|
||||
|
||||
export default function flatten(tree: LookupTree): FlattenedLookupTree {
|
||||
const result: FlattenedLookupTree = {};
|
||||
for (const [glyphId, entry] of Object.entries(tree.individual)) {
|
||||
result[glyphId] = flattenEntry(entry);
|
||||
}
|
||||
const result: FlattenedLookupTree = {};
|
||||
for (const [glyphId, entry] of Object.entries(tree.individual)) {
|
||||
result[glyphId] = flattenEntry(entry);
|
||||
}
|
||||
|
||||
for (const { range, entry } of tree.range) {
|
||||
const flattened = flattenEntry(entry);
|
||||
for (let glyphId = range[0]; glyphId < range[1]; glyphId++) {
|
||||
result[glyphId] = flattened;
|
||||
}
|
||||
for (const { range, entry } of tree.range) {
|
||||
const flattened = flattenEntry(entry);
|
||||
for (let glyphId = range[0]; glyphId < range[1]; glyphId++) {
|
||||
result[glyphId] = flattened;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
function flattenEntry(entry: LookupTreeEntry): FlattenedLookupTreeEntry {
|
||||
const result: FlattenedLookupTreeEntry = {};
|
||||
const result: FlattenedLookupTreeEntry = {};
|
||||
|
||||
if (entry.forward) {
|
||||
result.forward = flatten(entry.forward);
|
||||
}
|
||||
if (entry.forward) {
|
||||
result.forward = flatten(entry.forward);
|
||||
}
|
||||
|
||||
if (entry.reverse) {
|
||||
result.reverse = flatten(entry.reverse);
|
||||
}
|
||||
if (entry.reverse) {
|
||||
result.reverse = flatten(entry.reverse);
|
||||
}
|
||||
|
||||
if (entry.lookup) {
|
||||
result.lookup = entry.lookup;
|
||||
}
|
||||
if (entry.lookup) {
|
||||
result.lookup = entry.lookup;
|
||||
}
|
||||
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -13,236 +13,236 @@ import buildTreeGsubType8Format1 from './processors/8-1';
|
||||
import flatten from './flatten';
|
||||
|
||||
class FontImpl implements Font {
|
||||
private _font: opentype.Font;
|
||||
private _lookupTrees: { tree: FlattenedLookupTree; processForward: boolean; }[] = [];
|
||||
private _glyphLookups: { [glyphId: string]: number[] } = {};
|
||||
private _cache?: LRUCache<string, LigatureData | [number, number][]>;
|
||||
private _font: opentype.Font;
|
||||
private _lookupTrees: { tree: FlattenedLookupTree; processForward: boolean; }[] = [];
|
||||
private _glyphLookups: { [glyphId: string]: number[] } = {};
|
||||
private _cache?: LRUCache<string, LigatureData | [number, number][]>;
|
||||
|
||||
constructor(font: opentype.Font, options: Required<Options>) {
|
||||
this._font = font;
|
||||
constructor(font: opentype.Font, options: Required<Options>) {
|
||||
this._font = font;
|
||||
|
||||
if (options.cacheSize > 0) {
|
||||
this._cache = new LRUCache({
|
||||
max: options.cacheSize,
|
||||
length: ((val: LigatureData | [number, number][], key: string) => key.length) as any
|
||||
});
|
||||
}
|
||||
|
||||
const caltFeatures = this._font.tables.gsub && this._font.tables.gsub.features.filter((f: { tag: string }) => f.tag === 'calt') || [];
|
||||
const lookupIndices: number[] = caltFeatures
|
||||
.reduce((acc: number[], val: { feature: { lookupListIndexes: number[] } }) => [...acc, ...val.feature.lookupListIndexes], []);
|
||||
|
||||
const allLookups = this._font.tables.gsub && this._font.tables.gsub.lookups || [];
|
||||
const lookupGroups = allLookups.filter((l: unknown, i: number) => lookupIndices.some(idx => idx === i));
|
||||
|
||||
for (const [index, lookup] of lookupGroups.entries()) {
|
||||
const trees: LookupTree[] = [];
|
||||
switch (lookup.lookupType) {
|
||||
case 6:
|
||||
for (const [index, table] of lookup.subtables.entries()) {
|
||||
switch (table.substFormat) {
|
||||
case 1:
|
||||
trees.push(buildTreeGsubType6Format1(table, allLookups, index));
|
||||
break;
|
||||
case 2:
|
||||
trees.push(buildTreeGsubType6Format2(table, allLookups, index));
|
||||
break;
|
||||
case 3:
|
||||
trees.push(buildTreeGsubType6Format3(table, allLookups, index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
for (const [index, table] of lookup.subtables.entries()) {
|
||||
trees.push(buildTreeGsubType8Format1(table, index));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const tree = flatten(mergeTrees(trees));
|
||||
|
||||
this._lookupTrees.push({
|
||||
tree,
|
||||
processForward: lookup.lookupType !== 8
|
||||
});
|
||||
|
||||
for (const glyphId of Object.keys(tree)) {
|
||||
if (!this._glyphLookups[glyphId]) {
|
||||
this._glyphLookups[glyphId] = [];
|
||||
}
|
||||
|
||||
this._glyphLookups[glyphId].push(index);
|
||||
}
|
||||
}
|
||||
if (options.cacheSize > 0) {
|
||||
this._cache = new LRUCache({
|
||||
max: options.cacheSize,
|
||||
length: ((val: LigatureData | [number, number][], key: string) => key.length) as any
|
||||
});
|
||||
}
|
||||
|
||||
findLigatures(text: string): LigatureData {
|
||||
const cached = this._cache && this._cache.get(text);
|
||||
if (cached && !Array.isArray(cached)) {
|
||||
return cached;
|
||||
const caltFeatures = this._font.tables.gsub && this._font.tables.gsub.features.filter((f: { tag: string }) => f.tag === 'calt') || [];
|
||||
const lookupIndices: number[] = caltFeatures
|
||||
.reduce((acc: number[], val: { feature: { lookupListIndexes: number[] } }) => [...acc, ...val.feature.lookupListIndexes], []);
|
||||
|
||||
const allLookups = this._font.tables.gsub && this._font.tables.gsub.lookups || [];
|
||||
const lookupGroups = allLookups.filter((l: unknown, i: number) => lookupIndices.some(idx => idx === i));
|
||||
|
||||
for (const [index, lookup] of lookupGroups.entries()) {
|
||||
const trees: LookupTree[] = [];
|
||||
switch (lookup.lookupType) {
|
||||
case 6:
|
||||
for (const [index, table] of lookup.subtables.entries()) {
|
||||
switch (table.substFormat) {
|
||||
case 1:
|
||||
trees.push(buildTreeGsubType6Format1(table, allLookups, index));
|
||||
break;
|
||||
case 2:
|
||||
trees.push(buildTreeGsubType6Format2(table, allLookups, index));
|
||||
break;
|
||||
case 3:
|
||||
trees.push(buildTreeGsubType6Format3(table, allLookups, index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
for (const [index, table] of lookup.subtables.entries()) {
|
||||
trees.push(buildTreeGsubType8Format1(table, index));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const tree = flatten(mergeTrees(trees));
|
||||
|
||||
this._lookupTrees.push({
|
||||
tree,
|
||||
processForward: lookup.lookupType !== 8
|
||||
});
|
||||
|
||||
for (const glyphId of Object.keys(tree)) {
|
||||
if (!this._glyphLookups[glyphId]) {
|
||||
this._glyphLookups[glyphId] = [];
|
||||
}
|
||||
|
||||
const glyphIds: number[] = [];
|
||||
for (const char of text) {
|
||||
glyphIds.push(this._font.charToGlyphIndex(char));
|
||||
}
|
||||
this._glyphLookups[glyphId].push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no lookup groups, there's no point looking for
|
||||
// replacements. This gives us a minor performance boost for fonts with
|
||||
// no ligatures
|
||||
if (this._lookupTrees.length === 0) {
|
||||
return {
|
||||
inputGlyphs: glyphIds,
|
||||
outputGlyphs: glyphIds,
|
||||
contextRanges: []
|
||||
};
|
||||
}
|
||||
|
||||
const result = this._findInternal(glyphIds.slice());
|
||||
const finalResult: LigatureData = {
|
||||
inputGlyphs: glyphIds,
|
||||
outputGlyphs: result.sequence,
|
||||
contextRanges: result.ranges
|
||||
};
|
||||
if (this._cache) {
|
||||
this._cache.set(text, finalResult);
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
findLigatures(text: string): LigatureData {
|
||||
const cached = this._cache && this._cache.get(text);
|
||||
if (cached && !Array.isArray(cached)) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
findLigatureRanges(text: string): [number, number][] {
|
||||
// Short circuit the process if there are no possible ligatures in the
|
||||
// font
|
||||
if (this._lookupTrees.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cached = this._cache && this._cache.get(text);
|
||||
if (cached) {
|
||||
return Array.isArray(cached) ? cached : cached.contextRanges;
|
||||
}
|
||||
|
||||
const glyphIds: number[] = [];
|
||||
for (const char of text) {
|
||||
glyphIds.push(this._font.charToGlyphIndex(char));
|
||||
}
|
||||
|
||||
const result = this._findInternal(glyphIds);
|
||||
if (this._cache) {
|
||||
this._cache.set(text, result.ranges);
|
||||
}
|
||||
|
||||
return result.ranges;
|
||||
const glyphIds: number[] = [];
|
||||
for (const char of text) {
|
||||
glyphIds.push(this._font.charToGlyphIndex(char));
|
||||
}
|
||||
|
||||
private _findInternal(sequence: number[]): { sequence: number[]; ranges: [number, number][]; } {
|
||||
const ranges: [number, number][] = [];
|
||||
// If there are no lookup groups, there's no point looking for
|
||||
// replacements. This gives us a minor performance boost for fonts with
|
||||
// no ligatures
|
||||
if (this._lookupTrees.length === 0) {
|
||||
return {
|
||||
inputGlyphs: glyphIds,
|
||||
outputGlyphs: glyphIds,
|
||||
contextRanges: []
|
||||
};
|
||||
}
|
||||
|
||||
let nextLookup = this._getNextLookup(sequence, 0);
|
||||
while (nextLookup.index !== null) {
|
||||
const lookup = this._lookupTrees[nextLookup.index];
|
||||
if (lookup.processForward) {
|
||||
let lastGlyphIndex = nextLookup.last;
|
||||
for (let i = nextLookup.first; i < lastGlyphIndex; i++) {
|
||||
const result = walkTree(lookup.tree, sequence, i, i);
|
||||
if (result) {
|
||||
for (let j = 0; j < result.substitutions.length; j++) {
|
||||
const sub = result.substitutions[j];
|
||||
if (sub !== null) {
|
||||
sequence[i + j] = sub;
|
||||
}
|
||||
}
|
||||
const result = this._findInternal(glyphIds.slice());
|
||||
const finalResult: LigatureData = {
|
||||
inputGlyphs: glyphIds,
|
||||
outputGlyphs: result.sequence,
|
||||
contextRanges: result.ranges
|
||||
};
|
||||
if (this._cache) {
|
||||
this._cache.set(text, finalResult);
|
||||
}
|
||||
|
||||
mergeRange(
|
||||
ranges,
|
||||
result.contextRange[0] + i,
|
||||
result.contextRange[1] + i
|
||||
);
|
||||
return finalResult;
|
||||
}
|
||||
|
||||
// Substitutions can end up extending the search range
|
||||
if (i + result.length >= lastGlyphIndex) {
|
||||
lastGlyphIndex = i + result.length + 1;
|
||||
}
|
||||
findLigatureRanges(text: string): [number, number][] {
|
||||
// Short circuit the process if there are no possible ligatures in the
|
||||
// font
|
||||
if (this._lookupTrees.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
i += result.length - 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We don't need to do the lastGlyphIndex tracking here because
|
||||
// reverse processing isn't allowed to replace more than one
|
||||
// character at a time.
|
||||
for (let i = nextLookup.last - 1; i >= nextLookup.first; i--) {
|
||||
const result = walkTree(lookup.tree, sequence, i, i);
|
||||
if (result) {
|
||||
for (let j = 0; j < result.substitutions.length; j++) {
|
||||
const sub = result.substitutions[j];
|
||||
if (sub !== null) {
|
||||
sequence[i + j] = sub;
|
||||
}
|
||||
}
|
||||
const cached = this._cache && this._cache.get(text);
|
||||
if (cached) {
|
||||
return Array.isArray(cached) ? cached : cached.contextRanges;
|
||||
}
|
||||
|
||||
mergeRange(
|
||||
ranges,
|
||||
result.contextRange[0] + i,
|
||||
result.contextRange[1] + i
|
||||
);
|
||||
const glyphIds: number[] = [];
|
||||
for (const char of text) {
|
||||
glyphIds.push(this._font.charToGlyphIndex(char));
|
||||
}
|
||||
|
||||
i -= result.length - 1;
|
||||
}
|
||||
}
|
||||
const result = this._findInternal(glyphIds);
|
||||
if (this._cache) {
|
||||
this._cache.set(text, result.ranges);
|
||||
}
|
||||
|
||||
return result.ranges;
|
||||
}
|
||||
|
||||
private _findInternal(sequence: number[]): { sequence: number[]; ranges: [number, number][]; } {
|
||||
const ranges: [number, number][] = [];
|
||||
|
||||
let nextLookup = this._getNextLookup(sequence, 0);
|
||||
while (nextLookup.index !== null) {
|
||||
const lookup = this._lookupTrees[nextLookup.index];
|
||||
if (lookup.processForward) {
|
||||
let lastGlyphIndex = nextLookup.last;
|
||||
for (let i = nextLookup.first; i < lastGlyphIndex; i++) {
|
||||
const result = walkTree(lookup.tree, sequence, i, i);
|
||||
if (result) {
|
||||
for (let j = 0; j < result.substitutions.length; j++) {
|
||||
const sub = result.substitutions[j];
|
||||
if (sub !== null) {
|
||||
sequence[i + j] = sub;
|
||||
}
|
||||
}
|
||||
|
||||
nextLookup = this._getNextLookup(sequence, nextLookup.index + 1);
|
||||
}
|
||||
mergeRange(
|
||||
ranges,
|
||||
result.contextRange[0] + i,
|
||||
result.contextRange[1] + i
|
||||
);
|
||||
|
||||
return { sequence, ranges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lookup and glyph range for the first lookup that might
|
||||
* contain a match.
|
||||
*
|
||||
* @param sequence Input glyph sequence
|
||||
* @param start The first input to try
|
||||
*/
|
||||
private _getNextLookup(sequence: number[], start: number): { index: number | null; first: number; last: number; } {
|
||||
const result: { index: number | null; first: number; last: number; } = {
|
||||
index: null,
|
||||
first: Infinity,
|
||||
last: -1
|
||||
};
|
||||
|
||||
// Loop through each glyph and find the first valid lookup for it
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const lookups = this._glyphLookups[sequence[i]];
|
||||
if (!lookups) {
|
||||
continue;
|
||||
// Substitutions can end up extending the search range
|
||||
if (i + result.length >= lastGlyphIndex) {
|
||||
lastGlyphIndex = i + result.length + 1;
|
||||
}
|
||||
|
||||
for (let j = 0; j < lookups.length; j++) {
|
||||
const lookupIndex = lookups[j];
|
||||
if (lookupIndex >= start) {
|
||||
// Update the lookup information if it's the one we're
|
||||
// storing or earlier than it.
|
||||
if (result.index === null || lookupIndex <= result.index) {
|
||||
result.index = lookupIndex;
|
||||
|
||||
if (result.first > i) {
|
||||
result.first = i;
|
||||
}
|
||||
|
||||
result.last = i + 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
i += result.length - 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We don't need to do the lastGlyphIndex tracking here because
|
||||
// reverse processing isn't allowed to replace more than one
|
||||
// character at a time.
|
||||
for (let i = nextLookup.last - 1; i >= nextLookup.first; i--) {
|
||||
const result = walkTree(lookup.tree, sequence, i, i);
|
||||
if (result) {
|
||||
for (let j = 0; j < result.substitutions.length; j++) {
|
||||
const sub = result.substitutions[j];
|
||||
if (sub !== null) {
|
||||
sequence[i + j] = sub;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
mergeRange(
|
||||
ranges,
|
||||
result.contextRange[0] + i,
|
||||
result.contextRange[1] + i
|
||||
);
|
||||
|
||||
i -= result.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextLookup = this._getNextLookup(sequence, nextLookup.index + 1);
|
||||
}
|
||||
|
||||
return { sequence, ranges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lookup and glyph range for the first lookup that might
|
||||
* contain a match.
|
||||
*
|
||||
* @param sequence Input glyph sequence
|
||||
* @param start The first input to try
|
||||
*/
|
||||
private _getNextLookup(sequence: number[], start: number): { index: number | null; first: number; last: number; } {
|
||||
const result: { index: number | null; first: number; last: number; } = {
|
||||
index: null,
|
||||
first: Infinity,
|
||||
last: -1
|
||||
};
|
||||
|
||||
// Loop through each glyph and find the first valid lookup for it
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const lookups = this._glyphLookups[sequence[i]];
|
||||
if (!lookups) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < lookups.length; j++) {
|
||||
const lookupIndex = lookups[j];
|
||||
if (lookupIndex >= start) {
|
||||
// Update the lookup information if it's the one we're
|
||||
// storing or earlier than it.
|
||||
if (result.index === null || lookupIndex <= result.index) {
|
||||
result.index = lookupIndex;
|
||||
|
||||
if (result.first > i) {
|
||||
result.first = i;
|
||||
}
|
||||
|
||||
result.last = i + 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,11 +252,11 @@ class FontImpl implements Font {
|
||||
* @param buffer ArrayBuffer of the font to load
|
||||
*/
|
||||
export function loadBuffer(buffer: ArrayBuffer, options?: Options): Font {
|
||||
const font = opentype.parse(buffer);
|
||||
return new FontImpl(font, {
|
||||
cacheSize: 0,
|
||||
...options
|
||||
});
|
||||
const font = opentype.parse(buffer);
|
||||
return new FontImpl(font, {
|
||||
cacheSize: 0,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
export { Font, LigatureData, Options };
|
||||
|
||||
@@ -9,16 +9,16 @@ import { LookupTree, LookupTreeEntry } from './types';
|
||||
* over those in later trees when there is a choice.
|
||||
*/
|
||||
export default function mergeTrees(trees: LookupTree[]): LookupTree {
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
for (const tree of trees) {
|
||||
mergeSubtree(result, tree);
|
||||
}
|
||||
for (const tree of trees) {
|
||||
mergeSubtree(result, tree);
|
||||
}
|
||||
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,162 +28,162 @@ export default function mergeTrees(trees: LookupTree[]): LookupTree {
|
||||
* @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);
|
||||
// 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;
|
||||
}
|
||||
// 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 (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;
|
||||
}
|
||||
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--;
|
||||
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;
|
||||
}
|
||||
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));
|
||||
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)
|
||||
});
|
||||
for (const second of overlap.second) {
|
||||
if (Array.isArray(second)) {
|
||||
mainTree.range.push({
|
||||
range: second,
|
||||
entry: cloneEntry(resultEntry)
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[remainingRange] = cloneEntry(entry);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,37 +193,37 @@ function mergeSubtree(mainTree: LookupTree, mergeTree: LookupTree): void {
|
||||
* @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.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.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);
|
||||
}
|
||||
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;
|
||||
first: (number | [number, number])[];
|
||||
second: (number | [number, number])[];
|
||||
both: number | [number, number] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,42 +234,42 @@ interface Overlap {
|
||||
* @param second Second range
|
||||
*/
|
||||
function getRangeOverlap(first: [number, number], second: [number, number]): Overlap {
|
||||
const result: Overlap = {
|
||||
first: [],
|
||||
second: [],
|
||||
both: null
|
||||
};
|
||||
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);
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
// 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;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,30 +281,30 @@ function getRangeOverlap(first: [number, number], second: [number, number]): Ove
|
||||
* @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
|
||||
// Disjoint
|
||||
if (first < second[0] || first > second[1]) {
|
||||
return {
|
||||
first: [first],
|
||||
second: [second],
|
||||
both: null
|
||||
};
|
||||
}
|
||||
|
||||
if (second[0] < first) {
|
||||
result.second.push(rangeOrIndividual(second[0], first));
|
||||
}
|
||||
const result: Overlap = {
|
||||
first: [],
|
||||
second: [],
|
||||
both: first
|
||||
};
|
||||
|
||||
if (second[1] > first) {
|
||||
result.second.push(rangeOrIndividual(first + 1, second[1]));
|
||||
}
|
||||
if (second[0] < first) {
|
||||
result.second.push(rangeOrIndividual(second[0], first));
|
||||
}
|
||||
|
||||
return result;
|
||||
if (second[1] > first) {
|
||||
result.second.push(rangeOrIndividual(first + 1, second[1]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,11 +315,11 @@ function getIndividualOverlap(first: number, second: [number, number]): Overlap
|
||||
* @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];
|
||||
}
|
||||
if (end - start === 1) {
|
||||
return start;
|
||||
} else {
|
||||
return [start, end];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,27 +328,27 @@ function rangeOrIndividual(start: number, end: number): number | [number, number
|
||||
* @param entry Lookup tree entry to clone
|
||||
*/
|
||||
function cloneEntry(entry: LookupTreeEntry): LookupTreeEntry {
|
||||
const result: LookupTreeEntry = {};
|
||||
const result: LookupTreeEntry = {};
|
||||
|
||||
if (entry.forward) {
|
||||
result.forward = cloneTree(entry.forward);
|
||||
}
|
||||
if (entry.forward) {
|
||||
result.forward = cloneTree(entry.forward);
|
||||
}
|
||||
|
||||
if (entry.reverse) {
|
||||
result.reverse = cloneTree(entry.reverse);
|
||||
}
|
||||
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()
|
||||
};
|
||||
}
|
||||
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;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,16 +357,16 @@ function cloneEntry(entry: LookupTreeEntry): LookupTreeEntry {
|
||||
* @param tree Lookup tree to clone
|
||||
*/
|
||||
function cloneTree(tree: LookupTree): LookupTree {
|
||||
const individual: { [glyphId: string]: LookupTreeEntry; } = {};
|
||||
for (const [glyphId, entry] of Object.entries(tree.individual)) {
|
||||
individual[glyphId] = cloneEntry(entry);
|
||||
}
|
||||
const individual: { [glyphId: string]: 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)
|
||||
}))
|
||||
};
|
||||
return {
|
||||
individual,
|
||||
range: tree.range.map(({ range, entry }) => ({
|
||||
range: range.slice() as [number, number],
|
||||
entry: cloneEntry(entry)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,57 +8,57 @@
|
||||
* @param newRangeEnd End position of range to merge, exclusive
|
||||
*/
|
||||
export default function mergeRange(ranges: [number, number][], newRangeStart: number, newRangeEnd: number): [number, number][] {
|
||||
let inRange = false;
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
if (!inRange) {
|
||||
if (newRangeEnd <= range[0]) {
|
||||
// Case 1: New range is before the search range
|
||||
ranges.splice(i, 0, [newRangeStart, newRangeEnd]);
|
||||
return ranges;
|
||||
} else if (newRangeEnd <= range[1]) {
|
||||
// Case 2: New range is either wholly contained within the
|
||||
// search range or overlaps with the front of it
|
||||
range[0] = Math.min(newRangeStart, range[0]);
|
||||
return ranges;
|
||||
} else if (newRangeStart < range[1]) {
|
||||
// Case 3: New range either wholly contains the search range
|
||||
// or overlaps with the end of it
|
||||
range[0] = Math.min(newRangeStart, range[0]);
|
||||
inRange = true;
|
||||
} else {
|
||||
// Case 4: New range starts after the search range
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (newRangeEnd <= range[0]) {
|
||||
// Case 5: New range extends from previous range but doesn't
|
||||
// reach the current one
|
||||
ranges[i - 1][1] = newRangeEnd;
|
||||
return ranges;
|
||||
} else if (newRangeEnd <= range[1]) {
|
||||
// Case 6: New range extends from prvious range into the
|
||||
// current range
|
||||
ranges[i - 1][1] = Math.max(newRangeEnd, range[1]);
|
||||
ranges.splice(i, 1);
|
||||
inRange = false;
|
||||
return ranges;
|
||||
} else {
|
||||
// Case 7: New range extends from previous range past the
|
||||
// end of the current range
|
||||
ranges.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inRange) {
|
||||
// Case 8: New range extends past the last existing range
|
||||
ranges[ranges.length - 1][1] = newRangeEnd;
|
||||
let inRange = false;
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
if (!inRange) {
|
||||
if (newRangeEnd <= range[0]) {
|
||||
// Case 1: New range is before the search range
|
||||
ranges.splice(i, 0, [newRangeStart, newRangeEnd]);
|
||||
return ranges;
|
||||
} else if (newRangeEnd <= range[1]) {
|
||||
// Case 2: New range is either wholly contained within the
|
||||
// search range or overlaps with the front of it
|
||||
range[0] = Math.min(newRangeStart, range[0]);
|
||||
return ranges;
|
||||
} else if (newRangeStart < range[1]) {
|
||||
// Case 3: New range either wholly contains the search range
|
||||
// or overlaps with the end of it
|
||||
range[0] = Math.min(newRangeStart, range[0]);
|
||||
inRange = true;
|
||||
} else {
|
||||
// Case 4: New range starts after the search range
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Case 9: New range starts after the last existing range
|
||||
ranges.push([newRangeStart, newRangeEnd]);
|
||||
if (newRangeEnd <= range[0]) {
|
||||
// Case 5: New range extends from previous range but doesn't
|
||||
// reach the current one
|
||||
ranges[i - 1][1] = newRangeEnd;
|
||||
return ranges;
|
||||
} else if (newRangeEnd <= range[1]) {
|
||||
// Case 6: New range extends from prvious range into the
|
||||
// current range
|
||||
ranges[i - 1][1] = Math.max(newRangeEnd, range[1]);
|
||||
ranges.splice(i, 1);
|
||||
inRange = false;
|
||||
return ranges;
|
||||
} else {
|
||||
// Case 7: New range extends from previous range past the
|
||||
// end of the current range
|
||||
ranges.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
if (inRange) {
|
||||
// Case 8: New range extends past the last existing range
|
||||
ranges[ranges.length - 1][1] = newRangeEnd;
|
||||
} else {
|
||||
// Case 9: New range starts after the last existing range
|
||||
ranges.push([newRangeStart, newRangeEnd]);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
@@ -13,70 +13,70 @@ import { processInputPosition, processLookaheadPosition, processBacktrackPositio
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ChainingContextualSubstitutionTable.Format1, lookups: Lookup[], tableIndex: number): LookupTree {
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const firstGlyphs = listGlyphsByIndex(table.coverage);
|
||||
const firstGlyphs = listGlyphsByIndex(table.coverage);
|
||||
|
||||
for (const { glyphId, index } of firstGlyphs) {
|
||||
const chainRuleSet = table.chainRuleSets[index];
|
||||
for (const { glyphId, index } of firstGlyphs) {
|
||||
const chainRuleSet = table.chainRuleSets[index];
|
||||
|
||||
// If the chain rule set is null there's nothing to do with this table.
|
||||
if (!chainRuleSet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [subIndex, subTable] of chainRuleSet.entries()) {
|
||||
let currentEntries: EntryMeta[] = getInputTree(
|
||||
result,
|
||||
subTable.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
// We walk forward, then backward
|
||||
for (const [index, glyph] of subTable.input.entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
[glyph],
|
||||
index + 1,
|
||||
currentEntries,
|
||||
subTable.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const glyph of subTable.lookahead) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
[glyph],
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const glyph of subTable.backtrack) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
[glyph],
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, insert the lookup information
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
length: subTable.input.length + 1,
|
||||
index: tableIndex,
|
||||
subIndex,
|
||||
contextRange: [
|
||||
-1 * subTable.backtrack.length,
|
||||
1 + subTable.input.length + subTable.lookahead.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
// If the chain rule set is null there's nothing to do with this table.
|
||||
if (!chainRuleSet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return result;
|
||||
for (const [subIndex, subTable] of chainRuleSet.entries()) {
|
||||
let currentEntries: EntryMeta[] = getInputTree(
|
||||
result,
|
||||
subTable.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
// We walk forward, then backward
|
||||
for (const [index, glyph] of subTable.input.entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
[glyph],
|
||||
index + 1,
|
||||
currentEntries,
|
||||
subTable.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const glyph of subTable.lookahead) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
[glyph],
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const glyph of subTable.backtrack) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
[glyph],
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, insert the lookup information
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
length: subTable.input.length + 1,
|
||||
index: tableIndex,
|
||||
subIndex,
|
||||
contextRange: [
|
||||
-1 * subTable.backtrack.length,
|
||||
1 + subTable.input.length + subTable.lookahead.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -15,82 +15,82 @@ import { processInputPosition, processLookaheadPosition, processBacktrackPositio
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ChainingContextualSubstitutionTable.Format2, lookups: Lookup[], tableIndex: number): LookupTree {
|
||||
const results: LookupTree[] = [];
|
||||
const results: LookupTree[] = [];
|
||||
|
||||
const firstGlyphs = listGlyphsByIndex(table.coverage);
|
||||
const firstGlyphs = listGlyphsByIndex(table.coverage);
|
||||
|
||||
for (const { glyphId } of firstGlyphs) {
|
||||
const firstInputClass = getGlyphClass(table.inputClassDef, glyphId);
|
||||
for (const [glyphId, inputClass] of firstInputClass.entries()) {
|
||||
// istanbul ignore next - invalid font
|
||||
if (inputClass === null) {
|
||||
continue;
|
||||
}
|
||||
for (const { glyphId } of firstGlyphs) {
|
||||
const firstInputClass = getGlyphClass(table.inputClassDef, glyphId);
|
||||
for (const [glyphId, inputClass] of firstInputClass.entries()) {
|
||||
// istanbul ignore next - invalid font
|
||||
if (inputClass === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const classSet = table.chainClassSet[inputClass];
|
||||
const classSet = table.chainClassSet[inputClass];
|
||||
|
||||
// If the class set is null there's nothing to do with this table.
|
||||
if (!classSet) {
|
||||
continue;
|
||||
}
|
||||
// If the class set is null there's nothing to do with this table.
|
||||
if (!classSet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [subIndex, subTable] of classSet.entries()) {
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
for (const [subIndex, subTable] of classSet.entries()) {
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
let currentEntries: EntryMeta[] = getInputTree(
|
||||
result,
|
||||
subTable.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
let currentEntries: EntryMeta[] = getInputTree(
|
||||
result,
|
||||
subTable.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
for (const [index, classNum] of subTable.input.entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
listClassGlyphs(table.inputClassDef, classNum),
|
||||
index + 1,
|
||||
currentEntries,
|
||||
subTable.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const classNum of subTable.lookahead) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listClassGlyphs(table.lookaheadClassDef, classNum),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const classNum of subTable.backtrack) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listClassGlyphs(table.backtrackClassDef, classNum),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, all of the entries we've accumulated
|
||||
// should have a lookup defined
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex,
|
||||
length: subTable.input.length + 1,
|
||||
contextRange: [
|
||||
-1 * subTable.backtrack.length,
|
||||
1 + subTable.input.length + subTable.lookahead.length
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
for (const [index, classNum] of subTable.input.entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
listClassGlyphs(table.inputClassDef, classNum),
|
||||
index + 1,
|
||||
currentEntries,
|
||||
subTable.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return mergeTrees(results);
|
||||
for (const classNum of subTable.lookahead) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listClassGlyphs(table.lookaheadClassDef, classNum),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const classNum of subTable.backtrack) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listClassGlyphs(table.backtrackClassDef, classNum),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, all of the entries we've accumulated
|
||||
// should have a lookup defined
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex,
|
||||
length: subTable.input.length + 1,
|
||||
contextRange: [
|
||||
-1 * subTable.backtrack.length,
|
||||
1 + subTable.input.length + subTable.lookahead.length
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergeTrees(results);
|
||||
}
|
||||
|
||||
@@ -13,61 +13,61 @@ import { processInputPosition, processLookaheadPosition, processBacktrackPositio
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ChainingContextualSubstitutionTable.Format3, lookups: Lookup[], tableIndex: number): LookupTree {
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]);
|
||||
const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]);
|
||||
|
||||
for (const { glyphId } of firstGlyphs) {
|
||||
let currentEntries: EntryMeta[] = getInputTree(
|
||||
result,
|
||||
table.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
for (const { glyphId } of firstGlyphs) {
|
||||
let currentEntries: EntryMeta[] = getInputTree(
|
||||
result,
|
||||
table.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
for (const [index, coverage] of table.inputCoverage.slice(1).entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
index + 1,
|
||||
currentEntries,
|
||||
table.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.lookaheadCoverage) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.backtrackCoverage) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, all of the entries we've accumulated
|
||||
// should have a lookup defined
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex: 0,
|
||||
length: table.inputCoverage.length,
|
||||
contextRange: [
|
||||
-1 * table.backtrackCoverage.length,
|
||||
table.inputCoverage.length + table.lookaheadCoverage.length
|
||||
]
|
||||
};
|
||||
}
|
||||
for (const [index, coverage] of table.inputCoverage.slice(1).entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
index + 1,
|
||||
currentEntries,
|
||||
table.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
for (const coverage of table.lookaheadCoverage) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.backtrackCoverage) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, all of the entries we've accumulated
|
||||
// should have a lookup defined
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex: 0,
|
||||
length: table.inputCoverage.length,
|
||||
contextRange: [
|
||||
-1 * table.backtrackCoverage.length,
|
||||
table.inputCoverage.length + table.lookaheadCoverage.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -12,58 +12,58 @@ import { processLookaheadPosition, processBacktrackPosition, EntryMeta } from '.
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ReverseChainingContextualSingleSubstitutionTable, tableIndex: number): LookupTree {
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
const result: LookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const glyphs = listGlyphsByIndex(table.coverage);
|
||||
const glyphs = listGlyphsByIndex(table.coverage);
|
||||
|
||||
for (const { glyphId, index } of glyphs) {
|
||||
const initialEntry: LookupTreeEntry = {};
|
||||
if (Array.isArray(glyphId)) {
|
||||
result.range.push({
|
||||
entry: initialEntry,
|
||||
range: glyphId
|
||||
});
|
||||
} else {
|
||||
result.individual[glyphId] = initialEntry;
|
||||
}
|
||||
|
||||
let currentEntries: EntryMeta[] = [{
|
||||
entry: initialEntry,
|
||||
substitutions: [table.substitutes[index]]
|
||||
}];
|
||||
|
||||
// We walk forward, then backward
|
||||
for (const coverage of table.lookaheadCoverage) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.backtrackCoverage) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, insert the lookup information
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex: 0,
|
||||
length: 1,
|
||||
contextRange: [
|
||||
-1 * table.backtrackCoverage.length,
|
||||
1 + table.lookaheadCoverage.length
|
||||
]
|
||||
};
|
||||
}
|
||||
for (const { glyphId, index } of glyphs) {
|
||||
const initialEntry: LookupTreeEntry = {};
|
||||
if (Array.isArray(glyphId)) {
|
||||
result.range.push({
|
||||
entry: initialEntry,
|
||||
range: glyphId
|
||||
});
|
||||
} else {
|
||||
result.individual[glyphId] = initialEntry;
|
||||
}
|
||||
|
||||
return result;
|
||||
let currentEntries: EntryMeta[] = [{
|
||||
entry: initialEntry,
|
||||
substitutions: [table.substitutes[index]]
|
||||
}];
|
||||
|
||||
// We walk forward, then backward
|
||||
for (const coverage of table.lookaheadCoverage) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.backtrackCoverage) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, insert the lookup information
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex: 0,
|
||||
length: 1,
|
||||
contextRange: [
|
||||
-1 * table.backtrackCoverage.length,
|
||||
1 + table.lookaheadCoverage.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,78 +8,78 @@ import { ClassDefTable } from '../tables';
|
||||
* @param glyphId Index of the glyph to look for
|
||||
*/
|
||||
export default function getGlyphClass(table: ClassDefTable, glyphId: number | [number, number]): Map<number | [number, number], number | null> {
|
||||
switch (table.format) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-2
|
||||
case 2:
|
||||
if (Array.isArray(glyphId)) {
|
||||
return getRangeGlyphClass(table, glyphId);
|
||||
} else {
|
||||
return new Map([[
|
||||
glyphId,
|
||||
getIndividualGlyphClass(table, glyphId)
|
||||
]]);
|
||||
}
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-1
|
||||
default:
|
||||
return new Map([[glyphId, null]]);
|
||||
}
|
||||
switch (table.format) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-2
|
||||
case 2:
|
||||
if (Array.isArray(glyphId)) {
|
||||
return getRangeGlyphClass(table, glyphId);
|
||||
} else {
|
||||
return new Map([[
|
||||
glyphId,
|
||||
getIndividualGlyphClass(table, glyphId)
|
||||
]]);
|
||||
}
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-1
|
||||
default:
|
||||
return new Map([[glyphId, null]]);
|
||||
}
|
||||
}
|
||||
|
||||
function getRangeGlyphClass(table: ClassDefTable.Format2, glyphId: [number, number]): Map<number | [number, number], number | null> {
|
||||
let classStart: number = glyphId[0];
|
||||
let currentClass: number | null = getIndividualGlyphClass(table, classStart);
|
||||
let search: number = glyphId[0] + 1;
|
||||
let classStart: number = glyphId[0];
|
||||
let currentClass: number | null = getIndividualGlyphClass(table, classStart);
|
||||
let search: number = glyphId[0] + 1;
|
||||
|
||||
const result = new Map<[number, number] | number, number | null>();
|
||||
const result = new Map<[number, number] | number, number | null>();
|
||||
|
||||
while (search < glyphId[1]) {
|
||||
const clazz = getIndividualGlyphClass(table, search);
|
||||
if (clazz !== currentClass) {
|
||||
if (search - classStart <= 1) {
|
||||
result.set(classStart, currentClass);
|
||||
} else {
|
||||
result.set([classStart, search], currentClass);
|
||||
}
|
||||
}
|
||||
search++;
|
||||
}
|
||||
|
||||
if (search - classStart <= 1) {
|
||||
while (search < glyphId[1]) {
|
||||
const clazz = getIndividualGlyphClass(table, search);
|
||||
if (clazz !== currentClass) {
|
||||
if (search - classStart <= 1) {
|
||||
result.set(classStart, currentClass);
|
||||
} else {
|
||||
} else {
|
||||
result.set([classStart, search], currentClass);
|
||||
}
|
||||
}
|
||||
search++;
|
||||
}
|
||||
|
||||
return result;
|
||||
if (search - classStart <= 1) {
|
||||
result.set(classStart, currentClass);
|
||||
} else {
|
||||
result.set([classStart, search], currentClass);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getIndividualGlyphClass(table: ClassDefTable.Format2, glyphId: number): number | null {
|
||||
for (const range of table.ranges) {
|
||||
if (range.start <= glyphId && range.end >= glyphId) {
|
||||
return range.classId;
|
||||
}
|
||||
for (const range of table.ranges) {
|
||||
if (range.start <= glyphId && range.end >= glyphId) {
|
||||
return range.classId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function listClassGlyphs(table: ClassDefTable, index: number): (number | [number, number])[] {
|
||||
switch (table.format) {
|
||||
case 2:
|
||||
const results: (number | [number, number])[] = [];
|
||||
for (const range of table.ranges) {
|
||||
if (range.classId !== index) {
|
||||
continue;
|
||||
}
|
||||
switch (table.format) {
|
||||
case 2:
|
||||
const results: (number | [number, number])[] = [];
|
||||
for (const range of table.ranges) {
|
||||
if (range.classId !== index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (range.end === range.start) {
|
||||
results.push(range.start);
|
||||
} else {
|
||||
results.push([range.start, range.end + 1]);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
if (range.end === range.start) {
|
||||
results.push(range.start);
|
||||
} else {
|
||||
results.push([range.start, range.end + 1]);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,36 +8,36 @@ import { CoverageTable } from '../tables';
|
||||
* @param glyphId Index of the glyph to look for
|
||||
*/
|
||||
export default function getCoverageGlyphIndex(table: CoverageTable, glyphId: number): number | null {
|
||||
switch (table.format) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-1
|
||||
case 1:
|
||||
const index = table.glyphs.indexOf(glyphId);
|
||||
return index !== -1
|
||||
? index
|
||||
: null;
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-2
|
||||
case 2:
|
||||
const range = table.ranges
|
||||
.find(range => range.start <= glyphId && range.end >= glyphId);
|
||||
return range
|
||||
? range.index
|
||||
: null;
|
||||
}
|
||||
switch (table.format) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-1
|
||||
case 1:
|
||||
const index = table.glyphs.indexOf(glyphId);
|
||||
return index !== -1
|
||||
? index
|
||||
: null;
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-2
|
||||
case 2:
|
||||
const range = table.ranges
|
||||
.find(range => range.start <= glyphId && range.end >= glyphId);
|
||||
return range
|
||||
? range.index
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
export function listGlyphsByIndex(table: CoverageTable): { glyphId: number | [number, number]; index: number; }[] {
|
||||
switch (table.format) {
|
||||
case 1:
|
||||
return table.glyphs.map((glyphId, index) => ({ glyphId, index }));
|
||||
case 2:
|
||||
let results: { glyphId: number | [number, number]; index: number; }[] = [];
|
||||
for (const [index, range] of table.ranges.entries()) {
|
||||
if (range.end === range.start) {
|
||||
results.push({ glyphId: range.start, index });
|
||||
} else {
|
||||
results.push({ glyphId: [range.start, range.end + 1], index });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
switch (table.format) {
|
||||
case 1:
|
||||
return table.glyphs.map((glyphId, index) => ({ glyphId, index }));
|
||||
case 2:
|
||||
let results: { glyphId: number | [number, number]; index: number; }[] = [];
|
||||
for (const [index, range] of table.ranges.entries()) {
|
||||
if (range.end === range.start) {
|
||||
results.push({ glyphId: range.start, index });
|
||||
} else {
|
||||
results.push({ glyphId: [range.start, range.end + 1], index });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,160 +4,160 @@ import { SubstitutionLookupRecord, Lookup } from '../tables';
|
||||
import { getIndividualSubstitutionGlyph, getRangeSubstitutionGlyphs } from './substitution';
|
||||
|
||||
export interface EntryMeta {
|
||||
entry: LookupTreeEntry;
|
||||
substitutions: (number | null)[];
|
||||
entry: LookupTreeEntry;
|
||||
substitutions: (number | null)[];
|
||||
}
|
||||
|
||||
export function processInputPosition(
|
||||
glyphs: (number | [number, number])[],
|
||||
position: number,
|
||||
currentEntries: EntryMeta[],
|
||||
lookupRecords: SubstitutionLookupRecord[],
|
||||
lookups: Lookup[]
|
||||
glyphs: (number | [number, number])[],
|
||||
position: number,
|
||||
currentEntries: EntryMeta[],
|
||||
lookupRecords: SubstitutionLookupRecord[],
|
||||
lookups: Lookup[]
|
||||
): EntryMeta[] {
|
||||
const nextEntries: EntryMeta[] = [];
|
||||
for (const currentEntry of currentEntries) {
|
||||
currentEntry.entry.forward = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
for (const glyph of glyphs) {
|
||||
nextEntries.push(...getInputTree(
|
||||
currentEntry.entry.forward,
|
||||
lookupRecords,
|
||||
lookups,
|
||||
position,
|
||||
glyph
|
||||
).map(({ entry, substitution }) => ({
|
||||
entry,
|
||||
substitutions: [...currentEntry.substitutions, substitution]
|
||||
})));
|
||||
}
|
||||
const nextEntries: EntryMeta[] = [];
|
||||
for (const currentEntry of currentEntries) {
|
||||
currentEntry.entry.forward = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
for (const glyph of glyphs) {
|
||||
nextEntries.push(...getInputTree(
|
||||
currentEntry.entry.forward,
|
||||
lookupRecords,
|
||||
lookups,
|
||||
position,
|
||||
glyph
|
||||
).map(({ entry, substitution }) => ({
|
||||
entry,
|
||||
substitutions: [...currentEntry.substitutions, substitution]
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
return nextEntries;
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
export function processLookaheadPosition(
|
||||
glyphs: (number | [number, number])[],
|
||||
currentEntries: EntryMeta[]
|
||||
glyphs: (number | [number, number])[],
|
||||
currentEntries: EntryMeta[]
|
||||
): EntryMeta[] {
|
||||
const nextEntries: EntryMeta[] = [];
|
||||
for (const currentEntry of currentEntries) {
|
||||
for (const glyph of glyphs) {
|
||||
const entry: LookupTreeEntry = {};
|
||||
if (!currentEntry.entry.forward) {
|
||||
currentEntry.entry.forward = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
}
|
||||
nextEntries.push({
|
||||
entry,
|
||||
substitutions: currentEntry.substitutions
|
||||
});
|
||||
const nextEntries: EntryMeta[] = [];
|
||||
for (const currentEntry of currentEntries) {
|
||||
for (const glyph of glyphs) {
|
||||
const entry: LookupTreeEntry = {};
|
||||
if (!currentEntry.entry.forward) {
|
||||
currentEntry.entry.forward = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
}
|
||||
nextEntries.push({
|
||||
entry,
|
||||
substitutions: currentEntry.substitutions
|
||||
});
|
||||
|
||||
if (Array.isArray(glyph)) {
|
||||
currentEntry.entry.forward.range.push({
|
||||
entry,
|
||||
range: glyph
|
||||
});
|
||||
} else {
|
||||
currentEntry.entry.forward.individual[glyph] = entry;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(glyph)) {
|
||||
currentEntry.entry.forward.range.push({
|
||||
entry,
|
||||
range: glyph
|
||||
});
|
||||
} else {
|
||||
currentEntry.entry.forward.individual[glyph] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextEntries;
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
export function processBacktrackPosition(
|
||||
glyphs: (number | [number, number])[],
|
||||
currentEntries: EntryMeta[]
|
||||
glyphs: (number | [number, number])[],
|
||||
currentEntries: EntryMeta[]
|
||||
): EntryMeta[] {
|
||||
const nextEntries: EntryMeta[] = [];
|
||||
for (const currentEntry of currentEntries) {
|
||||
for (const glyph of glyphs) {
|
||||
const entry: LookupTreeEntry = {};
|
||||
if (!currentEntry.entry.reverse) {
|
||||
currentEntry.entry.reverse = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
}
|
||||
nextEntries.push({
|
||||
entry,
|
||||
substitutions: currentEntry.substitutions
|
||||
});
|
||||
const nextEntries: EntryMeta[] = [];
|
||||
for (const currentEntry of currentEntries) {
|
||||
for (const glyph of glyphs) {
|
||||
const entry: LookupTreeEntry = {};
|
||||
if (!currentEntry.entry.reverse) {
|
||||
currentEntry.entry.reverse = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
}
|
||||
nextEntries.push({
|
||||
entry,
|
||||
substitutions: currentEntry.substitutions
|
||||
});
|
||||
|
||||
if (Array.isArray(glyph)) {
|
||||
currentEntry.entry.reverse.range.push({
|
||||
entry,
|
||||
range: glyph
|
||||
});
|
||||
} else {
|
||||
currentEntry.entry.reverse.individual[glyph] = entry;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(glyph)) {
|
||||
currentEntry.entry.reverse.range.push({
|
||||
entry,
|
||||
range: glyph
|
||||
});
|
||||
} else {
|
||||
currentEntry.entry.reverse.individual[glyph] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextEntries;
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
export function getInputTree(tree: LookupTree, substitutions: SubstitutionLookupRecord[], lookups: Lookup[], inputIndex: number, glyphId: number | [number, number]): { entry: LookupTreeEntry; substitution: number | null; }[] {
|
||||
const result: { entry: LookupTreeEntry; substitution: number | null; }[] = [];
|
||||
if (!Array.isArray(glyphId)) {
|
||||
tree.individual[glyphId] = {};
|
||||
result.push({
|
||||
entry: tree.individual[glyphId],
|
||||
substitution: getSubstitutionAtPosition(substitutions, lookups, inputIndex, glyphId)
|
||||
});
|
||||
} else {
|
||||
const subs = getSubstitutionAtPositionRange(substitutions, lookups, inputIndex, glyphId);
|
||||
for (const [range, substitution] of subs) {
|
||||
const entry: LookupTreeEntry = {};
|
||||
if (Array.isArray(range)) {
|
||||
tree.range.push({ range, entry });
|
||||
} else {
|
||||
tree.individual[range] = {};
|
||||
}
|
||||
result.push({ entry, substitution });
|
||||
}
|
||||
const result: { entry: LookupTreeEntry; substitution: number | null; }[] = [];
|
||||
if (!Array.isArray(glyphId)) {
|
||||
tree.individual[glyphId] = {};
|
||||
result.push({
|
||||
entry: tree.individual[glyphId],
|
||||
substitution: getSubstitutionAtPosition(substitutions, lookups, inputIndex, glyphId)
|
||||
});
|
||||
} else {
|
||||
const subs = getSubstitutionAtPositionRange(substitutions, lookups, inputIndex, glyphId);
|
||||
for (const [range, substitution] of subs) {
|
||||
const entry: LookupTreeEntry = {};
|
||||
if (Array.isArray(range)) {
|
||||
tree.range.push({ range, entry });
|
||||
} else {
|
||||
tree.individual[range] = {};
|
||||
}
|
||||
result.push({ entry, substitution });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSubstitutionAtPositionRange(substitutions: SubstitutionLookupRecord[], lookups: Lookup[], index: number, range: [number, number]): Map<number | [number, number], number | null> {
|
||||
for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) {
|
||||
for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) {
|
||||
const sub = getRangeSubstitutionGlyphs(
|
||||
substitutionTable,
|
||||
range
|
||||
);
|
||||
for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) {
|
||||
for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) {
|
||||
const sub = getRangeSubstitutionGlyphs(
|
||||
substitutionTable,
|
||||
range
|
||||
);
|
||||
|
||||
if (!Array.from(sub.values()).every(val => val !== null)) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
if (!Array.from(sub.values()).every(val => val !== null)) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Map([[range, null]]);
|
||||
return new Map([[range, null]]);
|
||||
}
|
||||
|
||||
function getSubstitutionAtPosition(substitutions: SubstitutionLookupRecord[], lookups: Lookup[], index: number, glyphId: number): number | null {
|
||||
for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) {
|
||||
for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) {
|
||||
const sub = getIndividualSubstitutionGlyph(
|
||||
substitutionTable,
|
||||
glyphId
|
||||
);
|
||||
for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) {
|
||||
for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) {
|
||||
const sub = getIndividualSubstitutionGlyph(
|
||||
substitutionTable,
|
||||
glyphId
|
||||
);
|
||||
|
||||
if (sub !== null) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
if (sub !== null) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,53 +10,53 @@ import getCoverageGlyphIndex from './coverage';
|
||||
* @param glyphId The index of the glpyh to find substitutions for
|
||||
*/
|
||||
export function getRangeSubstitutionGlyphs(table: SubstitutionTable, glyphId: [number, number]): Map<[number, number] | number, number | null> {
|
||||
let replacementStart: number = glyphId[0];
|
||||
let currentReplacement: number | null = getIndividualSubstitutionGlyph(table, replacementStart);
|
||||
let search: number = glyphId[0] + 1;
|
||||
let replacementStart: number = glyphId[0];
|
||||
let currentReplacement: number | null = getIndividualSubstitutionGlyph(table, replacementStart);
|
||||
let search: number = glyphId[0] + 1;
|
||||
|
||||
const result = new Map<[number, number] | number, number | null>();
|
||||
const result = new Map<[number, number] | number, number | null>();
|
||||
|
||||
while (search < glyphId[1]) {
|
||||
const sub = getIndividualSubstitutionGlyph(table, search);
|
||||
if (sub !== currentReplacement) {
|
||||
if (search - replacementStart <= 1) {
|
||||
result.set(replacementStart, currentReplacement);
|
||||
} else {
|
||||
result.set([replacementStart, search], currentReplacement);
|
||||
}
|
||||
}
|
||||
|
||||
search++;
|
||||
}
|
||||
|
||||
if (search - replacementStart <= 1) {
|
||||
while (search < glyphId[1]) {
|
||||
const sub = getIndividualSubstitutionGlyph(table, search);
|
||||
if (sub !== currentReplacement) {
|
||||
if (search - replacementStart <= 1) {
|
||||
result.set(replacementStart, currentReplacement);
|
||||
} else {
|
||||
} else {
|
||||
result.set([replacementStart, search], currentReplacement);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
search++;
|
||||
}
|
||||
|
||||
if (search - replacementStart <= 1) {
|
||||
result.set(replacementStart, currentReplacement);
|
||||
} else {
|
||||
result.set([replacementStart, search], currentReplacement);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId: number): number | null {
|
||||
const coverageIndex = getCoverageGlyphIndex(table.coverage, glyphId);
|
||||
const coverageIndex = getCoverageGlyphIndex(table.coverage, glyphId);
|
||||
|
||||
// istanbul ignore next - invalid font
|
||||
if (coverageIndex === null) {
|
||||
return null;
|
||||
}
|
||||
// istanbul ignore next - invalid font
|
||||
if (coverageIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (table.substFormat) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#11-single-substitution-format-1
|
||||
case 1:
|
||||
// TODO: determine if there's a rhyme or reason to the 16-bit
|
||||
// wraparound and if it can ever be a different number
|
||||
return (glyphId + table.deltaGlyphId) % (2 ** 16);
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2
|
||||
case 2:
|
||||
// tslint:disable-next-line
|
||||
return table.substitute[coverageIndex] != null
|
||||
? table.substitute[coverageIndex]
|
||||
: null;
|
||||
}
|
||||
switch (table.substFormat) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#11-single-substitution-format-1
|
||||
case 1:
|
||||
// TODO: determine if there's a rhyme or reason to the 16-bit
|
||||
// wraparound and if it can ever be a different number
|
||||
return (glyphId + table.deltaGlyphId) % (2 ** 16);
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2
|
||||
case 2:
|
||||
// tslint:disable-next-line
|
||||
return table.substitute[coverageIndex] != null
|
||||
? table.substitute[coverageIndex]
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +1,112 @@
|
||||
export type SubstitutionTable = SubstitutionTable.Format1 | SubstitutionTable.Format2;
|
||||
export namespace SubstitutionTable {
|
||||
export interface Format1 {
|
||||
substFormat: 1;
|
||||
coverage: CoverageTable;
|
||||
deltaGlyphId: number;
|
||||
}
|
||||
export interface Format1 {
|
||||
substFormat: 1;
|
||||
coverage: CoverageTable;
|
||||
deltaGlyphId: number;
|
||||
}
|
||||
|
||||
export interface Format2 {
|
||||
substFormat: 2;
|
||||
coverage: CoverageTable;
|
||||
substitute: number[];
|
||||
}
|
||||
export interface Format2 {
|
||||
substFormat: 2;
|
||||
coverage: CoverageTable;
|
||||
substitute: number[];
|
||||
}
|
||||
}
|
||||
|
||||
export type CoverageTable = CoverageTable.Format1 | CoverageTable.Format2;
|
||||
export namespace CoverageTable {
|
||||
export interface Format1 {
|
||||
format: 1;
|
||||
glyphs: number[];
|
||||
}
|
||||
export interface Format1 {
|
||||
format: 1;
|
||||
glyphs: number[];
|
||||
}
|
||||
|
||||
export interface Format2 {
|
||||
format: 2;
|
||||
ranges: {
|
||||
start: number;
|
||||
end: number;
|
||||
index: number;
|
||||
}[];
|
||||
}
|
||||
export interface Format2 {
|
||||
format: 2;
|
||||
ranges: {
|
||||
start: number;
|
||||
end: number;
|
||||
index: number;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
export type ChainingContextualSubstitutionTable = ChainingContextualSubstitutionTable.Format1 |
|
||||
ChainingContextualSubstitutionTable.Format2 | ChainingContextualSubstitutionTable.Format3;
|
||||
ChainingContextualSubstitutionTable.Format2 | ChainingContextualSubstitutionTable.Format3;
|
||||
export namespace ChainingContextualSubstitutionTable {
|
||||
export interface Format1 {
|
||||
substFormat: 1;
|
||||
coverage: CoverageTable;
|
||||
chainRuleSets: ChainSubRuleTable[][];
|
||||
}
|
||||
export interface Format1 {
|
||||
substFormat: 1;
|
||||
coverage: CoverageTable;
|
||||
chainRuleSets: ChainSubRuleTable[][];
|
||||
}
|
||||
|
||||
export interface Format2 {
|
||||
substFormat: 2;
|
||||
coverage: CoverageTable;
|
||||
backtrackClassDef: ClassDefTable;
|
||||
inputClassDef: ClassDefTable;
|
||||
lookaheadClassDef: ClassDefTable;
|
||||
chainClassSet: (null | ChainSubClassRuleTable[])[];
|
||||
}
|
||||
export interface Format2 {
|
||||
substFormat: 2;
|
||||
coverage: CoverageTable;
|
||||
backtrackClassDef: ClassDefTable;
|
||||
inputClassDef: ClassDefTable;
|
||||
lookaheadClassDef: ClassDefTable;
|
||||
chainClassSet: (null | ChainSubClassRuleTable[])[];
|
||||
}
|
||||
|
||||
export interface Format3 {
|
||||
substFormat: 3;
|
||||
backtrackCoverage: CoverageTable[];
|
||||
inputCoverage: CoverageTable[];
|
||||
lookaheadCoverage: CoverageTable[];
|
||||
lookupRecords: SubstitutionLookupRecord[];
|
||||
}
|
||||
export interface Format3 {
|
||||
substFormat: 3;
|
||||
backtrackCoverage: CoverageTable[];
|
||||
inputCoverage: CoverageTable[];
|
||||
lookaheadCoverage: CoverageTable[];
|
||||
lookupRecords: SubstitutionLookupRecord[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReverseChainingContextualSingleSubstitutionTable {
|
||||
substFormat: 1;
|
||||
coverage: CoverageTable;
|
||||
backtrackCoverage: CoverageTable[];
|
||||
lookaheadCoverage: CoverageTable[];
|
||||
substitutes: number[];
|
||||
substFormat: 1;
|
||||
coverage: CoverageTable;
|
||||
backtrackCoverage: CoverageTable[];
|
||||
lookaheadCoverage: CoverageTable[];
|
||||
substitutes: number[];
|
||||
}
|
||||
|
||||
export type ClassDefTable = ClassDefTable.Format2;
|
||||
export namespace ClassDefTable {
|
||||
export interface Format2 {
|
||||
format: 2;
|
||||
ranges: {
|
||||
start: number;
|
||||
end: number;
|
||||
classId: number;
|
||||
}[];
|
||||
}
|
||||
export interface Format2 {
|
||||
format: 2;
|
||||
ranges: {
|
||||
start: number;
|
||||
end: number;
|
||||
classId: number;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface SubstitutionLookupRecord {
|
||||
sequenceIndex: number;
|
||||
lookupListIndex: number;
|
||||
sequenceIndex: number;
|
||||
lookupListIndex: number;
|
||||
}
|
||||
|
||||
export type ChainSubRuleTable = ChainSubClassRuleTable;
|
||||
export interface ChainSubClassRuleTable {
|
||||
backtrack: number[];
|
||||
input: number[];
|
||||
lookahead: number[];
|
||||
lookupRecords: SubstitutionLookupRecord[];
|
||||
backtrack: number[];
|
||||
input: number[];
|
||||
lookahead: number[];
|
||||
lookupRecords: SubstitutionLookupRecord[];
|
||||
}
|
||||
|
||||
export type Lookup = Lookup.Type1 | Lookup.Type6 | Lookup.Type8;
|
||||
export namespace Lookup {
|
||||
export interface Type1 {
|
||||
lookupType: 1;
|
||||
lookupFlag: number;
|
||||
subtables: SubstitutionTable[];
|
||||
}
|
||||
export interface Type1 {
|
||||
lookupType: 1;
|
||||
lookupFlag: number;
|
||||
subtables: SubstitutionTable[];
|
||||
}
|
||||
|
||||
export interface Type6 {
|
||||
lookupType: 6;
|
||||
lookupFlag: number;
|
||||
subtables: ChainingContextualSubstitutionTable[];
|
||||
}
|
||||
export interface Type6 {
|
||||
lookupType: 6;
|
||||
lookupFlag: number;
|
||||
subtables: ChainingContextualSubstitutionTable[];
|
||||
}
|
||||
|
||||
export interface Type8 {
|
||||
lookupType: 8;
|
||||
lookupFlag: number;
|
||||
subtables: ReverseChainingContextualSingleSubstitutionTable[];
|
||||
}
|
||||
export interface Type8 {
|
||||
lookupType: 8;
|
||||
lookupFlag: number;
|
||||
subtables: ReverseChainingContextualSingleSubstitutionTable[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
export interface SubstitutionResult {
|
||||
index: number;
|
||||
contextRange: [number, number];
|
||||
index: number;
|
||||
contextRange: [number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about ligatures found in a sequence of text
|
||||
*/
|
||||
export interface LigatureData {
|
||||
/**
|
||||
* The list of font glyphs in the input text.
|
||||
*/
|
||||
inputGlyphs: number[];
|
||||
/**
|
||||
* The list of font glyphs in the input text.
|
||||
*/
|
||||
inputGlyphs: number[];
|
||||
|
||||
/**
|
||||
* The list of font glyphs after performing replacements for font ligatures.
|
||||
*/
|
||||
outputGlyphs: number[];
|
||||
/**
|
||||
* The list of font glyphs after performing replacements for font ligatures.
|
||||
*/
|
||||
outputGlyphs: number[];
|
||||
|
||||
/**
|
||||
* Sorted array of ranges that must be rendered together to produce the
|
||||
* ligatures in the output sequence. The ranges are inclusive on the left and
|
||||
* exclusive on the right.
|
||||
*/
|
||||
contextRanges: [number, number][];
|
||||
/**
|
||||
* Sorted array of ranges that must be rendered together to produce the
|
||||
* ligatures in the output sequence. The ranges are inclusive on the left and
|
||||
* exclusive on the right.
|
||||
*/
|
||||
contextRanges: [number, number][];
|
||||
}
|
||||
|
||||
export interface Font {
|
||||
/**
|
||||
* Scans the provided text for font ligatures, returning an object with
|
||||
* metadata about the text and any ligatures found.
|
||||
*
|
||||
* @param text String to search for ligatures
|
||||
*/
|
||||
findLigatures(text: string): LigatureData;
|
||||
/**
|
||||
* Scans the provided text for font ligatures, returning an object with
|
||||
* metadata about the text and any ligatures found.
|
||||
*
|
||||
* @param text String to search for ligatures
|
||||
*/
|
||||
findLigatures(text: string): LigatureData;
|
||||
|
||||
/**
|
||||
* Scans the provided text for font ligatures, returning an array of ranges
|
||||
* where ligatures are located.
|
||||
*
|
||||
* @param text String to search for ligatures
|
||||
*/
|
||||
findLigatureRanges(text: string): [number, number][];
|
||||
/**
|
||||
* Scans the provided text for font ligatures, returning an array of ranges
|
||||
* where ligatures are located.
|
||||
*
|
||||
* @param text String to search for ligatures
|
||||
*/
|
||||
findLigatureRanges(text: string): [number, number][];
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
/**
|
||||
* Optional size of previous results to store, measured in total number of
|
||||
* characters from input strings. Defaults to no cache (0)
|
||||
*/
|
||||
cacheSize?: number;
|
||||
/**
|
||||
* Optional size of previous results to store, measured in total number of
|
||||
* characters from input strings. Defaults to no cache (0)
|
||||
*/
|
||||
cacheSize?: number;
|
||||
}
|
||||
|
||||
export interface LookupTree {
|
||||
individual: {
|
||||
[glyphId: string]: LookupTreeEntry;
|
||||
};
|
||||
range: {
|
||||
range: [number, number];
|
||||
entry: LookupTreeEntry;
|
||||
}[];
|
||||
individual: {
|
||||
[glyphId: string]: LookupTreeEntry;
|
||||
};
|
||||
range: {
|
||||
range: [number, number];
|
||||
entry: LookupTreeEntry;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface LookupTreeEntry {
|
||||
lookup?: LookupResult;
|
||||
forward?: LookupTree;
|
||||
reverse?: LookupTree;
|
||||
lookup?: LookupResult;
|
||||
forward?: LookupTree;
|
||||
reverse?: LookupTree;
|
||||
}
|
||||
|
||||
export interface LookupResult {
|
||||
substitutions: (number | null)[];
|
||||
length: number;
|
||||
index: number;
|
||||
subIndex: number;
|
||||
contextRange: [number, number];
|
||||
substitutions: (number | null)[];
|
||||
length: number;
|
||||
index: number;
|
||||
subIndex: number;
|
||||
contextRange: [number, number];
|
||||
}
|
||||
|
||||
export interface FlattenedLookupTree {
|
||||
[glyphId: string]: FlattenedLookupTreeEntry;
|
||||
[glyphId: string]: FlattenedLookupTreeEntry;
|
||||
}
|
||||
|
||||
export interface FlattenedLookupTreeEntry {
|
||||
lookup?: LookupResult;
|
||||
forward?: FlattenedLookupTree;
|
||||
reverse?: FlattenedLookupTree;
|
||||
lookup?: LookupResult;
|
||||
forward?: FlattenedLookupTree;
|
||||
reverse?: FlattenedLookupTree;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import { FlattenedLookupTree, LookupResult } from './types';
|
||||
|
||||
export default function walkTree(tree: FlattenedLookupTree, sequence: number[], startIndex: number, index: number): LookupResult | undefined {
|
||||
const glyphId = sequence[index];
|
||||
let subtree = tree[glyphId];
|
||||
if (!subtree) {
|
||||
return undefined;
|
||||
}
|
||||
const glyphId = sequence[index];
|
||||
let subtree = tree[glyphId];
|
||||
if (!subtree) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let lookup = subtree.lookup;
|
||||
if (subtree.reverse) {
|
||||
const reverseLookup = walkReverse(subtree.reverse, sequence, startIndex);
|
||||
|
||||
if (
|
||||
(!lookup && reverseLookup) ||
|
||||
(
|
||||
reverseLookup && lookup && (
|
||||
lookup.index > reverseLookup.index ||
|
||||
(lookup.index === reverseLookup.index && lookup.subIndex > reverseLookup.subIndex)
|
||||
)
|
||||
)
|
||||
) {
|
||||
lookup = reverseLookup;
|
||||
}
|
||||
}
|
||||
|
||||
if (++index >= sequence.length || !subtree.forward) {
|
||||
return lookup;
|
||||
}
|
||||
|
||||
const forwardLookup = walkTree(subtree.forward, sequence, startIndex, index);
|
||||
let lookup = subtree.lookup;
|
||||
if (subtree.reverse) {
|
||||
const reverseLookup = walkReverse(subtree.reverse, sequence, startIndex);
|
||||
|
||||
if (
|
||||
(!lookup && forwardLookup) ||
|
||||
(
|
||||
forwardLookup && lookup && (
|
||||
lookup.index > forwardLookup.index ||
|
||||
(lookup.index === forwardLookup.index && lookup.subIndex > forwardLookup.subIndex)
|
||||
)
|
||||
(!lookup && reverseLookup) ||
|
||||
(
|
||||
reverseLookup && lookup && (
|
||||
lookup.index > reverseLookup.index ||
|
||||
(lookup.index === reverseLookup.index && lookup.subIndex > reverseLookup.subIndex)
|
||||
)
|
||||
)
|
||||
) {
|
||||
lookup = forwardLookup;
|
||||
lookup = reverseLookup;
|
||||
}
|
||||
}
|
||||
|
||||
if (++index >= sequence.length || !subtree.forward) {
|
||||
return lookup;
|
||||
}
|
||||
|
||||
const forwardLookup = walkTree(subtree.forward, sequence, startIndex, index);
|
||||
|
||||
if (
|
||||
(!lookup && forwardLookup) ||
|
||||
(
|
||||
forwardLookup && lookup && (
|
||||
lookup.index > forwardLookup.index ||
|
||||
(lookup.index === forwardLookup.index && lookup.subIndex > forwardLookup.subIndex)
|
||||
)
|
||||
)
|
||||
) {
|
||||
lookup = forwardLookup;
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function walkReverse(tree: FlattenedLookupTree, sequence: number[], index: number): LookupResult | undefined {
|
||||
let subtree = tree[sequence[--index]];
|
||||
let lookup: LookupResult | undefined = subtree && subtree.lookup;
|
||||
while (subtree) {
|
||||
if (
|
||||
(!lookup && subtree.lookup) ||
|
||||
(subtree.lookup && lookup && lookup.index > subtree.lookup.index)
|
||||
) {
|
||||
lookup = subtree.lookup;
|
||||
}
|
||||
|
||||
if (--index < 0 || !subtree.reverse) {
|
||||
break;
|
||||
}
|
||||
|
||||
subtree = subtree.reverse[sequence[index]];
|
||||
let subtree = tree[sequence[--index]];
|
||||
let lookup: LookupResult | undefined = subtree && subtree.lookup;
|
||||
while (subtree) {
|
||||
if (
|
||||
(!lookup && subtree.lookup) ||
|
||||
(subtree.lookup && lookup && lookup.index > subtree.lookup.index)
|
||||
) {
|
||||
lookup = subtree.lookup;
|
||||
}
|
||||
|
||||
return lookup;
|
||||
if (--index < 0 || !subtree.reverse) {
|
||||
break;
|
||||
}
|
||||
|
||||
subtree = subtree.reverse[sequence[index]];
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user