2 space indent

This commit is contained in:
Daniel Imms
2026-01-03 09:25:39 -08:00
parent 21b8f77afb
commit ec03109c39
15 changed files with 1196 additions and 1196 deletions
@@ -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;
}
+210 -210
View File
@@ -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 };
File diff suppressed because it is too large Load Diff
@@ -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;
}