001package votorola.s.gwt.mediawiki; // Copyright 2011-2013, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import com.google.gwt.core.client.*;
004import com.google.gwt.dom.client.*;
005import com.google.gwt.regexp.shared.*;
006import com.google.gwt.user.client.Window;
007import java.util.*;
008import votorola.a.count.*;
009import votorola.a.count.gwt.*;
010import votorola.a.web.gwt.*;
011import votorola.g.lang.*;
012import votorola.g.web.gwt.*;
013import votorola.g.web.gwt.event.*;
014import votorola.s.gwt.stage.*;
015import votorola.s.gwt.stage.vote.*;
016
017
018/** <span id='layout'>A view</span> of difference shadows for MediaWiki draft pages.  It
019  * shows a single difference at a time, whichever is currently lit by the {@linkplain
020  * ShadowLight shadow light}.  The view includes a {@linkplain #topBox top bridge box}
021  * (t) composed of a {@linkplain BridgeBox#bridgeLink() bridge link} ] [ and a
022  * {@linkplain TopBox#segCounter() segment counter}.<pre>
023  *
024  *                              (t)-- ] [  +-----------+  +------------+  +--
025  *                                \        | user page |  | discussion |  |
026  *                                 \   +---+           +--+------------+--+--
027  *        +------------------------ \  |
028  *        |                          \
029  *        |                         |  3
030  *        |                         |  v
031  *        |        w i k i          |                 (showing Monobook skin,
032  *        |                         |  |                 Vector is similar)
033  *        |        l o g o          |  |
034  *        |                         |  |
035  *        |       i m a g e         |  |
036  *        |                         |  |
037  *        |                         |  |
038  *        |                         |  |
039  *        +-------------------------+  |
040  *                                     |
041  *                                    ]|[          ###########
042  *                                     |
043  *                                     |
044  *                                     |        (s)
045  *                                     |
046  *                                     |
047  *                                    ]|[         ###########################
048  *                                     |   ##################################
049  *                                     |   ##############
050  *                                     |
051  *                                    ]|[                        ########
052  *                                     |
053  *                                     |</pre>
054  *
055  * The shadow (s) is projected underneath the text of the author's draft in those regions
056  * (segments ###) where it differs from the other person's draft.  Each shadow segment is
057  * accompanied by a {@linkplain BridgeBox#bridgeLink() bridge link} ] [ targeting that
058  * particular segment of the difference.
059  *
060  * <h3 id='pgParam'>Page parameters</h3>
061  *
062  * <p>The following parameters are defined for a shadowed page in addition to those
063  * already defined by MediaWiki.</p>
064  *
065  * <table class='definition' style='margin-left:1em'>
066  *     <tr>
067  *         <th class='key'>Key</th>
068  *         <th>Value</th>
069  *         <th>Default</th>
070  *         </tr>
071  *     <tr><td class='key'>rep</td>
072  *
073  *         <td>The debug {@linkplain #rep() reporting threshold}.</td>
074  *
075  *         <td>0</td>
076  *
077  *         </tr>
078  *     <tr><td class='key'>rep_diff_user</td>
079  *
080  *         <td>Filters difference related debug reports by the username of the other
081  *         drafter.</td>
082  *
083  *         <td>Null, optional item.</td>
084  *
085  *         </tr>
086  *     </table>
087  */
088final class DifferenceShadowsV
089{
090
091
092    /** Constructs a DifferenceShadowsV for {@linkplain #init() init} to finish.  It will
093      * co-exist permanently with the stage.
094      */
095    DifferenceShadowsV( DifferenceShadows _shadows, final Stage stage )
096    {
097        shadows = _shadows;
098        topBox = TopBox.newTopBox( shadows, stage );
099    }
100
101
102
103    /** Completes the construction of the DifferenceShadowsV.  Call once only.
104      *
105      *     @throws Warning if a failure is detected that ought to be reported to the
106      *       user.
107      */
108    void init() throws Warning
109    {
110        final Document doc = Document.get();
111        final Element topNodeOrNull = doc.getElementById( "firstHeading" );
112        if( topNodeOrNull == null )
113        {
114            if( rep > 0 ) report( "unable to add topmost bridge link, no element id='firstHeading'", 1 );
115            // any appendMismatchLinksTo(topBox) are going to be invisible, but that
116            // cannot be helped
117        }
118        else insertLink( topBox, topNodeOrNull );
119
120      // Find a "content leader" node above the editable content.
121      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
122        Node contentLeader = doc.getElementById( "jump-to-nav" );
123        if( contentLeader == null )
124        {
125            contentLeader = topNodeOrNull;
126            if( contentLeader == null )
127            {
128                contentLeader = doc.getElementById( "content" ); // fail-safe, higher in doc
129                if( contentLeader == null )
130                {
131                    throw new Warning( "unable to show difference shadows, no content leader node" );
132                }
133            }
134        }
135        else contentLeader = contentLeader.getNextSibling();
136
137      // Concatenate the searchable text of the document into docC string and docN array.
138      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
139        final DifferenceParse p = new DifferenceParse();
140        for( Node node = contentLeader;; )
141        {
142            node = NodeX.nextNode( node, /*deeply*/true );
143            if( node == null ) break;
144
145            if( Element.is( node )) // try to detect end of editable content
146            {
147                final Element el = node.cast();
148                final String cl = el.getClassName();
149                if( "printfooter".equals(cl) || "visualClear".equals(cl)
150                 || "column-one".equals(el.getId()) ) break;
151
152                continue;
153            }
154
155            final short nodeType = node.getNodeType();
156            if( nodeType < 3/*Node.TEXT_NODE*/ || nodeType > 4/*Node.CDATA_SECTION_NODE*/ ) continue;
157
158            final Text text = node.cast();
159            text._setInt( "offset", p.docC.length() );
160            p.docN.add( text );
161            p.docC.append( text.getData() );
162        }
163        if( p.docC.length() == 0 ) // probably impossible
164        {
165            throw new Warning( "unable to show difference shadows, page has no text content" );
166        }
167
168      // Mark up the page with shadows.
169      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
170        if( rep > 0 ) report( "document text: [" + p.docC.toString() + "]", 2 );
171        shadows.run( new LightableDifference.Runner<ShadowedDiff>()
172        {
173            public void run( final ShadowedDiff diff ) { tryMark( p, diff ); }
174        });
175    }
176
177
178
179   // ------------------------------------------------------------------------------------
180
181
182    /** Shows a debug report to the user.  The report is shown as {@linkplain
183      * Stage#addWarning(String) stage warnings} when the view runs in {@linkplain
184      * GWT#isProdMode() production mode}, otherwise it is printed to the console.
185      *
186      *     @param level the detail level of the message, 1 or higher.
187      *
188      *     @see #rep()
189      */
190    void report( final String message, final int level )
191    {
192        if( level > rep ) return;
193
194        final StringBuilder b = GWTX.stringBuilderClear();
195        b.append( message );
196        b.append( "\n(rep=" );
197        b.append( rep );
198        b.append( " \u2265 " ); // greater than or equal
199        b.append( level );
200        b.append( ')' );
201        final String report = b.toString();
202        if( GWT.isProdMode() ) Stage.i().addWarning( report );
203        else
204        {
205            System.out.println( report );
206            System.out.println();
207        }
208    }
209
210
211        /** The debug reporting threshold as requested by the <a href='#pgParam'>rep</a>
212          * page parameter.  A level of zero (0) disables reporting entirely.  Levels of 1
213          * or higher enable progressively more detailed messages to be reported.
214          *
215          *     @see #report(String,int)
216          */
217        int rep() { return rep; }
218
219            private final int rep;
220
221            {
222                final String repString = Window.Location.getParameter( "rep" );
223                if( repString == null ) rep = 0;
224                else rep = Integer.parseInt( repString );
225            }
226
227
228        /** Shows a {@linkplain #report(String,int) debug report} to the user that may be
229          * filtered by difference properties.
230          *
231          *     @param level the detail level of the message, 1 or higher.
232          *     @param diff the argument for any <a href='#pgParam'>rep_diff_user</a>
233          *       filter.
234          */
235        void report( final String message, final int level, final ShadowedDiff diff )
236        {
237            final String username = diff._getString( topsSegments(diff)? "bUsername": "aUsername" );
238            if( rep_diff_user != null && !rep_diff_user.equals( username )) return; // filter out
239
240            final StringBuilder b = new StringBuilder();
241            b.append( "diff " );
242            b.append( diff.key() );
243            b.append( ", " );
244            b.append( username );
245            b.append( ", " );
246            b.append( message );
247            report( b.toString(), level );
248        }
249
250
251        private final String rep_diff_user = Window.Location.getParameter( "rep_diff_user" );
252
253
254
255    /** The source URL of the image for links to difference segments.
256      */
257    public static final String SRC_DIFF;
258
259
260
261    /** The source URL of the image for disabled links to difference segments.
262      */
263    public static final String SRC_DIFF_DISABLED;
264
265
266        /** The source URL of the image for links to unmatched difference segments.
267          */
268        private static final String SRC_DIFF_ERROR;
269
270
271        static
272        {
273            final String context = App.i().staticContextLocation();
274            SRC_DIFF = context + "/diff/diff.png";
275            SRC_DIFF_DISABLED = context + "/mediawiki/diffDisabled.png";
276            SRC_DIFF_ERROR = context + "/mediawiki/diffError.png";
277        }
278
279
280
281    /** The topmost bridge box (<a href='#layout'>t</a>).
282      */
283    TopBox topBox() {return topBox; }
284
285
286        private final TopBox topBox;
287
288
289
290   // ====================================================================================
291
292
293    /** A box of controls (<a href='#layout'>t or s</a>) that is shown for the shadowed
294      * difference currently lit by the {@linkplain ShadowLight shadow light}.
295      */
296    static class BridgeBox extends Element
297    {
298
299        protected BridgeBox() {} // "precisely one constructor... protected, empty, and no-argument"
300
301
302        /** Constructs a bridge box.
303          *
304          *     @param href the bridge link's 'href' attribute, if it has one.
305          */
306        static final BridgeBox newBridgeBox( final Document doc, final String extraClassName,
307          final String href )
308        {
309            final Element box = doc.createSpanElement();
310            box.setClassName( "BridgeBox" );
311            if( extraClassName != null ) box.addClassName( extraClassName );
312
313            final AnchorElement link = newBridgeLink( doc, SRC_DIFF, href );
314            box.appendChild( link );
315            return box.cast();
316        }
317
318
319        /** The link to the {@linkplain votorola.s.wic.diff.WP_D difference bridge}.
320          */
321        final AnchorElement bridgeLink() { return AnchorElement.as( Element.as( getFirstChild() )); }
322
323
324    }
325
326
327   // ====================================================================================
328
329
330    /** The topmost bridge box (<a href='#layout'>t</a>).
331      *
332      *     @see #topBox()
333      */
334    static final class TopBox extends BridgeBox implements PropertyChangeHandler
335    {
336
337        protected TopBox() {} // "precisely one constructor... protected, empty, and no-argument"
338
339
340        /** Constructs the top box.
341          */
342        static final TopBox newTopBox( final DifferenceShadows shadows, final Stage stage )
343        {
344            final Document doc = Document.get();
345            final TopBox box = newBridgeBox( doc, "top", /*relink sets href*/null ).cast();
346            box._set( "shadows", shadows );
347
348          // View.
349          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
350            box.appendChild( doc.createBRElement() );
351            final Element span = doc.createSpanElement();
352            box.appendChild( span );
353            {
354                final Text segCounter = doc.createTextNode( "" ); // init by relink
355                box._set( "segCounter", segCounter );
356                span.appendChild( segCounter );
357            }
358
359            final ImageElement segCounterArrow = doc.createImageElement();
360            box._set( "segCounterArrow", segCounterArrow );
361            segCounterArrow.setSrc( App.i().staticContextLocation() +
362              "/mediawiki/segCounterArrow.png" );
363            segCounterArrow.setClassName( "segCounterArrow" );
364            box.appendChild( doc.createBRElement() );
365            box.appendChild( segCounterArrow );
366
367          // Controller.
368          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
369            GWTX.i().bus().addHandlerToSource( PropertyChange.TYPE, /*source*/stage, box );
370              // no need to unregister, registry does not outlive this listener
371            box.relink( shadows, stage ); // init
372
373            return box;
374        }
375
376
377        private void relink( final DifferenceShadows shadows, final Stage stage )
378        {
379            final AnchorElement bridgeLink = bridgeLink();
380            final ShadowedDiff diff = shadows.diffFor( stage.getDifference() );
381            if( diff == null )
382            {
383                bridgeLink.getStyle().setCursor( Style.Cursor.DEFAULT ); /* arrow as
384                  opposed to pointer (Firefox 3.6), because no actual href */
385                bridgeLink.setHref( (String)null ); /* both null and "" actually target '.' in
386                  Firefox (3.6).  Maybe FIX by entirely deleting "href" property. */
387            }
388            else
389            {
390                bridgeLink.getStyle().clearCursor(); // to default
391                bridgeLink.setHref( diff.bridgeLoc() );
392            }
393        }
394
395
396        /** The text node that shows the number of segments in the shadowed difference.
397          */
398        Text segCounter() { return _get( "segCounter" ); }
399
400
401        /** The arrow image beneath the counter.
402          */
403        ImageElement segCounterArrow() { return _get( "segCounterArrow" ); }
404
405
406        private native DifferenceShadows shadows() /*-{ return this.shadows; }-*/;
407
408
409       // - P r o p e r t y - C h a n g e - H a n d l e r --------------------------------
410
411
412        public void onPropertyChange( final PropertyChange e )
413        {
414            if( e.propertyName().equals( "difference" )) relink( shadows(), (Stage)e.getSource() );
415        }
416
417    }
418
419
420
421   // ====================================================================================
422
423
424    /** Thrown when a failure is detected that ought to be shown to the user.
425      */
426    static final class Warning extends Exception
427    {
428
429        public Warning( String _message ) { super( _message ); }
430
431    }
432
433
434
435//// P r i v a t e ///////////////////////////////////////////////////////////////////////
436
437
438
439    /** Appends links for mismatched segments to the bridge box, one for each listed in
440      * p.mismatchedSegLocs.  Clears the list.
441      */
442    private void appendMismatchLinksTo( final BridgeBox bridgeBox, final String relClass,
443      final DifferenceParse p, final Document doc )
444    {
445        final int uN = p.mismatchedSegLocs.size();
446        if( uN == 0 ) return;
447
448        final Element y;
449        if( bridgeBox == topBox ) // then wrap links for relational styling
450        {
451            y = doc.createSpanElement();
452            y.setClassName( "orphanMismatch" );
453            y.addClassName( relClass );
454            bridgeBox.appendChild( y );
455        }
456        else y = bridgeBox;
457        boolean isFirst = true;
458        for( final String loc: p.mismatchedSegLocs )
459        {
460            y.appendChild( doc.createBRElement() );
461            final AnchorElement link = newBridgeLink( doc, SRC_DIFF_ERROR, loc );
462            if( isFirst ) link.setClassName( "leadError" );
463            y.appendChild( link );
464            isFirst = false;
465        }
466        p.mismatchedSegLocs.clear();
467    }
468
469
470
471    /** Inserts a link or link container somewhere before the specified node.  Takes care
472      * in doing so not to nest links and confuse the browser (Firefox).
473      */
474    private void insertLink( final Element linkContainer, Node node )
475    {
476        Node parent;
477        for(;; node = parent ) // avoid nesting, escape from any link we're inside of
478        {
479            parent = node.getParentNode();
480            if( !Element.is( parent )) continue; // mystery node, escape from it
481
482            final Element firstNonShadowAncestor; // find it:
483            for( Element ancestor = Element.as( parent );; )
484            {
485                if( !ancestor._hasProperty( "isShadow" )) // found it
486                {
487                    firstNonShadowAncestor = ancestor;
488                    break;
489                }
490                // else was injected by this view and might be descendant of link, so keep climbing:
491                ancestor = NodeX.nextAncestorElement( ancestor );
492            }
493            if( !"a".equals( firstNonShadowAncestor.getNodeName().toLowerCase() )) break;
494
495            parent = firstNonShadowAncestor; // skip any intervening shadow nodes
496        }
497        parent.insertBefore( linkContainer, node );
498    }
499
500
501
502    /** Marks up the page with the shadow of the specified difference in the form of span
503      * elements.  Each span element has an extended boolean property 'isShadow' that is
504      * always set to true, thus identifying the element as an injected shadow.  Also sets
505      * the value of diff.{@linkplain ShadowedDiff#segmentCount() segmentCount}.
506      *
507      *     @throws Warning if the marking fails in a detectable way.
508      */
509    private void mark( final DifferenceParse p, final ShadowedDiff diff ) throws Warning
510    {
511        p.setDiff( diff );
512        final boolean topsSeg = topsSegments( diff );
513
514      // Ensure the two drafts actually differ, and skip the two header lines.
515      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
516        final String diffText = p.diffText;
517        if( NO_DIFF_PATTERN.exec(diffText) != null ) return;
518
519        skipDiffSegment( p, '-', /*required*/true ); // --- header line
520        skipDiffSegment( p, '+', /*required*/true ); // +++ "
521
522      // Set up to loop through the hunks of the diff.
523      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
524        final SegmentPatternBuilder bSP = new SegmentPatternBuilder( DifferenceShadowsV.this, p );
525        final Document doc = Document.get();
526        int docCSearchStart = 0; // always points to the end bound of the last match
527        BridgeBox lastBridgeBox = topBox;
528        final String relClass = diff.stageRelClass();
529
530      // For each hunk and segment (hunk.seg) of the diff:
531      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
532        final int dN = diffText.length();
533        int segmentCount = 0; // in diff
534        int segOrdinalH = 0; // in hunk only
535        diff: for( int hunkCount = 0; p.d < dN; ++segOrdinalH )
536        {
537            if( skipDiffSegment( p, '@', /*required*/hunkCount == 0 )) // @@ hunk header
538            {
539                ++hunkCount;
540                segOrdinalH = 1;
541            }
542
543            ++segmentCount;
544            final String segName = hunkCount + "." + segOrdinalH;
545            final String segLoc = diff.bridgeLoc() + "#_" + segName;
546
547          // Parse the segment out of the diff, and translate it to a search pattern.
548          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
549            final RegExp pattern;
550            {
551                final String contextAbove = bSP.nextPattern( ' ' );
552                final String seg;
553                if( topsSeg )
554                {
555                    seg = bSP.nextPattern( '-' );
556                    skipDiffSegment( p, '+' ); // if any
557                }
558                else
559                {
560                    skipDiffSegment( p, '-' ); // if any
561                    seg = bSP.nextPattern( '+' );
562                }
563                final String contextBelow = bSP.nextPattern( ' ' );
564                final StringBuilder b = GWTX.stringBuilderClear();
565                b.append( contextAbove );
566                b.append( '(' );
567                b.append( seg );
568                b.append( ')' );
569                if( contextBelow.length() > 0 )
570                {
571                    b.append( "(?=" );
572                    b.append( contextBelow );
573                    b.append( ')' );
574                }
575                pattern = RegExp.compile( b.toString(), "g" );
576            }
577
578          // Search for the segment pattern in the document text.
579          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
580            pattern.setLastIndex( docCSearchStart );
581            int docC0; // offset of match (first char)
582            final int docCEnd; // end bound of match (last char + 1)
583            {
584                final MatchResult match = pattern.exec( p.docC.toString() );
585                if( match == null )
586                {
587                    if( rep > 0 ) report( "hunk.seg " + segName + ", mismatch " + docCSearchStart + "... :\n\n" + pattern, 1, diff );
588                    p.mismatchedSegLocs.add( segLoc );
589                    continue diff;
590                }
591
592                docCEnd = pattern.getLastIndex();
593                docC0 = docCEnd - match.getGroup(1).length();
594                docCSearchStart = docCEnd;
595            }
596
597            final boolean isDeletion; // whether seg is pure deletion from this text
598            if( docC0 == docCEnd )
599            {
600                isDeletion = true;
601                if( rep > 0 ) report( "hunk.seg " + segName + ", match delete between (" + (docC0-1) + ", " + docC0 + "):\n\n" + pattern, 1, diff );
602                if( docC0 == p.docC.length() ) --docC0;
603                  // entire doc deleted, fudge to avoid edge case
604            }
605            else
606            {
607                isDeletion = false;
608                if( rep > 0 ) report( "hunk.seg " + segName + ", match " + docC0 + ".." + (docCEnd-1) + ":\n\n" + pattern, 1, diff );
609            }
610
611          // Decorate any intervening mismatches.
612          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
613            appendMismatchLinksTo( lastBridgeBox, relClass, p, doc );
614
615          // Locate first node of matched text.
616          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
617            int n;
618            {
619                final JavaScriptObject o = JavaScriptObject.createObject();
620                o._setInt( "offset", docC0 );
621                n = Collections.binarySearch( p.docN, o, DifferenceParse.DOC_N_COMPARATOR );
622            }
623            if( n < 0 )
624            {
625                final int insertionPoint = -n - 1;
626                n = insertionPoint - 1;
627                  // The exactly matching node would fall between N and M, so the exactly
628                  // matching character falls within N.  That's where to start decorating.
629            }
630            Text node = p.docN.get( n );
631            {
632                final int headRoom = docC0 - node._getInt("offset");
633                if( headRoom > 0 ) // split off the head
634                {
635                    final Text tail = node.splitText( headRoom ); // head to be left unmarked
636                    tail._setInt( "offset", docC0 );
637                    node = tail;
638                    ++n;
639                    p.docN.add( n, tail ); // insert tail after head
640                }
641            }
642
643          // If it's a pure deletion, mark it up.
644          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
645            if( isDeletion ) // then no text to shadow, so only markup is link
646            {
647                lastBridgeBox = BridgeBox.newBridgeBox( doc, relClass, segLoc );
648                insertLink( lastBridgeBox, node );
649                continue diff;
650            }
651
652          // Else for each node of the matched text:
653          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
654            final Text firstNode = node;
655            for( int remainder = docCEnd - docC0;; )
656            {
657                final int nodeLength = node.getLength();
658                Text nextNode = null; // till proven otherwise
659                if( remainder < nodeLength ) // split off the tail
660                {
661                    final Text tail = node.splitText( remainder ); // keeping head to mark up
662                    tail._setInt( "offset", docCEnd );
663                    ++n;
664                    p.docN.add( n, tail ); // insert tail after head
665                }
666                else if( remainder > nodeLength )
667                {
668                    ++n; // there must be a next node
669                    nextNode = p.docN.get( n );
670                }
671             // else last node to mark up, and fully to the tail (it was not split)
672
673              // Mark up the node.
674              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
675                final Element span = doc.createSpanElement();
676                span.setClassName( "voShadow" );
677                span.addClassName( relClass );
678                span._setBoolean( "isShadow", true );
679                node.getParentNode().insertBefore( span, node );
680                span.appendChild( node );
681                if( node == firstNode ) // absolute == test OK because of "firstNode = node" above
682                {
683                    lastBridgeBox = BridgeBox.newBridgeBox( doc, relClass, segLoc );
684                    insertLink( lastBridgeBox, span );
685                }
686                if( nextNode == null ) break;
687
688                node = nextNode;
689                remainder = docCEnd - node._getInt("offset");
690            }
691        }
692        diff._setInt( "segmentCount", segmentCount );
693
694      // Decorate any trailing mismatches.
695      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
696        appendMismatchLinksTo( lastBridgeBox, relClass, p, doc );
697    }
698
699
700        private void tryMark( DifferenceParse _p, ShadowedDiff _diff )
701        {
702            try { mark( _p, _diff ); }
703            catch( final Warning warning ) { Stage.i().addWarning( warning.toString() ); }
704        }
705
706
707
708    /** Constructs a link to the difference bridge in which the first child is the link
709      * image.
710      *
711      *     @param href the link's 'href' attribute, if it has one.
712      */
713    private static final AnchorElement newBridgeLink( final Document doc, final String src,
714      final String href )
715    {
716        final AnchorElement link = doc.createAnchorElement();
717        if( href != null ) link.setHref( href );
718
719        final ImageElement img = doc.createImageElement();
720        img.setSrc( src );
721        link.appendChild( img );
722        return link;
723    }
724
725
726
727    /** The pattern of a negative 'diff' result, where the two files are identical.
728      */
729    private static final RegExp NO_DIFF_PATTERN = RegExp.compile(
730      "^Files .+ and .+ are identical" ); // cf. a.diff.DiffCache.NO_DIFF_PATTERN
731
732
733
734    private final DifferenceShadows shadows;
735
736
737
738    private boolean skipDiffSegment( final DifferenceParse p, final char prefix ) throws Warning
739    {
740        return skipDiffSegment( p, prefix, /*required*/false );
741    }
742
743
744
745    /** Skips to the next segment of the current diff, passing all contiguous lines that
746      * begin with the specified prefix.  Pointer p.d is left at the start of the next
747      * segment, or at p.diffText.length if there is none.
748      *
749      *     @param prefix the line prefix of the diff segment to skip.
750      *     @param isSegmentRequired true if the segment ought always to be there, false
751      *       if it might be missing.
752      *
753      *     @return true if anything was skipped, false otherwise.
754      *
755      *     @throws Warning if the current position is not a line start; or if the
756      *       expected prefix is not found and isSegmentRequired is true.
757      */
758    private boolean skipDiffSegment( final DifferenceParse p, final char prefix,
759      final boolean isSegmentRequired ) throws Warning
760    {
761        if( rep > 0 ) report( p.d + " --- skipDiffSegment '" + prefix + "'", 3, p.diff );
762        final String text = p.diffText;
763        final int dN = text.length();
764        if( p.d >= dN ) // end of text
765        {
766            if( !isSegmentRequired ) return false;
767
768            throw new Warning( "Expected '" + prefix + "' but found end of text: "
769              + p.positionMessage() );
770        }
771
772        boolean isLineStart = p.d == 0 || text.charAt(p.d-1) == '\n';
773        if( !isLineStart )
774        {
775            throw new Warning( "Expected '" + prefix + "' line start, but not found: "
776              + p.positionMessage() );
777        }
778
779        char ch = text.charAt( p.d );
780        if( ch != prefix )
781        {
782            if( !isSegmentRequired ) return false;
783
784            throw new Warning( "Expected '" + prefix + "' but found '" + ch + "': "
785              + p.positionMessage() );
786        }
787
788        ++p.d;
789        isLineStart = false;
790        for(; p.d < dN ; ++p.d )
791        {
792            ch = text.charAt( p.d );
793            if( isLineStart && ch != prefix ) break; // end of segment
794
795            isLineStart = ch == '\n';
796        }
797        return true;
798    }
799
800
801
802    /** Answers whether this draft's lines are first (-) in the difference segments (as
803      * opposed to second +), or eqivalently whether this draft page is the 'a' draft.
804      */
805    private static boolean topsSegments( final ShadowedDiff diff )
806    {
807        assert diff.text() != null: "diff belongs to DifferenceShadows";
808        return "a".equals( diff.selectand() ); // because this draft is always selectand
809    }
810
811
812}
813
814
815// NOTES
816//
817//   [Fla06] David Flanagan, 2006.  JavaScript: The Definitive Guide.  5th ed.  O'Reilly.
818//       Sebastapol, California.
819//
820//   [GH] Non-greedy repetition is actually greedy at the head of the pattern, because the
821//       matcher is required to return the first possible match. [Fla06p203]