@@ -99,6 +99,15 @@ export interface Highlighter {
9999 */
100100 unhighlightAll ( ) : void ;
101101
102+ /**
103+ * Encloses multiple nodes if they in the same line
104+ *
105+ * @param {HTMLElement[] } parts The elements to be selected
106+ * @param {HTMLElement } node The root node of the expression
107+ * @returns {HTMLElement[] } The elements that shoudl be highlighted
108+ */
109+ encloseNodes ( parts : HTMLElement [ ] , node : HTMLElement ) : HTMLElement [ ] ;
110+
102111 /**
103112 * Predicate to check if a node is an maction node.
104113 *
@@ -148,7 +157,7 @@ abstract class AbstractHighlighter implements Highlighter {
148157 /**
149158 * The Attribute for marking highlighted nodes.
150159 */
151- protected ATTR = 'sre-highlight-' + this . counter . toString ( ) ;
160+ protected ATTR = 'data- sre-highlight-' + this . counter . toString ( ) ;
152161
153162 /**
154163 * The foreground color.
@@ -165,6 +174,16 @@ abstract class AbstractHighlighter implements Highlighter {
165174 */
166175 protected mactionName = '' ;
167176
177+ /**
178+ * The CSS selector to use to find the line-box container.
179+ */
180+ protected static lineSelector = '' ;
181+
182+ /**
183+ * The attribute name for the line number.
184+ */
185+ protected static lineAttr = '' ;
186+
168187 /**
169188 * List of currently highlighted nodes and their original background color.
170189 */
@@ -233,6 +252,71 @@ abstract class AbstractHighlighter implements Highlighter {
233252 }
234253 }
235254
255+ /**
256+ * Create a container of a given size and position.
257+ *
258+ * @param {number } x The x-coordinate for the container
259+ * @param {number } y The y-coordinate for the container
260+ * @param {number } w The width for the container
261+ * @param {number } h The height for the container
262+ * @param {HTMLElement } node The mjx-container element
263+ * @param {HTMLElement } part The first node in the line to be enclosed
264+ * @returns {HTMLElement } The element of the given size
265+ */
266+ protected abstract createEnclosure (
267+ x : number ,
268+ y : number ,
269+ w : number ,
270+ h : number ,
271+ node : HTMLElement ,
272+ part : HTMLElement
273+ ) : HTMLElement ;
274+
275+ /**
276+ * @override
277+ */
278+ public encloseNodes ( parts : HTMLElement [ ] , node : HTMLElement ) : HTMLElement [ ] {
279+ if ( parts . length === 1 ) {
280+ return parts ;
281+ }
282+ const CLASS = this . constructor as typeof AbstractHighlighter ;
283+ const selector = CLASS . lineSelector ;
284+ const lineno = CLASS . lineAttr ;
285+ const lines : Map < string , HTMLElement [ ] > = new Map ( ) ;
286+ for ( const part of parts ) {
287+ const line = part . closest ( selector ) ;
288+ const n = line ? line . getAttribute ( lineno ) : '' ;
289+ if ( ! lines . has ( n ) ) {
290+ lines . set ( n , [ ] ) ;
291+ }
292+ lines . get ( n ) . push ( part ) ;
293+ }
294+ for ( const list of lines . values ( ) ) {
295+ if ( list . length > 1 ) {
296+ let [ L , T , R , B ] = [ Infinity , Infinity , - Infinity , - Infinity ] ;
297+ for ( const part of list ) {
298+ part . setAttribute ( 'data-mjx-enclosed' , 'true' ) ;
299+ const { left, top, right, bottom } = part . getBoundingClientRect ( ) ;
300+ if ( top === bottom && left === right ) continue ;
301+ if ( left < L ) L = left ;
302+ if ( top < T ) T = top ;
303+ if ( bottom > B ) B = bottom ;
304+ if ( right > R ) R = right ;
305+ }
306+ const enclosure = this . createEnclosure (
307+ L ,
308+ B ,
309+ R - L ,
310+ B - T ,
311+ node ,
312+ list [ 0 ]
313+ ) ;
314+ parts . push ( enclosure ) ;
315+ }
316+ }
317+ return parts ;
318+ }
319+
236320 /**
237321 * @override
238322 */
@@ -305,6 +389,9 @@ abstract class AbstractHighlighter implements Highlighter {
305389}
306390
307391class SvgHighlighter extends AbstractHighlighter {
392+ protected static lineSelector = '[data-mjx-linebox]' ;
393+ protected static lineAttr = 'data-mjx-lineno' ;
394+
308395 /**
309396 * @override
310397 */
@@ -332,31 +419,32 @@ class SvgHighlighter extends AbstractHighlighter {
332419 background : node . style . backgroundColor ,
333420 foreground : node . style . color ,
334421 } ;
335- node . style . backgroundColor = this . background ;
422+ if ( ! node . hasAttribute ( 'data-mjx-enclosed' ) ) {
423+ node . style . backgroundColor = this . background ;
424+ }
336425 node . style . color = this . foreground ;
337426 return info ;
338427 }
339- // This is a hack for v4.
340- // TODO: v4 Change
341- // const rect = (document ?? DomUtil).createElementNS(
342- const rect = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'rect' ) ;
343- rect . setAttribute (
344- 'sre-highlighter-added' , // Mark highlighting rect.
345- 'true'
346- ) ;
347- const padding = 40 ;
348- const bbox : SVGRect = ( node as any as SVGGraphicsElement ) . getBBox ( ) ;
349- rect . setAttribute ( 'x' , ( bbox . x - padding ) . toString ( ) ) ;
350- rect . setAttribute ( 'y' , ( bbox . y - padding ) . toString ( ) ) ;
351- rect . setAttribute ( 'width' , ( bbox . width + 2 * padding ) . toString ( ) ) ;
352- rect . setAttribute ( 'height' , ( bbox . height + 2 * padding ) . toString ( ) ) ;
353- const transform = node . getAttribute ( 'transform' ) ;
354- if ( transform ) {
355- rect . setAttribute ( 'transform' , transform ) ;
428+ if ( node . hasAttribute ( 'data-sre-highlighter-bbox' ) ) {
429+ node . setAttribute ( this . ATTR , 'true' ) ;
430+ node . setAttribute ( 'fill' , this . background ) ;
431+ return { node : node , foreground : 'none' } ;
432+ }
433+ if ( ! node . hasAttribute ( 'data-mjx-enclosed' ) ) {
434+ const { x, y, width, height } = (
435+ node as any as SVGGraphicsElement
436+ ) . getBBox ( ) ;
437+ const rect = this . createRect (
438+ x ,
439+ y ,
440+ width ,
441+ height ,
442+ node . getAttribute ( 'transform' )
443+ ) ;
444+ rect . setAttribute ( 'fill' , this . background ) ;
445+ node . parentNode . insertBefore ( rect , node ) ;
356446 }
357- rect . setAttribute ( 'fill' , this . background ) ;
358447 node . setAttribute ( this . ATTR , 'true' ) ;
359- node . parentNode . insertBefore ( rect , node ) ;
360448 info = { node : node , foreground : node . getAttribute ( 'fill' ) } ;
361449 if ( node . nodeName !== 'rect' ) {
362450 // We currently do not change foreground of collapsed nodes.
@@ -378,16 +466,95 @@ class SvgHighlighter extends AbstractHighlighter {
378466 * @override
379467 */
380468 public unhighlightNode ( info : Highlight ) {
381- const previous = info . node . previousSibling as HTMLElement ;
382- if ( previous && previous . hasAttribute ( 'sre-highlighter-added' ) ) {
469+ const node = info . node ;
470+ const previous = node . previousSibling as HTMLElement ;
471+ if ( node . hasAttribute ( 'data-sre-highlighter-bbox' ) ) {
472+ node . remove ( ) ;
473+ }
474+ node . removeAttribute ( 'data-mjx-enclosed' ) ;
475+ if ( previous && previous . hasAttribute ( 'data-sre-highlighter-added' ) ) {
383476 info . foreground
384- ? info . node . setAttribute ( 'fill' , info . foreground )
385- : info . node . removeAttribute ( 'fill' ) ;
386- info . node . parentNode . removeChild ( previous ) ;
477+ ? node . setAttribute ( 'fill' , info . foreground )
478+ : node . removeAttribute ( 'fill' ) ;
479+ previous . remove ( ) ;
387480 return ;
388481 }
389- info . node . style . backgroundColor = info . background ;
390- info . node . style . color = info . foreground ;
482+ node . style . backgroundColor = info . background ;
483+ node . style . color = info . foreground ;
484+ }
485+
486+ /**
487+ * @override
488+ */
489+ protected createEnclosure (
490+ x : number ,
491+ y : number ,
492+ w : number ,
493+ h : number ,
494+ _node : HTMLElement ,
495+ part : HTMLElement
496+ ) : HTMLElement {
497+ const [ x1 , y1 ] = this . screen2svg ( x , y , part ) ;
498+ const [ x2 , y2 ] = this . screen2svg ( x + w , y - h , part ) ;
499+ const rect = this . createRect (
500+ x1 ,
501+ y1 ,
502+ x2 - x1 ,
503+ y2 - y1 ,
504+ part . getAttribute ( 'transform' )
505+ ) ;
506+ rect . setAttribute ( 'data-sre-highlighter-bbox' , 'true' ) ;
507+ part . parentNode . insertBefore ( rect , part ) ;
508+ return rect ;
509+ }
510+
511+ /**
512+ * Convert screen coordinates in px to local SVG coordinates.
513+ *
514+ * @param {number } x The screen x coordinate
515+ * @param {number } y The screen y coordinate
516+ * @param {HTMLElement } part The element whose coordinate system is to be used
517+ * @returns {number[] } The x,y coordinates in the coordinates of part
518+ */
519+ protected screen2svg ( x : number , y : number , part : HTMLElement ) : number [ ] {
520+ const node = part as any as SVGGraphicsElement ;
521+ const P = DOMPoint . fromPoint ( { x, y } ) . matrixTransform (
522+ node . getScreenCTM ( ) . inverse ( )
523+ ) ;
524+ return [ P . x , P . y ] ;
525+ }
526+
527+ /**
528+ * Create a rectangle of the given size and position.
529+ *
530+ * @param {number } x The x position of the rectangle
531+ * @param {number } y The y position of the rectangle
532+ * @param {number } w The width of the rectangle
533+ * @param {number } h The height of the rectangle
534+ * @param {string } transform The transform to apply, if any
535+ * @returns {HTMLElement } The generated rectangle element
536+ */
537+ protected createRect (
538+ x : number ,
539+ y : number ,
540+ w : number ,
541+ h : number ,
542+ transform : string
543+ ) : HTMLElement {
544+ const padding = 40 ;
545+ const rect = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'rect' ) ;
546+ rect . setAttribute (
547+ 'data-sre-highlighter-added' , // Mark highlighting rect.
548+ 'true'
549+ ) ;
550+ rect . setAttribute ( 'x' , String ( x - padding ) ) ;
551+ rect . setAttribute ( 'y' , String ( y - padding ) ) ;
552+ rect . setAttribute ( 'width' , String ( w + 2 * padding ) ) ;
553+ rect . setAttribute ( 'height' , String ( h + 2 * padding ) ) ;
554+ if ( transform ) {
555+ rect . setAttribute ( 'transform' , transform ) ;
556+ }
557+ return rect as any as HTMLElement ;
391558 }
392559
393560 /**
@@ -408,6 +575,9 @@ class SvgHighlighter extends AbstractHighlighter {
408575}
409576
410577class ChtmlHighlighter extends AbstractHighlighter {
578+ protected static lineSelector = 'mjx-linebox' ;
579+ protected static lineAttr = 'lineno' ;
580+
411581 /**
412582 * @override
413583 */
@@ -426,7 +596,9 @@ class ChtmlHighlighter extends AbstractHighlighter {
426596 foreground : node . style . color ,
427597 } ;
428598 if ( ! this . isHighlighted ( node ) ) {
429- node . style . backgroundColor = this . background ;
599+ if ( ! node . hasAttribute ( 'data-mjx-enclosed' ) ) {
600+ node . style . backgroundColor = this . background ;
601+ }
430602 node . style . color = this . foreground ;
431603 }
432604 return info ;
@@ -436,8 +608,34 @@ class ChtmlHighlighter extends AbstractHighlighter {
436608 * @override
437609 */
438610 public unhighlightNode ( info : Highlight ) {
439- info . node . style . backgroundColor = info . background ;
440- info . node . style . color = info . foreground ;
611+ const node = info . node ;
612+ node . style . backgroundColor = info . background ;
613+ node . style . color = info . foreground ;
614+ node . removeAttribute ( 'data-mjx-enclosed' ) ;
615+ if ( node . tagName . toLowerCase ( ) === 'mjx-bbox' ) {
616+ node . remove ( ) ;
617+ }
618+ }
619+
620+ /**
621+ * @override
622+ */
623+ protected createEnclosure (
624+ x : number ,
625+ y : number ,
626+ w : number ,
627+ h : number ,
628+ node : HTMLElement
629+ ) : HTMLElement {
630+ const base = node . getBoundingClientRect ( ) ;
631+ const enclosure = document . createElement ( 'mjx-bbox' ) ;
632+ enclosure . style . width = w + 'px' ;
633+ enclosure . style . height = h + 'px' ;
634+ enclosure . style . left = x - base . left + 'px' ;
635+ enclosure . style . top = y - h - base . top + 'px' ;
636+ enclosure . style . position = 'absolute' ;
637+ node . prepend ( enclosure ) ;
638+ return enclosure ;
441639 }
442640
443641 /**
0 commit comments