diff --git a/eslint-local-rules.js b/eslint-local-rules.js new file mode 100644 index 00000000..fdcc8538 --- /dev/null +++ b/eslint-local-rules.js @@ -0,0 +1,43 @@ +'use strict'; + +module.exports = { + 'ban-foreach': { + meta: { + type: 'suggestion', + schema: [], + }, + create(context) { + return { + CallExpression(node) { + if (node.callee.property && node.callee.property.name === 'forEach') { + context.report({node, message: 'Use for() loops instead of .forEach()'}) + } + }, + } + }, + }, + 'import-extensions': { + meta: { + type: 'problem', + schema: [] + }, + create(context) { + const checkImportPath = (node) => { + const importPath = node.source.value; + const isRelative = importPath.startsWith('.') || importPath.startsWith('/'); + const extensionMissing = require('path').extname(importPath) === ''; + if (!isRelative || !extensionMissing) { + return; + } + context.report({ + node: node.source, + message: 'Import paths require a file extension to work in browser module context' + }); + }; + + return { + ImportDeclaration: checkImportPath + }; + }, + } +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4fdece24..34940643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,9 @@ "": { "name": "opentype.js", "license": "MIT", + "dependencies": { + "eslint-plugin-local-rules": "^1.3.2" + }, "bin": { "ot": "bin/ot" }, @@ -556,6 +559,11 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-local-rules": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-local-rules/-/eslint-plugin-local-rules-1.3.2.tgz", + "integrity": "sha512-X4ziX+cjlCYnZa+GB1ly3mmj44v2PeIld3tQVAxelY6AMrhHSjz6zsgsT6nt0+X5b7eZnvL/O7Q3pSSK2kF/+Q==" + }, "node_modules/eslint-scope": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", @@ -2278,6 +2286,11 @@ "text-table": "^0.2.0" } }, + "eslint-plugin-local-rules": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-local-rules/-/eslint-plugin-local-rules-1.3.2.tgz", + "integrity": "sha512-X4ziX+cjlCYnZa+GB1ly3mmj44v2PeIld3tQVAxelY6AMrhHSjz6zsgsT6nt0+X5b7eZnvL/O7Q3pSSK2kF/+Q==" + }, "eslint-scope": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", diff --git a/package.json b/package.json index eadd17b5..13f48871 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "es2021": true, "node": true }, + "plugins": ["eslint-plugin-local-rules"], "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": 2018, @@ -78,7 +79,12 @@ "semi": [ "error", "always" - ] + ], + "local-rules/ban-foreach": 2, + "local-rules/import-extensions": 2 } + }, + "dependencies": { + "eslint-plugin-local-rules": "^1.3.2" } } diff --git a/src/bidi.js b/src/bidi.js index e393af13..7d98c665 100644 --- a/src/bidi.js +++ b/src/bidi.js @@ -78,14 +78,15 @@ function tokenizeText() { */ function reverseArabicSentences() { const ranges = this.tokenizer.getContextRanges('arabicSentence'); - ranges.forEach(range => { + for(let i = 0; i < ranges.length; i++) { + const range = ranges[i]; let rangeTokens = this.tokenizer.getRangeTokens(range); this.tokenizer.replaceRange( range.startIndex, range.endOffset, rangeTokens.reverse() ); - }); + } } /** @@ -153,9 +154,10 @@ function applyArabicPresentationForms() { if (!Object.prototype.hasOwnProperty.call(this.featuresTags, script)) return; checkGlyphIndexStatus.call(this); const ranges = this.tokenizer.getContextRanges('arabicWord'); - ranges.forEach(range => { + for(let i = 0; i < ranges.length; i++) { + const range = ranges[i]; arabicPresentationForms.call(this, range); - }); + } } /** @@ -165,9 +167,10 @@ function applyArabicRequireLigatures() { if (!this.hasFeatureEnabled('arab', 'rlig')) return; checkGlyphIndexStatus.call(this); const ranges = this.tokenizer.getContextRanges('arabicWord'); - ranges.forEach(range => { + for(let i = 0; i < ranges.length; i++) { + const range = ranges[i]; arabicRequiredLigatures.call(this, range); - }); + } } /** @@ -177,16 +180,18 @@ function applyLatinLigatures() { if (!this.hasFeatureEnabled('latn', 'liga')) return; checkGlyphIndexStatus.call(this); const ranges = this.tokenizer.getContextRanges('latinWord'); - ranges.forEach(range => { + for(let i = 0; i < ranges.length; i++) { + const range = ranges[i]; latinLigature.call(this, range); - }); + } } function applyUnicodeVariationSequences() { const ranges = this.tokenizer.getContextRanges('unicodeVariationSequence'); - ranges.forEach(range => { + for(let i = 0; i < ranges.length; i++) { + const range = ranges[i]; unicodeVariationSequences.call(this, range); - }); + } } /** @@ -195,12 +200,12 @@ function applyUnicodeVariationSequences() { function applyThaiFeatures() { checkGlyphIndexStatus.call(this); const ranges = this.tokenizer.getContextRanges('thaiWord'); - ranges.forEach(range => { + for(let i = 0; i < ranges.length; i++) { + const range = ranges[i]; if (this.hasFeatureEnabled('thai', 'liga')) thaiLigatures.call(this, range); if (this.hasFeatureEnabled('thai', 'rlig')) thaiRequiredLigatures.call(this, range); if (this.hasFeatureEnabled('thai', 'ccmp')) thaiGlyphComposition.call(this, range); - }); - + } } /** diff --git a/src/features/applySubstitution.js b/src/features/applySubstitution.js index f6e1ce5d..ce507768 100644 --- a/src/features/applySubstitution.js +++ b/src/features/applySubstitution.js @@ -27,10 +27,11 @@ function singleSubstitutionFormat2(action, tokens, index) { * @param {number} index token index */ function chainingSubstitutionFormat3(action, tokens, index) { - action.substitution.forEach((subst, offset) => { - const token = tokens[index + offset]; + for(let i = 0; i < action.substitution.length; i++) { + const subst = action.substitution[i]; + const token = tokens[index + i]; token.setState(action.tag, subst); - }); + } } /** diff --git a/src/features/arab/arabicPresentationForms.js b/src/features/arab/arabicPresentationForms.js index a15de1ff..7d343a5b 100644 --- a/src/features/arab/arabicPresentationForms.js +++ b/src/features/arab/arabicPresentationForms.js @@ -52,10 +52,11 @@ function arabicPresentationForms(range) { const charContextParams = new ContextParams( tokens.map(token => token.char ), 0); - tokens.forEach((token, index) => { - if (isTashkeelArabicChar(token.char)) return; - contextParams.setCurrentIndex(index); - charContextParams.setCurrentIndex(index); + for(let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + if (isTashkeelArabicChar(token.char)) continue; + contextParams.setCurrentIndex(i); + charContextParams.setCurrentIndex(i); let CONNECT = 0; // 2 bits 00 (10: can connect next) (01: can connect prev) if (willConnectPrev(charContextParams)) CONNECT |= 1; if (willConnectNext(charContextParams)) CONNECT |= 2; @@ -65,18 +66,22 @@ function arabicPresentationForms(range) { case 2: (tag = 'init'); break; case 3: (tag = 'medi'); break; } - if (tags.indexOf(tag) === -1) return; + if (tags.indexOf(tag) === -1) continue; let substitutions = this.query.lookupFeature({ tag, script, contextParams }); - if (substitutions instanceof Error) return console.info(substitutions.message); - substitutions.forEach((action, index) => { + if (substitutions instanceof Error) { + console.info(substitutions.message); + continue; + } + for(let j = 0; j < substitutions.length; j++) { + const action = substitutions[j]; if (action instanceof SubstitutionAction) { - applySubstitution(action, tokens, index); - contextParams.context[index] = action.substitution; + applySubstitution(action, tokens, j); + contextParams.context[j] = action.substitution; } - }); - }); + } + } } export default arabicPresentationForms; diff --git a/src/features/arab/arabicRequiredLigatures.js b/src/features/arab/arabicRequiredLigatures.js index c51bf6d2..bc2c2fed 100644 --- a/src/features/arab/arabicRequiredLigatures.js +++ b/src/features/arab/arabicRequiredLigatures.js @@ -26,18 +26,19 @@ function arabicRequiredLigatures(range) { const script = 'arab'; let tokens = this.tokenizer.getRangeTokens(range); let contextParams = getContextParams(tokens); - contextParams.context.forEach((glyphIndex, index) => { + for (let index = 0; index < contextParams.context.length; index++) { contextParams.setCurrentIndex(index); let substitutions = this.query.lookupFeature({ tag: 'rlig', script, contextParams }); if (substitutions.length) { - substitutions.forEach( - action => applySubstitution(action, tokens, index) - ); + for(let i = 0; i < substitutions.length; i++) { + const action = substitutions[i]; + applySubstitution(action, tokens, index); + } contextParams = getContextParams(tokens); } - }); + } } export default arabicRequiredLigatures; diff --git a/src/features/latn/latinLigatures.js b/src/features/latn/latinLigatures.js index 475df611..a0959f38 100644 --- a/src/features/latn/latinLigatures.js +++ b/src/features/latn/latinLigatures.js @@ -26,18 +26,19 @@ function latinLigature(range) { const script = 'latn'; let tokens = this.tokenizer.getRangeTokens(range); let contextParams = getContextParams(tokens); - contextParams.context.forEach((glyphIndex, index) => { + for(let index = 0; index < contextParams.context.length; index++) { contextParams.setCurrentIndex(index); let substitutions = this.query.lookupFeature({ tag: 'liga', script, contextParams }); if (substitutions.length) { - substitutions.forEach( - action => applySubstitution(action, tokens, index) - ); + for(let i = 0; i < substitutions.length; i++) { + const action = substitutions[i]; + applySubstitution(action, tokens, index); + } contextParams = getContextParams(tokens); } - }); + } } export default latinLigature; diff --git a/src/features/thai/thaiGlyphComposition.js b/src/features/thai/thaiGlyphComposition.js index 4eff31f5..6249a6f0 100644 --- a/src/features/thai/thaiGlyphComposition.js +++ b/src/features/thai/thaiGlyphComposition.js @@ -23,18 +23,19 @@ function thaiGlyphComposition(range) { const script = 'thai'; let tokens = this.tokenizer.getRangeTokens(range); let contextParams = getContextParams(tokens, 0); - contextParams.context.forEach((glyphIndex, index) => { + for(let index = 0; index < contextParams.context.length; index++) { contextParams.setCurrentIndex(index); let substitutions = this.query.lookupFeature({ tag: 'ccmp', script, contextParams }); if (substitutions.length) { - substitutions.forEach( - action => applySubstitution(action, tokens, index) - ); + for(let i = 0; i < substitutions.length; i++) { + const action = substitutions[i]; + applySubstitution(action, tokens, index); + } contextParams = getContextParams(tokens, index); } - }); + } } export default thaiGlyphComposition; diff --git a/src/features/thai/thaiLigatures.js b/src/features/thai/thaiLigatures.js index a192d838..cb968a93 100644 --- a/src/features/thai/thaiLigatures.js +++ b/src/features/thai/thaiLigatures.js @@ -5,6 +5,9 @@ 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 @@ -23,18 +26,19 @@ function thaiLigatures(range) { const script = 'thai'; let tokens = this.tokenizer.getRangeTokens(range); let contextParams = getContextParams(tokens, 0); - contextParams.context.forEach((glyphIndex, index) => { + for(let index = 0; index < contextParams.context.length; index++) { contextParams.setCurrentIndex(index); let substitutions = this.query.lookupFeature({ tag: 'liga', script, contextParams }); if (substitutions.length) { - substitutions.forEach( - action => applySubstitution(action, tokens, index) - ); + for(let i = 0; i < substitutions.length; i++) { + const action = substitutions[i]; + applySubstitution(action, tokens, index); + } contextParams = getContextParams(tokens, index); } - }); + } } export default thaiLigatures; diff --git a/src/features/thai/thaiRequiredLigatures.js b/src/features/thai/thaiRequiredLigatures.js index 5dc1a7fb..4e0bbde0 100644 --- a/src/features/thai/thaiRequiredLigatures.js +++ b/src/features/thai/thaiRequiredLigatures.js @@ -5,6 +5,9 @@ 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 @@ -23,18 +26,19 @@ function thaiRequiredLigatures(range) { const script = 'thai'; let tokens = this.tokenizer.getRangeTokens(range); let contextParams = getContextParams(tokens, 0); - contextParams.context.forEach((glyphIndex, index) => { + for(let index = 0; index < contextParams.context.length; index++) { contextParams.setCurrentIndex(index); let substitutions = this.query.lookupFeature({ tag: 'rlig', script, contextParams }); if (substitutions.length) { - substitutions.forEach( - action => applySubstitution(action, tokens, index) - ); + for(let i = 0; i < substitutions.length; i++) { + const action = substitutions[i]; + applySubstitution(action, tokens, index); + } contextParams = getContextParams(tokens, index); } - }); + } } export default thaiRequiredLigatures; diff --git a/src/tables/gsub.js b/src/tables/gsub.js index f0801d5c..3fb27276 100644 --- a/src/tables/gsub.js +++ b/src/tables/gsub.js @@ -282,11 +282,12 @@ subtableMakers[5] = function makeLookup5(subtable) { // ("glyphCount" in the spec) comes before seqLookupCount [tableData[0], tableData[1]] = [tableData[1], tableData[0]]; - sequenceRule.lookupRecords.forEach((record, i) => { + for(let i = 0; i < sequenceRule.lookupRecords.length; i++) { + const record = sequenceRule.lookupRecords[i]; tableData = tableData .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex}) .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex}); - }); + } return new table.Table('sequenceRuleTable', tableData); })); }))); @@ -302,12 +303,12 @@ subtableMakers[5] = function makeLookup5(subtable) { return new table.Table('classSeqRuleSetTable', table.tableList('classSeqRule', classSeqRuleSet, function(classSeqRule) { let tableData = table.ushortList('classes', classSeqRule.classes, classSeqRule.classes.length + 1) .concat(table.ushortList('seqLookupCount', [], classSeqRule.lookupRecords.length)); - - classSeqRule.lookupRecords.forEach((record, i) => { + for(let i = 0; i < classSeqRule.lookupRecords.length; i++) { + const record = classSeqRule.lookupRecords[i]; tableData = tableData .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex}) .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex}); - }); + } return new table.Table('classSeqRuleTable', tableData); })); }))); @@ -318,15 +319,17 @@ subtableMakers[5] = function makeLookup5(subtable) { tableData.push({name: 'inputGlyphCount', type: 'USHORT', value: subtable.coverages.length}); tableData.push({name: 'substitutionCount', type: 'USHORT', value: subtable.lookupRecords.length}); - subtable.coverages.forEach((coverage, i) => { + for(let i = 0; i < subtable.coverages.length; i++) { + const coverage = subtable.coverages[i]; tableData.push({name: 'inputCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)}); - }); + } - subtable.lookupRecords.forEach((record, i) => { + for(let i = 0; i < subtable.lookupRecords.length; i++) { + const record = subtable.lookupRecords[i]; tableData = tableData .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex}) .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex}); - }); + } let returnTable = new table.Table('contextualSubstitutionTable', tableData); @@ -348,11 +351,12 @@ subtableMakers[6] = function makeLookup6(subtable) { .concat(table.ushortList('lookaheadGlyph', chainRule.lookahead, chainRule.lookahead.length)) .concat(table.ushortList('substitution', [], chainRule.lookupRecords.length)); - chainRule.lookupRecords.forEach((record, i) => { + for(let i = 0; i < chainRule.lookupRecords.length; i++) { + const record = chainRule.lookupRecords[i]; tableData = tableData .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex}) .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex}); - }); + } return new table.Table('chainRuleTable', tableData); })); }))); @@ -365,24 +369,30 @@ subtableMakers[6] = function makeLookup6(subtable) { ]; tableData.push({name: 'backtrackGlyphCount', type: 'USHORT', value: subtable.backtrackCoverage.length}); - subtable.backtrackCoverage.forEach((coverage, i) => { + for(let i = 0; i < subtable.backtrackCoverage.length; i++) { + const coverage = subtable.backtrackCoverage[i]; tableData.push({name: 'backtrackCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)}); - }); + } tableData.push({name: 'inputGlyphCount', type: 'USHORT', value: subtable.inputCoverage.length}); - subtable.inputCoverage.forEach((coverage, i) => { + + for(let i = 0; i < subtable.inputCoverage.length; i++) { + const coverage = subtable.inputCoverage[i]; tableData.push({name: 'inputCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)}); - }); + } tableData.push({name: 'lookaheadGlyphCount', type: 'USHORT', value: subtable.lookaheadCoverage.length}); - subtable.lookaheadCoverage.forEach((coverage, i) => { + + for(let i = 0; i < subtable.lookaheadCoverage.length; i++) { + const coverage = subtable.lookaheadCoverage[i]; tableData.push({name: 'lookaheadCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)}); - }); + } tableData.push({name: 'substitutionCount', type: 'USHORT', value: subtable.lookupRecords.length}); - subtable.lookupRecords.forEach((record, i) => { + for(let i = 0; i < subtable.lookupRecords.length; i++) { + const record = subtable.lookupRecords[i]; tableData = tableData .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex}) .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex}); - }); + } let returnTable = new table.Table('chainContextTable', tableData); diff --git a/src/tokenizer.js b/src/tokenizer.js index 86670db3..ab5e64dc 100644 --- a/src/tokenizer.js +++ b/src/tokenizer.js @@ -10,6 +10,8 @@ function Token(char) { this.char = char; this.state = {}; this.activeState = null; + + } /** @@ -79,29 +81,32 @@ function initializeCoreEvents(events) { 'replaceToken', 'replaceRange', 'composeRUD', 'updateContextsRanges' ]; - coreEvents.forEach(eventId => { + for(let i = 0; i < coreEvents.length; i++) { + const eventId = coreEvents[i]; Object.defineProperty(this.events, eventId, { value: new Event(eventId) }); - }); + } if (events) { - coreEvents.forEach(eventId => { + for(let i = 0; i < coreEvents.length; i++) { + const eventId = coreEvents[i]; const event = events[eventId]; if (typeof event === 'function') { this.events[eventId].subscribe(event); } - }); + } } const requiresContextUpdate = [ 'insertToken', 'removeToken', 'removeRange', 'replaceToken', 'replaceRange', 'composeRUD' ]; - requiresContextUpdate.forEach(eventId => { + for(let i = 0; i < requiresContextUpdate.length; i++) { + const eventId = requiresContextUpdate[i]; this.events[eventId].subscribe( this.updateContextsRanges ); - }); + } } /** @@ -374,9 +379,10 @@ Tokenizer.prototype.on = function(eventName, eventHandler) { Tokenizer.prototype.dispatch = function(eventName, args) { const event = this.events[eventName]; if (event instanceof Event) { - event.subscribers.forEach(subscriber => { + for(let i = 0; i < event.subscribers.length; i++) { + const subscriber = event.subscribers[i]; subscriber.apply(this, args || []); - }); + } } }; @@ -480,7 +486,8 @@ Tokenizer.prototype.setEndOffset = function (offset, contextName) { */ Tokenizer.prototype.runContextCheck = function(contextParams) { const index = contextParams.index; - this.contextCheckers.forEach(contextChecker => { + for(let i = 0; i < this.contextCheckers.length; i++) { + const contextChecker = this.contextCheckers[i]; let contextName = contextChecker.contextName; let openRange = this.getContext(contextName).openRange; if (!openRange && contextChecker.checkStart(contextParams)) { @@ -493,7 +500,7 @@ Tokenizer.prototype.runContextCheck = function(contextParams) { const range = this.setEndOffset(offset, contextName); this.dispatch('contextEnd', [contextName, range]); } - }); + } }; /**