diff --git a/docs/index.html b/docs/index.html index ed92e27e..972f1f2e 100755 --- a/docs/index.html +++ b/docs/index.html @@ -114,7 +114,7 @@

Free Software

hinting: form.hinting.checked, features: { liga: form.ligatures.checked, - rlig: form.ligatures.checked + rlig: true } }; previewCtx.clearRect(0, 0, 940, 300); diff --git a/src/bidi.js b/src/bidi.js index bbc77907..23df1bfc 100644 --- a/src/bidi.js +++ b/src/bidi.js @@ -10,7 +10,7 @@ import arabicSentenceCheck from './features/arab/contextCheck/arabicSentence.js' import arabicPresentationForms from './features/arab/arabicPresentationForms.js'; import arabicRequiredLigatures from './features/arab/arabicRequiredLigatures.js'; import latinWordCheck from './features/latn/contextCheck/latinWord.js'; -import latinLigature from './features/latn/latinLigatures.js'; +import { latinRequiredLigature, latinLigature } from './features/latn/latinLigatures.js'; import thaiWordCheck from './features/thai/contextCheck/thaiWord.js'; import unicodeVariationSequenceCheck from './features/unicode/contextCheck/variationSequenceCheck.js'; import unicodeVariationSequences from './features/unicode/variationSequences.js'; @@ -281,15 +281,26 @@ function applyArabicRequireLigatures() { } /** - * Apply required arabic ligatures + * Apply required and normal latin ligatures */ function applyLatinLigatures() { - if (!this.hasFeatureEnabled('latn', 'liga')) return; + const script = 'latn'; + if (!Object.prototype.hasOwnProperty.call(this.featuresTags, script)) return; + const tags = this.featuresTags[script]; + if ((tags.indexOf('rlig') === -1) && (tags.indexOf('liga') === -1)) return; checkGlyphIndexStatus.call(this); const ranges = this.tokenizer.getContextRanges('latinWord'); + for(let i = 0; i < ranges.length; i++) { const range = ranges[i]; - latinLigature.call(this, range); + + if (tags.indexOf('rlig') >= 0) { + latinRequiredLigature.call(this, range); + } + + if (tags.indexOf('liga') >= 0) { + latinLigature.call(this, range); + } } } diff --git a/src/features/arab/arabicRequiredLigatures.js b/src/features/arab/arabicRequiredLigatures.js index c2fe38c0..87c72cb8 100644 --- a/src/features/arab/arabicRequiredLigatures.js +++ b/src/features/arab/arabicRequiredLigatures.js @@ -2,43 +2,14 @@ * Apply Arabic required ligatures feature to a range of tokens */ -import { ContextParams } from '../../tokenizer.js'; -import applySubstitution from '../applySubstitution.js'; - -// @TODO: use commonFeatureUtils.js for reduction of code duplication -// once #564 has been merged. - -/** - * 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); -} +import { applyFeatureToRange } from '../commonFeatureUtils.js'; /** * Apply Arabic required ligatures to a context range * @param {ContextRange} range a range of tokens */ function arabicRequiredLigatures(range) { - const script = 'arab'; - let tokens = this.tokenizer.getRangeTokens(range); - let contextParams = getContextParams(tokens); - for (let index = 0; index < contextParams.context.length; index++) { - contextParams.setCurrentIndex(index); - let substitutions = this.query.lookupFeature({ - tag: 'rlig', script, contextParams - }); - if (substitutions.length) { - for(let i = 0; i < substitutions.length; i++) { - const action = substitutions[i]; - applySubstitution.call(this, action, tokens, index); - } - contextParams = getContextParams(tokens); - } - } + applyFeatureToRange.apply(this, ['arab', 'rlig', range]); } export default arabicRequiredLigatures; diff --git a/src/features/commonFeatureUtils.js b/src/features/commonFeatureUtils.js new file mode 100644 index 00000000..f83c470a --- /dev/null +++ b/src/features/commonFeatureUtils.js @@ -0,0 +1,38 @@ +import { ContextParams } from '../tokenizer.js'; +import applySubstitution from './applySubstitution.js'; + +/** + * 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 feature substitutions to a context range + * @param {string} script a script tag + * @param {string} feature 4-letter feature code + * @param {ContextRange} range a range of tokens + */ +function applyFeatureToRange(script, feature, range) { + let tokens = this.tokenizer.getRangeTokens(range); + let contextParams = getContextParams(tokens); + for (let index = 0; index < contextParams.context.length; index++) { + contextParams.setCurrentIndex(index); + let substitutions = this.query.lookupFeature({ + tag: feature, script, contextParams + }); + if (substitutions.length) { + for(let i = 0; i < substitutions.length; i++) { + const action = substitutions[i]; + applySubstitution(action, tokens, index); + } + contextParams = getContextParams(tokens); + } + } +} + +export { getContextParams, applyFeatureToRange }; diff --git a/src/features/latn/latinLigatures.js b/src/features/latn/latinLigatures.js index 5d06da02..f80eab47 100644 --- a/src/features/latn/latinLigatures.js +++ b/src/features/latn/latinLigatures.js @@ -2,43 +2,23 @@ * Apply Latin ligature feature to a range of tokens */ -import { ContextParams } from '../../tokenizer.js'; -import applySubstitution from '../applySubstitution.js'; - -// @TODO: use commonFeatureUtils.js for reduction of code duplication -// once #564 has been merged. +import { applyFeatureToRange } from '../commonFeatureUtils.js'; /** - * Update context params - * @param {any} tokens a list of tokens - * @param {number} index current item index + * Apply Latin required ligatures to a context range + * @param {ContextRange} range a range of tokens */ -function getContextParams(tokens, index) { - const context = tokens.map(token => token.activeState.value); - return new ContextParams(context, index || 0); +function latinRequiredLigature(range) { + applyFeatureToRange.apply(this, ['latn', 'rlig', range]); } /** - * Apply Arabic required ligatures to a context range + * Apply Latin ligatures to a context range * @param {ContextRange} range a range of tokens */ function latinLigature(range) { - const script = 'latn'; - let tokens = this.tokenizer.getRangeTokens(range); - let contextParams = getContextParams(tokens); - for(let index = 0; index < contextParams.context.length; index++) { - contextParams.setCurrentIndex(index); - let substitutions = this.query.lookupFeature({ - tag: 'liga', script, contextParams - }); - if (substitutions.length) { - for(let i = 0; i < substitutions.length; i++) { - const action = substitutions[i]; - applySubstitution.call(this, action, tokens, index); - } - contextParams = getContextParams(tokens); - } - } + applyFeatureToRange.apply(this, ['latn', 'liga', range]); } export default latinLigature; +export { latinRequiredLigature, latinLigature }; diff --git a/test/bidi.js b/test/bidi.js index 648fef6c..2d7f57b8 100644 --- a/test/bidi.js +++ b/test/bidi.js @@ -7,8 +7,11 @@ const loadSync = (url, opt) => parse(readFileSync(url), opt); describe('bidi.js', function() { let latinFont; let arabicFont; + let scriptFont; let bidiFira; let bidiScheherazade; + let bidiPecita; + let bidiPecitaNoRlig; let arabicTokenizer; before(function () { @@ -40,6 +43,28 @@ describe('bidi.js', function() { tags: ['liga', 'rlig'] }]; bidiFira.applyFeatures(latinFont, latinFeatures); + /** + * script font for rlig tests + */ + scriptFont = loadSync('./test/fonts/Pecita.ttf'); + bidiPecita = new Bidi(); + bidiPecita.registerModifier( + 'glyphIndex', null, token => scriptFont.charToGlyphIndex(token.char) + ); + bidiPecitaNoRlig = new Bidi(); + bidiPecitaNoRlig.registerModifier( + 'glyphIndex', null, token => scriptFont.charToGlyphIndex(token.char) + ); + const scriptFeatures = [{ + script: 'latn', + tags: ['liga', 'rlig'] + }]; + const scriptFeaturesNoRlig = [{ + script: 'latn', + tags: ['liga'] + }]; + bidiPecita.applyFeatures(scriptFont, scriptFeatures); + bidiPecitaNoRlig.applyFeatures(scriptFont, scriptFeaturesNoRlig); }); describe('arabic contexts', function() { it('should match arabic words in a given text', function() { @@ -83,6 +108,14 @@ describe('bidi.js', function() { let glyphIndexes = bidiScheherazade.getTextGlyphs('َّ'); // Arabic word 'َّ' : 'Fatha & Shadda' assert.deepEqual(glyphIndexes, [1311]); }); + it('should apply required latin ligature', function () { + let glyphIndexes = bidiPecita.getTextGlyphs('quick'); + assert.deepEqual(glyphIndexes, [4130, 79, 3676]); // "qu" and "ck" rlig + }); + it('should render differently without required latin ligatures', function () { + let glyphIndexes = bidiPecitaNoRlig.getTextGlyphs('quick'); // no rligs + assert.deepEqual(glyphIndexes, [87, 91, 79, 73, 81]); + }); it('should apply latin ligature', function () { let glyphIndexes = bidiFira.getTextGlyphs('fi'); // fi => fi assert.deepEqual(glyphIndexes, [1145]); diff --git a/test/fonts/Pecita.ttf b/test/fonts/Pecita.ttf new file mode 100644 index 00000000..4ac891f4 Binary files /dev/null and b/test/fonts/Pecita.ttf differ