#!/usr/bin/env node 'use strict'; /* eslint-disable no-use-before-define */ function generateCSS( symbolsFile, cssFile, inputType ) { let cssRules = []; // Whole CSS rules const rerenderAll = process.argv.slice( 2 ).includes( '--all' ), unmodifiedClasses = {}, cssClasses = {}, // Unique part of class name and whether baseline is shifted generatedRules = [], currentRule = [], symbolList = [], // Symbols whose CSS rules need to be added or adjusted cssPrefix = '.ve-ui-mwLatexSymbol-', fs = require( 'fs' ), http = require( 'http' ), querystring = require( 'querystring' ), mathoidMaxConnections = 20, // If symbol.alignBaseline is true, a background-position property will be added to the // CSS rule to shift the baseline of the SVG to be a certain proportion of the way up the // button. singleButtonHeight = 1.8, // Height of the single-height math dialog buttons in em baseline = 0.65; // Proportion of the way down the button the baseline should be // eslint-disable-next-line security/detect-non-literal-fs-filename const symbolsData = fs.readFileSync( symbolsFile ).toString(); let cssData; try { // eslint-disable-next-line security/detect-non-literal-fs-filename cssData = fs.readFileSync( cssFile ).toString(); } catch ( e ) {} function encodeURIComponentForCSS( str ) { return encodeURIComponent( str ) .replace( /[!'*()]/g, ( chr ) => '%' + chr.charCodeAt( 0 ).toString( 16 ) ); } /** * Make the className, replacing any non-alphanumerics with their character code * * The reverse of function would look like this, although we have no use for it yet: * * return className.replace( /_([0-9]+)_/g, ( all, one ) => String.fromCharCode( +one ) } ); * * @param {string} tex TeX input * @return {string} Class name */ function texToClass( tex ) { return tex.replace( /[^\w]/g, ( c ) => '_' + c.charCodeAt( 0 ) + '_' ); } function makeRequest( symbol ) { const tex = symbol.tex || symbol.insert, data = querystring.stringify( { q: inputType === 'chem' ? '\\ce{' + tex + '}' : tex, type: inputType } ), // API call to mathoid options = { host: 'localhost', port: '10044', path: '/', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength( data ) } }; // Populate and make the API call const request = http.request( options, ( res ) => { let body = ''; res.setEncoding( 'utf8' ); res.on( 'data', ( innerData ) => { body += innerData; } ); res.on( 'end', () => { const className = texToClass( tex ), bodyData = JSON.parse( body ), svg = bodyData.svg; if ( Object.prototype.hasOwnProperty.call( generatedRules, className ) ) { console.log( className + ' already generated' ); onEnd(); return; } generatedRules[ className ] = true; if ( !svg ) { console.log( tex + ' FAILED: ' + body ); onEnd(); return; } let cssRule = cssPrefix + className + ' {\n' + '\tbackground-image: url( data:image/svg+xml,' + encodeURIComponentForCSS( svg ) + ' );\n'; if ( symbol.alignBaseline ) { // Convert buttonHeight from em to ex, because SVG height is given in ex. (This is an // approximation, since the em:ex ratio differs from font to font.) const buttonHeight = symbol.largeLayout ? singleButtonHeight * 4 : singleButtonHeight * 1.9931; // height and verticalAlign rely on the format of the SVG parameters // HACK: Adjust these by a factor of 0.8 to match VE's default font size of 0.8em const height = parseFloat( bodyData.mathoidStyle.match( /height:\s*([\d.]+)ex/ )[ 1 ] ) * 0.8; const verticalAlign = -parseFloat( bodyData.mathoidStyle.match( /vertical-align:\s*([-\d.]+)ex/ )[ 1 ] ) * 0.8; // CSS percentage positioning is based on the difference between the image and container sizes const heightDifference = buttonHeight - height; const offset = 100 * ( verticalAlign - height + ( baseline * buttonHeight ) ) / heightDifference; cssRule += '\tbackground-position: 50% ' + offset + '%;\n' + '}'; cssRules.push( cssRule ); console.log( tex + ' -> ' + className ); } else { cssRule += '}'; cssRules.push( cssRule ); console.log( tex + ' -> ' + className ); } onEnd(); } ); } ); request.setTimeout( 10000 ); request.write( data ); request.end(); runNext(); } function onEnd() { count--; runNext(); } function runNext() { if ( count < mathoidMaxConnections && symbolList.length ) { count++; makeRequest( symbolList.shift() ); } if ( !symbolList.length && !count ) { cssRules.sort(); // eslint-disable-next-line security/detect-non-literal-fs-filename fs.writeFileSync( cssFile, '/*!\n' + ' * This file is GENERATED by tools/makeSvgsAndCss.js\n' + ' * DO NOT EDIT\n' + ' */\n' + '/* stylelint-disable plugin/no-unsupported-browser-features */' + '\n' + cssRules.join( '\n\n' ) + '\n' ); } } if ( cssData ) { let currentClassName; const cssLines = cssData.split( '\n' ); for ( let i = 0; i < cssLines.length; i++ ) { if ( cssLines[ i ].indexOf( cssPrefix ) === 0 ) { currentClassName = cssLines[ i ].slice( cssPrefix.length, -2 ); currentRule.push( cssLines[ i ] ); cssClasses[ currentClassName ] = false; // Default to false } else if ( currentRule.length ) { currentRule.push( cssLines[ i ] ); if ( cssLines[ i ].indexOf( '\tbackground-position' ) === 0 ) { cssClasses[ currentClassName ] = true; } if ( cssLines[ i ].indexOf( '}' ) === 0 ) { cssRules.push( currentRule.join( '\n' ) ); currentRule.splice( 0, currentRule.length ); } } } } const symbolObject = JSON.parse( symbolsData ); for ( const group in symbolObject ) { const symbols = symbolObject[ group ]; for ( let i = 0; i < symbols.length; i++ ) { const symbol = symbols[ i ]; if ( symbol.duplicate || symbol.notWorking ) { continue; } const currentClassName = texToClass( symbol.tex || symbol.insert ); const alignBaseline = !symbol.alignBaseline; // If symbol is not in the old CSS file, or its alignBaseline status has changed, // add it to symbolList. Check to make sure it hasn't already been added. if ( rerenderAll || cssClasses[ currentClassName ] === undefined || ( unmodifiedClasses[ currentClassName ] !== true && cssClasses[ currentClassName ] === alignBaseline ) ) { symbolList.push( symbol ); } else { // At the end of this loop, any CSS class names that aren't in unmodifiedClasses // will be deleted from cssRules. cssRules will then only contain rules that will // stay unmodified. unmodifiedClasses[ currentClassName ] = true; } } } console.log( '----' ); console.log( 'Comparing ' + cssFile + ' and ' + symbolsFile ); console.log( Object.keys( cssClasses ).length + ' images found in ' + cssFile ); console.log( symbolList.length + ' symbols need rendering' ); if ( !rerenderAll ) { console.log( Object.keys( unmodifiedClasses ).length + ' symbols already rendered' ); console.log( 'To re-render all symbols, use --all' ); } // Keep only classes that will stay the same. Remove classes that are being adjusted and // classes of symbols that have been deleted from the JSON. cssRules = cssRules.filter( ( rule ) => { const currentClassName = rule.split( '\n' )[ 0 ].slice( cssPrefix.length, -2 ); if ( unmodifiedClasses[ currentClassName ] ) { return true; } console.log( 'Removing or adjusting: ' + currentClassName ); return false; } ); let count = 0; runNext(); } generateCSS( '../mathSymbols.json', '../ve.ui.MWMathSymbols.css', 'tex' ); generateCSS( '../chemSymbols.json', '../ve.ui.MWChemSymbols.css', 'chem' );