mirror of
https://github.com/kiliman/operator-mono-lig.git
synced 2026-08-12 02:10:52 +02:00
306 lines
8.2 KiB
JavaScript
306 lines
8.2 KiB
JavaScript
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 buildGsubTables = (_dom, ligature, options) => {
|
|
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);
|
|
|
|
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);
|
|
);
|
|
}
|
|
|
|
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);
|
|
|
|
lookupListDom = xpath.select('LookupList', gsubDom, true);
|
|
if (options.italicsHack) {
|
|
// remove all lookups from LookupList
|
|
Array.from(lookupListDom.childNodes).forEach((c) =>
|
|
lookupListDom.removeChild(c)
|
|
);
|
|
}
|
|
|
|
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.buildGsubTables = buildGsubTables;
|
|
exports.finalizeGsubTables = finalizeGsubTables;
|