mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Math
synced 2024-12-22 04:12:49 +00:00
16f8fdc161
Change-Id: I3455051bbaebd19febaa61217c340826fed8f5c0
236 lines
7.7 KiB
JavaScript
Executable file
236 lines
7.7 KiB
JavaScript
Executable file
#!/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' );
|