2018-05-09 12:01:36 +05:30
import flash from '~/flash' ;
2019-09-04 21:01:54 +05:30
import { s _ _ , sprintf } from '~/locale' ;
2017-08-17 22:00:37 +05:30
// Renders math using KaTeX in any element with the
// `js-render-math` class
//
// ### Example Markup
//
// <code class="js-render-math"></div>
//
2019-09-04 21:01:54 +05:30
const MAX _MATH _CHARS = 1000 ;
const MAX _RENDER _TIME _MS = 2000 ;
// These messages might be used with inline errors in the future. Keep them around. For now, we will
// display a single error message using flash().
// const CHAR_LIMIT_EXCEEDED_MSG = sprintf(
// s__(
// 'math|The following math is too long. For performance reasons, math blocks are limited to %{maxChars} characters. Try splitting up this block, or include an image instead.',
// ),
// { maxChars: MAX_MATH_CHARS },
// );
// const RENDER_TIME_EXCEEDED_MSG = s__(
// "math|The math in this entry is taking too long to render. Any math below this point won't be shown. Consider splitting it among multiple entries.",
// );
const RENDER _FLASH _MSG = sprintf (
s _ _ (
'math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead.' ,
) ,
{ maxChars : MAX _MATH _CHARS } ,
) ;
// Wait for the browser to reflow the layout. Reflowing SVG takes time.
// This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object".
const waitForReflow = fn => {
window . requestAnimationFrame ( fn ) ;
} ;
/ * *
* Renders math blocks sequentially while protecting against DoS attacks . Math blocks have a maximum character limit of MAX _MATH _CHARS . If rendering math takes longer than MAX _RENDER _TIME _MS , all subsequent math blocks are skipped and an error message is shown .
* /
class SafeMathRenderer {
/ *
How this works :
The performance bottleneck in rendering math is in the browser trying to reflow the generated SVG .
During this time , the JS is blocked and the page becomes unresponsive .
We want to render math blocks one by one until a certain time is exceeded , after which we stop
rendering subsequent math blocks , to protect against DoS . However , browsers do reflowing in an
asynchronous task , so we can ' t time it synchronously .
SafeMathRenderer essentially does the following :
1. Replaces all math blocks with placeholders so that they ' re not mistakenly rendered twice .
2. Places each placeholder element in a queue .
3. Renders the element at the head of the queue and waits for reflow .
4. After reflow , gets the elapsed time since step 3 and repeats step 3 until the queue is empty .
* /
queue = [ ] ;
totalMS = 0 ;
constructor ( elements , katex ) {
this . elements = elements ;
this . katex = katex ;
this . renderElement = this . renderElement . bind ( this ) ;
this . render = this . render . bind ( this ) ;
}
renderElement ( ) {
if ( ! this . queue . length ) {
return ;
2017-08-17 22:00:37 +05:30
}
2019-09-04 21:01:54 +05:30
const el = this . queue . shift ( ) ;
const text = el . textContent ;
el . removeAttribute ( 'style' ) ;
if ( this . totalMS >= MAX _RENDER _TIME _MS || text . length > MAX _MATH _CHARS ) {
if ( ! this . flashShown ) {
flash ( RENDER _FLASH _MSG ) ;
this . flashShown = true ;
}
// Show unrendered math code
const codeElement = document . createElement ( 'pre' ) ;
codeElement . className = 'code' ;
codeElement . textContent = el . textContent ;
el . parentNode . replaceChild ( codeElement , el ) ;
// Render the next math
this . renderElement ( ) ;
} else {
this . startTime = Date . now ( ) ;
try {
el . innerHTML = this . katex . renderToString ( text , {
displayMode : el . getAttribute ( 'data-math-style' ) === 'display' ,
throwOnError : true ,
maxSize : 20 ,
maxExpand : 20 ,
} ) ;
2019-09-30 23:59:55 +05:30
} catch ( e ) {
2019-09-04 21:01:54 +05:30
// Don't show a flash for now because it would override an existing flash message
el . textContent = s _ _ ( 'math|There was an error rendering this math block' ) ;
// el.style.color = '#d00';
el . className = 'katex-error' ;
}
// Give the browser time to reflow the svg
waitForReflow ( ( ) => {
const deltaTime = Date . now ( ) - this . startTime ;
this . totalMS += deltaTime ;
this . renderElement ( ) ;
} ) ;
}
}
render ( ) {
// Replace math blocks with a placeholder so they aren't rendered twice
this . elements . forEach ( el => {
const placeholder = document . createElement ( 'span' ) ;
placeholder . style . display = 'none' ;
placeholder . setAttribute ( 'data-math-style' , el . getAttribute ( 'data-math-style' ) ) ;
placeholder . textContent = el . textContent ;
el . parentNode . replaceChild ( placeholder , el ) ;
this . queue . push ( placeholder ) ;
} ) ;
// If we wait for the browser thread to settle down a bit, math rendering becomes 5-10x faster
// and less prone to timeouts.
setTimeout ( this . renderElement , 400 ) ;
}
2018-03-17 18:26:18 +05:30
}
export default function renderMath ( $els ) {
if ( ! $els . length ) return ;
Promise . all ( [
import ( /* webpackChunkName: 'katex' */ 'katex' ) ,
2018-05-09 12:01:36 +05:30
import ( /* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css' ) ,
2018-12-13 13:39:08 +05:30
] )
. then ( ( [ katex ] ) => {
2019-09-04 21:01:54 +05:30
const renderer = new SafeMathRenderer ( $els . get ( ) , katex ) ;
renderer . render ( ) ;
2018-12-13 13:39:08 +05:30
} )
2019-09-04 21:01:54 +05:30
. catch ( ( ) => { } ) ;
2018-03-17 18:26:18 +05:30
}