@@ -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,103 @@ 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' ) ) {
383- info . foreground
384- ? info . node . setAttribute ( 'fill' , info . foreground )
385- : info . node . removeAttribute ( 'fill' ) ;
386- info . node . parentNode . removeChild ( previous ) ;
469+ const node = info . node ;
470+ if ( node . hasAttribute ( 'data-sre-highlighter-bbox' ) ) {
471+ node . remove ( ) ;
472+ return ;
473+ }
474+ if ( node . tagName === 'svg' || node . tagName === 'MJX-CONTAINER' ) {
475+ if ( ! node . hasAttribute ( 'data-mjx-enclosed' ) ) {
476+ node . style . backgroundColor = info . background ;
477+ }
478+ node . removeAttribute ( 'data-mjx-enclosed' ) ;
479+ node . style . color = info . foreground ;
387480 return ;
388481 }
389- info . node . style . backgroundColor = info . background ;
390- info . node . style . color = info . foreground ;
482+ const previous = node . previousSibling as HTMLElement ;
483+ if ( previous ?. hasAttribute ( 'data-sre-highlighter-added' ) ) {
484+ previous . remove ( ) ;
485+ }
486+ node . removeAttribute ( 'data-mjx-enclosed' ) ;
487+ if ( info . foreground ) {
488+ node . setAttribute ( 'fill' , info . foreground ) ;
489+ } else {
490+ node . removeAttribute ( 'fill' ) ;
491+ }
492+ }
493+
494+ /**
495+ * @override
496+ */
497+ protected createEnclosure (
498+ x : number ,
499+ y : number ,
500+ w : number ,
501+ h : number ,
502+ _node : HTMLElement ,
503+ part : HTMLElement
504+ ) : HTMLElement {
505+ const [ x1 , y1 ] = this . screen2svg ( x , y , part ) ;
506+ const [ x2 , y2 ] = this . screen2svg ( x + w , y - h , part ) ;
507+ const rect = this . createRect (
508+ x1 ,
509+ y1 ,
510+ x2 - x1 ,
511+ y2 - y1 ,
512+ part . getAttribute ( 'transform' )
513+ ) ;
514+ rect . setAttribute ( 'data-sre-highlighter-bbox' , 'true' ) ;
515+ part . parentNode . insertBefore ( rect , part ) ;
516+ return rect ;
517+ }
518+
519+ /**
520+ * Convert screen coordinates in px to local SVG coordinates.
521+ *
522+ * @param {number } x The screen x coordinate
523+ * @param {number } y The screen y coordinate
524+ * @param {HTMLElement } part The element whose coordinate system is to be used
525+ * @returns {number[] } The x,y coordinates in the coordinates of part
526+ */
527+ protected screen2svg ( x : number , y : number , part : HTMLElement ) : number [ ] {
528+ const node = part as any as SVGGraphicsElement ;
529+ const P = DOMPoint . fromPoint ( { x, y } ) . matrixTransform (
530+ node . getScreenCTM ( ) . inverse ( )
531+ ) ;
532+ return [ P . x , P . y ] ;
533+ }
534+
535+ /**
536+ * Create a rectangle of the given size and position.
537+ *
538+ * @param {number } x The x position of the rectangle
539+ * @param {number } y The y position of the rectangle
540+ * @param {number } w The width of the rectangle
541+ * @param {number } h The height of the rectangle
542+ * @param {string } transform The transform to apply, if any
543+ * @returns {HTMLElement } The generated rectangle element
544+ */
545+ protected createRect (
546+ x : number ,
547+ y : number ,
548+ w : number ,
549+ h : number ,
550+ transform : string
551+ ) : HTMLElement {
552+ const padding = 40 ;
553+ const rect = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'rect' ) ;
554+ rect . setAttribute (
555+ 'data-sre-highlighter-added' , // Mark highlighting rect.
556+ 'true'
557+ ) ;
558+ rect . setAttribute ( 'x' , String ( x - padding ) ) ;
559+ rect . setAttribute ( 'y' , String ( y - padding ) ) ;
560+ rect . setAttribute ( 'width' , String ( w + 2 * padding ) ) ;
561+ rect . setAttribute ( 'height' , String ( h + 2 * padding ) ) ;
562+ if ( transform ) {
563+ rect . setAttribute ( 'transform' , transform ) ;
564+ }
565+ return rect as any as HTMLElement ;
391566 }
392567
393568 /**
@@ -408,6 +583,9 @@ class SvgHighlighter extends AbstractHighlighter {
408583}
409584
410585class ChtmlHighlighter extends AbstractHighlighter {
586+ protected static lineSelector = 'mjx-linebox' ;
587+ protected static lineAttr = 'lineno' ;
588+
411589 /**
412590 * @override
413591 */
@@ -426,7 +604,9 @@ class ChtmlHighlighter extends AbstractHighlighter {
426604 foreground : node . style . color ,
427605 } ;
428606 if ( ! this . isHighlighted ( node ) ) {
429- node . style . backgroundColor = this . background ;
607+ if ( ! node . hasAttribute ( 'data-mjx-enclosed' ) ) {
608+ node . style . backgroundColor = this . background ;
609+ }
430610 node . style . color = this . foreground ;
431611 }
432612 return info ;
@@ -436,8 +616,34 @@ class ChtmlHighlighter extends AbstractHighlighter {
436616 * @override
437617 */
438618 public unhighlightNode ( info : Highlight ) {
439- info . node . style . backgroundColor = info . background ;
440- info . node . style . color = info . foreground ;
619+ const node = info . node ;
620+ node . style . backgroundColor = info . background ;
621+ node . style . color = info . foreground ;
622+ node . removeAttribute ( 'data-mjx-enclosed' ) ;
623+ if ( node . tagName . toLowerCase ( ) === 'mjx-bbox' ) {
624+ node . remove ( ) ;
625+ }
626+ }
627+
628+ /**
629+ * @override
630+ */
631+ protected createEnclosure (
632+ x : number ,
633+ y : number ,
634+ w : number ,
635+ h : number ,
636+ node : HTMLElement
637+ ) : HTMLElement {
638+ const base = node . getBoundingClientRect ( ) ;
639+ const enclosure = document . createElement ( 'mjx-bbox' ) ;
640+ enclosure . style . width = w + 'px' ;
641+ enclosure . style . height = h + 'px' ;
642+ enclosure . style . left = x - base . left + 'px' ;
643+ enclosure . style . top = y - h - base . top + 'px' ;
644+ enclosure . style . position = 'absolute' ;
645+ node . prepend ( enclosure ) ;
646+ return enclosure ;
441647 }
442648
443649 /**
0 commit comments