diff --git a/gsub.js b/gsub.js deleted file mode 100644 index c30655e..0000000 --- a/gsub.js +++ /dev/null @@ -1,313 +0,0 @@ -const xpath = require('xpath'); -const NodeType = {}; -NodeType.TEXT_NODE = 3; - -let dom; -let gsubDom; -let scriptListDom; -let featureListDom; -let lookupListDom; -let featureDom; -let lookupIndex = 0; -let chainIndex = 0; -const substLookupMap = {}; - -const initGsubTables = (_dom, options) => { - dom = _dom; - - gsubDom = xpath.select('ttFont/GSUB', dom, true); - - // look for 'calt' feature - featureListDom = xpath.select('FeatureList', gsubDom, true); - lookupListDom = xpath.select('LookupList', gsubDom, true); - - if (options.italicsHack) { - // remove all features from FeatureList - Array.from(featureListDom.childNodes).forEach((c) => { - featureListDom.removeChild(c); - }); - - Array.from(xpath.select('ttFont/GSUB//FeatureIndex', dom)).forEach((c) => { - c.parentNode.removeChild(c); - }); - - // remove all lookups from LookupList - Array.from(lookupListDom.childNodes).forEach((c) => - lookupListDom.removeChild(c) - ); - } -}; - -const buildGsubTables = (_dom, ligature) => { - dom = _dom; - - if (/_/.test(ligature.glyph) === false) { - // not a ligature, so see if glyph exists in font (as substitute) - const glyphNode = xpath.select( - `ttFont/GlyphOrder/GlyphID[@name="${ligature.name}"]`, - dom, - true - ); - if (!glyphNode) { - return; - } - } - - const glyphs = ligature.name.replace(/\.liga$/, '').split('_'); - const isSingleGlyph = glyphs.length == 1; - - gsubDom = xpath.select('ttFont/GSUB', dom, true); - - // look for 'calt' feature - featureListDom = xpath.select('FeatureList', gsubDom, true); - - let featureRecord; - const featureTag = xpath.select( - 'FeatureRecord/FeatureTag[@value="calt"]', - featureListDom, - true - ); - if (!featureTag) { - const featureListCount = xpath.select( - 'count(FeatureRecord)', - featureListDom, - true - ); - featureRecord = createElementWithAttributes('FeatureRecord', { - index: featureListCount, - }); - featureRecord.appendChild( - createElementWithAttributes('FeatureTag', { value: 'calt' }) - ); - featureRecord.appendChild(dom.createElement('Feature')); - featureListDom.appendChild(featureRecord); - } else { - featureRecord = featureTag.parentNode; - } - featureDom = xpath.select('Feature', featureRecord, true); - - const featureIndex = featureRecord.getAttribute('index'); - - // add feature to scriptlist for DFLT and latn if not present - addFeatureToScriptList('DFLT', featureIndex); - addFeatureToScriptList('latn', featureIndex); - - lookupIndex = xpath.select('count(Lookup)', lookupListDom, true); - - const lookupDom = createElementWithAttributes('Lookup', { - index: lookupIndex++, - }); - appendChildren( - lookupDom, - createElementWithAttributes('LookupType', { value: isSingleGlyph ? 1 : 6 }), - createElementWithAttributes('LookupFlag', { value: 0 }) - ); - - // add contextual lookup to feature - const featureLookupCount = xpath.select( - 'count(LookupListIndex)', - featureDom, - true - ); - featureDom.appendChild( - createElementWithAttributes('LookupListIndex', { - index: featureLookupCount, - value: lookupDom.getAttribute('index'), - }) - ); - - if (isSingleGlyph) { - lookupDom.appendChild( - getSubstLookup(ligature.name, ligature.glyph, lookupDom) - ); - } else { - lookupDom.appendChild(buildBacktrackFirst(glyphs)); - lookupDom.appendChild(buildLookAheadLast(glyphs)); - lookupDom.appendChild(buildLigatureSubst(glyphs, ligature.glyph)); - // remaining context - for (let n = glyphs.length - 2; n >= 0; n--) { - lookupDom.appendChild( - buildChainContext(getLIGs(n), [glyphs[n]], glyphs.slice(n + 1), 'LIG') - ); - } - } - lookupListDom.appendChild(lookupDom); -}; - -const finalizeGsubTables = () => { - // get current LookupList count - let newIndex = xpath.select('count(Lookup)', lookupListDom, true); - - // append added lookups - Object.entries(substLookupMap).forEach(([, lookup]) => { - // fixup existing lookups with new index - const oldIndex = lookup.getAttribute('index'); - const substLookups = xpath.select( - `//ChainContextSubst/SubstLookupRecord/LookupListIndex[@value="${oldIndex}"]`, - lookupListDom, - false - ); - substLookups.forEach((substLookup) => { - substLookup.setAttribute('value', newIndex); - }); - lookup.setAttribute('index', newIndex++); - lookupListDom.appendChild(lookup); - }); -}; - -//const dump = dom => console.log(format(serialize(dom))); - -const addFeatureToScriptList = (tag, featureIndex) => { - scriptListDom = xpath.select('ScriptList', gsubDom, true); - const defaultLangSys = xpath.select( - `ScriptRecord/ScriptTag[@value="${tag}"]/../Script/DefaultLangSys`, - scriptListDom, - true - ); - const featureIndexNode = xpath.select( - `FeatureIndex[@value=${featureIndex}]`, - defaultLangSys, - true - ); - if (!featureIndexNode) { - const count = xpath.select('count(FeatureIndex)', defaultLangSys, true); - defaultLangSys.appendChild( - createElementWithAttributes('FeatureIndex', { - index: count, - value: featureIndex, - }) - ); - } -}; - -const buildBacktrackFirst = (glyphs) => { - // backtrack = first glyph - // input = first glphy - // lookAhead = [1..n] - return buildChainContext([glyphs[0]], [glyphs[0]], glyphs.slice(1)); -}; - -const buildLookAheadLast = (glyphs) => { - // backtrack = none - // input = first glyph - // lookAhead = [1..n] + [n] - return buildChainContext( - [], - [glyphs[0]], - [...glyphs.slice(1), glyphs.slice(-1)] - ); -}; - -const buildLigatureSubst = (glyphs, lookup) => { - // backtrack = LIG * n-1 - // input = last glyph - // lookAhead = none - return buildChainContext( - getLIGs(glyphs.length - 1), - glyphs.slice(-1), - [], - lookup - ); -}; - -const getLIGs = (length) => new Array(length).fill('LIG', 0, length); - -const buildChainContext = (backtrack, input, lookAhead, substitute) => { - const chainDom = createElementWithAttributes('ChainContextSubst', { - index: chainIndex++, - Format: 3, - }); - - buildCoverage(chainDom, 'BacktrackCoverage', backtrack); - buildCoverage(chainDom, 'InputCoverage', input); - buildCoverage(chainDom, 'LookAheadCoverage', lookAhead); - - if (substitute) { - const lookup = getSubstLookup(input[0], substitute); - const listIndex = lookup.getAttribute('index'); - const substLookupRecordDom = chainDom.appendChild( - createElementWithAttributes('SubstLookupRecord', { index: 0 }) - ); - appendChildren( - substLookupRecordDom, - createElementWithAttributes('SequenceIndex', { value: 0 }), - createElementWithAttributes('LookupListIndex', { value: listIndex }) - ); - } - return chainDom; -}; - -const buildCoverage = (chainDom, tagName, coverage) => { - if (coverage) { - coverage.forEach((glyph, i) => { - const coverageDom = createElementWithAttributes(tagName, { - index: i++, - Format: 1, - }); - coverageDom.appendChild( - createElementWithAttributes('Glyph', { value: glyph }) - ); - chainDom.appendChild(coverageDom); - }); - } -}; - -const getSubstLookup = (input, output, lookupDom) => { - if (!lookupDom) { - lookupDom = substLookupMap[output]; - } - - if (!lookupDom) { - lookupDom = createElementWithAttributes('Lookup', { - index: Object.keys(substLookupMap).length + 1, - }); - lookupDom.appendChild( - createElementWithAttributes('LookupType', { value: 1 }) - ); - lookupDom.appendChild( - createElementWithAttributes('LookupFlag', { value: 0 }) - ); - substLookupMap[output] = lookupDom; - } - - let singleSubstDom = xpath.select('SingleSubst', lookupDom, true); - - if (!singleSubstDom) { - singleSubstDom = createElementWithAttributes('SingleSubst', { - index: 0, - Format: 2, - }); - lookupDom.appendChild(singleSubstDom); - } - - // look for input subst - const substitionDom = xpath.select( - `Substitution[@in="${input}"]`, - singleSubstDom, - true - ); - if (!substitionDom) { - singleSubstDom.appendChild( - createElementWithAttributes('Substitution', { in: input, out: output }) - ); - } - return lookupDom; -}; - -const createElementWithAttributes = (tagName, attributes) => { - const element = dom.createElement(tagName); - Object.entries(attributes).forEach((attribute) => { - element.setAttribute(attribute[0], attribute[1]); - }); - return element; -}; - -const appendChildren = (node, ...children) => { - children.forEach((child) => node.appendChild(child)); -}; - -//const serialize = dom => new XMLSerializer().serializeToString(dom); - -exports.initGsubTables = initGsubTables; -exports.buildGsubTables = buildGsubTables; -exports.finalizeGsubTables = finalizeGsubTables; diff --git a/index.js b/index.js index 0d3363f..ad68ffe 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,11 @@ +// @ts-nocheck const fs = require('fs'); const os = require('os'); if (!os.EOL) { os.EOL = process.platform === 'win32' ? '\r\n' : '\n'; } -const gsub = require('./gsub'); +const calt = require('./calt'); const xpath = require('xpath'); const { DOMParser, XMLSerializer } = require('xmldom'); @@ -60,11 +61,13 @@ const buildFont = (profile, options) => { //[...ligatures, { name: 'LIG', glyph: 'LIG' }]; processPatch('names', patchNames, dom, ligatures, profile); - processPatch('gsub', patchGsub, dom, ligatures, options); processPatch('charstrings', patchCharStrings, dom, ligaturesWithLIG); processPatch('glyphs', patchGlyphs, dom, ligaturesWithLIG); processPatch('hmtx', patchHmtx, dom); + const feature = calt.gencalt(ligatures); + fs.writeFileSync('./features/calt.fea', feature); + console.log(`Writing ligature font file ${dstFileName}`); fs.writeFileSync(dstFileName, format(serialize(dom))); };