Socket
Socket
Sign inDemoInstall

opentype.js

Package Overview
Dependencies
2
Maintainers
2
Versions
47
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.12.0 to 1.0.0

src/features/applySubstitution.js

2

bower.json
{
"name": "opentype.js",
"version": "0.12.0",
"version": "1.0.0",
"main": "dist/opentype.js",

@@ -5,0 +5,0 @@ "keywords": [

{
"name": "opentype.js",
"description": "OpenType font parser",
"version": "0.12.0",
"version": "1.0.0",
"author": {

@@ -6,0 +6,0 @@ "name": "Frederik De Bleser",

@@ -0,4 +1,37 @@

1.0.0 (April 17, 2019)
* Render arabic rtl text properly (PR #361, partial fix of #364) (thanks @solomancode!)
* #361 introduced a breaking change to `Font.prototype.defaultRenderOptions`
Before
```js
Font.prototype.defaultRenderOptions = {
kerning: true,
features: {
liga: true,
rlig: true
}
};
```
Now
```js
Font.prototype.defaultRenderOptions = {
kerning: true,
features: [
/**
* these 4 features are required to render Arabic text properly
* and shouldn't be turned off when rendering arabic text.
*/
{ script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
{ script: 'latn', tags: ['liga', 'rlig'] }
]
};
```
Also as this project is now using SemVer, the breaking change required a new major version, 1.0.0!
0.12.0 (April 17, 2019)
=====================
* Fix Glyph.getPath() issue (PR #362, fixes #363) (thanks @solomancode!)
* Add lowMemory mode (PR #329) (thanks @debussy2k!)
* Update README (PR #377) (thanks @jolg42!)

@@ -5,0 +38,0 @@

@@ -7,2 +7,3 @@ /**

import Tokenizer from './tokenizer';
import FeatureQuery from './features/featureQuery';
import arabicWordCheck from './features/arab/contextCheck/arabicWord';

@@ -12,2 +13,4 @@ import arabicSentenceCheck from './features/arab/contextCheck/arabicSentence';

import arabicRequiredLigatures from './features/arab/arabicRequiredLigatures';
import latinWordCheck from './features/latn/contextCheck/latinWord';
import latinLigature from './features/latn/latinLigatures';

@@ -21,3 +24,3 @@ /**

this.tokenizer = new Tokenizer();
this.features = [];
this.featuresTags = {};
}

@@ -39,2 +42,3 @@

Bidi.prototype.contextChecks = ({
latinWordCheck,
arabicWordCheck,

@@ -47,8 +51,6 @@ arabicSentenceCheck

*/
function registerArabicWordCheck() {
const checks = this.contextChecks.arabicWordCheck;
function registerContextChecker(checkId) {
const check = this.contextChecks[`${checkId}Check`];
return this.tokenizer.registerContextChecker(
'arabicWord',
checks.arabicWordStartCheck,
checks.arabicWordEndCheck
checkId, check.startCheck, check.endCheck
);

@@ -58,14 +60,2 @@ }

/**
* Register arabic sentence check
*/
function registerArabicSentenceCheck() {
const checks = this.contextChecks.arabicSentenceCheck;
return this.tokenizer.registerContextChecker(
'arabicSentence',
checks.arabicSentenceStartCheck,
checks.arabicSentenceEndCheck
);
}
/**
* Perform pre tokenization procedure then

@@ -75,4 +65,5 @@ * tokenize text input

function tokenizeText() {
registerArabicWordCheck.call(this);
registerArabicSentenceCheck.call(this);
registerContextChecker.call(this, 'latinWord');
registerContextChecker.call(this, 'arabicWord');
registerContextChecker.call(this, 'arabicSentence');
return this.tokenizer.tokenize(this.text);

@@ -98,31 +89,33 @@ }

/**
* Subscribe arabic presentation form features
* @param {feature} feature a feature to apply
* Register supported features tags
* @param {script} script script tag
* @param {Array} tags features tags list
*/
Bidi.prototype.subscribeArabicForms = function(feature) {
this.tokenizer.events.contextEnd.subscribe(
(contextName, range) => {
if (contextName === 'arabicWord') {
return arabicPresentationForms.call(
this.tokenizer, range, feature
);
}
}
Bidi.prototype.registerFeatures = function (script, tags) {
const supportedTags = tags.filter(
tag => this.query.supports({script, tag})
);
if (!this.featuresTags.hasOwnProperty(script)) {
this.featuresTags[script] = supportedTags;
} else {
this.featuresTags[script] =
this.featuresTags[script].concat(supportedTags);
}
};
/**
* Apply Gsub features
* @param {feature} features a list of features
* Apply GSUB features
* @param {Array} tagsList a list of features tags
* @param {string} script a script tag
* @param {Font} font opentype font instance
*/
Bidi.prototype.applyFeatures = function (features) {
for (let i = 0; i < features.length; i++) {
const feature = features[i];
if (feature) {
const script = feature.script;
if (!this.features[script]) {
this.features[script] = {};
}
this.features[script][feature.tag] = feature;
}
Bidi.prototype.applyFeatures = function (font, features) {
if (!font) throw new Error(
'No valid font was provided to apply features'
);
if (!this.query) this.query = new FeatureQuery(font);
for (let f = 0; f < features.length; f++) {
const feature = features[f];
if (!this.query.supports({script: feature.script})) continue;
this.registerFeatures(feature.script, feature.tags);
}

@@ -157,3 +150,4 @@ };

function applyArabicPresentationForms() {
if (!this.features.hasOwnProperty('arab')) return;
const script = 'arab';
if (!this.featuresTags.hasOwnProperty(script)) return;
checkGlyphIndexStatus.call(this);

@@ -170,4 +164,6 @@ const ranges = this.tokenizer.getContextRanges('arabicWord');

function applyArabicRequireLigatures() {
if (!this.features.hasOwnProperty('arab')) return;
if (!this.features.arab.hasOwnProperty('rlig')) return;
const script = 'arab';
if (!this.featuresTags.hasOwnProperty(script)) return;
const tags = this.featuresTags[script];
if (tags.indexOf('rlig') === -1) return;
checkGlyphIndexStatus.call(this);

@@ -181,2 +177,41 @@ const ranges = this.tokenizer.getContextRanges('arabicWord');

/**
* Apply required arabic ligatures
*/
function applyLatinLigatures() {
const script = 'latn';
if (!this.featuresTags.hasOwnProperty(script)) return;
const tags = this.featuresTags[script];
if (tags.indexOf('liga') === -1) return;
checkGlyphIndexStatus.call(this);
const ranges = this.tokenizer.getContextRanges('latinWord');
ranges.forEach(range => {
latinLigature.call(this, range);
});
}
/**
* Check if a context is registered
* @param {string} contextId context id
*/
Bidi.prototype.checkContextReady = function (contextId) {
return !!this.tokenizer.getContext(contextId);
};
/**
* Apply features to registered contexts
*/
Bidi.prototype.applyFeaturesToContexts = function () {
if (this.checkContextReady('arabicWord')) {
applyArabicPresentationForms.call(this);
applyArabicRequireLigatures.call(this);
}
if (this.checkContextReady('latinWord')) {
applyLatinLigatures.call(this);
}
if (this.checkContextReady('arabicSentence')) {
reverseArabicSentences.call(this);
}
};
/**
* process text input

@@ -189,5 +224,3 @@ * @param {string} text an input text

tokenizeText.call(this);
applyArabicPresentationForms.call(this);
applyArabicRequireLigatures.call(this);
reverseArabicSentences.call(this);
this.applyFeaturesToContexts();
}

@@ -194,0 +227,0 @@ };

@@ -30,2 +30,10 @@ // ╭─┄┄┄────────────────────────┄─────────────────────────────────────────────╮

/**
* Check if a char is Latin
* @param {string} c a single char
*/
export function isLatinChar(c) {
return /[A-z]/.test(c);
}
/**
* Check if a char is whitespace char

@@ -32,0 +40,0 @@ * @param {string} c a single char

@@ -7,2 +7,5 @@ /**

import { isIsolatedArabicChar, isTashkeelArabicChar } from '../../char';
import { SubstitutionAction } from '../featureQuery';
import applySubstitution from '../applySubstitution';
/**

@@ -43,40 +46,36 @@ * Check if a char can be connected to it's preceding char

function arabicPresentationForms(range) {
const features = this.features.arab;
const rangeTokens = this.tokenizer.getRangeTokens(range);
if (rangeTokens.length === 1) return;
const getSubstitutionIndex = substitution => (
substitution.length === 1 &&
substitution[0].id === 12 &&
substitution[0].substitution
);
const applyForm = (tag, token, params) => {
if (!features.hasOwnProperty(tag)) return;
let substitution = features[tag].lookup(params) || null;
let substIndex = getSubstitutionIndex(substitution)[0];
if (substIndex >= 0) {
return token.setState(tag, substIndex);
}
};
const tokensParams = new ContextParams(rangeTokens, 0);
const charContextParams = new ContextParams(rangeTokens.map(t=>t.char), 0);
rangeTokens.forEach((token, i) => {
const script = 'arab';
const tags = this.featuresTags[script];
const tokens = this.tokenizer.getRangeTokens(range);
if (tokens.length === 1) return;
let contextParams = new ContextParams(
tokens.map(token => token.getState('glyphIndex')
), 0);
const charContextParams = new ContextParams(
tokens.map(token => token.char
), 0);
tokens.forEach((token, index) => {
if (isTashkeelArabicChar(token.char)) return;
tokensParams.setCurrentIndex(i);
charContextParams.setCurrentIndex(i);
contextParams.setCurrentIndex(index);
charContextParams.setCurrentIndex(index);
let CONNECT = 0; // 2 bits 00 (10: can connect next) (01: can connect prev)
if (willConnectPrev(charContextParams)) CONNECT |= 1;
if (willConnectNext(charContextParams)) CONNECT |= 2;
let tag;
switch (CONNECT) {
case 0: // isolated * original form
return;
case 1: // fina
applyForm('fina', token, tokensParams);
break;
case 2: // init
applyForm('init', token, tokensParams);
break;
case 3: // medi
applyForm('medi', token, tokensParams);
break;
case 1: (tag = 'fina'); break;
case 2: (tag = 'init'); break;
case 3: (tag = 'medi'); break;
}
if (tags.indexOf(tag) === -1) return;
let substitutions = this.query.lookupFeature({
tag, script, contextParams
});
if (substitutions instanceof Error) return console.info(substitutions.message);
substitutions.forEach((action, index) => {
if (action instanceof SubstitutionAction) {
applySubstitution(action, tokens, index);
contextParams.context[index] = action.substitution;
}
});
});

@@ -83,0 +82,0 @@ }

@@ -6,4 +6,15 @@ /**

import { ContextParams } from '../../tokenizer';
import applySubstitution from '../applySubstitution';
/**
* Update context params
* @param {any} tokens a list of tokens
* @param {number} index current item index
*/
function getContextParams(tokens, index) {
const context = tokens.map(token => token.activeState.value);
return new ContextParams(context, index || 0);
}
/**
* Apply Arabic required ligatures to a context range

@@ -13,38 +24,17 @@ * @param {ContextRange} range a range of tokens

function arabicRequiredLigatures(range) {
const features = this.features.arab;
if (!features.hasOwnProperty('rlig')) return;
const script = 'arab';
let tokens = this.tokenizer.getRangeTokens(range);
for (let i = 0; i < tokens.length; i++) {
const lookupParams = new ContextParams(tokens, i);
let substitution = features.rlig.lookup(lookupParams) || null;
const chainingContext = (
substitution.length === 1 &&
substitution[0].id === 63 &&
substitution[0].substitution
);
const ligature = (
substitution.length === 1 &&
substitution[0].id === 41 &&
substitution[0].substitution[0]
);
const token = tokens[i];
if (!!ligature) {
token.setState('rlig', [ligature.ligGlyph]);
for (let c = 0; c < ligature.components.length; c++) {
const component = ligature.components[c];
const lookaheadToken = lookupParams.get(c + 1);
if (lookaheadToken.activeState.value === component) {
lookaheadToken.state.deleted = true;
}
}
} else if (chainingContext) {
const substIndex = (
chainingContext &&
chainingContext.length === 1 &&
chainingContext[0].id === 12 &&
chainingContext[0].substitution
let contextParams = getContextParams(tokens);
contextParams.context.forEach((glyphIndex, index) => {
contextParams.setCurrentIndex(index);
let substitutions = this.query.lookupFeature({
tag: 'rlig', script, contextParams
});
if (substitutions.length) {
substitutions.forEach(
action => applySubstitution(action, tokens, index)
);
if (!!substIndex && substIndex >= 0) token.setState('rlig', substIndex);
contextParams = getContextParams(tokens);
}
}
});
}

@@ -51,0 +41,0 @@

@@ -40,3 +40,5 @@ /**

export { arabicSentenceStartCheck, arabicSentenceEndCheck };
export default { arabicSentenceStartCheck, arabicSentenceEndCheck };
export default {
startCheck: arabicSentenceStartCheck,
endCheck: arabicSentenceEndCheck
};

@@ -28,3 +28,5 @@ /**

export { arabicWordStartCheck, arabicWordEndCheck };
export default { arabicWordStartCheck, arabicWordEndCheck };
export default {
startCheck: arabicWordStartCheck,
endCheck: arabicWordEndCheck
};

@@ -8,157 +8,64 @@ /**

// DEFAULT TEXT BASE DIRECTION
let BASE_DIR = 'ltr';
/**
* Create feature query instance
* @param {Font} font opentype font instance
* @param {string} baseDir text base direction
*/
function FeatureQuery(font, baseDir) {
function FeatureQuery(font) {
this.font = font;
this.features = {};
BASE_DIR = !!baseDir ? baseDir : BASE_DIR;
}
/**
* Create a new feature lookup
* @param {string} tag feature tag
* @param {feature} feature reference to feature at gsub table
* @param {FeatureLookups} feature lookups associated with this feature
* @param {string} script gsub script tag
* @typedef SubstitutionAction
* @type Object
* @property {number} id substitution type
* @property {string} tag feature tag
* @property {any} substitution substitution value(s)
*/
function Feature(tag, feature, featureLookups, script) {
this.tag = tag;
this.featureRef = feature;
this.lookups = featureLookups.lookups;
this.script = script;
}
/**
* Create a coverage table lookup
* @param {any} coverageTable gsub coverage table
* Create a substitution action instance
* @param {SubstitutionAction} action
*/
function Coverage(coverageTable) {
this.table = coverageTable;
function SubstitutionAction(action) {
this.id = action.id;
this.tag = action.tag;
this.substitution = action.substitution;
}
/**
* Create a ligature set lookup
* @param {any} ligatureSets gsub ligature set
* Lookup a coverage table
* @param {number} glyphIndex glyph index
* @param {CoverageTable} coverage coverage table
*/
function LigatureSets(ligatureSets) {
this.ligatureSets = ligatureSets;
}
function lookupCoverage(glyphIndex, coverage) {
if (!glyphIndex) return -1;
switch (coverage.format) {
case 1:
return coverage.glyphs.indexOf(glyphIndex);
/**
* Lookup a glyph ligature
* @param {ContextParams} contextParams context params to lookup
* @param {number} ligSetIndex ligature set index at ligature sets
*/
LigatureSets.prototype.lookup = function (contextParams, ligSetIndex) {
const ligatureSet = this.ligatureSets[ligSetIndex];
const matchComponents = (components, indexes) => {
if (components.length > indexes.length) return null;
for (let c = 0; c < components.length; c++) {
const component = components[c];
const index = indexes[c];
if (component !== index) return false;
}
return true;
};
for (let s = 0; s < ligatureSet.length; s++) {
const ligSetItem = ligatureSet[s];
const lookaheadIndexes = contextParams.lookahead.map(
token => token.activeState.value
);
if (BASE_DIR === 'rtl') lookaheadIndexes.reverse();
const componentsMatch = matchComponents(
ligSetItem.components, lookaheadIndexes
);
if (componentsMatch) return ligSetItem;
case 2:
let ranges = coverage.ranges;
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (glyphIndex >= range.start && glyphIndex <= range.end) {
let offset = glyphIndex - range.start;
return range.index + offset;
}
}
break;
default:
return -1; // not found
}
return null;
};
/**
* Create a feature substitution
* @param {any} lookups a reference to gsub lookups
* @param {Lookuptable} lookupTable a feature lookup table
* @param {any} subtable substitution table
*/
function Substitution(lookups, lookupTable, subtable) {
this.lookups = lookups;
this.subtable = subtable;
this.lookupTable = lookupTable;
if (subtable.hasOwnProperty('coverage')) {
this.coverage = new Coverage(
subtable.coverage
);
}
if (subtable.hasOwnProperty('inputCoverage')) {
this.inputCoverage = subtable.inputCoverage.map(
table => new Coverage(table)
);
}
if (subtable.hasOwnProperty('backtrackCoverage')) {
this.backtrackCoverage = subtable.backtrackCoverage.map(
table => new Coverage(table)
);
}
if (subtable.hasOwnProperty('lookaheadCoverage')) {
this.lookaheadCoverage = subtable.lookaheadCoverage.map(
table => new Coverage(table)
);
}
if (subtable.hasOwnProperty('ligatureSets')) {
this.ligatureSets = new LigatureSets(subtable.ligatureSets);
}
return -1;
}
/**
* Create a lookup table lookup
* @param {number} index table index at gsub lookups
* @param {any} lookups a reference to gsub lookups
*/
function LookupTable(index, lookups) {
this.index = index;
this.subtables = lookups[index].subtables.map(
subtable => new Substitution(
lookups, lookups[index], subtable
)
);
}
function FeatureLookups(lookups, lookupListIndexes) {
this.lookups = lookupListIndexes.map(
index => new LookupTable(index, lookups)
);
}
/**
* Lookup a lookup table subtables
* @param {ContextParams} contextParams context params to lookup
*/
LookupTable.prototype.lookup = function (contextParams) {
let substitutions = [];
for (let i = 0; i < this.subtables.length; i++) {
const subsTable = this.subtables[i];
let substitution = subsTable.lookup(contextParams);
if (substitution !== null || substitution.length) {
substitutions = substitutions.concat(substitution);
}
}
return substitutions;
};
/**
* Handle a single substitution - format 2
* @param {ContextParams} contextParams context params to lookup
*/
function singleSubstitutionFormat2(contextParams) {
let glyphIndex = contextParams.current.activeState.value;
glyphIndex = Array.isArray(glyphIndex) ? glyphIndex[0] : glyphIndex;
let substituteIndex = this.coverage.lookup(glyphIndex);
if (substituteIndex === -1) return [];
return [this.subtable.substitute[substituteIndex]];
function singleSubstitutionFormat2(glyphIndex, subtable) {
let substituteIndex = lookupCoverage(glyphIndex, subtable.coverage);
if (substituteIndex === -1) return null;
return subtable.substitute[substituteIndex];
}

@@ -169,3 +76,3 @@

* @param {any} coverageList a list of coverage tables
* @param {any} contextParams context params to lookup
* @param {ContextParams} contextParams context params to lookup
*/

@@ -176,5 +83,5 @@ function lookupCoverageList(coverageList, contextParams) {

const coverage = coverageList[i];
let glyphIndex = contextParams.current.activeState.value;
let glyphIndex = contextParams.current;
glyphIndex = Array.isArray(glyphIndex) ? glyphIndex[0] : glyphIndex;
const lookupIndex = coverage.lookup(glyphIndex);
const lookupIndex = lookupCoverage(glyphIndex, coverage);
if (lookupIndex !== -1) {

@@ -190,9 +97,9 @@ lookupList.push(lookupIndex);

* Handle chaining context substitution - format 3
* @param {any} contextParams context params to lookup
* @param {ContextParams} contextParams context params to lookup
*/
function chainingSubstitutionFormat3(contextParams) {
function chainingSubstitutionFormat3(contextParams, subtable) {
const lookupsCount = (
this.inputCoverage.length +
this.lookaheadCoverage.length +
this.backtrackCoverage.length
subtable.inputCoverage.length +
subtable.lookaheadCoverage.length +
subtable.backtrackCoverage.length
);

@@ -202,8 +109,8 @@ if (contextParams.context.length < lookupsCount) return [];

let inputLookups = lookupCoverageList(
this.inputCoverage, contextParams
subtable.inputCoverage, contextParams
);
if (inputLookups === -1) return [];
// LOOKAHEAD LOOKUP //
const lookaheadOffset = this.inputCoverage.length - 1;
if (contextParams.lookahead.length < this.lookaheadCoverage.length) return [];
const lookaheadOffset = subtable.inputCoverage.length - 1;
if (contextParams.lookahead.length < subtable.lookaheadCoverage.length) return [];
let lookaheadContext = contextParams.lookahead.slice(lookaheadOffset);

@@ -215,3 +122,3 @@ while (lookaheadContext.length && isTashkeelArabicChar(lookaheadContext[0].char)) {

let lookaheadLookups = lookupCoverageList(
this.lookaheadCoverage, lookaheadParams
subtable.lookaheadCoverage, lookaheadParams
);

@@ -224,23 +131,29 @@ // BACKTRACK LOOKUP //

}
if (backtrackContext.length < this.backtrackCoverage.length) return [];
if (backtrackContext.length < subtable.backtrackCoverage.length) return [];
const backtrackParams = new ContextParams(backtrackContext, 0);
let backtrackLookups = lookupCoverageList(
this.backtrackCoverage, backtrackParams
subtable.backtrackCoverage, backtrackParams
);
const contextRulesMatch = (
inputLookups.length === this.inputCoverage.length &&
lookaheadLookups.length === this.lookaheadCoverage.length &&
backtrackLookups.length === this.backtrackCoverage.length
inputLookups.length === subtable.inputCoverage.length &&
lookaheadLookups.length === subtable.lookaheadCoverage.length &&
backtrackLookups.length === subtable.backtrackCoverage.length
);
let substitutions = [];
if (contextRulesMatch) {
let lookupRecords = this.subtable.lookupRecords;
for (let i = 0; i < lookupRecords.length; i++) {
const lookupRecord = lookupRecords[i];
for (let j = 0; j < inputLookups.length; j++) {
const inputContext = new ContextParams([contextParams.get(j)], 0);
let lookupIndex = lookupRecord.lookupListIndex;
const lookupTable = new LookupTable(lookupIndex, this.lookups);
let lookup = lookupTable.lookup(inputContext);
substitutions = substitutions.concat(lookup);
for (let i = 0; i < subtable.lookupRecords.length; i++) {
const lookupRecord = subtable.lookupRecords[i];
const lookupListIndex = lookupRecord.lookupListIndex;
const lookupTable = this.getLookupByIndex(lookupListIndex);
for (let s = 0; s < lookupTable.subtables.length; s++) {
const subtable = lookupTable.subtables[s];
const lookup = this.getLookupMethod(lookupTable, subtable);
const substitutionType = this.getSubstitutionType(lookupTable, subtable);
if (substitutionType === '12') {
for (let n = 0; n < inputLookups.length; n++) {
const glyphIndex = contextParams.get(n);
const substitution = lookup(glyphIndex);
if (substitution) substitutions.push(substitution);
}
}
}

@@ -254,93 +167,48 @@ }

* Handle ligature substitution - format 1
* @param {any} contextParams context params to lookup
* @param {ContextParams} contextParams context params to lookup
*/
function ligatureSubstitutionFormat1(contextParams) {
function ligatureSubstitutionFormat1(contextParams, subtable) {
// COVERAGE LOOKUP //
let glyphIndex = contextParams.current.activeState.value;
let ligSetIndex = this.coverage.lookup(glyphIndex);
if (ligSetIndex === -1) return [];
// COMPONENTS LOOKUP * note that components is logically ordered
let ligGlyphs = this.ligatureSets.lookup(contextParams, ligSetIndex);
return ligGlyphs ? [ligGlyphs] : [];
}
/**
* [ LOOKUP TYPES ]
* -------------------------------
* Single 1;
* Multiple 2;
* Alternate 3;
* Ligature 4;
* Context 5;
* ChainingContext 6;
* ExtensionSubstitution 7;
* ReverseChainingContext 8;
* -------------------------------
* @param {any} contextParams context params to lookup
*/
Substitution.prototype.lookup = function (contextParams) {
const substitutions = [];
const lookupType = this.lookupTable.lookupType;
const substFormat = this.subtable.substFormat;
if (lookupType === 1 && substFormat === 2) {
let substitution = singleSubstitutionFormat2.call(this, contextParams);
if (substitution.length > 0) {
substitutions.push({ id: 12, substitution });
let glyphIndex = contextParams.current;
let ligSetIndex = lookupCoverage(glyphIndex, subtable.coverage);
if (ligSetIndex === -1) return null;
// COMPONENTS LOOKUP
// (!) note, components are ordered in the written direction.
let ligature;
let ligatureSet = subtable.ligatureSets[ligSetIndex];
for (let s = 0; s < ligatureSet.length; s++) {
ligature = ligatureSet[s];
for (let l = 0; l < ligature.components.length; l++) {
const lookaheadItem = contextParams.lookahead[l];
const component = ligature.components[l];
if (lookaheadItem !== component) break;
if (l === ligature.components.length - 1) return ligature;
}
}
if (lookupType === 6 && substFormat === 3) {
const substitution = chainingSubstitutionFormat3.call(this, contextParams);
if (substitution.length > 0) {
substitutions.push({ id: 63, substitution });
}
}
if (lookupType === 4 && substFormat === 1) {
const substitution = ligatureSubstitutionFormat1.call(this, contextParams);
if (substitution.length > 0) {
substitutions.push({ id: 41, substitution });
}
}
return substitutions;
};
return null;
}
/**
* Lookup a coverage table
* @param {number} glyphIndex to lookup
* Handle decomposition substitution - format 1
* @param {number} glyphIndex glyph index
* @param {any} subtable subtable
*/
Coverage.prototype.lookup = function (glyphIndex) {
if (!glyphIndex) return -1;
switch (this.table.format) {
case 1:
return this.table.glyphs.indexOf(glyphIndex);
function decompositionSubstitutionFormat1(glyphIndex, subtable) {
let substituteIndex = lookupCoverage(glyphIndex, subtable.coverage);
if (substituteIndex === -1) return null;
return subtable.sequences[substituteIndex];
}
case 2:
let ranges = this.table.ranges;
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (glyphIndex >= range.start && glyphIndex <= range.end) {
let offset = glyphIndex - range.start;
return range.index + offset;
}
}
break;
default:
return -1; // not found
}
return -1;
};
/**
* Lookup a feature for a substitution or more
* @param {any} contextParams context params to lookup
* Get default script features indexes
*/
Feature.prototype.lookup = function(contextParams) {
let lookups = [];
for (let i = 0; i < this.lookups.length; i++) {
const lookupTable = this.lookups[i];
let lookup = lookupTable.lookup(contextParams);
if (lookup !== null || lookup.length) {
lookups = lookups.concat(lookup);
}
FeatureQuery.prototype.getDefaultScriptFeaturesIndexes = function () {
const scripts = this.font.tables.gsub.scripts;
for (let s = 0; s < scripts.length; s++) {
const script = scripts[s];
if (script.tag === 'DFLT') return (
script.script.defaultLangSys.featureIndexes
);
}
return lookups;
return [];
};

@@ -353,5 +221,5 @@

FeatureQuery.prototype.getScriptFeaturesIndexes = function(scriptTag) {
if (!scriptTag) return [];
const tables = this.font.tables;
if (!tables.gsub) return [];
if (!scriptTag) return this.getDefaultScriptFeaturesIndexes();
const scripts = this.font.tables.gsub.scripts;

@@ -376,3 +244,3 @@ for (let i = 0; i < scripts.length; i++) {

}
return [];
return this.getDefaultScriptFeaturesIndexes();
};

@@ -383,3 +251,3 @@

* @param {any} features gsub features
* @param {*} scriptTag script tag
* @param {string} scriptTag script tag
*/

@@ -389,7 +257,5 @@ FeatureQuery.prototype.mapTagsToFeatures = function (features, scriptTag) {

for (let i = 0; i < features.length; i++) {
const tag = features[i].tag;
const feature = features[i].feature;
const tag = features[i].tag;
const lookups = this.font.tables.gsub.lookups;
const featureLookups = new FeatureLookups(lookups, feature.lookupListIndexes);
tags[tag] = new Feature(tag, feature, featureLookups, scriptTag);
tags[tag] = feature;
}

@@ -416,6 +282,181 @@ this.features[scriptTag].tags = tags;

/**
* Get substitution type
* @param {any} lookupTable lookup table
* @param {any} subtable subtable
*/
FeatureQuery.prototype.getSubstitutionType = function(lookupTable, subtable) {
const lookupType = lookupTable.lookupType.toString();
const substFormat = subtable.substFormat.toString();
return lookupType + substFormat;
};
/**
* Get lookup method
* @param {any} lookupTable lookup table
* @param {any} subtable subtable
*/
FeatureQuery.prototype.getLookupMethod = function(lookupTable, subtable) {
let substitutionType = this.getSubstitutionType(lookupTable, subtable);
switch (substitutionType) {
case '12':
return glyphIndex => singleSubstitutionFormat2.apply(
this, [glyphIndex, subtable]
);
case '63':
return contextParams => chainingSubstitutionFormat3.apply(
this, [contextParams, subtable]
);
case '41':
return contextParams => ligatureSubstitutionFormat1.apply(
this, [contextParams, subtable]
);
case '21':
return glyphIndex => decompositionSubstitutionFormat1.apply(
this, [glyphIndex, subtable]
);
default:
throw new Error(
`lookupType: ${lookupTable.lookupType} - ` +
`substFormat: ${subtable.substFormat} ` +
`is not yet supported`
);
}
};
/**
* [ LOOKUP TYPES ]
* -------------------------------
* Single 1;
* Multiple 2;
* Alternate 3;
* Ligature 4;
* Context 5;
* ChainingContext 6;
* ExtensionSubstitution 7;
* ReverseChainingContext 8;
* -------------------------------
*
*/
/**
* @typedef FQuery
* @type Object
* @param {string} tag feature tag
* @param {string} script feature script
* @param {ContextParams} contextParams context params
*/
/**
* Lookup a feature using a query parameters
* @param {FQuery} query feature query
*/
FeatureQuery.prototype.lookupFeature = function (query) {
let contextParams = query.contextParams;
let currentIndex = contextParams.index;
const feature = this.getFeature({
tag: query.tag, script: query.script
});
if (!feature) return new Error(
`font '${this.font.names.fullName.en}' ` +
`doesn't support feature '${query.tag}' ` +
`for script '${query.script}'.`
);
const lookups = this.getFeatureLookups(feature);
const substitutions = [].concat(contextParams.context);
for (let l = 0; l < lookups.length; l++) {
const lookupTable = lookups[l];
const subtables = this.getLookupSubtables(lookupTable);
for (let s = 0; s < subtables.length; s++) {
const subtable = subtables[s];
const substType = this.getSubstitutionType(lookupTable, subtable);
const lookup = this.getLookupMethod(lookupTable, subtable);
let substitution;
switch (substType) {
case '12':
substitution = lookup(contextParams.current);
if (substitution) {
substitutions.splice(currentIndex, 1, new SubstitutionAction({
id: 12, tag: query.tag, substitution
}));
}
break;
case '63':
substitution = lookup(contextParams);
if (Array.isArray(substitution) && substitution.length) {
substitutions.splice(currentIndex, 1, new SubstitutionAction({
id: 63, tag: query.tag, substitution
}));
}
break;
case '41':
substitution = lookup(contextParams);
if (substitution) {
substitutions.splice(currentIndex, 1, new SubstitutionAction({
id: 41, tag: query.tag, substitution
}));
}
break;
case '21':
substitution = lookup(contextParams.current);
if (substitution) {
substitutions.splice(currentIndex, 1, new SubstitutionAction({
id: 21, tag: query.tag, substitution
}));
}
break;
}
contextParams = new ContextParams(substitutions, currentIndex);
if (Array.isArray(substitution) && !substitution.length) continue;
substitution = null;
}
}
return substitutions.length ? substitutions : null;
};
/**
* Checks if a font supports a specific features
* @param {FQuery} query feature query object
*/
FeatureQuery.prototype.supports = function (query) {
if (!query.script) return false;
this.getScriptFeatures(query.script);
const supportedScript = this.features.hasOwnProperty(query.script);
if (!query.tag) return supportedScript;
const supportedFeature = (
this.features[query.script].some(feature => feature.tag === query.tag)
);
return supportedScript && supportedFeature;
};
/**
* Get lookup table subtables
* @param {any} lookupTable lookup table
*/
FeatureQuery.prototype.getLookupSubtables = function (lookupTable) {
return lookupTable.subtables || null;
};
/**
* Get lookup table by index
* @param {number} index lookup table index
*/
FeatureQuery.prototype.getLookupByIndex = function (index) {
const lookups = this.font.tables.gsub.lookups;
return lookups[index] || null;
};
/**
* Get lookup tables for a feature
* @param {string} feature
*/
FeatureQuery.prototype.getFeatureLookups = function (feature) {
// TODO: memoize
return feature.lookupListIndexes.map(this.getLookupByIndex.bind(this));
};
/**
* Query a feature by it's properties
* @param {any} query an object that describes the properties of a query
*/
FeatureQuery.prototype.getFeature = function (query) {
FeatureQuery.prototype.getFeature = function getFeature(query) {
if (!this.font) return { FAIL: `No font was found`};

@@ -425,6 +466,11 @@ if (!this.features.hasOwnProperty(query.script)) {

}
return this.features[query.script].tags[query.tag] || null;
const scriptFeatures = this.features[query.script];
if (!scriptFeatures) return (
{ FAIL: `No feature for script ${query.script}`}
);
if (!scriptFeatures.tags[query.tag]) return null;
return this.features[query.script].tags[query.tag];
};
export default FeatureQuery;
export { Feature };
export { FeatureQuery, SubstitutionAction };

@@ -12,3 +12,2 @@ // The Font object

import Bidi from './bidi';
import FeatureQuery from './features/featureQuery';

@@ -152,2 +151,20 @@ /**

/**
* Update features
* @param {any} options features options
*/
Font.prototype.updateFeatures = function (options) {
// TODO: update all features options not only 'latn'.
return this.defaultRenderOptions.features.map(feature => {
if (feature.script === 'latn') {
return {
script: 'latn',
tags: feature.tags.filter(tag => options[tag])
};
} else {
return feature;
}
});
};
/**
* Convert the given text to a list of Glyph objects.

@@ -162,3 +179,2 @@ * Note that there is no strict one-to-one mapping between characters and

Font.prototype.stringToGlyphs = function(s, options) {
options = options || this.defaultRenderOptions;

@@ -171,11 +187,9 @@ const bidi = new Bidi();

const arabFeatureQuery = new FeatureQuery(this);
const arabFeatures = ['init', 'medi', 'fina', 'rlig'];
bidi.applyFeatures(
arabFeatures.map(tag => {
let query = { tag, script: 'arab' };
let feature = arabFeatureQuery.getFeature(query);
if (!!feature) return feature;
})
);
// roll-back to default features
let features = options ?
this.updateFeatures(options.features) :
this.defaultRenderOptions.features;
bidi.applyFeatures(this, features);
const indexes = bidi.getTextGlyphs(s);

@@ -185,23 +199,2 @@

// Apply substitutions on glyph indexes
if (options.features) {
const script = options.script || this.substitution.getDefaultScriptName();
let manyToOne = [];
if (options.features.liga) manyToOne = manyToOne.concat(this.substitution.getFeature('liga', script, options.language));
if (options.features.rlig) manyToOne = manyToOne.concat(this.substitution.getFeature('rlig', script, options.language));
for (let i = 0; i < length; i += 1) {
for (let j = 0; j < manyToOne.length; j++) {
const ligature = manyToOne[j];
const components = ligature.sub;
const compCount = components.length;
let k = 0;
while (k < compCount && components[k] === indexes[i + k]) k++;
if (k === compCount) {
indexes.splice(i, compCount, ligature.by);
length = length - compCount + 1;
}
}
}
}
// convert glyph indexes to glyph objects

@@ -286,6 +279,10 @@ const glyphs = new Array(length);

kerning: true,
features: {
liga: true,
rlig: true
}
features: [
/**
* these 4 features are required to render Arabic text properly
* and shouldn't be turned off when rendering arabic text.
*/
{ script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
{ script: 'latn', tags: ['liga', 'rlig'] }
]
};

@@ -292,0 +289,0 @@

@@ -42,2 +42,9 @@ /**

/**
* @typedef ContextParams
* @type Object
* @property {array} context context items
* @property {number} currentIndex current item index
*/
/**
* Create a context params

@@ -44,0 +51,0 @@ * @param {array} context a list of items

import assert from 'assert';
import Bidi from '../src/bidi';
import Tokenizer from '../src/tokenizer';
import FeatureQuery from '../src/features/featureQuery';
import { loadSync } from '../src/opentype';
describe('bidi.js', function() {
let font;
let bidi;
let query;
let arabicTok;
let arabicWordCheck;
let arabicSentenceCheck;
let latinFont;
let arabicFont;
let bidiFira;
let bidiScheherazade;
let arabicTokenizer;
before(function () {
font = loadSync('./fonts/Scheherazade-Bold.ttf');
bidi = new Bidi('ltr');
query = new FeatureQuery(font);
arabicWordCheck = bidi.contextChecks.arabicWordCheck;
arabicSentenceCheck = bidi.contextChecks.arabicSentenceCheck;
/**
* arab
*/
arabicFont = loadSync('./fonts/Scheherazade-Bold.ttf');
bidiScheherazade = new Bidi();
bidiScheherazade.registerModifier(
'glyphIndex', null, token => arabicFont.charToGlyphIndex(token.char)
);
const requiredArabicFeatures = [{
script: 'arab',
tags: ['init', 'medi', 'fina', 'rlig']
}];
bidiScheherazade.applyFeatures(arabicFont, requiredArabicFeatures);
bidiScheherazade.getTextGlyphs(''); // initialize bidi.
arabicTokenizer = bidiScheherazade.tokenizer;
/**
* latin
*/
latinFont = loadSync('./fonts/FiraSansMedium.woff');
bidiFira = new Bidi();
bidiFira.registerModifier(
'glyphIndex', null, token => latinFont.charToGlyphIndex(token.char)
);
const latinFeatures = [{
script: 'latn',
tags: ['liga', 'rlig']
}];
bidiFira.applyFeatures(latinFont, latinFeatures);
});
describe('arabic contexts', function() {
before(function () {
arabicTok = new Tokenizer();
arabicTok.registerContextChecker(
'arabicWord',
arabicWordCheck.arabicWordStartCheck,
arabicWordCheck.arabicWordEndCheck
);
arabicTok.registerContextChecker(
'arabicSentence',
arabicSentenceCheck.arabicSentenceStartCheck,
arabicSentenceCheck.arabicSentenceEndCheck
);
});
it('should match arabic words in a given text', function() {
arabicTok.tokenize('Hello السلام عليكم');
const ranges = arabicTok.getContextRanges('arabicWord');
const words = ranges.map(range => arabicTok.rangeToText(range));
const tokenizer = bidiScheherazade.tokenizer;
tokenizer.tokenize('Hello السلام عليكم');
const ranges = tokenizer.getContextRanges('arabicWord');
const words = ranges.map(range => tokenizer.rangeToText(range));
assert.deepEqual(words, ['السلام', 'عليكم']);
});
it('should match mixed arabic sentence', function() {
arabicTok.tokenize('The king said: ائتوني به أستخلصه لنفسي');
const ranges = arabicTok.getContextRanges('arabicSentence');
const sentences = ranges.map(range => arabicTok.rangeToText(range))[0];
arabicTokenizer.tokenize('The king said: ائتوني به أستخلصه لنفسي');
const ranges = arabicTokenizer.getContextRanges('arabicSentence');
const sentences = ranges.map(range => arabicTokenizer.rangeToText(range))[0];
assert.equal(sentences, 'ائتوني به أستخلصه لنفسي');

@@ -50,3 +58,3 @@ });

it('should adjust then render layout direction of bidi text', function() {
const bidiText = bidi.getBidiText('Be kind, فما كان الرفق في شيء إلا زانه ، ولا نزع من شيء إلا شانه');
const bidiText = bidiScheherazade.getBidiText('Be kind, فما كان الرفق في شيء إلا زانه ، ولا نزع من شيء إلا شانه');
assert.equal(bidiText, 'Be kind, هناش الإ ءيش نم عزن الو ، هناز الإ ءيش يف قفرلا ناك امف');

@@ -56,21 +64,6 @@ });

describe('applyFeatures', function () {
before(function () {
const arabicPresentationForms = ['init', 'medi', 'fina', 'rlig'].map(
tag => query.getFeature({
tag, script: 'arab'
})
);
const charToGlyphIndex = token => font.charToGlyphIndex(token.char);
bidi.registerModifier('glyphIndex', null, charToGlyphIndex);
bidi.tokenizer.registerContextChecker(
'arabicWord',
arabicWordCheck.arabicWordStartCheck,
arabicWordCheck.arabicWordEndCheck
);
bidi.applyFeatures(arabicPresentationForms);
});
it('should apply arabic presentation forms', function() {
bidi.getBidiText('Hello السلام عليكم');
const ranges = bidi.tokenizer.getContextRanges('arabicWord');
const PeaceTokens = bidi.tokenizer.getRangeTokens(ranges[1]);
bidiScheherazade.getTextGlyphs('Hello السلام عليكم');
const ranges = bidiScheherazade.tokenizer.getContextRanges('arabicWord');
const PeaceTokens = bidiScheherazade.tokenizer.getRangeTokens(ranges[1]);
const PeaceForms = PeaceTokens.map(token => {

@@ -85,7 +78,15 @@ if (token.state.init) return 'init';

it('should apply arabic required letter ligature', function () {
let glyphIndexes = bidi.getTextGlyphs('لا'); // Arabic word 'لا' : 'no'
let glyphIndexes = bidiScheherazade.getTextGlyphs('لا'); // Arabic word 'لا' : 'no'
assert.deepEqual(glyphIndexes, [1341, 1330]);
});
it('should apply arabic required composition ligature', function () {
let glyphIndexes = bidiScheherazade.getTextGlyphs('َّ'); // Arabic word 'َّ' : 'Fatha & Shadda'
assert.deepEqual(glyphIndexes, [1311]);
});
it('should apply latin ligature', function () {
let glyphIndexes = bidiFira.getTextGlyphs('fi'); // fi => fi
assert.deepEqual(glyphIndexes, [1145]);
});
});
});
import assert from 'assert';
import { loadSync } from '../src/opentype';
import FeatureQuery from '../src/features/featureQuery';
import Tokenizer, { ContextParams } from '../src/tokenizer';
import { ContextParams } from '../src/tokenizer';
describe('featureQuery.js', function() {
let font;
let query;
let tokenizer;
let arabFeatureInit;
let arabFeatureMedi;
let arabFeatureFina;
let arabFeatureRlig;
let arabicFont;
let latinFont;
let query = {};
before(function () {
font = loadSync('./fonts/Scheherazade-Bold.ttf');
query = new FeatureQuery(font);
tokenizer = new Tokenizer();
const script = 'arab';
arabFeatureInit = query.getFeature({tag: 'init', script});
arabFeatureMedi = query.getFeature({tag: 'medi', script});
arabFeatureFina = query.getFeature({tag: 'fina', script});
arabFeatureRlig = query.getFeature({tag: 'rlig', script});
const charToGlyphIndex = token => font.charToGlyphIndex(token.char);
tokenizer.registerModifier('glyphIndex', null, charToGlyphIndex);
/**
* arab
*/
arabicFont = loadSync('./fonts/Scheherazade-Bold.ttf');
query.arabic = new FeatureQuery(arabicFont);
/**
* latin
*/
latinFont = loadSync('./fonts/FiraSansMedium.woff');
query.latin = new FeatureQuery(latinFont);
});
describe('getScriptFeature', function () {
it('should return features indexes of a given script tag', function () {
let featuresIndexes = query.getScriptFeaturesIndexes('arab');
assert.equal(featuresIndexes.length, 24);
/** arab */
let arabFeaturesIndexes = query.arabic.getScriptFeaturesIndexes('arab');
assert.equal(arabFeaturesIndexes.length, 24);
/** latin */
let latnFeaturesIndexes = query.latin.getScriptFeaturesIndexes('latn');
assert.equal(latnFeaturesIndexes.length, 20);
});
it('should return features of a given script', function () {
let features = query.getScriptFeatures('arab');
assert.equal(features.length, 24);
/** arab */
let arabFeatures = query.arabic.getScriptFeatures('arab');
assert.equal(arabFeatures.length, 24);
/** latin */
let latnFeatures = query.latin.getScriptFeatures('latn');
assert.equal(latnFeatures.length, 20);
});
it('should return a feature instance', function () {
assert.deepEqual(arabFeatureInit.tag, 'init');
it('should return a feature lookup tables', function () {
/** arab */
const initFeature = query.arabic.getFeature({tag: 'init', script: 'arab'});
assert.deepEqual(initFeature.lookupListIndexes, [7]);
const initFeatureLookups = query.arabic.getFeatureLookups(initFeature);
assert.deepEqual(initFeatureLookups[0], arabicFont.tables.gsub.lookups[7]);
/** latin */
const ligaFeature = query.latin.getFeature({tag: 'liga', script: 'latn'});
assert.deepEqual(ligaFeature.lookupListIndexes, [35]);
const ligaFeatureLookups = query.latin.getFeatureLookups(ligaFeature);
assert.deepEqual(ligaFeatureLookups[0], latinFont.tables.gsub.lookups[35]);
});
it('find a substitute - single substitution format 2 (12)', function () {
// Arabic Presentation Forms 'init' 'medi' 'fina'
const tokens = tokenizer.tokenize('محمد');
const initContextParams = new ContextParams(tokens, 0); // 'م'
const initSubstitution = arabFeatureInit.lookup(initContextParams);
assert.deepEqual(initSubstitution.length, 1); // should return a single substitution
assert.deepEqual(initSubstitution[0].id, 12); // should return a substitution of type 1 format 2
assert.deepEqual(initSubstitution[0].substitution[0], 1046); // should return letter 'ﻣ' : 'Meem' initial form index
const mediContextParams = new ContextParams(tokens, 1); // 'ح'
const mediSubstitution = arabFeatureMedi.lookup(mediContextParams);
assert.deepEqual(initSubstitution.length, 1); // should return a single substitution
assert.deepEqual(initSubstitution[0].id, 12); // should return a substitution of type 1 format 2
assert.deepEqual(mediSubstitution[0].substitution[0], 798); // should return letter 'ﺤ' : 'Haa' medi form index
const finaContextParams = new ContextParams(tokens, 3); // 'د'
const finaSubstitution = arabFeatureFina.lookup(finaContextParams);
assert.deepEqual(initSubstitution.length, 1); // should return a single substitution
assert.deepEqual(initSubstitution[0].id, 12); // should return a substitution of type 1 format 2
assert.deepEqual(finaSubstitution[0].substitution[0], 549); // should return letter 'د' : 'Dal' fina form index
it('should return lookup subtables', function () {
/** arab */
const initFeature = query.arabic.getFeature({tag: 'init', script: 'arab'});
const initFeatureLookups = query.arabic.getFeatureLookups(initFeature);
const initLookupSubtables = query.arabic.getLookupSubtables(initFeatureLookups[0]);
assert.deepEqual(initLookupSubtables.length, arabicFont.tables.gsub.lookups[7].subtables.length);
/** latin */
const ligaFeature = query.latin.getFeature({tag: 'liga', script: 'latn'});
const ligaFeatureLookups = query.latin.getFeatureLookups(ligaFeature);
const ligaLookupSubtables = query.latin.getLookupSubtables(ligaFeatureLookups[0]);
assert.deepEqual(ligaLookupSubtables.length, latinFont.tables.gsub.lookups[35].subtables.length);
});
it('find a substitute - chaining context substitution format 3 (63)', function () {
const tokens = tokenizer.tokenize('لان'); // arabic word 'لان' : 'soften' indexes after applying presentation forms
tokens[0].setState('init', [1039]); // set 'ل' : Lam init form value
tokens[1].setState('fina', [524]); // set 'ا' : Alef fina form value
let rligContextParams = new ContextParams(tokens, 0); // first letter 'ل' : Lam index
let substitutions = arabFeatureRlig.lookup(rligContextParams);
assert.deepEqual(substitutions.length, 1);
assert.deepEqual(substitutions[0].id, 63);
let chainingSubst = substitutions[0].substitution;
assert.deepEqual(chainingSubst.length, 1);
assert.deepEqual(chainingSubst[0].id, 12);
assert.deepEqual(chainingSubst[0].substitution[0], 1330); // 1039 => rlig (63) => 1330
it('should return subtable lookup method', function () {
/** arab */
const initFeature = query.arabic.getFeature({tag: 'init', script: 'arab'});
const initFeatureLookups = query.arabic.getFeatureLookups(initFeature);
const initLookupTable = initFeatureLookups[0];
const initLookupSubtables = query.arabic.getLookupSubtables(initLookupTable);
const initSubtable = initLookupSubtables[0];
const initSubsType = query.arabic.getSubstitutionType(initLookupTable, initSubtable);
const initLookupFn = query.arabic.getLookupMethod(initLookupTable, initSubtable);
assert.deepEqual(initSubsType, 12); // supported: single substitution '12'
assert.deepEqual(typeof initLookupFn, 'function');
/** latin */
const ligaFeature = query.latin.getFeature({tag: 'liga', script: 'latn'});
const ligaFeatureLookups = query.latin.getFeatureLookups(ligaFeature);
const ligaLookupTable = ligaFeatureLookups[0];
const ligaLookupSubtables = query.latin.getLookupSubtables(ligaLookupTable);
const ligaSubtable = ligaLookupSubtables[0];
const ligaSubsType = query.latin.getSubstitutionType(ligaLookupTable, ligaSubtable);
const ligaLookupFn = query.latin.getLookupMethod(ligaLookupTable, ligaSubtable);
assert.deepEqual(ligaSubsType, 41); // supported: ligature substitution '41'
assert.deepEqual(typeof ligaLookupFn, 'function');
});
it('find a substitute - ligature substitution format 1 (41)', function () {
const tokens = tokenizer.tokenize('َّ'); // arabic 'َّ' شده متبوعه بفتحه : Shadda followed by Fatha
let rligContextParams = new ContextParams(tokens, 0);
let substitutions = arabFeatureRlig.lookup(rligContextParams);
assert.deepEqual(substitutions.length, 1);
assert.deepEqual(substitutions[0].id, 41);
assert.deepEqual(substitutions[0].substitution, [{ ligGlyph: 1311, components: [1081]}]);
it('should find a substitute - single substitution format 2 (12)', function () {
const feature = query.arabic.getFeature({tag: 'init', script: 'arab'});
const featureLookups = query.arabic.getFeatureLookups(feature);
const lookupSubtables = query.arabic.getLookupSubtables(featureLookups[0]);
const substitutionType = query.arabic.getSubstitutionType(featureLookups[0], lookupSubtables[0]);
assert.equal(substitutionType, 12);
const lookup = query.arabic.getLookupMethod(featureLookups[0], lookupSubtables[0]);
const substitution = lookup(351);
assert.equal(substitution, 910);
});
it('should find a substitute - chaining context substitution format 3 (63)', function () {
const feature = query.arabic.getFeature({tag: 'rlig', script: 'arab'});
const featureLookups = query.arabic.getFeatureLookups(feature);
const lookupSubtables = query.arabic.getLookupSubtables(featureLookups[0]);
const substitutionType = query.arabic.getSubstitutionType(featureLookups[0], lookupSubtables[0]);
assert.equal(substitutionType, 63);
const lookup = query.arabic.getLookupMethod(featureLookups[0], lookupSubtables[0]);
let contextParams = new ContextParams([882, 520], 0);
const substitutions = lookup(contextParams);
assert.deepEqual(substitutions, [1348]);
});
it('should find a substitute - required ligature substitution format 1 (41)', function () {
/** arab */
const initFeature = query.arabic.getFeature({tag: 'rlig', script: 'arab'});
const initFeatureLookups = query.arabic.getFeatureLookups(initFeature);
const lookupSubtables = query.arabic.getLookupSubtables(initFeatureLookups[1]);
const substitutionType = query.arabic.getSubstitutionType(initFeatureLookups[1], lookupSubtables[0]);
assert.equal(substitutionType, 41);
const lookup = query.arabic.getLookupMethod(initFeatureLookups[1], lookupSubtables[0]);
let contextParams = new ContextParams([1075, 1081], 0);
const substitutions = lookup(contextParams);
assert.deepEqual(substitutions, { ligGlyph: 1311, components: [1081]});
});
it('should find a substitute - ligature substitution format 1 (41)', function () {
/** latin */
const ligaFeature = query.latin.getFeature({tag: 'liga', script: 'latn'});
const ligaFeatureLookups = query.latin.getFeatureLookups(ligaFeature);
const ligaLookupTable = ligaFeatureLookups[0];
const ligaLookupSubtables = query.latin.getLookupSubtables(ligaLookupTable);
const ligaSubtable = ligaLookupSubtables[0];
const ligaSubsType = query.latin.getSubstitutionType(ligaLookupTable, ligaSubtable);
const lookup = query.latin.getLookupMethod(ligaLookupTable, ligaSubtable);
assert.deepEqual(ligaSubsType, 41); // supported single substitution '41'
let contextParams = new ContextParams([73, 76], 0);
const substitutions = lookup(contextParams);
assert.deepEqual(substitutions, { ligGlyph: 1145, components: [76]});
});
it('should decompose a glyph - multiple substitution format 1 (21)', function () {
const feature = query.arabic.getFeature({tag: 'ccmp', script: 'arab'});
const featureLookups = query.arabic.getFeatureLookups(feature);
const lookupSubtables = query.arabic.getLookupSubtables(featureLookups[1]);
const substitutionType = query.arabic.getSubstitutionType(featureLookups[1], lookupSubtables[0]);
assert.equal(substitutionType, 21);
const lookup = query.arabic.getLookupMethod(featureLookups[1], lookupSubtables[0]);
const substitution = lookup(271);
assert.deepEqual(substitution, [273, 1087]);
});
});
});

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc