mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
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 };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user