diff --git a/build.bat b/build.bat index 320343f..ff18de5 100644 --- a/build.bat +++ b/build.bat @@ -3,19 +3,25 @@ setlocal if not exist .\build\* mkdir build +set flags= +if "%1"=="--italics-hack-off" ( + set flags=%1 + shift +) rem only build passed in font -if not "%1%"=="" ( - call :build_font %1 +if not "%1"=="" ( + call :build_font %1 %flags% exit /b ) rem build all fonts -for /d %%d in (.\ligature\*) do call :build_font %%~nd +for /d %%d in (.\ligature\*) do call :build_font %%~nd %flags% exit /b :build_font set lig=%1 +set flags=%2 set otf=%lig:Lig-=-% if not exist .\original\%otf%.otf exit /b @@ -23,6 +29,6 @@ if not exist .\ligature\%lig%\glyphs\* exit /b @echo Building %lig% ttx -f .\original\%otf%.otf -node index.js %otf% -for %%f in (.\build\%lig%*.ttx) do ttx -f %%f +node index.js %otf% %flags% +ttx -f .\build\%lig%.ttx exit /b diff --git a/build.sh b/build.sh index a27374d..3ccf9d0 100755 --- a/build.sh +++ b/build.sh @@ -4,6 +4,7 @@ mkdir -p build build_font() { lig="$1" + flags="$2" otf=${lig/Lig-/-} if [ ! -e "./original/$otf.otf" ] @@ -17,20 +18,24 @@ build_font() { echo Building $1 ttx -f "./original/$otf.otf" - node index.js $otf + node index.js $otf $flags - for f in ./build/$1*.ttx ; do - ttx -f $f - done + ttx -f ./build/$1.ttx } +flags= +if [ "$1" = "--italics-hack-off" ] +then + flags=$1 + shift +fi if [ -n "$1" ] then # build specified font - build_font $1 + build_font $1 $flags else # build all available fonts for d in ./ligature/*/ ; do - build_font $(basename $d) + build_font $(basename $d) $flags done fi diff --git a/gsub.js b/gsub.js index 3e8f6ef..7fe456e 100644 --- a/gsub.js +++ b/gsub.js @@ -12,7 +12,33 @@ let lookupIndex = 0; let chainIndex = 0; const substLookupMap = {}; -const buildGsubTables = (_dom, ligature) => { +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, options) => { dom = _dom; if (/_/.test(ligature.glyph) === false) { @@ -48,7 +74,7 @@ const buildGsubTables = (_dom, ligature) => { true ); featureRecord = createElementWithAttributes('FeatureRecord', { - index: featureListCount + index: featureListCount, }); featureRecord.appendChild( createElementWithAttributes('FeatureTag', { value: 'calt' }) @@ -66,11 +92,10 @@ const buildGsubTables = (_dom, ligature) => { addFeatureToScriptList('DFLT', featureIndex); addFeatureToScriptList('latn', featureIndex); - lookupListDom = xpath.select('LookupList', gsubDom, true); lookupIndex = xpath.select('count(Lookup)', lookupListDom, true); const lookupDom = createElementWithAttributes('Lookup', { - index: lookupIndex++ + index: lookupIndex++, }); appendChildren( lookupDom, @@ -87,7 +112,7 @@ const buildGsubTables = (_dom, ligature) => { featureDom.appendChild( createElementWithAttributes('LookupListIndex', { index: featureLookupCount, - value: lookupDom.getAttribute('index') + value: lookupDom.getAttribute('index'), }) ); @@ -122,12 +147,11 @@ const finalizeGsubTables = () => { lookupListDom, false ); - substLookups.forEach(substLookup => { + substLookups.forEach((substLookup) => { substLookup.setAttribute('value', newIndex); }); lookup.setAttribute('index', newIndex++); lookupListDom.appendChild(lookup); - lookupListDom.appendChild(lookup); }); }; @@ -150,20 +174,20 @@ const addFeatureToScriptList = (tag, featureIndex) => { defaultLangSys.appendChild( createElementWithAttributes('FeatureIndex', { index: count, - value: featureIndex + value: featureIndex, }) ); } }; -const buildBacktrackFirst = glyphs => { +const buildBacktrackFirst = (glyphs) => { // backtrack = first glyph // input = first glphy // lookAhead = [1..n] return buildChainContext([glyphs[0]], [glyphs[0]], glyphs.slice(1)); }; -const buildLookAheadLast = glyphs => { +const buildLookAheadLast = (glyphs) => { // backtrack = none // input = first glyph // lookAhead = [1..n] + [n] @@ -186,12 +210,12 @@ const buildLigatureSubst = (glyphs, lookup) => { ); }; -const getLIGs = length => new Array(length).fill('LIG', 0, length); +const getLIGs = (length) => new Array(length).fill('LIG', 0, length); const buildChainContext = (backtrack, input, lookAhead, substitute) => { const chainDom = createElementWithAttributes('ChainContextSubst', { index: chainIndex++, - Format: 3 + Format: 3, }); buildCoverage(chainDom, 'BacktrackCoverage', backtrack); @@ -218,7 +242,7 @@ const buildCoverage = (chainDom, tagName, coverage) => { coverage.forEach((glyph, i) => { const coverageDom = createElementWithAttributes(tagName, { index: i++, - Format: 1 + Format: 1, }); coverageDom.appendChild( createElementWithAttributes('Glyph', { value: glyph }) @@ -235,7 +259,7 @@ const getSubstLookup = (input, output, lookupDom) => { if (!lookupDom) { lookupDom = createElementWithAttributes('Lookup', { - index: Object.keys(substLookupMap).length + 1 + index: Object.keys(substLookupMap).length + 1, }); lookupDom.appendChild( createElementWithAttributes('LookupType', { value: 1 }) @@ -251,7 +275,7 @@ const getSubstLookup = (input, output, lookupDom) => { if (!singleSubstDom) { singleSubstDom = createElementWithAttributes('SingleSubst', { index: 0, - Format: 2 + Format: 2, }); lookupDom.appendChild(singleSubstDom); } @@ -272,17 +296,18 @@ const getSubstLookup = (input, output, lookupDom) => { const createElementWithAttributes = (tagName, attributes) => { const element = dom.createElement(tagName); - Object.entries(attributes).forEach(attribute => { + Object.entries(attributes).forEach((attribute) => { element.setAttribute(attribute[0], attribute[1]); }); return element; }; const appendChildren = (node, ...children) => { - children.forEach(child => node.appendChild(child)); + 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 fd3e051..0d3363f 100644 --- a/index.js +++ b/index.js @@ -17,8 +17,12 @@ const NodeType = {}; NodeType.TEXT_NODE = 3; let dom; +let italicsHack = false; function main() { fontName = process.argv[2]; + italicsHack = + fontName.includes('Italic') && process.argv[3] !== '--italics-hack-off'; + ligFontName = fontName.split('-').join('Lig-'); const srcFileName = `./original/${fontName}.ttx`; @@ -29,12 +33,12 @@ function main() { const profiles = getProfiles(); // process settings (there may be more than one font to build) - profiles.forEach(profile => buildFont(profile)); + profiles.forEach((profile) => buildFont(profile, { italicsHack })); console.log('Done'); } -const buildFont = profile => { +const buildFont = (profile, options) => { // add suffix to dstFileName if present const dstFileName = `./build/${ligFontName}${profile.suffixWithLeadingHyphen}.ttx`; console.log( @@ -44,19 +48,19 @@ const buildFont = profile => { const ligatures = sortLigatures( fs .readdirSync(`./ligature/${ligFontName}/glyphs`) - .filter(file => /\.xml$/.test(file)) - .map(file => file.replace('.xml', '')) + .filter((file) => /\.xml$/.test(file)) + .map((file) => file.replace('.xml', '')) ) - .filter(name => !/\d+\.liga$/.test(name)) // skip alternates (ends with .#) - .filter(name => filterLigatures(name, profile.ligatures)) - .map(name => mapLigatures(name, profile.ligatures)) - .filter(entry => filterGlyphsExists(entry)); + .filter((name) => !/\d+\.liga$/.test(name)) // skip alternates (ends with .#) + .filter((name) => filterLigatures(name, profile.ligatures)) + .map((name) => mapLigatures(name, profile.ligatures)) + .filter((entry) => filterGlyphsExists(entry)); const ligaturesWithLIG = ligatures; //[...ligatures, { name: 'LIG', glyph: 'LIG' }]; processPatch('names', patchNames, dom, ligatures, profile); - processPatch('gsub', patchGsub, dom, ligatures); + processPatch('gsub', patchGsub, dom, ligatures, options); processPatch('charstrings', patchCharStrings, dom, ligaturesWithLIG); processPatch('glyphs', patchGlyphs, dom, ligaturesWithLIG); processPatch('hmtx', patchHmtx, dom); @@ -75,8 +79,8 @@ const getProfiles = () => { suffix: '', suffixWithLeadingSpace: '', suffixWithLeadingHyphen: '', - ligatures: [] - } + ligatures: [], + }, ]; } @@ -86,8 +90,8 @@ const getProfiles = () => { const content = fs.readFileSync(profilePath, 'utf-8'); content .split(/\r|\r\n|\n/g) - .filter(line => /^[#]/.test(line) === false && line.length > 0) - .forEach(line => { + .filter((line) => /^[#]/.test(line) === false && line.length > 0) + .forEach((line) => { const ch = line.trim()[0]; if (ch === '[') { let name = line.substr(1, line.indexOf(']') - 1); @@ -96,7 +100,7 @@ const getProfiles = () => { suffix: name === 'default' ? '' : name, suffixWithLeadingSpace: name === 'default' ? '' : ' ' + name, suffixWithLeadingHyphen: name === 'default' ? '' : '-' + name, - ligatures: [] + ligatures: [], }; profiles.push(profile); } else { @@ -113,12 +117,12 @@ const getProfiles = () => { const filterLigatures = (name, mappings) => { // loop through mappings and return if name applies or not // skip if setting is !name - return mappings.filter(entry => entry === '!' + name).length === 0; + return mappings.filter((entry) => entry === '!' + name).length === 0; }; const mapLigatures = (name, mappings) => { let mapping = { name, glyph: name }; - mappings.forEach(entry => { + mappings.forEach((entry) => { const [val1, val2] = entry.split('='); // handle lig=altliga if (val1 === name) { @@ -135,10 +139,10 @@ const filterGlyphsExists = ({ glyph }) => { return exists; }; -const sortLigatures = ligatures => { +const sortLigatures = (ligatures) => { // sort by most glyphs then alphabetically const sorted = ligatures - .map(ligature => { + .map((ligature) => { return { count: ligature.split('_').length + 1, ligature: ligature }; }) .sort( @@ -146,7 +150,7 @@ const sortLigatures = ligatures => { -compareProperty(a.count, b.count) || // sort by count descending compareProperty(a.ligature, b.ligature) // then by ligature alphabetically ) - .map(entry => entry.ligature); + .map((entry) => entry.ligature); return sorted; }; @@ -158,20 +162,20 @@ const compareProperty = (a, b) => { } }; -const loadXml = name => { +const loadXml = (name) => { const fileName = `./ligature/${ligFontName}/${name}.xml`; const xml = fs.readFileSync(fileName, 'utf-8'); return new DOMParser().parseFromString(xml); }; -const processPatch = (name, patchFunc, dom, ligatures, profile) => { +const processPatch = (name, patchFunc, dom, ligatures, profile, options) => { console.log(`Patching ${name}`); - patchFunc(dom, ligatures, profile); + patchFunc(dom, ligatures, profile, options); }; const PlatformId = { mac: 1, - win: 3 + win: 3, }; const NameId = { @@ -182,7 +186,7 @@ const NameId = { version: 5, postscriptName: 6, windowsFamilyName: 16, - fontStyleName: 17 + fontStyleName: 17, }; const patchNames = (dom, ligatures, profile) => { @@ -253,7 +257,7 @@ const patchGlyphs = (dom, ligatures) => { // only import glyphs specified let n = glyphsCount; // get ligature glyphs - ligatures.forEach(ligature => { + ligatures.forEach((ligature) => { targetGlyphs.appendChild( createElementWithAttributes('GlyphID', { id: n++, name: ligature.glyph }) ); @@ -262,18 +266,19 @@ const patchGlyphs = (dom, ligatures) => { setAttribute(dom, '/ttFont/maxp/numGlyphs', 'value', n); }; -const patchGsub = (dom, ligatures) => { +const patchGsub = (dom, ligatures, options) => { // don't patch if no ligatures other than LIG if (ligatures.length <= 1) return; + gsub.initGsubTables(dom, options); - ligatures.forEach(ligature => { + ligatures.forEach((ligature) => { // build gsub tables - gsub.buildGsubTables(dom, ligature); + gsub.buildGsubTables(dom, ligature, options); }); gsub.finalizeGsubTables(); }; -const patchHmtx = dom => { +const patchHmtx = (dom) => { const mtxCount = xpath.select('count(/ttFont/hmtx/mtx)', dom, true); setAttribute(dom, '/ttFont/hhea/numberOfHMetrics', 'value', mtxCount); @@ -306,7 +311,7 @@ const patchCharStrings = (dom, ligatures) => { ); const targetGsubrs = xpath.select('/ttFont/CFF/GlobalSubrs', dom, true); const fingerprints = {}; - ligatures.forEach(ligature => { + ligatures.forEach((ligature) => { console.log( `* ${ligature.name}${ ligature.name === ligature.glyph ? '' : ' => ' + ligature.glyph @@ -318,11 +323,11 @@ const patchCharStrings = (dom, ligatures) => { const subrs = { sourcePath: '/Glyph/Subrs', - target: targetSubrs + target: targetSubrs, }; const gsubrs = { sourcePath: '/Glyph/GlobalSubrs', - target: targetGsubrs + target: targetGsubrs, }; patchCharStringSubrs(glyphDom, node, fingerprints, subrs, gsubrs); @@ -332,16 +337,16 @@ const patchCharStrings = (dom, ligatures) => { createElementWithAttributes('mtx', { name: ligature.glyph, width: glyphDom.getAttribute('width'), - lsb: glyphDom.getAttribute('lsb') + lsb: glyphDom.getAttribute('lsb'), }) ); const code = glyphDom.getAttribute('code'); if (code) { - Array.from(targetCmaps).forEach(node => { + Array.from(targetCmaps).forEach((node) => { node.appendChild( createElementWithAttributes('map', { code, - name: ligature.glyph + name: ligature.glyph, }) ); }); @@ -354,7 +359,7 @@ const patchCharStringSubrs = (glyphDom, node, fingerprints, subrs, gsubrs) => { const lines = node.childNodes[0].textContent.split(/\r|\r\n|\n/g); const newLines = []; - lines.forEach(line => { + lines.forEach((line) => { if (line.trim().length === 0) return; const matches = line.match(/(.*?)\{([0-9a-z]+)\} (callsubr|callgsubr)$/); if (matches != null) { @@ -403,21 +408,21 @@ const copyConfigAttribute = (dom, configDom, path, name) => { const createElementWithAttributes = (tagName, attributes) => { const element = dom.createElement(tagName); - Object.entries(attributes).forEach(attribute => { + Object.entries(attributes).forEach((attribute) => { element.setAttribute(attribute[0], attribute[1]); }); return element; }; //const dump = dom => console.log(serialize(dom)); -const serialize = dom => - new XMLSerializer().serializeToString(dom, false, node => { +const serialize = (dom) => + new XMLSerializer().serializeToString(dom, false, (node) => { if (node.nodeType === NodeType.TEXT_NODE) { if (regExWhitespace.test(node.data)) return null; const data = node.data .split(/\r|\r\n|\n/g) - .filter(s => /\S+/.test(s)) - .map(s => s.replace(/^\s+/g, '')) + .filter((s) => /\S+/.test(s)) + .map((s) => s.replace(/^\s+/g, '')) .join(os.EOL); return node.ownerDocument.createTextNode(data); diff --git a/sample.html b/sample.html index f55b981..5c8d5ad 100644 --- a/sample.html +++ b/sample.html @@ -5,7 +5,7 @@ } - +