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]