Content deleted Content added
1.2.1 (October 09, 2014) update for synced one-to-one port with mediawiki:Extension:WikEdDiff |
another background that could use some darkmode friendly color |
||
(10 intermediate revisions by 3 users not shown) | |||
Line 3:
// ==UserScript==
// @name wikEd diff
// @version 1.2.
// @date October
// @description improved word-based diff library with block move detection
// @homepage https://en.wikipedia.org/wiki/User:Cacycle/diff
Line 12:
// ==/UserScript==
/**
* wikEd diff: inline-style difference engine with block move support
*
* Improved JavaScript diff library that returns html/css-formatted new text version with
* highlighted deletions, insertions, and block moves. It is compatible with all browsers and is
* not dependent on external libraries.
*
* WikEdDiff.php and the JavaScript library wikEd diff are synced one-to-one ports. Changes and
Line 26:
* MediaWiki extension: https://www.mediawiki.org/wiki/Extension:wikEdDiff
*
* This difference engine applies a word-based algorithm that uses unique words as anchor points
* to identify matching text and moved blocks (Paul Heckel: A technique for isolating differences
* between files. Communications of the ACM 21(4):264 (1978)).
*
* Additional features:
Line 36:
* - Resolution down to characters level
* - Unicode and multilingual support
* - Stepwise split (paragraphs, lines, sentences, words, characters)
* - Recursive diff
* - Optimized code for resolving unmatched sequences
Line 70:
* .newText new text
* .oldText old text
* .maxWords word count of longest linked block
* .html diff html
* .error
* .bordersDown[] linked region borders downwards, [new index, old index]
* .bordersUp[] linked region borders upwards, [new index, old index]
* .symbols: symbols table for whole text at all refinement levels
* .token[] hash table of parsed tokens for passes 1 - 3, points to symbol[i]
* .symbol[]: array of objects that hold token counters and pointers:
* .newCount new text token counter (NC)
* .oldCount old text token counter (OC)
* .newToken token index in text.newText.tokens
* .oldToken token index in text.oldText.tokens
* .linked flag: at least one unique token pair has been linked
*
*
*
*
* .
* .
* .
* .
*
* .words word count
* .chars char length
* .type '=', '-', '+', '|' (same, deletion, insertion, mark)
* .section section number
* .group group number of block
* .fixed belongs to a fixed (not moved) group
* .moved moved block group number corresponding with mark block
* .text text of block tokens
*
* .
* .
* .
* .groups[]: array, section blocks that are consecutive in old text order
* .oldNumber first block oldNumber
* .blockStart first block index
* .blockEnd last block index
* .unique contains unique
* .maxWords word count of longest linked block
* .words word count
* .chars char count
* .fixed not moved from original position
* .movedFrom group position this group has been moved from
* .color color number of moved group
*
* .fragments[]: diff fragment list ready for markup, abstraction layer for customization
* .text block or mark text
* .color moved block or mark color number
* .type '=', '-', '+' same, deletion, insertion
* '<', '>' mark left, mark right
* '(<', '(>', ')' block start and end
* '~', ' ~', '~ ' omission indicators
* '[', ']', ',' fragment start and end, fragment separator
* '{', '}' container start and end
*
*/
// JSHint options
/* jshint -W004, -W100, newcap: true, browser: true, jquery: true, sub: true, bitwise: true,
curly: true, evil: true, forin: true, freeze: true, globalstrict: true, immed: true, latedef: true, loopfunc: true, quotmark: single, strict: true, undef: true */ /* global console */
Line 135 ⟶ 138:
'use strict';
/** Define global objects. */
var wikEdDiffConfig;
var WED;
Line 141 ⟶ 144:
/**
* wikEd diff main class.
*
* @class WikEdDiff
Line 147 ⟶ 150:
var WikEdDiff = function () {
/** @var array config Configuration and customization settings. */
this.config = {
/** Core diff settings (with default values). */
/**
Line 160 ⟶ 163:
/**
* @var bool config.showBlockMoves
* Enable block move layout with highlighted blocks and marks at
*/
'showBlockMoves': true,
Line 169 ⟶ 172:
*/
'charDiff': true,
/**
* @var bool config.repeatedDiff
* Enable repeated diff to resolve problematic sequences (true)
*/
'repeatedDiff': true,
/**
Line 221 ⟶ 230:
/**
* @var bool config.debug
* Show debug infos and stats (block, group, and fragment data
*/
'debug': false,
Line 237 ⟶ 246:
'unitTesting': false,
/** RegExp character classes. */
// UniCode letter support for regexps
// 'regExpLetters':
'a-zA-Z0-9' + (
'00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-' +
'037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA' +
'05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-' +
'07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D' +
'09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE' +
'09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A39' +
'0A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE0' +
'0AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B83' +
'0B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C' +
'0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-' +
'0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-' +
'0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E87' +
'0E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC4' +
'0EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066' +
'106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D' +
'1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-1315' +
'1318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-' +
'17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C' +
'1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA0' +
'1BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F15' +
'1F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC4' +
'1FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-2113' +
'21152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-' +
'2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE' +
'2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-' +
'3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCC' +
'A000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-' +
'A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3' +
'A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-' +
'AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-' +
'AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9' +
'FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3D' +
'FD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-' +
'FFCFFFD2-FFD7FFDA-FFDC'
).replace( /(\w{4})/g, '\\u$1' ),
// New line characters without and with \n and \r
Line 250 ⟶ 296:
// Full stops without '.'
'regExpFullStops':
'\\u0589\\u06D4\\u0701\\u0702\\u0964\\u0DF4\\u1362\\u166E\\u1803\\u1809' +
'\\u2CF9\\u2CFE\\u2E3C\\u3002\\uA4FF\\uA60E\\uA6F3\\uFE52\\uFF0E\\uFF61',
// New paragraph characters without \n and \r
Line 256 ⟶ 304:
// Exclamation marks without '!'
'regExpExclamationMarks':
'\\u01C3\\u01C3\\u01C3\\u055C\\u055C\\u07F9\\u1944\\u1944' +
'\\u203C\\u203C\\u2048\\u2048\\uFE15\\uFE57\\uFF01',
// Question marks without '?'
'regExpQuestionMarks':
'\\u037E\\u055E\\u061F\\u1367\\u1945\\u2047\\u2049' +
'\\u2CFA\\u2CFB\\u2E2E\\uA60F\\uA6F7\\uFE56\\uFF1F',
/** Clip settings. */
// Find clip position: characters from right
'clipHeadingLeft': 1500,
'clipParagraphLeftMax': 1500,
Line 273 ⟶ 325:
'clipCharsLeft': 500,
// Find clip position: characters from right
'clipHeadingRight': 1500,
'clipParagraphRightMax': 1500,
Line 297 ⟶ 349:
// Insert
'.wikEdDiffInsert {' +
'font-weight: bold; background-color: #bbddff; ' + 'color: #222; border-radius: 0.25em; padding: 0.2em 1px; '} ' +
'.wikEdDiffInsertBlank { background-color: #66bbff; } ' + '.wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; } ' +
// Delete
'.wikEdDiffDelete {' +
'font-weight: bold; background-color: #ffe49c; ' + 'color: #222; border-radius: 0.25em; padding: 0.2em 1px; '} ' +
'.wikEdDiffDeleteBlank { background-color: #ffd064; } ' + '.wikEdDiffFragment:hover .wikEdDiffDeleteBlank { background-color: #ffe49c; } ' +
// Block
'.wikEdDiffBlock {' +
'font-weight: bold; background-color: #e8e8e8; ' + 'border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; '
'.
'.
'.
'.
'.
'.
'.
'.
'.
'.wikEdDiffBlock8 { background-color: #a0e8a0; } ' +
'.wikEdDiffBlockHighlight {' + 'background-color: #777; color: #fff; ' + 'border: solid #777; border-width: 1px 0; '} ' +
// Mark
'.wikEdDiffMarkLeft, .wikEdDiffMarkRight {' +
'font-weight: bold; background-color: #ffe49c; ' + 'color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; '} ' +
'.wikEdDiffMarkLeft:before { content: "{cssMarkLeft}"; } ' + '.wikEdDiffMarkRight:before { content: "{cssMarkRight}"; } ' +
'.wikEdDiffMarkLeft.wikEdDiffNoUnicode:before { content: "<"; } ' +
'.wikEdDiffMarkRight.wikEdDiffNoUnicode:before { content: ">"; } ' +
'.wikEdDiffMark { background-color: #e8e8e8; color: #666; } ' +
'.wikEdDiffMark0 { background-color: #ffff60; } ' +
'.wikEdDiffMark1 { background-color: #c8f880; } ' +
'.wikEdDiffMark2 { background-color: #ffd0f0; } ' +
'.wikEdDiffMark3 { background-color: #a0ffff; } ' +
'.wikEdDiffMark4 { background-color: #fff860; } ' +
'.wikEdDiffMark5 { background-color: #b0c0ff; } ' +
'.wikEdDiffMark6 { background-color: #e0c0ff; } ' +
'.wikEdDiffMark7 { background-color: #ffa8a8; } ' +
'.wikEdDiffMark8 { background-color: #98e898; } ' +
'.wikEdDiffMarkHighlight { background-color: #777; color: #fff; } ' +
// Wrappers
'.wikEdDiffContainer { } ' +
'.wikEdDiffFragment {' +
'white-space: pre-wrap; background-color: var(--background-color-base, #fff); border: #bbb solid; ' + 'border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; ' + 'font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; '} ' +
'.wikEdDiffNoChange { background: var(--background-color-interactive, #eaecf0); border: 1px #bbb solid; border-radius: 0.5em; ' +
'line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; ' +
'text-align: center; ' +
'} ' +
'.wikEdDiffSeparator { margin-bottom: 1em; } ' +
'.wikEdDiffOmittedChars { } ' +
// Newline
'.wikEdDiffNewline:before { content: "¶"; color: transparent; } ' +
'.wikEdDiffBlock:hover .wikEdDiffNewline:before { color: #aaa; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffNewline:before { color: transparent; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffNewline:before { color: #ccc; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffNewline:before, ' +
'.wikEdDiffInsert:hover .wikEdDiffNewline:before' + '{ color: #999; } ' + '.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffNewline:before, ' +
'.wikEdDiffDelete:hover .wikEdDiffNewline:before' + '{ color: #aaa; } ' + // Tab
'.wikEdDiffTab { position: relative; } ' +
'.wikEdDiffTabSymbol { position: absolute; top: -0.2em; } ' +
'.wikEdDiffTabSymbol:before { content: "→"; font-size: smaller; color: #ccc; } ' +
'.wikEdDiffBlock .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffInsert .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffDelete .wikEdDiffTabSymbol:before { color: #bbb; } ' +
// Space
'.wikEdDiffSpace { position: relative; } ' +
'.wikEdDiffSpaceSymbol { position: absolute; top: -0.2em; left: -0.05em; } ' +
'.wikEdDiffSpaceSymbol:before { content: "·"; color: transparent; } ' +
'.wikEdDiffBlock:hover .wikEdDiffSpaceSymbol:before { color: #999; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffSpaceSymbol:before { color: transparent; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffSpaceSymbol:before { color: #ddd; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffInsert:hover .wikEdDiffSpaceSymbol:before ' + '{ color: #888; } ' + '.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffDelete:hover .wikEdDiffSpaceSymbol:before ' + '{ color: #999; } ' + // Error
'.wikEdDiffError .wikEdDiffFragment,' +
'.wikEdDiffError .wikEdDiffNoChange' + '{ background: #faa; }' };
/** Add regular expressions to configuration settings. */
this.config.regExp = {
Line 385 ⟶ 469:
// Split into paragraphs, after double newlines
'paragraph': new RegExp(
'
this.config.regExpNewParagraph +
']
'g'
),
// Split into
'line': new RegExp(
'\\r\\n|\\n|\\r|[' +
this.config.regExpNewLinesAll +
']',
'g'
),
// Split into sentences /[^ ].*?[.!?:;]+(?= |$)/
'sentence': new RegExp(
'[^'
'].*?[.!?:;' +
this.config.regExpFullStops +
this.config.regExpExclamationMarks +
this.config.regExpQuestionMarks +
']+(?=[' +
this.config.regExpBlanks +
']
'g'
),
// Split into inline chunks
Line 409 ⟶ 506:
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
// Split into words, multi-char markup, and chars
// regExpLetters speed-up: \\w+
'word': new RegExp(
'(\\w+|[_' +
this.config.regExpLetters +
'])+([\'
this.config.regExpLetters +
']
'g'
),
// Split into chars
'character': /./g
},
// RegExp to detect blank tokens
'blankOnlyToken': new RegExp(
'[^' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']'
),
// RegExps for sliding gaps: newlines and space/word breaks
'slideStop': new RegExp(
'[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']$'
),
'slideBorder': new RegExp(
'[' +
this.config.regExpBlanks +
']$'
),
// RegExps for counting words
'countWords': new RegExp(
'(\\w+|[_' +
this.config.regExpLetters + '])+([\'
this.config.regExpLetters + ']
'g'
),
'countChunks': new RegExp(
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
Line 443 ⟶ 563:
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
// RegExp detecting blank-only and single-char blocks
Line 450 ⟶ 571:
// RegExps for clipping
'clipLine': new RegExp(
'[' + this.config.regExpNewLinesAll +
this.config.regExpNewParagraph + ']+' 'g'
),
'clipHeading': new RegExp(
'( ^|\\n)(==+.+?==+|\\{\\||\\|\\}).*?(?=\\n|$)', 'g' ),
'clipParagraph': new RegExp(
'( (\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph + '])+' 'g'
),
'clipBlank': new RegExp(
'[' +
this.config.regExpBlanks + ']+' 'g'
),
'clipTrimNewLinesLeft': new RegExp(
'[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimNewLinesRight': new RegExp(
'^[' +
this.config.regExpNewLinesAll + this.config.regExpNewParagraph + ']+' 'g'
),
'clipTrimBlanksLeft': new RegExp(
'[' +
Line 466 ⟶ 608:
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g' ), 'clipTrimBlanksRight': new RegExp(
'^[' +
Line 472 ⟶ 616:
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g' )
};
/** Add messages to configuration settings. */
this.config.msg = {
Line 490 ⟶ 636:
/**
* Add output html fragments to configuration settings.
*
*
*
*
*/
this.config.htmlCode = {
Line 504 ⟶ 650:
'containerStart': '<div class="wikEdDiffContainer" id="wikEdDiffContainer">',
'containerEnd':
'fragmentStart': '<
'fragmentEnd':
'separator':
'insertStart':
'<span class="wikEdDiffInsert" title="' + this.config.msg['wiked-diff-ins'] + '">', 'insertStartBlank':
'<span class="wikEdDiffInsert wikEdDiffInsertBlank" title="' + this.config.msg['wiked-diff-ins'] + '">', 'insertEnd':
'deleteStart':
'<span class="wikEdDiffDelete" title="' + this.config.msg['wiked-diff-del'] + '">', 'deleteStartBlank':
'<span class="wikEdDiffDelete wikEdDiffDeleteBlank" title="' + this.config.msg['wiked-diff-del'] + '">', 'deleteEnd':
'blockStart':
'title="{title}" id="wikEdDiffBlock{number}"' + 'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">', 'blockColoredStart':
'<span class="wikEdDiffBlock wikEdDiffBlock wikEdDiffBlock{number}"' +
'title="{title}" id="wikEdDiffBlock{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
'blockEnd': '</span>',
'markLeft':
'title="{title}" id="wikEdDiffMark{number}"' + 'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>', 'markLeftColored':
'<span class="wikEdDiffMarkLeft{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
'markRight':
'title="{title}" id="wikEdDiffMark{number}"' + 'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>', 'markRightColored':
'<span class="wikEdDiffMarkRight{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
'newline': '<span class="wikEdDiffNewline">\n</span>',
'tab':
'space':
'omittedChars': '<span class="wikEdDiffOmittedChars">…</span>',
'errorStart': '<div class="wikEdDiffError" title="Error: diff not consistent with versions!">',
'errorEnd':
};
/*
* Add JavaScript event handler function to configuration settings
* Highlights corresponding block and mark elements on hover and jumps between them on click
* Code for use in non-jQuery environments and legacy browsers (at least IE 8 compatible)
*
* @option Event|undefined event Browser event if available
Line 550 ⟶ 726:
// IE compatibility
if
event = window.event;
}
Line 558 ⟶ 734:
var block = document.getElementById( 'wikEdDiffBlock' + number );
var mark = document.getElementById( 'wikEdDiffMark' + number );
if ( block === null || mark === null ) {
return;
}
// Highlight corresponding mark/block pairs
if ( type === 'mouseover' ) {
element.onmouseover = null;
element.onmouseout = function ( event ) {
Line 576 ⟶ 752:
// Remove mark/block highlighting
if
element.onmouseout = null;
element.onmouseover = function ( event ) {
Line 583 ⟶ 759:
// Reset, allow outside container (e.g. legend)
if ( type !== 'click' ) {
block.className = block.className.replace( / wikEdDiffBlockHighlight/g, '' );
mark.className = mark.className.replace( / wikEdDiffMarkHighlight/g, '' );
// GetElementsByClassName
Line 591 ⟶ 767:
if ( container !== null ) {
var spans = container.getElementsByTagName( 'span' );
if ( spans[i].className.indexOf( ' wikEdDiffBlockHighlight' ) !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffBlockHighlight/g, '' );
}
else if ( spans[i].className.indexOf( ' wikEdDiffMarkHighlight'
spans[i].className = spans[i].className.replace( / wikEdDiffMarkHighlight/g, '' );
}
Line 606 ⟶ 783:
// Scroll to corresponding mark/block element
if ( type === 'click' ) {
// Get corresponding element
var corrElement;
if ( element === block ) {
corrElement = mark;
}
Line 617 ⟶ 794:
}
// Get element height (getOffsetTop
var corrElementPos = 0;
var node = corrElement;
Line 654 ⟶ 831:
};
/** Internal data structures. */
/** @var WikEdDiffText newText New text version object with text and token list */
Line 661 ⟶ 838:
/** @var WikEdDiffText oldText Old text version object with text and token list */
this.oldText = null;
/** @var object symbols Symbols table for whole text at all refinement levels */
this.symbols = {
token: [],
hashTable: {},
linked: false
};
/** @var array bordersDown Matched region borders downwards */
this.bordersDown = [];
/** @var array bordersUp Matched region borders upwards */
this.bordersUp = [];
/** @var array blocks Block data (consecutive text tokens) in new text order */
this.blocks = [];
/** @var int maxWords Maximal detected word count of all linked blocks */
this.maxWords = 0;
/** @var array groups Section blocks that are consecutive in old text order */
Line 677 ⟶ 870:
this.recursionTimer = [];
/** Output data. */
/** @var bool error Unit tests have detected a diff error */
Line 690 ⟶ 883:
/**
* Constructor, initialize settings, load js and css.
*
* @param[in] object wikEdDiffConfig Custom customization settings
Line 699 ⟶ 892:
// Import customizations from wikEdDiffConfig{}
if ( typeof wikEdDiffConfig === 'object' ) {
this.deepCopy( wikEdDiffConfig, this.config );
}
Line 710 ⟶ 903:
// Add block handler to head if running under Greasemonkey
if ( typeof GM_info === 'object' ) {
var script = 'var wikEdDiffBlockHandler = ' + this.config.blockHandler.toString() + ';';
this.addScript( script );
Line 723 ⟶ 916:
/**
* Main diff method.
*
* @param string oldString Old text version
* @param string newString New text version
* @param[out] array fragment
* Diff fragment list ready for markup, abstraction layer for customized diffs * @param[out] string html Html code of diff
* @return string Html code of diff
Line 748 ⟶ 942:
// Strip trailing newline (.js only)
if ( this.config.stripTrailingNewline === true ) {
if
newString = newString.substr( 0, newString.length - 1 );
oldString = oldString.substr( 0, oldString.length - 1 );
Line 759 ⟶ 953:
// Trap trivial changes: no change
if ( this.newText.text === this.oldText.text ) {
this.html =
this.config.htmlCode.containerStart +
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd +
this.config.htmlCode.containerEnd;
return this.html;
Line 773 ⟶ 965:
// Trap trivial changes: old text deleted
if (
( this.newText.text.charAt( this.newText.text.length - 1 ) === '\n' )
)
) {
Line 791 ⟶ 983:
// Trap trivial changes: new text deleted
if (
( this.oldText.text.charAt( this.oldText.text.length - 1 ) === '\n' )
)
) {
Line 806 ⟶ 998:
return this.html;
}
// Split new and old text into paragraps
if ( this.config.timer === true ) {
this.time( 'paragraph split' );
}
this.newText.splitText( 'paragraph' );
this.oldText.splitText( 'paragraph' );
if ( this.config.timer === true ) {
this.timeEnd( 'paragraph split' );
}
// Calculate diff
this.calculateDiff(
// Refine different paragraphs into
if ( this.config.timer === true ) {
this.time( 'line split' );
}
this.newText.splitRefine( 'line' );
this.oldText.splitRefine( 'line' );
if ( this.config.timer === true ) {
this.timeEnd( 'line split' );
}
// Calculate refined diff
this.calculateDiff( 'line' );
// Refine different lines into sentences
if ( this.config.timer === true ) {
this.time( 'sentence split' );
}
this.newText.splitRefine( 'sentence' );
this.oldText.splitRefine( 'sentence' );
if ( this.config.timer === true ) {
this.timeEnd( 'sentence split' );
}
// Calculate refined diff
this.calculateDiff(
// Refine different
if ( this.config.timer === true ) {
this.time( 'chunk split' );
Line 839 ⟶ 1,049:
// Calculate refined diff
this.calculateDiff(
// Refine different
if ( this.config.timer === true ) {
this.time( 'word split' );
Line 852 ⟶ 1,062:
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff(
// Slide gaps
Line 877 ⟶ 1,087:
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff(
// Slide gaps
Line 889 ⟶ 1,099:
}
}
// Free memory
this.symbols = undefined;
this.bordersDown = undefined;
this.bordersUp = undefined;
this.newText.words = undefined;
this.oldText.words = undefined;
// Enumerate token lists
Line 902 ⟶ 1,119:
this.timeEnd( 'blocks' );
}
// Free memory
this.newText.tokens = undefined;
this.oldText.tokens = undefined;
// Assemble blocks into fragment table
this.getDiffFragments();
// Free memory
this.blocks = undefined;
this.groups = undefined;
this.sections = undefined;
// Stop diff timer
Line 950 ⟶ 1,176:
this.html =
this.config.htmlCode.containerStart +
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd +
this.config.htmlCode.containerEnd;
}
Line 966 ⟶ 1,190:
if ( this.config.timer === true ) {
this.timeEnd( 'total' );
}
Line 992 ⟶ 1,211:
this.splitRefineChars = function () {
/** Find corresponding gaps. */
// Cycle
var gaps = [];
var gap = null;
var i = this.newText.first;
var j = this.oldText.first;
while
// Get token links
Line 1,009 ⟶ 1,228:
// Start of gap in new and old
if
gap = gaps.length;
gaps.push( {
Line 1,023 ⟶ 1,242:
// Count chars and tokens in gap
else if
gaps[gap].newLast = i;
gaps[gap].newTokens ++;
Line 1,029 ⟶ 1,248:
// Gap ended
else if
gap = null;
}
Line 1,040 ⟶ 1,259:
}
// Cycle
for ( var gap = 0; gap < gapsLength; gap ++ ) {
// Cycle
var j = gaps[gap].oldFirst;
while (
j !== null &&
this.oldText.tokens[j] !== null &&
this.oldText.tokens[j].link === null
) {
// Count old chars and tokens in gap
Line 1,055 ⟶ 1,279:
}
/** Select gaps of identical token number and strong similarity of all tokens. */
for ( var gap = 0; gap < gapsLength; gap ++ ) {
var charSplit = true;
// Not same gap length
if ( gaps[gap].newTokens !== gaps[gap].oldTokens ) {
// One word became separated by space, dash, or any string
if
var token = this.newText.tokens[ gaps[gap].newFirst ].token;
var tokenFirst = this.oldText.tokens[ gaps[gap].oldFirst ].token;
var tokenLast = this.oldText.tokens[ gaps[gap].oldLast ].token;
if (
token.indexOf( tokenFirst ) !== 0 ||
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
}
else if
var token = this.oldText.tokens[ gaps[gap].oldFirst ].token;
var tokenFirst = this.newText.tokens[ gaps[gap].newFirst ].token;
var tokenLast = this.newText.tokens[ gaps[gap].newLast ].token;
if (
token.indexOf( tokenFirst ) !== 0 ||
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
Line 1,086 ⟶ 1,317:
}
// Cycle
else {
var i = gaps[gap].newFirst;
Line 1,107 ⟶ 1,338:
// Not same token length
if ( newToken.length !== oldToken.length ) {
// Test for addition or deletion of internal string in tokens
Line 1,114 ⟶ 1,345:
var left = 0;
while ( left < shorterToken.length ) {
if ( newToken.charAt( left ) !== oldToken.charAt( left ) ) {
break;
}
Line 1,123 ⟶ 1,354:
var right = 0;
while ( right < shorterToken.length ) {
if (
newToken.charAt( newToken.length - 1 - right ) !==
oldToken.charAt( oldToken.length - 1 - right )
) {
break;
}
Line 1,130 ⟶ 1,364:
// No simple insertion or deletion of internal string
if ( left + right !== shorterToken.length ) {
// Not addition or deletion of flanking strings in tokens
// if ( longerToken.indexOf( shorterToken ) === -1 ) {
// Same text at start or end shorter than different text
if
// Do not split into chars in this gap
charSplit = false;
break;
Line 1,147 ⟶ 1,382:
// Same token length
else if ( newToken !== oldToken ) {
// Tokens less than 50 % identical
var ident = 0;
if ( shorterToken.charAt( pos ) === longerToken.charAt( pos ) ) {
ident ++;
}
Line 1,165 ⟶ 1,401:
// Next list elements
if ( i === gaps[gap].newLast ) {
break;
}
Line 1,175 ⟶ 1,411:
}
/** Refine words into chars in selected gaps. */
for ( var gap = 0; gap < gapsLength; gap ++ ) {
if ( gaps[gap].charSplit === true ) {
// Cycle
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
var newGapLength = i - gaps[gap].newLast;
var oldGapLength = j - gaps[gap].oldLast;
while
// Link identical tokens (spaces) to keep char refinement to words
if (
newGapLength === oldGapLength &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
Line 1,204 ⟶ 1,444:
// Next list elements
if ( i === gaps[gap].newLast ) {
i = null;
}
if ( j === gaps[gap].oldLast ) {
j = null;
}
Line 1,224 ⟶ 1,464:
/**
* Move gaps with ambiguous identical fronts to last newline border or otherwise last word border.
*
* @param[in/out] wikEdDiffText text, textLinked These two are newText and oldText
*/
this.slideGaps = function ( text, textLinked ) {
var regExpSlideBorder = this.config.regExp.slideBorder;
var regExpSlideStop = this.config.regExp.slideStop;
// Cycle through tokens list
var i = text.first;
var gapStart = null;
while
// Remember gap start
if
gapStart = i;
}
// Find gap end
else if
var gapFront = gapStart;
var gapBack = text.tokens[i].prev;
Line 1,249 ⟶ 1,492:
var back = text.tokens[gapBack].next;
if (
back !== null text.tokens[back].link !== null ) {
text.tokens[front].link = text.tokens[back].link;
Line 1,267 ⟶ 1,512:
var front = text.tokens[gapFront].prev;
var back = gapBack;
var gapFrontBlankTest =
var frontStop = front;
if
while (
back !== null ) {
if ( front !== null ) {
// Stop at line break
frontStop = front;
break;
}
// Stop at first word border (blank/word or word/blank
if (
regExpSlideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
frontStop = front;
}
}
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
}
Line 1,298 ⟶ 1,544:
var back = gapBack;
while (
back !== null front !== frontStop text.tokens[back].link === null ) {
text.tokens[back].link = text.tokens[front].link;
Line 1,323 ⟶ 1,572:
* Pass 1: parse new text into symbol table
* Pass 2: parse old text into symbol table
* Pass 3: connect unique
* Pass 4: connect adjacent identical tokens downwards
* Pass 5: connect adjacent identical tokens upwards
Line 1,331 ⟶ 1,580:
*
* @param array symbols Symbol table object
* @param string level Split level: 'paragraph', 'line', 'sentence', 'chunk', 'word',
*
* Optionally for recursive or repeated calls:
* @param bool
* @param bool recurse Enable recursion
* @param int newStart, newEnd, oldStart, oldEnd Text object tokens indices
Line 1,340 ⟶ 1,589:
* @param[in/out] WikEdDiffText newText, oldText Text object, tokens list link property
*/
this.calculateDiff = function (
level,
recurse,
repeating,
newStart,
oldStart,
up,
recursionLevel
) {
// Set defaults
if ( repeating === undefined ) { repeating = false; }
if ( recurse === undefined ) { recurse = false; }
if ( newStart === undefined ) { newStart = this.newText.first; }
if ( oldStart === undefined ) { oldStart = this.oldText.first; }
if (
if ( recursionLevel === undefined ) { recursionLevel = 0; }
// Start timers
if
this.time( level );
}
if
this.time( level + recursionLevel );
}
// Get object symbols table and linked region borders
var symbols;
var bordersDown;
var bordersUp;
if ( recursionLevel === 0 && repeating === false ) {
symbols = this.symbols;
bordersDown = this.bordersDown;
bordersUp = this.bordersUp;
}
// Create empty local symbols table and linked region borders arrays
else {
symbols = {
token: [],
hashTable: {},
linked: false
};
bordersDown = [];
bordersUp = [];
}
// Updated versions of linked region borders
var bordersUpNext = [];
var bordersDownNext = [];
/**
Line 1,369 ⟶ 1,645:
*/
// Cycle
var i = newStart;
while
if ( this.newText.tokens[i].link === null ) {
Line 1,377 ⟶ 1,653:
var token = this.newText.tokens[i].token;
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
symbols.
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
} );
}
Line 1,396 ⟶ 1,671:
}
//
else if (
break;
}
// Get next token
if ( up === false ) {
i = this.newText.tokens[i].next;
}
else {
i = this.newText.tokens[i].prev;
}
}
Line 1,407 ⟶ 1,689:
*/
// Cycle
var j = oldStart;
while
if ( this.oldText.tokens[j].link === null ) {
Line 1,415 ⟶ 1,697:
var token = this.oldText.tokens[j].token;
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
symbols.
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
} );
}
Line 1,437 ⟶ 1,718:
}
//
else if (
break;
}
// Get next token
if ( up === false ) {
j = this.oldText.tokens[j].next;
}
else {
j = this.oldText.tokens[j].prev;
}
}
Line 1,448 ⟶ 1,736:
*/
// Cycle
for ( var i = 0; i < symbolsLength; i ++ ) {
// Find tokens in the symbol table that occur only once in both versions
if
var newToken = symbols.token[i].newToken;
var oldToken = symbols.token[i].oldToken;
var newTokenObj = this.newText.tokens[newToken];
var oldTokenObj = this.oldText.tokens[oldToken];
// Connect from new to old and from old to new
if (
// Do not use spaces as unique markers
if (
this.config.regExp.blankOnlyToken.test( newTokenObj.token ) === true
) {
// Link new and old tokens
oldTokenObj.link = newToken;
symbols.linked = true;
// Save linked region borders
bordersDown.push( [newToken, oldToken] );
bordersUp.push( [newToken, oldToken] );
// Check if token contains unique word
if ( recursionLevel === 0 ) {
var unique = false;
if ( level === 'character' ) {
unique = true;
}
else {
var token =
var words =
( token.match( this.config.regExp.countWords ) || [] ).concat( ( token.match( this.config.regExp.countChunks ) || [] ) );
// Unique if longer than min block length
if ( wordsLength >= this.config.blockMinLength ) {
unique = true;
}
Line 1,483 ⟶ 1,785:
// Unique if it contains at least one unique word
else {
for ( var
if (
this.oldText.words[word] === 1 &&
this.newText.words[word] === 1 &&
Object.prototype.hasOwnProperty.call( this.oldText.words, word ) === true &&
Object.prototype.hasOwnProperty.call( this.newText.words, word ) === true
) {
unique = true;
break;
Line 1,494 ⟶ 1,802:
// Set unique
if ( unique === true ) {
}
}
Line 1,510 ⟶ 1,818:
*/
//
var
for ( var match = 0; match < bordersLength; match ++ ) {
var i =
var j = bordersDown[match][1];
//
var jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
// Cycle through new text list gap region downwards
while (
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
// Connect if
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
// Not a match yet, maybe in next refinement level
else {
bordersDownNext.push( [iMatch, jMatch] );
break;
}
// Next token down
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
/**
Line 1,548 ⟶ 1,862:
*/
//
var
for ( var match = 0; match < bordersLength; match ++ ) {
var i =
var j = bordersUp[match][1];
//
var jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
// Cycle through new text gap region upwards
while (
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
// Connect if
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
// Not a match yet, maybe in next refinement level
else {
bordersUpNext.push( [iMatch, jMatch] );
break;
}
// Next token up
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
}
/**
* Connect adjacent identical tokens downwards from text start
*
*/
// Only for full text diff
if (
// From start
var i = this.newText.first;
var j = this.oldText.first;
var iMatch = null;
var jMatch = null;
// Cycle
// while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
if ( iMatch !== null ) {
bordersDownNext.push( [iMatch, jMatch] );
}
// From end
iMatch = null;
jMatch = null;
// Cycle
// while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
if ( iMatch !== null ) {
bordersUpNext.push( [iMatch, jMatch] );
}
}
// Save updated linked region borders to object
if ( recursionLevel === 0 && repeating === false ) {
this.bordersDown = bordersDownNext;
this.bordersUp = bordersUpNext;
}
// Merge local updated linked region borders into object
else {
this.bordersDown = this.bordersDown.concat( bordersDownNext );
this.bordersUp = this.bordersUp.concat( bordersUpNext );
}
/**
* Repeat once with empty symbol table to link hidden unresolved common tokens in cross-overs.
* ("and" in "and this a and b that" -> "and this a and b that")
*/
if ( repeating === false && this.config.repeatedDiff === true ) {
this.calculateDiff( level, recurse, repeat, newStart, oldStart, up, recursionLevel );
}
/**
* Refine by recursively diffing
* At word and character level only.
* Helps against gaps caused by addition of common tokens around sequences of common tokens. */
if (
recurse === true &&
this.config['recursiveDiff'] === true &&
recursionLevel < this.config.recursionMax
) {
/**
* Recursively diff
*/
// Cycle
var
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersDownNext[match][0];
var j = bordersDownNext[match][1];
// Next token down
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
//
if (
j !== null &&
var repeat = false;
var dirUp = false;
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
}
}
/**
* Recursively diff
*/
// Cycle
var
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersUpNext[match][0];
var j = bordersUpNext[match][1];
//
j = this.oldText.tokens[j].prev;
//
if (
i !== null &&
j !== null &&
var
var dirUp = true;
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
}
}
}
Line 1,781 ⟶ 2,055:
// Stop timers
if
if ( this.recursionTimer[recursionLevel] === undefined ) {
this.recursionTimer[recursionLevel] = 0;
Line 1,787 ⟶ 2,061:
this.recursionTimer[recursionLevel] += this.timeEnd( level + recursionLevel, true );
}
if
this.timeRecursionEnd( level );
this.timeEnd( level );
Line 1,797 ⟶ 2,071:
/**
* Main method for processing raw diff data, extracting deleted, inserted, and moved blocks.
*
* Scheme of blocks, sections, and groups (old block numbers):
Line 1,830 ⟶ 2,104:
// Set longest sequence of increasing groups in sections as fixed (not moved)
this.setFixed();
// Convert groups to insertions/deletions if maximum block length is too short
// Only for more complex texts that actually have blocks of minimum block length
var unlinkCount = 0;
if (
this.config.unlinkBlocks === true &&
this.config.blockMinLength > 0 &&
this.maxWords >= this.config.blockMinLength
) {
if ( this.config.timer === true ) {
this.time( '
}
// Repeat as long as unlinking is possible
var unlinked = true;
while
// Convert '=' to '+'/'-' pairs
Line 1,859 ⟶ 2,132:
// Repeat block detection from start
this.maxWords = 0;
this.getSameBlocks();
this.getSections();
Line 1,866 ⟶ 2,140:
}
if ( this.config.timer === true ) {
this.timeEnd( '
}
}
Line 1,886 ⟶ 2,160:
// Debug log
if
console.log( 'Unlink count: ', unlinkCount );
}
Line 1,898 ⟶ 2,172:
/**
* Collect identical corresponding matching ('=') blocks from old text and sort by new text.
*
* @param[in] WikEdDiffText newText, oldText Text objects
Line 1,904 ⟶ 2,178:
*/
this.getSameBlocks = function () {
if ( this.config.timer === true ) {
this.time( 'getSameBlocks' );
}
var blocks = this.blocks;
Line 1,910 ⟶ 2,188:
blocks.splice( 0 );
// Cycle through old text to find
var j = this.oldText.first;
var i = null;
Line 1,916 ⟶ 2,194:
// Skip '-' blocks
while
j = this.oldText.tokens[j].next;
}
Line 1,930 ⟶ 2,208:
var unique = false;
var text = '';
while
count ++;
if ( this.newText.tokens[i].unique === true ) {
unique = true;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
Line 1,968 ⟶ 2,245:
// Number blocks in new text order
for ( var block = 0; block < blocksLength; block ++ ) {
blocks[block].newBlock = block;
}
if ( this.config.timer === true ) {
this.timeEnd( 'getSameBlocks' );
}
return;
Line 1,977 ⟶ 2,259:
/**
* Collect independent block sections with no block move crosses
*
*
* @param[out] array sections Sections table object
Line 1,983 ⟶ 2,265:
*/
this.getSections = function () {
if ( this.config.timer === true ) {
this.time( 'getSections' );
}
var blocks = this.blocks;
Line 1,991 ⟶ 2,277:
// Cycle through blocks
for ( var block = 0; block < blocksLength; block ++ ) {
var sectionStart = block;
Line 2,000 ⟶ 2,287:
// Check right
for ( var j = sectionStart + 1; j <
// Check for crossing over to the left
Line 2,027 ⟶ 2,314:
block = sectionEnd;
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getSections' );
}
return;
Line 2,033 ⟶ 2,323:
/**
* Find groups of continuous old text blocks.
*
* @param[out] array groups Groups table object
Line 2,039 ⟶ 2,329:
*/
this.getGroups = function () {
if ( this.config.timer === true ) {
this.time( 'getGroups' );
}
var blocks = this.blocks;
Line 2,047 ⟶ 2,341:
// Cycle through blocks
for ( var block = 0; block < blocksLength; block ++ ) {
var groupStart = block;
var groupEnd = block;
Line 2,059 ⟶ 2,354:
// Check right
for ( var i = groupEnd + 1; i <
// Check for crossing over to the left
if ( blocks[i].oldBlock !== oldBlock + 1 ) {
break;
}
Line 2,108 ⟶ 2,403:
} );
block = groupEnd;
// Set global word count of longest linked block
if ( maxWords > this.maxWords ) {
this.maxWords = maxWords;
}
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getGroups' );
}
return;
Line 2,115 ⟶ 2,418:
/**
* Set longest sequence of increasing groups in sections as fixed (not moved).
*
* @param[in] array sections Sections table object
Line 2,122 ⟶ 2,425:
*/
this.setFixed = function () {
if ( this.config.timer === true ) {
this.time( 'setFixed' );
}
var blocks = this.blocks;
Line 2,128 ⟶ 2,435:
// Cycle through sections
for ( var section = 0; section < sectionsLength; section ++ ) {
var blockStart = sections[section].blockStart;
var blockEnd = sections[section].blockEnd;
Line 2,150 ⟶ 2,458:
// Mark fixed groups
for ( var i = 0; i < maxPathLength; i ++ ) {
var group = maxPath[i];
groups[group].fixed = true;
Line 2,159 ⟶ 2,468:
}
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'setFixed' );
}
return;
Line 2,165 ⟶ 2,477:
/**
* Recusively find path of groups in increasing old group order with longest char length.
*
* @param int start Path start group
Line 2,219 ⟶ 2,531:
/**
*
* if too short and too common.
* Prevents fragmentated diffs for very different versions.
*
* @param[in] array blocks Blocks table object
* @param[in/out] WikEdDiffText newText, oldText Text object, linked property
* @param[in/out] array groups Groups table object
* @return bool True if text tokens were unlinked
*/
this.unlinkBlocks = function () {
var blocks = this.blocks;
var groups = this.groups;
// Cycle through groups
var unlinked = false;
var groupsLength = groups.length;
for ( var group = 0; group < groupsLength; group ++ ) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
// Unlink whole group if no block is at least blockMinLength words long and unique
if ( groups[group].maxWords < this.config.blockMinLength && groups[group].unique === false ) {
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === '=' ) {
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
// Otherwise unlink block flanks
else {
// Unlink blocks from start
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === '=' ) {
// Stop unlinking if more than one word or a unique word
if ( blocks[block].words > 1 || blocks[block].unique === true ) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
blockStart = block;
}
}
// Unlink blocks from end
for ( var block = blockEnd; block > blockStart; block -- ) {
if ( blocks[block].type === '=' ) {
// Stop unlinking if more than one word or a unique word
if (
blocks[block].words > 1 ||
( blocks[block].words === 1 && blocks[block].unique === true )
) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
}
return unlinked;
};
/**
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs.
*
* @param[in] array blocks Blocks table object
* @param[out] WikEdDiffText newText, oldText Text objects, link property
*/
this.unlinkSingleBlock = function ( block ) {
// Cycle through old text
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
// Unlink tokens
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
this.oldText.tokens[j].link = null;
j = this.oldText.tokens[j].next;
}
return;
};
/**
* Collect deletion ('-') blocks from old text.
*
* @param[in] WikEdDiffText oldText Old Text object
Line 2,225 ⟶ 2,628:
*/
this.getDelBlocks = function () {
if ( this.config.timer === true ) {
this.time( 'getDelBlocks' );
}
var blocks = this.blocks;
// Cycle through old text to find
var j = this.oldText.first;
var i = null;
Line 2,237 ⟶ 2,644:
var count = 0;
var text = '';
while
count ++;
text += this.oldText.tokens[j].token;
Line 2,267 ⟶ 2,674:
if ( j !== null ) {
i = this.oldText.tokens[j].link;
while
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getDelBlocks' );
}
return;
Line 2,278 ⟶ 2,688:
/**
* Position deletion '-' blocks into new text order.
* Deletion blocks move with fixed reference:
* Old: 1 D 2 1 D 2
Line 2,285 ⟶ 2,695:
* Fixed: * *
* newNumber: 1 1 2 2
*
* Marks '|' and deletions '-' get newNumber of reference block
*
*
* @param[in/out] array blocks Blocks table, newNumber, section, group, and fixed properties
Line 2,292 ⟶ 2,703:
*/
this.positionDelBlocks = function () {
if ( this.config.timer === true ) {
this.time( 'positionDelBlocks' );
}
var blocks = this.blocks;
Line 2,303 ⟶ 2,718:
// Cycle through blocks in old text order
for ( var block = 0; block < blocksOldLength; block ++ ) {
var delBlock = blocksOld[block];
// '-' block only
if ( delBlock.type !== '-' ) {
continue;
}
// Find fixed '=' reference block from original block position to position '-' block
// // Get old text prev block
Line 2,331 ⟶ 2,748:
// Move after prev block if fixed
var refBlock = null;
if
refBlock = prevBlock;
}
// Move before next block if fixed
else if
refBlock = nextBlock;
}
// Move after prev block if not start of group
else if (
prevBlock !== null &&
prevBlock.type === '=' &&
prevBlockNumber !== groups[ prevBlock.group ].blockEnd
) {
refBlock = prevBlock;
}
// Move before next block if not start of group
else if (
nextBlock !== null &&
nextBlock.type === '=' &&
nextBlockNumber !== groups[ nextBlock.group ].blockStart
) {
refBlock = nextBlock;
}
Line 2,353 ⟶ 2,778:
else {
for ( var fixed = block; fixed >= 0; fixed -- ) {
if
refBlock = blocksOld[fixed];
break;
Line 2,377 ⟶ 2,802:
this.sortBlocks();
if ( this.config.timer === true ) {
this.timeEnd( 'positionDelBlocks' );
}
return;
Line 2,468 ⟶ 2,810:
/**
* Collect insertion ('+') blocks from new text.
*
* @param[in] WikEdDiffText newText New Text object
Line 2,474 ⟶ 2,816:
*/
this.getInsBlocks = function () {
if ( this.config.timer === true ) {
this.time( 'getInsBlocks' );
}
var blocks = this.blocks;
Line 2,482 ⟶ 2,828:
// Jump over linked (matched) block
while
i = this.newText.tokens[i].next;
}
Line 2,491 ⟶ 2,837:
var count = 0;
var text = '';
while
count ++;
text += this.newText.tokens[i].token;
Line 2,521 ⟶ 2,867:
this.sortBlocks();
if ( this.config.timer === true ) {
this.timeEnd( 'getInsBlocks' );
}
return;
};
Line 2,526 ⟶ 2,875:
/**
* Sort blocks by new text token number and update groups.
*
* @param[in/out] array groups Groups table object
Line 2,547 ⟶ 2,896:
// Cycle through blocks and update groups with new block numbers
var group = null;
for ( var block = 0; block < blocksLength; block ++ ) {
var blockGroup = blocks[block].group;
if ( blockGroup !== null ) {
Line 2,563 ⟶ 2,913:
/**
* Set group numbers of insertion '+' blocks.
*
* @param[in/out] array groups Groups table object
Line 2,569 ⟶ 2,919:
*/
this.setInsGroups = function () {
if ( this.config.timer === true ) {
this.time( 'setInsGroups' );
}
var blocks = this.blocks;
Line 2,574 ⟶ 2,928:
// Set group numbers of '+' blocks inside existing groups
for ( var group = 0; group < groupsLength; group ++ ) {
var fixed = groups[group].fixed;
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
Line 2,587 ⟶ 2,942:
// Cycle through blocks
for ( var block = 0; block < blocksLength; block ++ ) {
// Skip existing groups
Line 2,607 ⟶ 2,963:
} );
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'setInsGroups' );
}
return;
Line 2,613 ⟶ 2,972:
/**
* Mark original positions of moved groups.
* Scheme: moved block marks at original positions relative to fixed groups:
* Groups: 3 7
Line 2,623 ⟶ 2,982:
* | |> 9 (no next larger fixed)
* Fixed: * *
*
* Mark direction: groups.movedGroup.blockStart < groups.group.blockStart
* Group side:
*
* Marks '|' and deletions '-' get newNumber of reference block
*
*
* @param[in/out] array groups Groups table object, movedFrom property
Line 2,632 ⟶ 2,993:
*/
this.insertMarks = function () {
if ( this.config.timer === true ) {
this.time( 'insertMarks' );
}
var blocks = this.blocks;
Line 2,642 ⟶ 3,007:
// Enumerate copy
for ( var i = 0; i < blocksOldLength; i ++ ) {
blocksOld[i].number = i;
}
Line 2,657 ⟶ 3,023:
// Create lookup table: original to sorted
var lookupSorted = [];
for ( var i = 0; i <
lookupSorted[ blocksOld[i].number ] = i;
}
// Cycle through groups (moved group)
for ( var moved = 0; moved < groupsLength; moved ++ ) {
var movedGroup = groups[moved];
if ( movedGroup.fixed !== false ) {
Line 2,669 ⟶ 3,036:
var movedOldNumber = movedGroup.oldNumber;
// Find fixed '=' reference block from original block position to position '|' block
// // Get old text prev block
Line 2,687 ⟶ 3,055:
// Move after prev block if fixed
var refBlock = null;
if
refBlock = prevBlock;
}
// Move before next block if fixed
else if
refBlock = nextBlock;
}
Line 2,699 ⟶ 3,067:
else {
for ( var fixed = lookupSorted[ movedGroup.blockStart ] - 1; fixed >= 0; fixed -- ) {
if
refBlock = blocksOld[fixed];
break;
Line 2,762 ⟶ 3,130:
this.sortBlocks();
if ( this.config.timer === true ) {
this.timeEnd( 'insertMarks' );
}
return;
};
Line 2,767 ⟶ 3,138:
/**
* Collect diff fragment list for markup, create abstraction layer for customized diffs.
* Adds the following fagment types:
* '=', '-', '+' same, deletion, insertion
Line 2,792 ⟶ 3,163:
// Cycle through groups
for ( var group = 0; group < groupsSortLength; group ++ ) {
var blockStart = groupsSort[group].blockStart;
var blockEnd = groupsSort[group].blockEnd;
Line 2,818 ⟶ 3,190:
// Add '=' unchanged text and moved block
if
fragments.push( {
text: blocks[block].text,
Line 2,827 ⟶ 3,199:
// Add '<' and '>' marks
else if ( type === '|' ) {
var movedGroup = groups[ blocks[block].moved ];
// Get mark text
var markText = '';
for (
movedBlock <= movedGroup.blockEnd;
movedBlock ++
) {
if ( blocks[movedBlock].type === '=' || blocks[movedBlock].type === '-' ) {
markText += blocks[movedBlock].text;
}
Line 2,867 ⟶ 3,243:
// Cycle through fragments, join consecutive fragments of same type (i.e. '-' blocks)
for ( var fragment = 1; fragment < fragmentsLength; fragment ++ ) {
// Check if joinable
if (
fragments[fragment].type === fragments[fragment - 1].type &&
fragments[fragment].color === fragments[fragment - 1].color &&
fragments[fragment].text !== '' && fragments[fragment - 1].text !== ''
) {
Line 2,892 ⟶ 3,269:
/**
* Clip unchanged sections from unmoved block text.
* Adds the following fagment types:
* '~', ' ~', '~ ' omission indicators
Line 2,904 ⟶ 3,281:
// Skip if only one fragment in containers, no change
if
return;
}
Line 2,939 ⟶ 3,316:
// Cycle through fragments
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
// Skip if not an unmoved and unchanged block
var type = fragments[fragment].type;
var color = fragments[fragment].color;
if
continue;
}
Line 2,950 ⟶ 3,328:
// Skip if too short for clipping
var text = fragments[fragment].text;
if ( textLength < minRight && textLength < minLeft ) {
continue;
}
Line 2,965 ⟶ 3,344:
lines.unshift( 0 );
}
if ( lastIndex !==
lines.push(
}
Line 2,987 ⟶ 3,366:
paragraphs.unshift( 0 );
}
if ( lastIndex !==
paragraphs.push(
}
Line 2,998 ⟶ 3,377:
// Find clip pos from left, skip for first non-container block
if ( fragment !== 2 ) {
// Maximum lines to search from left
var rangeLeftMax =
if ( this.config.clipLinesLeftMax < lines.length ) {
rangeLeftMax = lines[this.config.clipLinesLeftMax];
Line 3,008 ⟶ 3,387:
// Find first heading from left
if ( rangeLeft === null ) {
for ( var j = 0; j < headingsLength; j ++ ) {
if break;
}
Line 3,020 ⟶ 3,400:
// Find first paragraph from left
if ( rangeLeft === null ) {
for ( var j = 0; j < paragraphsLength; j ++ ) {
if (
paragraphs[j] > this.config.clipParagraphLeftMax ||
paragraphs[j] > rangeLeftMax
) {
break;
}
Line 3,034 ⟶ 3,418:
// Find first line break from left
if ( rangeLeft === null ) {
if ( lines[j] > this.config.clipLineLeftMax || lines[j] > rangeLeftMax ) {
break;
}
Line 3,046 ⟶ 3,431:
}
// Find first blank from left
if ( rangeLeft === null ) {
this.config.regExp.clipBlank.lastIndex = this.config.clipBlankLeftMin;
if ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if (
regExpMatch.index < this.config.clipBlankLeftMax &&
regExpMatch.index < rangeLeftMax
) {
rangeLeft = regExpMatch.index;
rangeLeftType = 'blank';
Line 3,073 ⟶ 3,461:
// Find clip pos from right, skip for last non-container block
if ( fragment !== fragments.length - 3 ) {
// Maximum lines to search from right
Line 3,084 ⟶ 3,472:
if ( rangeRight === null ) {
for ( var j = headings.length - 1; j >= 0; j -- ) {
if (
headings[j] < textLength - this.config.clipHeadingRight ||
headings[j] < rangeRightMin
) {
break;
}
Line 3,096 ⟶ 3,487:
if ( rangeRight === null ) {
for ( var j = paragraphs.length - 1; j >= 0 ; j -- ) {
if (
paragraphs[j] < textLength - this.config.clipParagraphRightMax ||
paragraphs[j] < rangeRightMin
) {
break;
}
if ( paragraphs[j] <
rangeRight = paragraphs[j];
rangeRightType = 'paragraph';
Line 3,110 ⟶ 3,504:
if ( rangeRight === null ) {
for ( var j = lines.length - 1; j >= 0; j -- ) {
if (
lines[j] < textLength - this.config.clipLineRightMax ||
lines[j] < rangeRightMin
) {
break;
}
if ( lines[j] <
rangeRight = lines[j];
rangeRightType = 'line';
Line 3,123 ⟶ 3,520:
// Find last blank from right
if ( rangeRight === null ) {
var startPos =
if ( startPos < rangeRightMin ) {
startPos = rangeRightMin;
Line 3,130 ⟶ 3,527:
var lastPos = null;
while ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if ( regExpMatch.index >
if ( lastPos !== null ) {
rangeRight = lastPos;
Line 3,143 ⟶ 3,540:
// Fixed number of chars from right
if ( rangeRight === null ) {
if (
rangeRight =
rangeRightType = 'chars';
}
Line 3,157 ⟶ 3,554:
// Check if we skip clipping if ranges are close together
if
// Skip if overlapping ranges
Line 3,172 ⟶ 3,569:
// Skip if lines too close
var skipLines = 0;
if ( lines[j] > rangeRight || skipLines > this.config.clipSkipLines ) {
break;
}
Line 3,186 ⟶ 3,584:
// Skip if nothing to clip
if
continue;
}
Line 3,200 ⟶ 3,598:
// Get omission indicators, remove trailing blanks
if ( rangeLeftType === 'chars' ) {
omittedLeft = '~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
else if ( rangeLeftType === 'blank' ) {
omittedLeft = ' ~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
Line 3,210 ⟶ 3,608:
}
// Split right text
var textRight = null;
var omittedRight = null;
Line 3,220 ⟶ 3,618:
// Get omission indicators, remove leading blanks
if ( rangeRightType === 'chars' ) {
omittedRight = '~';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
else if ( rangeRightType === 'blank' ) {
omittedRight = '~ ';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
Line 3,232 ⟶ 3,630:
// Remove split element
fragments.splice( fragment, 1 );
fragmentsLength --;
// Add left text to fragments list
if ( rangeLeft !== null ) {
fragments.splice( fragment ++, 0, { text: textLeft, type: '=', color: null } );
fragmentsLength ++;
if ( omittedLeft !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedLeft, color: null } );
fragmentsLength ++;
}
}
// Add fragment container and separator to list
if
fragments.splice( fragment ++, 0, { text: '', type: ']', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: ',', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: '[', color: null } );
fragmentsLength += 3;
}
Line 3,252 ⟶ 3,654:
if ( omittedRight !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedRight, color: null } );
fragmentsLength ++;
}
fragments.splice( fragment ++, 0, { text: textRight, type: '=', color: null } );
fragmentsLength ++;
}
}
Line 3,267 ⟶ 3,671:
/**
* Create html formatted diff code from diff fragments.
*
* @param[in] array fragments Fragments array, abstraction layer for diff code
Line 3,279 ⟶ 3,683:
// No change, only one unchanged block in containers
if
this.html = '';
return;
}
Line 3,289 ⟶ 3,690:
// Cycle through fragments
var htmlFragments = [];
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
var text = fragments[fragment].text;
var type = fragments[fragment].type;
Line 3,302 ⟶ 3,704:
// Add container start markup
if ( type === '{' ) {
html = this.config.htmlCode.containerStart;
}
// Add container end markup
else if ( type === '}' ) {
html = this.config.htmlCode.containerEnd;
}
// Add fragment start markup
if ( type === '[' ) {
html = this.config.htmlCode.fragmentStart;
}
// Add fragment end markup
else if ( type === ']' ) {
html = this.config.htmlCode.fragmentEnd;
}
// Add fragment separator markup
else if ( type === ',' ) {
html = this.config.htmlCode.separator;
}
// Add omission markup
if ( type === '~' ) {
html = this.config.htmlCode.omittedChars;
}
// Add omission markup
if ( type === ' ~' ) {
html = ' ' + this.config.htmlCode.omittedChars;
}
// Add omission markup
if ( type === '~ ' ) {
html = this.config.htmlCode.omittedChars + ' ';
}
// Add colored left-pointing block start markup
else if ( type === '(<' ) {
if ( version !== 'old' ) {
// Get title
Line 3,366 ⟶ 3,768:
// Add colored right-pointing block start markup
else if ( type === '(>' ) {
if ( version !== 'old' ) {
// Get title
Line 3,390 ⟶ 3,792:
// Add colored block end markup
else if ( type === ' )' ) {
if ( version !== 'old' ) {
html = this.config.htmlCode.blockEnd;
}
Line 3,397 ⟶ 3,799:
// Add '=' (unchanged) text and moved block
if ( type === '=' ) {
text = this.htmlEscape( text );
if ( color !== null ) {
if ( version !== 'old' ) {
html = this.markupBlanks( text, true );
}
Line 3,410 ⟶ 3,812:
// Add '-' text
else if ( type === '-' ) {
if ( version !== 'new' ) {
// For old version skip '-' inside moved group
if
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
Line 3,429 ⟶ 3,831:
// Add '+' text
else if ( type === '+' ) {
if ( version !== 'old' ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
Line 3,444 ⟶ 3,846:
// Add '<' and '>' code
else if
if ( version !== 'new' ) {
// Display as deletion at original position
if
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
if ( version === 'old' ) {
if ( this.config.coloredBlocks === true ) {
html =
this.htmlCustomize( this.config.htmlCode.blockColoredStart, color ) + text + this.config.htmlCode.blockEnd; }
else {
html =
this.htmlCustomize( this.config.htmlCode.blockStart, color ) + text + this.config.htmlCode.blockEnd; }
}
else {
if ( blank === true ) {
html =
this.config.htmlCode.deleteStartBlank + text + this.config.htmlCode.deleteEnd; }
else {
Line 3,471 ⟶ 3,882:
// Display as mark
else {
if ( type === '<' ) {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markLeftColored, color, text );
Line 3,501 ⟶ 3,912:
/**
* Customize html code fragments.
* Replaces:
* {number}: class/color/block/mark/id number
Line 3,530 ⟶ 3,941:
var gapMark = ' [...] ';
if ( title.length > max ) {
title =
title.substr( 0, max - gapMark.length - end ) + gapMark + title.substr( title.length - end ); }
title = this.htmlEscape( title );
Line 3,542 ⟶ 3,956:
/**
* Replace html-sensitive characters in output text with character entities.
*
* @param string html Html code to be escaped
Line 3,558 ⟶ 3,972:
/**
* Markup tabs, newlines, and spaces in diff fragment text.
*
* @param bool highlight Highlight newlines and
* @param string html Text code to be marked-up
* @return string Marked-up text
Line 3,566 ⟶ 3,980:
this.markupBlanks = function ( html, highlight ) {
if ( highlight === true ) {
html = html.replace( / /g, this.config.htmlCode.space);
html = html.replace( /\n/g, this.config.htmlCode.newline);
}
html = html.replace( /\t/g, this.config.htmlCode.tab);
return html;
};
Line 3,576 ⟶ 3,990:
/**
* Count real words in text.
*
* @param string text Text for word counting
Line 3,588 ⟶ 4,002:
/**
* Test diff code for consistency with input versions.
* Prints results to debug console.
*
* @param[in] WikEdDiffText newText, oldText Text objects
Line 3,599 ⟶ 4,013:
var diff = this.html.replace( /<[^>]*>/g, '');
var text = this.htmlEscape( this.newText.text );
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with new text version!' );
this.error = true;
console.log( 'new text:\n', text );
Line 3,613 ⟶ 4,029:
var diff = this.html.replace( /<[^>]*>/g, '');
var text = this.htmlEscape( this.oldText.text );
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with old text version!' );
this.error = true;
console.log( 'old text:\n', text );
Line 3,628 ⟶ 4,046:
/**
* Dump blocks object to browser console.
*
* @param string name Block name
Line 3,638 ⟶ 4,056:
blocks = this.blocks;
}
var dump =
'\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq' +
'\twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \ttext\n';
var blocksLength = blocks.length;
for ( var i = 0; i < blocksLength; i ++ ) {
dump +=
i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' +
blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \t' + blocks[i].oldStart + ' \t' +
blocks[i].count + ' \t' + blocks[i].unique + ' \t' + blocks[i].words + ' \t' +
blocks[i].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' +
blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + blocks[i].moved + ' \t' +
this.debugShortenText( blocks[i].text ) + '\n';
}
console.log( name + ':\n' + dump );
Line 3,647 ⟶ 4,074:
/**
* Dump groups object to browser console.
*
* @param string name Group name
Line 3,657 ⟶ 4,084:
groups = this.groups;
}
var dump =
'\ni \toldNm \tblSta \tblEnd \tuniq \tmaxWo' + '\twords \tchars \tfixed \toldNm \tmFrom \tcolor\n'; var groupsLength = groupsLength;
for ( var i = 0; i < groups.length; i ++ ) {
dump +=
i + ' \t' + groups[i].oldNumber + ' \t' + groups[i].blockStart + ' \t' +
groups[i].blockEnd + ' \t' + groups[i].unique + ' \t' + groups[i].maxWords + ' \t' +
groups[i].words + ' \t' + groups[i].chars + ' \t' + groups[i].fixed + ' \t' +
groups[i].oldNumber + ' \t' + groups[i].movedFrom + ' \t' + groups[i].color + '\n';
}
console.log( name + ':\n' + dump );
Line 3,666 ⟶ 4,100:
/**
* Dump fragments array to browser console.
*
* @param string name Fragments name
Line 3,675 ⟶ 4,109:
var fragments = this.fragments;
var dump = '\ni \ttype \tcolor \ttext\n';
for ( var i = 0; i < fragmentsLength; i ++ ) {
dump += i + ' \t"' + fragments[i].type + '" \t' + fragments[i].color + ' \t' + this.debugShortenText( fragments[i].text, 120, 40 ) + '\n'; }
console.log( name + ':\n' + dump );
Line 3,683 ⟶ 4,120:
/**
* Dump borders array to browser console.
*
* @param string name Arrays name
* @param[in] array border Match border array
*/
this.debugBorders = function ( name, borders ) {
var dump = '\ni \t[ new \told ]\n';
var bordersLength = borders.length;
for ( var i = 0; i < bordersLength; i ++ ) {
dump += i + ' \t[ ' + borders[i][0] + ' \t' + borders[i][1] + ' ]\n';
}
console.log( name, dump );
};
/**
* Shorten text for dumping.
*
* @param string text Text to be shortened
Line 3,692 ⟶ 4,146:
this.debugShortenText = function ( text, max, end ) {
if ( typeof text !== 'string' ) {
text = text.toString();
}
Line 3,712 ⟶ 4,166:
/**
* Start timer 'label', analogous to JavaScript console timer.
* Usage: this.time( 'label' );
*
* @param string label Timer label
* @param[out] array timer Current time in milliseconds (float)
*/
this.time = function ( label ) {
Line 3,725 ⟶ 4,179:
/**
* Stop timer 'label', analogous to JavaScript console timer.
* Logs time in milliseconds since start to browser console.
* Usage: this.timeEnd( 'label' );
*
* @param string label Timer label
* @param bool noLog Do not log result
* @return float Time in milliseconds
*/
this.timeEnd = function ( label, noLog ) {
Line 3,742 ⟶ 4,196:
this.timer[label] = undefined;
if ( noLog !== true ) {
console.log( label + ': ' + diff.toFixed( 2 ) + ' ms' );
}
}
Line 3,750 ⟶ 4,204:
/**
* Log recursion timer results to browser console.
* Usage: this.timeRecursionEnd();
*
Line 3,761 ⟶ 4,215:
// Subtract times spent in deeper recursions
for ( var i = 0; i < timerEnd; i ++ ) {
this.recursionTimer[i] -= this.recursionTimer[i + 1];
}
// Log recursion times
for ( var i = 0; i < timerLength; i ++ ) {
console.log( text + ' recursion ' + i + ': ' + this.recursionTimer[i].toFixed( 2 ) + ' ms' );
}
}
Line 3,776 ⟶ 4,232:
/**
* Log variable values to debug console.
* Usage: this.debug( 'var', var );
*
* @param string name Object identifier
Line 3,795 ⟶ 4,251:
/**
* Add script to document head.
*
* @param string code JavaScript code
Line 3,817 ⟶ 4,273:
/**
* Add stylesheet to document head, cross-browser >= IE6.
*
* @param string css CSS code
Line 3,845 ⟶ 4,301:
/**
* Recursive deep copy from target over source for customization import.
*
* @param object source Source object
Line 3,871 ⟶ 4,327:
/**
* Data and methods for single text version (old or new one).
*
* @class WikEdDiffText
Line 3,895 ⟶ 4,351:
/**
* Constructor, initialize text object.
*
* @param string text Text of version
Line 3,902 ⟶ 4,358:
this.init = function () {
if ( typeof text !== 'string' ) {
text = text.toString();
}
Line 3,909 ⟶ 4,365:
this.text = text.replace( /\r\n?/g, '\n');
//
if ( this.parent.config.timer === true ) {
this.parent.time( 'wordParse' );
Line 3,923 ⟶ 4,379:
/**
* Parse and count words and chunks for identification of unique words.
*
* @param string regExp Regular expression for counting words
Line 3,931 ⟶ 4,387:
this.wordParse = function ( regExp ) {
var regExpMatch = this.text.match( regExp );
var
if ( Object.prototype.hasOwnProperty.call( this.words, word ) === false ) {
this.words[word] = 1;
}
else {
this.words[word] ++;
}
}
}
Line 3,946 ⟶ 4,405:
/**
* Split text into paragraph, line, sentence, chunk, word, or character tokens.
*
* @param string level Level of splitting: paragraph, line, sentence, chunk, word, or character
* @param int|null token Index of token to be split, otherwise uses full text
* @param[in] string text Full text to be split
Line 3,977 ⟶ 4,436:
var regExpMatch;
var lastIndex = 0;
while ( ( regExpMatch = regExp.exec( text ) ) !== null ) {
if ( regExpMatch.index > lastIndex ) {
split.push( text.substring( lastIndex, regExpMatch.index ) );
}
split.push( regExpMatch[0] );
lastIndex =
}
if ( lastIndex < text.length ) {
Line 3,988 ⟶ 4,448:
}
// Cycle
for ( var i = 0; i < splitLength; i ++ ) {
// Insert current item, link to previous
this.tokens
token: split[i],
prev: prev,
Line 3,999 ⟶ 4,460:
number: null,
unique: false
} );
number ++;
Line 4,011 ⟶ 4,472:
// Connect last new item and existing next item
if
if ( prev !== null ) {
this.tokens[prev].next = next;
Line 4,031 ⟶ 4,492:
// First or last token has been split
else {
if ( token === this.first ) {
this.first = first;
}
if ( token === this.last ) {
this.last = prev;
}
Line 4,044 ⟶ 4,505:
/**
* Split unique unmatched tokens into smaller tokens.
*
* @param string level Level of splitting: line, sentence, chunk, or word
* @param[in] array tokens Tokens list
*/
Line 4,053 ⟶ 4,514:
// Cycle through tokens list
var i = this.first;
while
// Refine unique unmatched tokens into smaller tokens
Line 4,066 ⟶ 4,527:
/**
* Enumerate text token list before detecting blocks.
*
* @param[out] array tokens Tokens list
Line 4,075 ⟶ 4,536:
var number = 0;
var i = this.first;
while
this.tokens[i].number = number;
number ++;
Line 4,085 ⟶ 4,546:
/**
* Dump tokens object to browser console.
*
* @param string name Text name
Line 4,095 ⟶ 4,556:
var tokens = this.tokens;
var dump = 'first: ' + this.first + '\tlast: ' + this.last + '\n';
dump += '\ni \tlink \t(
var i = this.first;
while
dump +=
i + ' \t' + tokens[i].link + ' \t(' + tokens[i].prev + ' \t' + tokens[i].next + ') \t' +
tokens[i].unique + ' \t#' + tokens[i].number + ' \t' +
parent.debugShortenText( tokens[i].token ) + '\n';
i = tokens[i].next;
}
|