User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.0.22 (September 15, 2014) fix newline and space markup
1.1.0 (September 21, 2014) major clean-up/partial rewrite: objectified, findMaxPath bug fix, unit tests, del block positioning, marks as blocks, unique words in paragraph/sentence tokens
Line 3:
// ==UserScript==
// @name wDiff
// @version 1.01.2200
// @date September 1521, 2014
// @description improved word-based diff library with block move detection
// @homepage https://en.wikipedia.org/wiki/User:Cacycle/diff
Line 40:
 
Usage:
var diffHtml = wDiff.Diffdiff(oldString, newString, full);
diffHtml = wDiff.ShortenOutput(diffHtml);
 
Datastructures (abbreviations from publication):
 
textwDiff: objects for textnamespace relatedobject data(global)
.configurations see top of code below for configuration and customization options
.newText, new text
.oldText: old text
.string: new or old text to be diffed
.tokens[]: token data list for new or old string (N and O)
.prev: previous list item
.next: next list item
.token: token string
.link: index of corresponding token in new or old text (OA and NA)
.number: list enumeration number
.parsed: token has been added to symbol table
.unique: token is unique word in text
.first: index of first token in tokens list
.last: index of last token in tokens list
.words{}: word count
.diff: diff html
 
symbolsclass Text: diff text object for(new symbolsor tableold dataversion)
.string: text
.token[]: associative array (hash) of parsed tokens for passes 1 - 3, points to symbol[i]
.words{}: word count hash
.symbol[]: array of objects that hold token counters and pointers:
.newCountfirst: new text index of first token counterin tokens (NC)list
.oldCountlast: old text index of last token counterin tokens (OC)list
.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
 
.tokens[]: token list for new or old string (doubly-linked list) (N and O)
.prev: previous list item
.next: next list item
.token: token string
.link: index of corresponding token in new or old text (OA and NA)
.number: list enumeration number
.unique: token is unique word in text
 
class TextDiff: diff object
blocks[]: array of objects that holds block (consecutive text tokens) data in order of the new text
.oldBlock:newText, number of block in oldnew text order
.newBlockoldText: number of block in newold text order
.oldNumberhtml: old text token number of first tokendiff html
.newNumber: new text token number of first token
.oldStart: old text token index of first token
.count number of tokens
.unique: contains unique matched token
.words: word count
.chars: char length
.type: 'same', 'del', 'ins'
.section: section number
.group: group number of block
.fixed: belongs to a fixed (not moved) group
.string: string of block tokens
 
groups[] .symbols: section blocks thatobject arefor consecutivesymbols intable old textdata
.token[]: associative array (hash) of parsed tokens for passes 1 - 3, points to symbol[i]
.oldNumber: first block oldNumber
.symbol[]: array of objects that hold token counters and pointers:
.blockStart: first block index
.blockEndnewCount: last blocknew indextext token counter (NC)
.uniqueoldCount: old containstext uniquetoken matchedcounter token(OC)
.maxWordsnewToken: word counttoken ofindex longestin blocktext.newText.tokens
.wordsoldToken: token index wordin counttext.oldText.tokens
.linked: flag: at least one unique token pair has been linked
.chars: char count
 
.fixed: not moved from original position
.movedblocks[]: listarray of groupsobjects that haveholds beenblock moved(consecutive fromtext thistokens) positiondata in order of the new text
.movedFromoldBlock: position this groupnumber hasof beenblock movedin fromold text order
.colornewBlock: number of colorblock numberin ofnew movedtext grouporder
.diffoldNumber: old text token number groupof difffirst token
.newNumber: new text token number of first token
.oldStart: old text token index of first token
.count number of tokens
.unique: contains unique matched token
.words: word count
.chars: char length
.type: 'same', 'del', 'ins', 'mark'
.section: section number
.group: group number of block
.fixed: belongs to a fixed (not moved) group
.moved: 'mark' block associated moved block group number
.string: string of block tokens
 
.groups[]: section blocks that are consecutive in old text
.oldNumber: first block oldNumber
.blockStart: first block index
.blockEnd: last block index
.unique: contains unique matched token
.maxWords: word count of longest 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
 
*/
 
// JSHint options: W004: is already defined, W097: Use the function form of "use strict", W100: This character may get silently deleted by one or more browsers
/* jshint -W004, -W097, -W100, newcap: false, 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 111 ⟶ 113:
'use strict';
 
// define global objectobjects
var wDiff; if (wDiff === undefined) { wDiff = {}; }
var WED;
 
//
// start of configuration and customization settings
//
 
//
Line 133 ⟶ 139:
// display blocks in different colors
if (wDiff.coloredBlocks === undefined) { wDiff.coloredBlocks = false; }
 
// show debug infos and stats
if (wDiff.debug === undefined) { wDiff.debug = false; }
 
// show debug infos and stats
if (wDiff.debugTime === undefined) { wDiff.debugTime = false; }
 
// run unit tests
if (wDiff.unitTesting === undefined) { wDiff.unitTesting = false; }
 
// UniCode letter support for regexps, from http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
Line 179 ⟶ 194:
if (wDiff.regExpSlideBorder === undefined) { wDiff.regExpSlideBorder = new RegExp('[^' + wDiff.letters + ']$'); }
 
// regExpregExps for counting words
if (wDiff.regExpWordCountregExpWord === undefined) { wDiff.regExpWordCountregExpWord = new RegExp('[' + wDiff.letters + ']+([\'’_]?[' + wDiff.letters + ']+)*', 'g'); }
if (wDiff.regExpChunk === undefined) { wDiff.regExpChunk = wDiff.regExpSplit.chunk; }
 
// regExp detecting blank-only and single-char blocks
Line 214 ⟶ 230:
 
// maximal fragment distance to join close fragments
if (wDiff.fragmentJoinLines === undefined) { wDiff.fragmentJoinLines = 5; }
if (wDiff.fragmentJoinChars === undefined) { wDiff.fragmentJoinChars = 1000; }
 
//
Line 238 ⟶ 254:
// block
'.wDiffBlockLeft, .wDiffBlockRight { font-weight: bold; background-color: #e8e8e8; border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; }' +
'.wDiffBlockHighlight { background-color: #777; color: #fff; border: solid #777; border-width: 1px 0; }' +
'.wDiffBlock { }' +
'.wDiffBlock0 { background-color: #ffff60ffff80; }' +
'.wDiffBlock1 { background-color: #c0ff60d0ff80; }' +
'.wDiffBlock2 { background-color: #ffd8ffffd8f0; }' +
'.wDiffBlock3 { background-color: #a0ffffc0ffff; }' +
'.wDiffBlock4 { background-color: #ffe840fff888; }' +
'.wDiffBlock5 { background-color: #bbccff; }' +
'.wDiffBlock6 { background-color: #ffaaffe8c8ff; }' +
'.wDiffBlock7 { background-color: #ffbbbb; }' +
'.wDiffBlock8 { background-color: #a0e8a0; }' +
'.wDiffBlockHighlight { background-color: #777; color: #fff; border: solid #777; border-width: 1px 0; }' +
 
// mark
'.wDiffMarkLeft, .wDiffMarkRight { font-weight: bold; background-color: #ffe49c; color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; }' +
'.wDiffMarkRight:before { content: "' + wDiff.symbolMarkRight + '"; }' +
'.wDiffMarkLeft:before { content: "' + wDiff.symbolMarkLeft + '"; }' +
'.wDiffMark { background-color: #e8e8e8; color: #666; }' +
'.wDiffMark0 { background-color: #ffff60; }' +
'.wDiffMark1 { background-color: #c8f880; }' +
'.wDiffMark2 { background-color: #ffd0f0; }' +
'.wDiffMark3 { background-color: #a0ffff; }' +
'.wDiffMark4 { background-color: #fff860; }' +
'.wDiffMark5 { background-color: #b0c0ff; }' +
'.wDiffMark6 { background-color: #e0c0ff; }' +
'.wDiffMark7 { background-color: #ffa8a8; }' +
'.wDiffMark8 { background-color: #98e898; }' +
'.wDiffMarkHighlight { background-color: #777; color: #fff; }' +
'.wDiffMark0 { color: #ffff60; }' +
'.wDiffMark1 { color: #c0ff60; }' +
'.wDiffMark2 { color: #ffd8ff; }' +
'.wDiffMark3 { color: #a0ffff; }' +
'.wDiffMark4 { color: #ffd840; }' +
'.wDiffMark5 { color: #bbccff; }' +
'.wDiffMark6 { color: #ff99ff; }' +
'.wDiffMark7 { color: #ff9999; }' +
'.wDiffMark8 { color: #90d090; }' +
 
// wrappers
Line 271 ⟶ 287:
'.wDiffNoChange { white-space: pre-wrap; background: #f0f0f0; 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: 0.5em; margin: 1em 0; }' +
'.wDiffSeparator { margin-bottom: 1em; }' +
'.wDiffOmittedChars { }' +
 
// newline
Line 307 ⟶ 323:
if (wDiff.styleInsertBlank === undefined) { wDiff.styleInsertBlank = ''; }
if (wDiff.styleDeleteBlank === undefined) { wDiff.styleDeleteBlank = ''; }
if (wDiff.styleBlockLeft styleBlock === undefined) { wDiff.styleBlockLeftstyleBlock = ''; }
if (wDiff.styleBlockLeft === undefined) { wDiff.styleBlockLeft = ''; }
if (wDiff.styleBlockRight === undefined) { wDiff.styleBlockRight = ''; }
if (wDiff.styleBlockHighlight === undefined) { wDiff.styleBlockHighlight = ''; }
if (wDiff.styleBlockColor === undefined) { wDiff.styleBlockColor = []; }
if (wDiff.styleMark === undefined) { wDiff.styleMark = ''; }
if (wDiff.styleMarkLeft === undefined) { wDiff.styleMarkLeft = ''; }
if (wDiff.styleMarkRight === undefined) { wDiff.styleMarkRight = ''; }
Line 326 ⟶ 344:
 
//
// output html for core diff
//
 
// dynamic replacements: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
// class plus html comment are required indicators for wDiffTextDiff.ShortenOutputshortenOutput()
 
if (wDiff.blockEvent === undefined) { wDiff.blockEvent = ' onmouseover="wDiff.BlockHandler(undefined, this, \'mouseover\');"'; }
if (wDiff.blockEvent === undefined) { wDiff.blockEvent = ' onmouseover="wDiff.blockHandler(undefined, this, \'mouseover\');"'; }
 
if (wDiff.htmlContainerStart === undefined) { wDiff.htmlContainerStart = '<div class="wDiffContainer" id="wDiffContainer" style="' + wDiff.styleContainer + '">'; }
if (wDiff.htmlContainerEnd === undefined) { wDiff.htmlContainerEnd = '</div>'; }
 
if (wDiff.htmlDeleteStart === undefined) { wDiff.htmlDeleteStart = '<span class="wDiffDelete" style="' + wDiff.styleDelete + '" title="−">'; }
if (wDiff.htmlDeleteStartBlank === undefined) { wDiff.htmlDeleteStartBlank = '<span class="wDiffDelete wDiffDeleteBlank" style="' + wDiff.styleDelete + ' ' + wDiff.styleDeleteBlank + '" title="−">'; }
if (wDiff.htmlDeleteEnd === undefined) { wDiff.htmlDeleteEnd = '</span><!--wDiffDelete-->'; }
 
if (wDiff.htmlInsertStart === undefined) { wDiff.htmlInsertStart = '<span class="wDiffInsert" style="' + wDiff.styleInsert + '" title="+">'; }
if (wDiff.htmlInsertStartBlank === undefined) { wDiff.htmlInsertStartBlank = '<span class="wDiffInsert wDiffInsertBlank" style="' + wDiff.styleInsert + ' ' + wDiff.styleInsertBlank + '" title="+">'; }
if (wDiff.htmlInsertEnd === undefined) { wDiff.htmlInsertEnd = '</span><!--wDiffInsert-->'; }
 
if (wDiff.htmlBlockLeftStarthtmartlDeleteSt === undefined) { wDiff.htmlBlockLeftStarthtmlDeleteStart = '<span class="wDiffBlockLeft wDiffBlock{class}wDiffDelete" style="' + wDiff.styleBlockLeftstyleDelete + '{block}" title="' + wDiff.symbolMarkLeft + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
if (wDiff.htmlBlockLeftEnd htmlDeleteStartBlank === undefined) { wDiff.htmlBlockLeftEnd htmlDeleteStartBlank = '</span><!--wDiffBlockLeft-- class="wDiffDelete wDiffDeleteBlank" style="' + wDiff.styleDeleteBlank + '" title="−">'; }
if (wDiff.htmlDeleteEnd === undefined) { wDiff.htmlDeleteEnd = '</span><!--wDiffDelete-->'; }
 
if (wDiff.htmlBlockLeftStart === undefined) {
if (wDiff.htmlBlockRightStart === undefined) { wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight wDiffBlock{class}" style="' + wDiff.styleBlockRight + '{block}" title="' + wDiff.symbolMarkRight + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
if (wDiff.coloredBlocks === false) {
if (wDiff.htmlBlockRightEnd === undefined) { wDiff.htmlBlockRightEnd = '</span><!--wDiffBlockRight-->'; }
wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft" style="' + wDiff.styleBlockLeft + '" title="' + wDiff.symbolMarkLeft + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>';
}
else {
wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft wDiffBlock wDiffBlock{class}" style="' + wDiff.styleBlockLeft + wDiff.styleBlock + '{block}" title="' + wDiff.symbolMarkLeft + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>';
}
}
if (wDiff.htmlBlockLeftEnd === undefined) { wDiff.htmlBlockLeftEnd = '</span><!--wDiffBlockLeft-->'; }
 
if (wDiff.htmlBlockRightStart === undefined) {
if (wDiff.htmlMarkLeft === undefined) { wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft wDiffMark{class}" style="' + wDiff.styleMarkLeft + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->'; }
if (wDiff.coloredBlocks === false) {
if (wDiff.htmlMarkRight === undefined) { wDiff.htmlMarkRight = '<span class="wDiffMarkRight wDiffMark{class}" style="' + wDiff.styleMarkRight + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->'; }
wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight" style="' + wDiff.styleBlockRight + '" title="' + wDiff.symbolMarkRight + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>';
}
else {
wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight wDiffBlock wDiffBlock{class}" style="' + wDiff.styleBlockRight + wDiff.styleBlock + '{block}" title="' + wDiff.symbolMarkRight + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>';
}
}
if (wDiff.htmlBlockRightEnd === undefined) { wDiff.htmlBlockRightEnd = '</span><!--wDiffBlockRight-->'; }
 
if (wDiff.htmlMarkLeft === undefined) {
if (wDiff.coloredBlocks === false) {
wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft" style="' + wDiff.styleMarkLeft + '"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->';
}
else {
wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft wDiffMark wDiffMark{class}" style="' + wDiff.styleMarkLeft + wDiff.styleMark + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->';
}
}
if (wDiff.htmlMarkRight === undefined) {
if (wDiff.coloredBlocks === false) {
wDiff.htmlMarkRight = '<span class="wDiffMarkRight" style="' + wDiff.styleMarkRight + '"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->';
}
else {
wDiff.htmlMarkRight = '<span class="wDiffMarkRight wDiffMark wDiffMark{class}" style="' + wDiff.styleMarkRight + wDiff.styleMark + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->';
}
}
 
if (wDiff.htmlNewline === undefined) { wDiff.htmlNewline = '<span class="wDiffNewline" style="' + wDiff.styleNewline + '">\n</span>'; }
Line 357 ⟶ 404:
if (wDiff.htmlSpace === undefined) { wDiff.htmlSpace = '<span class="wDiffSpace" style="' + wDiff.styleSpace + '"><span class="wDiffSpaceSymbol" style="' + wDiff.styleSpaceSymbol + '"></span> </span>'; }
 
// shorten output
//
// html for shorten output
//
 
if (wDiff.htmlFragmentStart === undefined) { wDiff.htmlFragmentStart = '<pre class="wDiffFragment" style="' + wDiff.styleFragment + '">'; }
if (wDiff.htmlFragmentEnd === undefined) { wDiff.htmlFragmentEnd = '</pre>'; }
 
if (wDiff.htmlNoChange === undefined) { wDiff.htmlNoChange = '<pre class="wDiffNoChange" style="' + wDiff.styleNoChange + '" title="="></pre>'; }
Line 369 ⟶ 414:
 
//
// javascript handler for output code, compatible with IE 8 compatible
//
 
// wDiff.BlockHandlerblockHandler: event handler for block and mark elements
if (wDiff.BlockHandlerblockHandler === undefined) { wDiff.BlockHandlerblockHandler = function (event, element, type) {
 
// IE compatibility
Line 388 ⟶ 433:
if (type == 'mouseover') {
element.onmouseover = null;
element.onmouseout = function (event) { wDiff.BlockHandlerblockHandler(event, element, 'mouseout'); };
element.onclick = function (event) { wDiff.BlockHandlerblockHandler(event, element, 'click'); };
block.className += ' wDiffBlockHighlight';
mark.className += ' wDiffMarkHighlight';
Line 397 ⟶ 442:
if ( (type == 'mouseout') || (type == 'click') ) {
element.onmouseout = null;
element.onmouseover = function (event) { wDiff.BlockHandlerblockHandler(event, element, 'mouseover'); };
 
// getElementsByClassName
Line 426 ⟶ 471:
}
 
// get element height (getOffsetTop)
var corrElementPos = 0;
var node = corrElement;
Line 433 ⟶ 478:
} while ( (node = node.offsetParent) !== null );
 
// get scroll element under mouse cursorheight
var top;
if (window.pageYOffset !== undefined) {
Line 442 ⟶ 487:
}
 
// get cursor pos
var cursor;
if (event.pageY !== undefined) {
Line 450 ⟶ 496:
}
 
// get line height
var line = 12;
if (window.getComputedStyle !== undefined) {
Line 455 ⟶ 502:
}
 
// scroll element under mouse cursor
window.scroll(0, corrElementPos + top - cursor + line / 2);
}
return;
}; }
 
 
//
// end of configuration and customization settings
// start of diff code
//
 
 
// wDiff.Initinit(): initialize wDiff
// called from: on code load
// calls: wDiff.AddStyleSheetaddStyleSheet(), .addScript()
 
wDiff.Initinit = function () {
 
// legacy for short time
// compatibility fixes for old names of functions
windowwDiff.StringDiffDiff = wDiff.Diffdiff;
window.WDiffString = wDiff.Diff;
window.WDiffShortenOutput = wDiff.ShortenOutput;
 
// shortcut to wikEd.Debug()
if (WED === undefined) {
if (typeof console == 'object') {
WED = console.log;
}
else {
WED = window.alert;
}
}
 
// add styles to head
wDiff.AddStyleSheetaddStyleSheet(wDiff.stylesheet);
 
// add block handler to head if running under Greasemonkey
if (typeof GM_info == 'object') {
var script = 'var wDiff; if (wDiff === undefined) { wDiff = {}; } wDiff.BlockHandlerblockHandler = ' + wDiff.BlockHandlerblockHandler.toString();
wDiff.AddScriptaddScript(script);
}
return;
Line 498 ⟶ 535:
 
 
// wDiff.Diffdiff(): main method of wDiff, runs the diff and shortens the output
// called from: user land
// input: oldString, newString, strings containing the texts to be diffed
// calls: new TextDiff, TextDiff.shortenOutput(), this.unitTests()
// called from: user code
// calls: wDiff.Split(), wDiff.SplitRefine(), wDiff.CalculateDiff(), wDiff.DetectBlocks(), wDiff.AssembleDiff()
// returns: diff html code, call wDiff.ShortenOutput() for shortening this output
 
wDiff.Diffdiff = function (oldString, newString, full) {
 
// create text diff object
var diff = '';
var textDiff = new wDiff.TextDiff(oldString, newString, this);
 
// legacy for short time
// wikEd.debugTimer.push(['diff?', new Date]);
wDiff.textDiff = textDiff;
wDiff.ShortenOutput = wDiff.textDiff.shortenOutput;
 
// IEstart / Mac fixtimer
if (wDiff.debugTime === true) {
oldString = oldString.replace(/\r\n?/g, '\n');
console.time('diff');
newString = newString.replace(/\r\n?/g, '\n');
}
 
// preparerun textthe data objectdiff
textDiff.diff();
var text = {
newText: {
string: newString,
tokens: [],
first: null,
last: null,
words: {}
},
oldText: {
string: oldString,
tokens: [],
first: null,
last: null,
words: {}
},
diff: ''
};
 
// start timer
// trap trivial changes: no change
if (oldStringwDiff.debugTime === newStringtrue) {
console.timeEnd('diff');
text.diff = wDiff.HtmlEscape(newString);
wDiff.HtmlFormat(text);
return text.diff;
}
 
// shorten output
// trap trivial changes: old text deleted
if (full !== true) {
if ( (oldString === null) || (oldString.length === 0) ) {
 
text.diff = wDiff.htmlInsertStart + wDiff.HtmlEscape(newString) + wDiff.htmlInsertEnd;
// start timer
wDiff.HtmlFormat(text);
if (wDiff.debugTime === true) {
return text.diff;
console.time('shorten');
}
 
textDiff.shortenOutput();
 
// stop timer
if (wDiff.debugTime === true) {
console.timeEnd('shorten');
}
}
 
// stop timer
// trap trivial changes: new text deleted
if ( (newString === null) || (newStringwDiff.lengthdebugTime === 0) true) {
console.timeEnd('diff');
text.diff = wDiff.htmlDeleteStart + wDiff.HtmlEscape(oldString) + wDiff.htmlDeleteEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
// run unit tests
// parse and count words in texts for later identification of unique words
if (wDiff.unitTesting === true) {
wDiff.CountTextWords(text.newText);
wDiff.CountTextWordsunitTests(text.oldTexttextDiff);
}
return textDiff.html;
};
 
// new symbols object
var symbols = {
token: [],
hash: {},
linked: false
};
 
// wDiff.unitTests(): test diff for consistency between input and output
// split new and old text into paragraps
// input: textDiff: text diff object after calling .diff()
wDiff.Split(text.newText, 'paragraph');
// called from: .diff()
wDiff.Split(text.oldText, 'paragraph');
 
wDiff.unitTests = function (textDiff) {
// calculate diff
wDiff.CalculateDiff(text, symbols, 'paragraph');
 
// start timer
// refine different paragraphs into sentences
if (wDiff.debugTime === true) {
wDiff.SplitRefine(text.newText, 'sentence');
console.time('unit tests');
wDiff.SplitRefine(text.oldText, 'sentence');
}
 
var html = textDiff.html;
// calculate refined diff
wDiff.CalculateDiff(text, symbols, 'sentence');
 
// check if output is consistent with new text
// refine different paragraphs into chunks
textDiff.assembleDiff('new');
wDiff.SplitRefine(text.newText, 'chunk');
var diff = textDiff.html.replace(/<[^>]*>/g, '');
wDiff.SplitRefine(text.oldText, 'chunk');
var text = textDiff.htmlEscape(textDiff.newText.string);
if (diff != text) {
console.log('Error: wDiff unit test failure: output not consistent with new text');
console.log('new text:\n', text);
console.log('new diff:\n', diff);
}
else {
console.log('OK: wDiff unit test passed: output consistent with new text');
}
 
// check if output is consistent with old text
// calculate refined diff
textDiff.assembleDiff('old');
wDiff.CalculateDiff(text, symbols, 'chunk');
var diff = textDiff.html.replace(/<[^>]*>/g, '');
var text = textDiff.htmlEscape(textDiff.oldText.string);
if (diff != text) {
console.log('Error: wDiff unit test failure: output not consistent with old text');
console.log('old text:\n', text);
console.log('old diff:\n', diff);
}
else {
console.log('OK: wDiff unit test passed: output consistent with old text');
}
 
textDiff.html = html;
// refine different sentences into words
wDiff.SplitRefine(text.newText, 'word');
wDiff.SplitRefine(text.oldText, 'word');
 
// stop timer
// calculate refined diff information with recursion for unresolved gaps
if (wDiff.CalculateDiff(text,debugTime symbols, 'word',=== true); {
console.timeEnd('unit tests');
}
return;
};
 
// slide up gaps
wDiff.SlideGaps(text.newText, text.oldText);
wDiff.SlideGaps(text.oldText, text.newText);
 
//
// split tokens into chars in selected unresolved gaps
// wDiff.Text class: data and methods for single text version (old or new)
if (wDiff.charDiff === true) {
// called from: TextDiff.init()
wDiff.SplitRefineChars(text);
//
 
wDiff.Text = function (string, parent) {
// calculate refined diff information with recursion for unresolved gaps
wDiff.CalculateDiff(text, symbols, 'character', true);
 
this.parent = parent;
// slide up gaps
this.string = null;
wDiff.SlideGaps(text.newText, text.oldText);
this.tokens = [];
wDiff.SlideGaps(text.oldText, text.newText);
this.first = null;
}
this.last = null;
this.words = {};
 
// enumerate tokens lists
wDiff.EnumerateTokens(text.newText);
wDiff.EnumerateTokens(text.oldText);
 
//
// detect moved blocks
// Text.init(): initialize text object
var blocks = [];
//
var groups = [];
wDiff.DetectBlocks(text, blocks, groups);
 
this.init = function () {
// assemble diff blocks into formatted html text
diff = wDiff.AssembleDiff(text, blocks, groups);
 
if (typeof string != 'string') {
// wikEd.debugTimer.push(['diff=', new Date]);
string = string.toString();
// wikEd.DebugTimer();
}
 
// IE / Mac fix
return diff;
this.string = string.replace(/\r\n?/g, '\n');
};
 
this.wordParse(wDiff.regExpWord);
this.wordParse(wDiff.regExpChunk);
return;
};
 
// wDiff.CountTextWords: parse and count words in text for later identification of unique words
// changes: text (text.newText or text.oldText) .words
// called from: wDiff.Diff()
 
// Text.wordParse(): parse and count words and chunks for identification of unique words
wDiff.CountTextWords = function (text) {
// called from: .init()
// changes: .words
 
this.wordParse = function (regExp) {
var regExpMatch;
 
while ( (regExpMatch = wDiff.regExpWordCount.exec(text.string)) !== null) {
var word = text.words[ regExpMatch[0] ];
while ( (regExpMatch = regExp.exec(this.string)) !== null) {
if (word === undefined) {
var word = 1regExpMatch[0];
if (this.words[word] === undefined) {
}
this.words[word] = 1;
else {
word ++;}
else {
this.words[word] ++;
}
}
return;
}
return};
};
 
 
// wDiffText.Splitsplit(): split text into paragraph, sentence, or word tokens
// input: text (text.newText or text.oldText), object containing text data and strings; regExp, regular expression for splitting text into tokens; token, tokens index of token to be split
// called from: TextDiff.diff(), .splitRefine()
// changes: text (text.newText or text.oldText): text.tokens list, text.first, text.last
// changes: .tokens list, .first, .last
// called from: wDiff.Diff()
 
wDiff this.Splitsplit = function (text, level, token) {
 
var prev = null;
var next = null;
var current = textthis.tokens.length;
var first = current;
var string = '';
 
// split full text or specified token
if (token === undefined) {
string = textthis.string;
}
else {
prev = textthis.tokens[token].prev;
next = textthis.tokens[token].next;
string = textthis.tokens[token].token;
}
 
// split text into tokens, regExp match as separator
var number = 0;
var split = [];
var regExpMatch;
var lastIndex = 0;
while ( (regExpMatch = wDiff.regExpSplit[level].exec(string)) !== null) {
if (regExpMatch.index > lastIndex) {
split.push(string.substring(lastIndex, regExpMatch.index));
}
split.push(regExpMatch[0]);
lastIndex = wDiff.regExpSplit[level].lastIndex;
}
if (lastIndex < string.length) {
split.push(string.substring(lastIndex));
}
split.push(regExpMatch[0]);
lastIndex = wDiff.regExpSplit[level].lastIndex;
}
if (lastIndex < string.length) {
split.push(string.substring(lastIndex));
}
 
// cycle trough new tokens
for (var i = 0; i < split.length; i ++) {
 
// insert current item, link to previous
text this.tokens[current] = {
token: split[i],
prev: prev,
next: null,
link: null,
number: null,
parsed unique: false,
};
unique: false
} number ++;
number ++;
 
// link previous item to current
if (prev !== null) {
text this.tokens[prev].next = current;
}
prev = current;
current ++;
}
prev = current;
current ++;
}
 
// connect last new item and existing next item
if ( (number > 0) && (token !== undefined) ) {
if (prev !== null) {
text this.tokens[prev].next = next;
}
if (next !== null) {
this.tokens[next].prev = prev;
}
}
if (next !== null) {
text.tokens[next].prev = prev;
}
}
 
// set text first and last token index
if (number > 0) {
 
// initial text split
if (token === undefined) {
text this.first = 0;
text this.last = prev;
}
 
// first or last token has been split
else {
if (token == textthis.first) {
text this.first = first;
}
if (token == this.last) {
this.last = prev;
}
}
}
if (token == text.last) {
return;
text.last = prev;
};
 
 
// Text.splitRefine(): split unique unmatched tokens into smaller tokens
// changes: text (text.newText or text.oldText) .tokens list
// called from: TextDiff.diff()
// calls: .split()
 
this.splitRefine = function (regExp) {
 
// cycle through tokens list
var i = this.first;
while ( (i !== null) && (this.tokens[i] !== null) ) {
 
// refine unique unmatched tokens into smaller tokens
if (this.tokens[i].link === null) {
this.split(regExp, i);
}
i = this.tokens[i].next;
}
return;
}
return};
};
 
 
// Text.enumerateTokens(): enumerate text token list
// wDiff.SplitRefine: split unique unmatched tokens into smaller tokens
// called from: TextDiff.diff()
// changes: text (text.newText or text.oldText) .tokens list
// called fromchanges: wDiff.Diff()tokens list
// calls: wDiff.Split()
 
wDiff this.SplitRefineenumerateTokens = function (text, regExp) {
 
// cycle throughenumerate tokens list
var inumber = text.first0;
var i = this.first;
while ( (i !== null) && (textthis.tokens[i] !== null) ) {
this.tokens[i].number = number;
number ++;
i = this.tokens[i].next;
}
return;
};
 
 
// refine unique unmatched tokens into smaller tokens
// Text.debugText(): dump text object for debugging
if (text.tokens[i].link === null) {
// input: text: title
wDiff.Split(text, regExp, i);
 
this.debugText = function (text) {
 
var dump = 'first: ' + this.first + '\tlast: ' + this.last + '\n';
dump += '\ni \tlink \t(prev \tnext) \tuniq \t#num \t"token"\n';
var i = this.first;
while ( (i !== null) && (this.tokens[i] !== null) ) {
dump += i + ' \t' + this.tokens[i].link + ' \t(' + this.tokens[i].prev + ' \t' + this.tokens[i].next + ') \t' + this.tokens[i].unique + ' \t#' + this.tokens[i].number + ' \t' + parent.debugShortenString(this.tokens[i].token) + '\n';
i = this.tokens[i].next;
}
console.log(text + ':\n' + dump);
i = text.tokens[i].next;
return;
}
return};
 
 
// initialize text object
this.init();
};
 
 
//
// wDiff.SplitRefineChars: split tokens into chars in the following unresolved regions (gaps):
// wDiff.TextDiff class: main wDiff class, includes all data structures and methods required for a diff
// - one token became separated by space, dash, or any string
// called from: wDiff.diff()
// - same number of tokens in gap and strong similarity of all tokens:
//
// - addition or deletion of flanking strings in tokens
 
// - addition or deletion of internal string in tokens
wDiff.TextDiff = function (oldString, newString) {
// - same length and at least 50 % identity
 
// - same start or end, same text longer than different text
this.newText = null;
// - same length and at least 50 % identity
this.oldText = null;
// identical tokens including space separators will be linked, resulting in word-wise char-level diffs
this.blocks = [];
// changes: text (text.newText or text.oldText) .tokens list
this.groups = [];
// called from: wDiff.Diff()
this.sections = [];
// calls: wDiff.Split()
this.html = '';
// steps:
// find corresponding gaps
// select gaps of identical token number and strong similarity in all tokens
// refine words into chars in selected gaps
 
wDiff.SplitRefineChars = function (text) {
 
//
// TextDiff.init(): initialize diff object
// find corresponding gaps
//
 
this.init = function () {
// cycle trough new text tokens list
var gaps = [];
var gap = null;
var i = text.newText.first;
var j = text.oldText.first;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
 
this.newText = new wDiff.Text(newString, this);
// get token links
this.oldText = new wDiff.Text(oldString, this);
var newLink = text.newText.tokens[i].link;
return;
var oldLink = null;
};
if (j !== null) {
oldLink = text.oldText.tokens[j].link;
}
 
 
// start of gap in new and old
// TextDiff.diff(): main method
if ( (gap === null) && (newLink === null) && (oldLink === null) ) {
// input: version: 'new', 'old', show only one marked-up version, .oldString, .newString
gap = gaps.length;
// called from: wDiff.diff()
gaps.push({
// calls: Text.split(), Text.splitRefine(), .calculateDiff(), .slideGaps(), .enumerateTokens(), .detectBlocks(), .assembleDiff()
newFirst: i,
// changes: .html
newLast: i,
 
newTokens: 1,
this.diff = function (version) {
oldFirst: j,
 
oldLast: j,
// trap trivial changes: no change
oldTokens: null,
if (this.newText.string == this.oldText.string) {
charSplit: null
})return;
}
 
// counttrap charstrivial andchanges: tokensold intext gapdeleted
if ( (this.oldText.string === '') || ( (this.oldText.string == '\n') && (this.newText.string.charAt(this.newText.string.length - 1) == '\n') ) ) {
else if ( (gap !== null) && (newLink === null) ) {
this.html = wDiff.htmlInsertStart + this.htmlEscape(this.newText.string) + wDiff.htmlInsertEnd;
gaps[gap].newLast = i;
return;
gaps[gap].newTokens ++;
}
 
// trap trivial changes: new text deleted
// gap ended
if ( (this.newText.string === '') || ( (this.newText.string == '\n') && (this.oldText.string.charAt(this.oldText.string.length - 1) == '\n') ) ) {
else if ( (gap !== null) && (newLink !== null) ) {
this.html = wDiff.htmlDeleteStart + this.htmlEscape(this.oldText.string) + wDiff.htmlDeleteEnd;
gap = null;
return;
}
 
// nextnew listsymbols elementsobject
ifvar (newLinksymbols !== null) {
token: [],
j = text.oldText.tokens[newLink].next;
hash: {},
linked: false
};
 
// split new and old text into paragraps
this.newText.split('paragraph');
this.oldText.split('paragraph');
 
// calculate diff
this.calculateDiff(symbols, 'paragraph');
 
// refine different paragraphs into sentences
this.newText.splitRefine('sentence');
this.oldText.splitRefine('sentence');
 
// calculate refined diff
this.calculateDiff(symbols, 'sentence');
 
// refine different paragraphs into chunks
this.newText.splitRefine('chunk');
this.oldText.splitRefine('chunk');
 
// calculate refined diff
this.calculateDiff(symbols, 'chunk');
 
// refine different sentences into words
this.newText.splitRefine('word');
this.oldText.splitRefine('word');
 
// calculate refined diff information with recursion for unresolved gaps
this.calculateDiff(symbols, 'word', true);
 
// slide gaps
this.slideGaps(this.newText, this.oldText);
this.slideGaps(this.oldText, this.newText);
 
// split tokens into chars in selected unresolved gaps
if (wDiff.charDiff === true) {
this.splitRefineChars();
 
// calculate refined diff information with recursion for unresolved gaps
this.calculateDiff(symbols, 'character', true);
 
// slide gaps
this.slideGaps(this.newText, this.oldText);
this.slideGaps(this.oldText, this.newText);
}
i = text.newText.tokens[i].next;
}
 
// enumerate token lists
// cycle trough gaps and add old text gap data
this.newText.enumerateTokens();
for (var gap = 0; gap < gaps.length; gap ++) {
this.oldText.enumerateTokens();
 
// detect moved blocks
// cycle trough old text tokens list
this.detectBlocks();
var j = gaps[gap].oldFirst;
while ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.oldText.tokens[j].link === null) ) {
 
// countassemble olddiff charsblocks andinto tokensformatted in gaphtml
this.assembleDiff(version);
gaps[gap].oldLast = j;
gaps[gap].oldTokens ++;
 
if (wDiff.debug === true) {
j = text.oldText.tokens[j].next;
console.log('HTML:\n', this.html);
}
return;
}
};
 
//
// select gaps of identical token number and strong similarity of all tokens
//
 
// TextDiff.splitRefineChars(): split tokens into chars in the following unresolved regions (gaps):
for (var gap = 0; gap < gaps.length; gap ++) {
// - one token became separated by space, dash, or any string
var charSplit = true;
// - same number of tokens in gap and strong similarity of all tokens:
// - addition or deletion of flanking strings in tokens
// - addition or deletion of internal string in tokens
// - same length and at least 50 % identity
// - same start or end, same text longer than different text
// - same length and at least 50 % identity
// identical tokens including space separators will be linked, resulting in word-wise char-level diffs
// changes: text (text.newText or text.oldText) .tokens list
// called from: .diff()
// calls: Text.split()
// steps:
// find corresponding gaps
// select gaps of identical token number and strong similarity in all tokens
// refine words into chars in selected gaps
 
this.splitRefineChars = function () {
// not same gap length
if (gaps[gap].newTokens != gaps[gap].oldTokens) {
 
//
// one word became separated by space, dash, or any string
// find corresponding gaps
if ( (gaps[gap].newTokens == 1) && (gaps[gap].oldTokens == 3) ) {
//
if (text.newText.tokens[ gaps[gap].newFirst ].token != text.oldText.tokens[ gaps[gap].oldFirst ].token + text.oldText.tokens[ gaps[gap].oldLast ].token ) {
 
continue;
// cycle trough new text tokens list
}
var gaps = [];
var gap = null;
var i = this.newText.first;
var j = this.oldText.first;
while ( (i !== null) && (this.newText.tokens[i] !== null) ) {
 
// get token links
var newLink = this.newText.tokens[i].link;
var oldLink = null;
if (j !== null) {
oldLink = this.oldText.tokens[j].link;
}
 
else if ( (gaps[gap].oldTokens == 1) && (gaps[gap].newTokens == 3) ) {
// start of gap in new and old
if (text.oldText.tokens[ gaps[gap].oldFirst ].token != text.newText.tokens[ gaps[gap].newFirst ].token + text.newText.tokens[ gaps[gap].newLast ].token ) {
if ( (gap === null) && (newLink === null) && (oldLink === null) ) {
continue;
gap = gaps.length;
}
gaps.push({
newFirst: i,
newLast: i,
newTokens: 1,
oldFirst: j,
oldLast: j,
oldTokens: null,
charSplit: null
});
}
 
else {
// count chars and tokens in gap
continue;
else if ( (gap !== null) && (newLink === null) ) {
gaps[gap].newLast = i;
gaps[gap].newTokens ++;
}
 
// gap ended
else if ( (gap !== null) && (newLink !== null) ) {
gap = null;
}
 
// next list elements
if (newLink !== null) {
j = this.oldText.tokens[newLink].next;
}
i = this.newText.tokens[i].next;
}
 
// cycle trough newgaps textand tokensadd listold andtext setgap charSplitdata
for (var igap = gaps[0; gap] < gaps.newFirstlength; gap ++) {
var j = gaps[gap].oldFirst;
while (i !== null) {
var newToken = text.newText.tokens[i].token;
var oldToken = text.oldText.tokens[j].token;
 
// getcycle shortertrough andold longertext tokentokens list
var shorterTokenj = gaps[gap].oldFirst;
while ( (j !== null) && (this.oldText.tokens[j] !== null) && (this.oldText.tokens[j].link === null) ) {
var longerToken;
 
if (newToken.length < oldToken.length) {
// count old chars and tokens in gap
shorterToken = newToken;
longerTokengaps[gap].oldLast = oldTokenj;
gaps[gap].oldTokens ++;
}
 
else {
j = this.oldText.tokens[j].next;
shorterToken = oldToken;
longerToken = newToken;
}
}
 
//
// not same token length
// select gaps of identical token number and strong similarity of all tokens
if (newToken.length != oldToken.length) {
//
 
for (var gap = 0; gap < gaps.length; gap ++) {
// test for addition or deletion of internal string in tokens
var charSplit = true;
 
// not same gap length
// find number of identical chars from left
if (gaps[gap].newTokens != gaps[gap].oldTokens) {
var left = 0;
 
while (left < shorterToken.length) {
// one word became separated by space, dash, or any string
if (newToken.charAt(left) != oldToken.charAt(left)) {
if ( (gaps[gap].newTokens == 1) && (gaps[gap].oldTokens == 3) ) {
break;
if (this.newText.tokens[ gaps[gap].newFirst ].token != this.oldText.tokens[ gaps[gap].oldFirst ].token + this.oldText.tokens[ gaps[gap].oldLast ].token ) {
continue;
}
left ++;
}
else if ( (gaps[gap].oldTokens == 1) && (gaps[gap].newTokens == 3) ) {
 
if (this.oldText.tokens[ gaps[gap].oldFirst ].token != this.newText.tokens[ gaps[gap].newFirst ].token + this.newText.tokens[ gaps[gap].newLast ].token ) {
// find number of identical chars from right
var right = 0 continue;
while (right < shorterToken.length) {
if (newToken.charAt(newToken.length - 1 - right) != oldToken.charAt(oldToken.length - 1 - right)) {
break;
}
right ++;
}
else {
continue;
}
}
 
// cycle trough new text tokens list and set charSplit
// no simple insertion or deletion of internal string
var i = gaps[gap].newFirst;
if (left + right != shorterToken.length) {
var j = gaps[gap].oldFirst;
while (i !== null) {
var newToken = this.newText.tokens[i].token;
var oldToken = this.oldText.tokens[j].token;
 
// get shorter and longer token
// not addition or deletion of flanking strings in tokens (smaller token not part of larger token)
ifvar (longerToken.indexOf(shorterToken) == -1) {;
var longerToken;
if (newToken.length < oldToken.length) {
shorterToken = newToken;
longerToken = oldToken;
}
else {
shorterToken = oldToken;
longerToken = newToken;
}
 
// not same token length
// same text at start or end shorter than different text
if ( (left < shorterTokennewToken.length /!= 2) && (right < shorterTokenoldToken.length / 2) ) {
 
// test for addition or deletion of internal string in tokens
// do not split into chars this gap
 
charSplit = false;
// find number of identical chars from left
var left = 0;
while (left < shorterToken.length) {
if (newToken.charAt(left) != oldToken.charAt(left)) {
break;
}
left ++;
}
}
}
 
// same token length
else if (newToken != oldToken) {
 
// tokensfind lessnumber thanof 50identical %chars identicalfrom right
var identright = 0;
for while (var pos = 0; posright < shorterToken.length; pos ++) {
if (shorterTokennewToken.charAt(posnewToken.length - 1 - right) =!= longerTokenoldToken.charAt(posoldToken.length - 1 - right)) {
ident ++ break;
}
right ++;
}
}
if (ident/shorterToken.length < 0.49) {
 
// dono notsimple splitinsertion intoor charsdeletion thisof gapinternal string
if (left + right != shorterToken.length) {
charSplit = false;
break;
}
}
 
// not addition or deletion of flanking strings in tokens (smaller token not part of larger token)
// next list elements
if (ilongerToken.indexOf(shorterToken) == gaps[gap].newLast-1) {
break;
}
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
gaps[gap].charSplit = charSplit;
}
 
// same text at start or end shorter than different text
//
if ( (left < shorterToken.length / 2) && (right < shorterToken.length / 2) ) {
// refine words into chars in selected gaps
//
 
// do not split into chars this gap
for (var gap = 0; gap < gaps.length; gap ++) {
if (gaps[gap]. charSplit === true) {false;
break;
}
}
}
}
 
// same token length
// cycle trough new text tokens list
else if (newToken != oldToken) {
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
while (i !== null) {
var newToken = text.newText.tokens[i].token;
var oldToken = text.oldText.tokens[j].token;
 
// linktokens identicalless tokensthan (spaces)50 % identical
if var (newTokenident == oldToken) {0;
for (var pos = 0; pos < shorterToken.length; pos ++) {
text.newText.tokens[i].link = j;
if (shorterToken.charAt(pos) == longerToken.charAt(pos)) {
text.oldText.tokens[j].link = i;
} ident ++;
}
}
if (ident/shorterToken.length < 0.49) {
 
// refinedo differentnot wordssplit into chars this gap
else charSplit {= false;
break;
wDiff.Split(text.newText, 'character', i);
}
wDiff.Split(text.oldText, 'character', j);
}
 
Line 991 ⟶ 1,159:
break;
}
i = textthis.newText.tokens[i].next;
j = textthis.oldText.tokens[j].next;
}
gaps[gap].charSplit = charSplit;
}
}
 
//
// WED('Gap', wDiff.DebugGaps(gaps));
// refine words into chars in selected gaps
//
 
for (var gap = 0; gap < gaps.length; gap ++) {
return;
if (gaps[gap].charSplit === true) {
};
 
// cycle trough new text tokens list
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
while (i !== null) {
var newToken = this.newText.tokens[i].token;
var oldToken = this.oldText.tokens[j].token;
 
// link identical tokens (spaces)
// wDiff.SlideGaps: move gaps with ambiguous identical fronts to last newline or, if absent, last word border
if (newToken == oldToken) {
// changes: text (text.newText or text.oldText) .tokens list
this.newText.tokens[i].link = j;
// called from: wDiff.Diff()
this.oldText.tokens[j].link = i;
}
 
// refine different words into chars
wDiff.SlideGaps = function (text, textLinked) {
else {
 
this.newText.split('character', i);
// cycle through tokens list
this.oldText.split('character', j);
var i = text.first;
}
var gapStart = null;
while ( (i !== null) && (text.tokens[i] !== null) ) {
 
// remembernext gaplist startelements
if ( (gapStarti === null) && (text.tokensgaps[igap].link === null) newLast) {
gapStart = i break;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
}
return;
};
 
// find gap end
else if ( (gapStart !== null) && (text.tokens[i].link !== null) ) {
 
// TextDiff.slideGaps(): move gaps with ambiguous identical fronts to last newline or, if absent, last word border
// slide down as deep as possible
// called from: .diff(), .detectBlocks()
var front = gapStart;
// changes: .newText/.oldText .tokens list
var back = i;
 
var frontTest = null;
this.slideGaps = function (text, textLinked) {
var backTest = null;
 
while (
// cycle through tokens list
(front !== null) && (back !== null) &&
var i = text.first;
(text.tokens[front].link === null) && (text.tokens[back].link !== null) &&
var gapStart = null;
(text.tokens[front].token === text.tokens[back].token)
while ( (i !== null) && (text.tokens[i] !== null) ) {
) {
 
text.tokens[front].link = text.tokens[back].link;
// remember gap start
textLinked.tokens[ text.tokens[front].link ].link = front;
if ( (gapStart === null) && (text.tokens[backi].link === null;) ) {
frontTestgapStart = fronti;
backTest = back;
front = text.tokens[front].next;
back = text.tokens[back].next;
}
 
// find gap end
// test slide up, remember last line break or word border
else if ( (gapStart !== null) && (text.tokens[i].link !== null) ) {
var frontStop = null;
 
while (
// slide down as deep as possible
(frontTest !== null) && (backTest !== null) &&
var front = gapStart;
(text.tokens[frontTest].link !== null) && (text.tokens[backTest].link === null) &&
var back = i;
(text.tokens[frontTest].token == text.tokens[backTest].token)
var frontTest = null;
) {
var backTest = null;
if (wDiff.regExpSlideStop.test(text.tokens[frontTest].token) === true) {
while (
frontStop = frontTest;
(front !== null) && (back !== null) &&
break;
(text.tokens[front].link === null) && (text.tokens[back].link !== null) &&
(text.tokens[front].token === text.tokens[back].token)
) {
text.tokens[front].link = text.tokens[back].link;
textLinked.tokens[ text.tokens[front].link ].link = front;
text.tokens[back].link = null;
frontTest = front;
backTest = back;
front = text.tokens[front].next;
back = text.tokens[back].next;
}
else if ( (frontStop === null) && (wDiff.regExpSlideBorder.test(text.tokens[frontTest].token) === true) ) {
frontStop = frontTest;
}
frontTest = text.tokens[frontTest].prev;
backTest = text.tokens[backTest].prev;
}
 
// actuallytest slide up, toremember last line break or, if absent, word border
if var (frontStop !== null) {;
while (
(frontfrontTest !== null) && (backbackTest !== null) && (front !== frontStop) &&
(text.tokens[frontfrontTest].link !== null) && (text.tokens[backbackTest].link === null) &&
(text.tokens[frontfrontTest].token == text.tokens[backbackTest].token)
) {
if (wDiff.regExpSlideStop.test(text.tokens[backfrontTest].linktoken) === text.tokens[front].link;true) {
frontStop = frontTest;
textLinked.tokens[ text.tokens[back].link ].link = back;
break;
text.tokens[front].link = null;
}
front = text.tokens[front].prev;
else if ( (frontStop === null) && (wDiff.regExpSlideBorder.test(text.tokens[frontTest].token) === true) ) {
back = text.tokens[back].prev;
frontStop = frontTest;
}
frontTest = text.tokens[frontTest].prev;
backTest = text.tokens[backTest].prev;
}
 
// actually slide up to line break or, if absent, word border
if (frontStop !== null) {
while (
(front !== null) && (back !== null) && (front !== frontStop) &&
(text.tokens[front].link !== null) && (text.tokens[back].link === null) &&
(text.tokens[front].token == text.tokens[back].token)
) {
text.tokens[back].link = text.tokens[front].link;
textLinked.tokens[ text.tokens[back].link ].link = back;
text.tokens[front].link = null;
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
}
gapStart = null;
}
gapStarti = nulltext.tokens[i].next;
}
return;
i = text.tokens[i].next;
};
return;
};
 
 
// TextDiff.calculateDiff(): calculate diff information, can be called repeatedly during refining
// wDiff.EnumerateTokens: enumerate text token list
// input: level: 'paragraph', 'sentence', 'chunk', 'word', or 'character'
// changes: text (text.newText or text.oldText) .tokens list
// optionally for recursive calls: recurse, newStart, newEnd, oldStart, oldEnd (tokens list indexes), recursionLevel
// called from: wDiff.Diff()
// called from: .diff()
// calls: itself recursively
// changes: .oldText/.newText.tokens[].link, links corresponding tokens from old and new text
// steps:
// pass 1: parse new text into symbol table
// pass 2: parse old text into symbol table
// pass 3: connect unique matched tokens
// pass 4: connect adjacent identical tokens downwards
// pass 5: connect adjacent identical tokens upwards
// recursively diff still unresolved regions downwards
// recursively diff still unresolved regions upwards
 
this.calculateDiff = function (symbols, level, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
wDiff.EnumerateTokens = function (text) {
 
// start timer
// enumerate tokens list
if ( (wDiff.debugTime === true) && (recursionLevel === undefined) ) {
var number = 0;
console.time(level);
var i = text.first;
}
while ( (i !== null) && (text.tokens[i] !== null) ) {
text.tokens[i].number = number;
number ++;
i = text.tokens[i].next;
}
return;
};
 
// set defaults
if (newStart === undefined) { newStart = this.newText.first; }
if (newEnd === undefined) { newEnd = this.newText.last; }
if (oldStart === undefined) { oldStart = this.oldText.first; }
if (oldEnd === undefined) { oldEnd = this.oldText.last; }
if (recursionLevel === undefined) { recursionLevel = 0; }
 
// limit recursion depth
// wDiff.CalculateDiff: calculate diff information, can be called repeatedly during refining
if (recursionLevel > 10) {
// input: text: object containing text data and tokens; level: 'paragraph', 'sentence', 'word', or 'character'
return;
// optionally for recursive calls: newStart, newEnd, oldStart, oldEnd (tokens list indexes), recursionLevel
}
// changes: text.oldText/newText.tokens[].link, links corresponding tokens from old and new text
// steps:
// pass 1: parse new text into symbol table
// pass 2: parse old text into symbol table
// pass 3: connect unique matched tokens
// pass 4: connect adjacent identical tokens downwards
// pass 5: connect adjacent identical tokens upwards
// recursively diff still unresolved regions downwards
// recursively diff still unresolved regions upwards
 
//
wDiff.CalculateDiff = function (text, symbols, level, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
// pass 1: parse new text into symbol table
//
 
// cycle trough new text tokens list
// if (recursionLevel === undefined) { wikEd.debugTimer.push([level + '?', new Date]); }
var i = newStart;
while ( (i !== null) && (this.newText.tokens[i] !== null) ) {
 
// add new entry to symbol table
// set defaults
var token = this.newText.tokens[i].token;
if (newStart === undefined) { newStart = text.newText.first; }
if (Object.prototype.hasOwnProperty.call(symbols.hash, token) === false) {
if (newEnd === undefined) { newEnd = text.newText.last; }
var current = symbols.token.length;
if (oldStart === undefined) { oldStart = text.oldText.first; }
symbols.hash[token] = current;
if (oldEnd === undefined) { oldEnd = text.oldText.last; }
symbols.token[current] = {
if (recursionLevel === undefined) { recursionLevel = 0; }
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
};
}
 
// or update existing entry
// limit recursion depth
else {
if (recursionLevel > 10) {
return;
}
 
// increment token counter for new text
//
var hashToArray = symbols.hash[token];
// pass 1: parse new text into symbol table
symbols.token[hashToArray].newCount ++;
//
}
 
// next list element
// cycle trough new text tokens list
if (i == newEnd) {
var i = newStart;
break;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
}
 
i = this.newText.tokens[i].next;
// add new entry to symbol table
var token = text.newText.tokens[i].token;
if (Object.prototype.hasOwnProperty.call(symbols.hash, token) === false) {
var current = symbols.token.length;
symbols.hash[token] = current;
symbols.token[current] = {
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
};
}
 
//
// or update existing entry
// pass 2: parse old text into symbol table
else {
//
 
// incrementcycle tokentrough counterold fortext newtokens textlist
var hashToArrayj = symbols.hash[token]oldStart;
while ( (j !== null) && (this.oldText.tokens[j] !== null) ) {
symbols.token[hashToArray].newCount ++;
}
 
// add new entry to symbol table
// next list element
var token = this.oldText.tokens[j].token;
if (i == newEnd) {
if (Object.prototype.hasOwnProperty.call(symbols.hash, token) === false) {
break;
var current = symbols.token.length;
}
symbols.hash[token] = current;
i = text.newText.tokens[i].next;
symbols.token[current] = {
}
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
};
}
 
// or update existing entry
//
else {
// pass 2: parse old text into symbol table
//
 
// increment token counter for old text
// cycle trough old text tokens list
var hashToArray = symbols.hash[token];
var j = oldStart;
symbols.token[hashToArray].oldCount ++;
while ( (j !== null) && (text.oldText.tokens[j] !== null) ) {
 
// add newtoken entrynumber tofor symbolold tabletext
symbols.token[hashToArray].oldToken = j;
var token = text.oldText.tokens[j].token;
}
if (Object.prototype.hasOwnProperty.call(symbols.hash, token) === false) {
 
var current = symbols.token.length;
// next list element
symbols.hash[token] = current;
if (j === oldEnd) {
symbols.token[current] = {
newCount: 0,break;
}
oldCount: 1,
j = this.oldText.tokens[j].next;
newToken: null,
oldToken: j
};
}
 
//
// or update existing entry
// pass 3: connect unique tokens
else {
//
 
// cycle trough symbol array
// increment token counter for old text
for (var hashToArrayi = 0; i < symbols.hash[token].length; i ++) {
symbols.token[hashToArray].oldCount ++;
 
// find tokens in the symbol table that occur only once in both versions
// add token number for old text
if ( (symbols.token[hashToArrayi].oldTokennewCount == 1) && (symbols.token[i].oldCount == 1) ) j;{
var newToken = symbols.token[i].newToken;
}
var oldToken = symbols.token[i].oldToken;
 
// do not use spaces as unique markers
// next list element
if (/^\s+$/.test(this.newText.tokens[newToken].token) === false) {
if (j === oldEnd) {
break;
}
j = text.oldText.tokens[j].next;
}
 
//
// pass 3: connect unique tokens
//
 
// connect from new to old and from old to new
// cycle trough symbol array
if (this.newText.tokens[newToken].link === null) {
for (var i = 0; i < symbols.token.length; i ++) {
this.newText.tokens[newToken].link = oldToken;
this.oldText.tokens[oldToken].link = newToken;
symbols.linked = true;
 
// check if token contains unique word
// find tokens in the symbol table that occur only once in both versions
if ( (symbols.token[i].newCountrecursionLevel === 10) && (symbols.token[i].oldCountlevel =!= 1'character') ) {
var newTokenunique = symbols.token[i].newTokenfalse;
var oldTokentoken = symbolsthis.tokennewText.tokens[inewToken].oldTokentoken;
var words = (token.match(wDiff.regExpWord) || []).concat(token.match(wDiff.regExpChunk) || []);
 
// dounique notif uselonger spacesthan asmin uniqueblock markerslength
if (words.length >= wDiff.blockMinLength) {
if (/^\s+$/.test(text.newText.tokens[newToken].token) === false) {
unique = true;
}
 
// connectunique fromif newit tocontains oldat andleast fromone oldunique to newword
else {
if (text.newText.tokens[newToken].link === null) {
for (var word = 0; word < words.length; word ++) {
text.newText.tokens[newToken].link = oldToken;
if ( (this.oldText.words[ words[word] ] == 1) && (this.newText.words[ words[word] ] == 1) ) {
text.oldText.tokens[oldToken].link = newToken;
symbols.linked unique = true;
break;
}
}
}
 
// check ifset unique word
if ( (levelunique == 'word') && (recursionLevel === 0) true) {
var token = text this.newText.tokens[newToken].tokenunique = true;
if ( (text this.oldText.wordstokens[tokenoldToken] == 1) && (text.newText.words[token]unique == 1) ) {true;
}
text.newText.tokens[newToken].unique = true;
text.oldText.tokens[oldToken].unique = true;
}
}
Line 1,235 ⟶ 1,443:
}
}
}
 
// continue passes only if unique tokens have been linked previously
if (symbols.linked === true) {
 
//
// pass 4: connect adjacent identical tokens downwards
//
 
// get surrounding connected tokens
var i = newStart;
if (textthis.newText.tokens[i].prev !== null) {
i = textthis.newText.tokens[i].prev;
}
var iStop = newEnd;
if (text.newText.tokens[iStop].next !== null) {
iStop = text.newText.tokens[iStop].next;
}
var j = null;
 
// cycle trough new text tokens list down
do {
 
// connected pair
var link = text.newText.tokens[i].link;
if (link !== null) {
j = text.oldText.tokens[link].next;
}
var iStop = newEnd;
 
// connect if (this.newText.tokens[iStop].next are!== thenull) same{
iStop = this.newText.tokens[iStop].next;
else if ( (j !== null) && (text.oldText.tokens[j].link === null) && (text.newText.tokens[i].token == text.oldText.tokens[j].token) ) {
text.newText.tokens[i].link = j;
text.oldText.tokens[j].link = i;
j = text.oldText.tokens[j].next;
}
var j = null;
 
// cycle trough new text tokens list down
// not same
elsedo {
j = null;
}
i = text.newText.tokens[i].next;
} while (i !== iStop);
 
// connected pair
//
var link = this.newText.tokens[i].link;
// pass 5: connect adjacent identical tokens upwards
if (link !== null) {
//
j = this.oldText.tokens[link].next;
}
 
// getconnect surrounding connectedif tokens are the same
else if ( (j !== null) && (this.oldText.tokens[j].link === null) && (this.newText.tokens[i].token == this.oldText.tokens[j].token) ) {
var i = newEnd;
if (text this.newText.tokens[i].nextlink !== null) {j;
i = text this.newTextoldText.tokens[ij].nextlink = i;
j = this.oldText.tokens[j].next;
}
}
var iStop = newStart;
if (text.newText.tokens[iStop].prev !== null) {
iStop = text.newText.tokens[iStop].prev;
}
var j = null;
 
// not same
// cycle trough new text tokens list up
do else {
j = null;
}
i = this.newText.tokens[i].next;
} while (i !== iStop);
 
// connected pair
// pass 5: connect adjacent identical tokens upwards
var link = text.newText.tokens[i].link;
//
if (link !== null) {
j = text.oldText.tokens[link].prev;
}
 
// connectget ifsurrounding connected tokens are the same
var i = newEnd;
else if ( (j !== null) && (text.oldText.tokens[j].link === null) && (text.newText.tokens[i].token == text.oldText.tokens[j].token) ) {
textif (this.newText.tokens[i].linknext !== null) j;{
texti = this.oldTextnewText.tokens[ji].link = inext;
j = text.oldText.tokens[j].prev;
}
var iStop = newStart;
 
if (this.newText.tokens[iStop].prev !== null) {
// not same
iStop = this.newText.tokens[iStop].prev;
else {
j = null;
}
var j = null;
i = text.newText.tokens[i].prev;
} while (i !== iStop);
 
// cycle trough new text tokens list up
//
do {
// connect adjacent identical tokens downwards from text start, treat boundary as connected, stop after first connected token
//
 
// connected pair
// only for full text diff
if var ( (newStartlink == textthis.newText.first) && (newEnd == texttokens[i].newText.last) ) {link;
if (link !== null) {
j = this.oldText.tokens[link].prev;
}
 
// connect if tokens are the same
// from start
else if ( (j !== null) && (this.oldText.tokens[j].link === null) && (this.newText.tokens[i].token == this.oldText.tokens[j].token) ) {
var i = text.newText.first;
this.newText.tokens[i].link = j;
var j = text.oldText.first;
this.oldText.tokens[j].link = i;
j = this.oldText.tokens[j].prev;
}
 
// not same
// cycle trough new text tokens list down, connect identical tokens, stop after first connected token
else {
while ( (i !== null) && (j !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) && (text.newText.tokens[i].token == text.oldText.tokens[j].token) ) {
text.newText.tokens[i].link j = jnull;
}
text.oldText.tokens[j].link = i;
ji = textthis.oldTextnewText.tokens[ji].nextprev;
} while (i !== iStop);
i = text.newText.tokens[i].next;
}
 
// from end
// connect adjacent identical tokens downwards from text start, treat boundary as connected, stop after first connected token
var i = text.newText.last;
//
var j = text.oldText.last;
 
// only for full text diff
// cycle trough old text tokens list up, connect identical tokens, stop after first connected token
whileif ( (inewStart !== null) && (j !== null) && (textthis.newText.tokens[i].link === nullfirst) && (text.oldText.tokens[j].linknewEnd === null) && (textthis.newText.tokens[i].token == text.oldText.tokens[j].tokenlast) ) {
text.newText.tokens[i].link = j;
text.oldText.tokens[j].link = i;
j = text.oldText.tokens[j].prev;
i = text.newText.tokens[i].prev;
}
}
 
// from start
var i = this.newText.first;
// refine by recursively diffing unresolved regions caused by addition of common tokens around sequences of common tokens, only at word level split
var j = this.oldText.first;
//
 
// cycle trough new text tokens list down, connect identical tokens, stop after first connected token
if ( (recurse === true) && (wDiff.recursiveDiff === true) ) {
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;
j = this.oldText.tokens[j].next;
i = this.newText.tokens[i].next;
}
 
// from end
var i = this.newText.last;
var j = this.oldText.last;
 
// cycle trough old text tokens list up, connect identical tokens, stop after first connected token
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;
j = this.oldText.tokens[j].prev;
i = this.newText.tokens[i].prev;
}
}
 
//
// refine by recursively diffing unresolved regions caused by addition of common tokens around sequences of common tokens, only at word level split
// recursively diff still unresolved regions downwards
//
 
if ( (recurse === true) && (wDiff.recursiveDiff === true) ) {
// cycle trough new text tokens list
var i = newStart;
var j = oldStart;
 
//
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
// recursively diff still unresolved regions downwards
//
 
// getcycle jtrough fromnew previoustext tokens matchlist
var iPrevi = text.newText.tokens[i].prevnewStart;
ifvar (iPrevj !== null) {oldStart;
var jPrev = text.newText.tokens[iPrev].link;
if (jPrev !== null) {
j = text.oldText.tokens[jPrev].next;
}
}
 
while ( (i !== null) && (this.newText.tokens[i] !== null) ) {
// check for the start of an unresolved sequence
if ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) ) {
 
// get j from previous tokens match
// determine the limits of the unresolved new sequence
var iStartiPrev = this.newText.tokens[i].prev;
varif iEnd(iPrev !== null;) {
var iLengthjPrev = 0this.newText.tokens[iPrev].link;
var if iNext(jPrev !== null) i;{
while j ( (iNext !== null) && (textthis.newTextoldText.tokens[iNextjPrev].link === null) ) {next;
iEnd = iNext;
iLength ++;
if (iEnd == newEnd) {
break;
}
iNext = text.newText.tokens[iNext].next;
}
 
// determinecheck for the limitsstart of thean unresolved old sequence
if ( (j !== null) && (this.oldText.tokens[j] !== null) && (this.newText.tokens[i].link === null) && (this.oldText.tokens[j].link === null) ) {
var jStart = j;
 
var jEnd = null;
// determine the limits of the unresolved new sequence
var jLength = 0;
var jNextiStart = ji;
var iEnd = null;
while ( (jNext !== null) && (text.oldText.tokens[jNext].link === null) ) {
jEndvar iLength = jNext0;
jLengthvar ++iNext = i;
while ( (iNext !== null) && (this.newText.tokens[iNext].link === null) ) {
if (jEnd == oldEnd) {
breakiEnd = iNext;
iLength ++;
if (iEnd == newEnd) {
break;
}
iNext = this.newText.tokens[iNext].next;
}
jNext = text.oldText.tokens[jNext].next;
}
 
// recursivelydetermine diffthe limits of the unresolved old sequence
var jStart = j;
if ( (iLength > 1) || (jLength > 1) ) {
var jEnd = null;
var jLength = 0;
var jNext = j;
while ( (jNext !== null) && (this.oldText.tokens[jNext].link === null) ) {
jEnd = jNext;
jLength ++;
if (jEnd == oldEnd) {
break;
}
jNext = this.oldText.tokens[jNext].next;
}
 
// newrecursively symbolsdiff objectthe forunresolved sub-regionsequence
if ( (iLength > 1) || (jLength > 1) ) {
var symbolsRecurse = {
 
token: [],
hash:// new symbols {},object for sub-region
linked:var falsesymbolsRecurse = {
}; token: [],
hash: {},
wDiff.CalculateDiff(text, symbolsRecurse, level, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
linked: false
};
this.calculateDiff(symbolsRecurse, level, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
}
i = iEnd;
}
i = iEnd;
}
 
// next list element
if (i == newEnd) {
break;
}
i = this.newText.tokens[i].next;
}
i = text.newText.tokens[i].next;
}
 
//
// recursively diff still unresolved regions upwards
//
 
// cycle trough new text tokens list
var i = newEnd;
var j = oldEnd;
while ( (i !== null) && (textthis.newText.tokens[i] !== null) ) {
 
// get j from next matched tokens
var iPrev = textthis.newText.tokens[i].next;
if (iPrev !== null) {
var jPrev = textthis.newText.tokens[iPrev].link;
if (jPrev !== null) {
j = textthis.oldText.tokens[jPrev].prev;
}
}
}
 
// check for the start of an unresolved sequence
if ( (j !== null) && (textthis.oldText.tokens[j] !== null) && (textthis.newText.tokens[i].link === null) && (textthis.oldText.tokens[j].link === null) ) {
 
// determine the limits of the unresolved new sequence
var iStart = null;
var iEnd = i;
var iLength = 0;
var iNext = i;
while ( (iNext !== null) && (textthis.newText.tokens[iNext].link === null) ) {
iStart = iNext;
iLength ++;
if (iStart == newStart) {
break;
}
iNext = this.newText.tokens[iNext].prev;
}
iNext = text.newText.tokens[iNext].prev;
}
 
// determine the limits of the unresolved old sequence
var jStart = null;
var jEnd = j;
var jLength = 0;
var jNext = j;
while ( (jNext !== null) && (textthis.oldText.tokens[jNext].link === null) ) {
jStart = jNext;
jLength ++;
if (jStart == oldStart) {
break;
}
jNext = this.oldText.tokens[jNext].prev;
}
jNext = text.oldText.tokens[jNext].prev;
}
 
// recursively diff the unresolved sequence
if ( (iLength > 1) || (jLength > 1) ) {
 
// new symbols object for sub-region
var symbolsRecurse = {
token: [],
hash: {},
linked: false
};
wDiff this.CalculateDiffcalculateDiff(text, symbolsRecurse, level, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
}
i = iStart;
}
i = iStart;
}
 
// next list element
if (i == newStart) {
break;
}
i = this.newText.tokens[i].prev;
}
i = text.newText.tokens[i].prev;
}
}
}
 
// stop timer
// if (recursionLevel === 0) { wikEd.debugTimer.push([level + '=', new Date]); }
if ( (wDiff.debugTime === true) && (recursionLevel === 0) ) {
console.timeEnd(level);
}
return;
};
 
return;
};
 
// TextDiff.detectBlocks(): main method for extracting deleted, inserted, and moved blocks from raw diff data
// called from: .diff()
// calls: .getSameBlocks(), .getSections(), .getGroups(), .setFixed(), getDelBlocks(), .positionDelBlocks(), .unlinkBlocks(), .getInsBlocks(), .setInsGroups(), .insertMarks()
// input:
// text: object containing text tokens list
// blocks: empty array for block data
// groups: empty array for group data
// changes: .text, .blocks, .groups
// scheme of blocks, sections, and groups (old block numbers):
// old: 1 2 3D4 5E6 7 8 9 10 11
// | ‾/-/_ X | >|< |
// new: 1 I 3D4 2 E6 5 N 7 10 9 8 11
// section: 0 0 0 1 1 2 2 2
// group: 0 10 111 2 33 4 11 5 6 7 8 9
// fixed: + +++ - ++ - + + - - +
// type: = + =-= = -= = + = = = = =
 
this.detectBlocks = function () {
// wDiff.DetectBlocks: extract block data for inserted, deleted, or moved blocks from diff data in text object
// input:
// text: object containing text tokens list
// blocks: empty array for block data
// groups: empty array for group data
// changes: text, blocks, groups
// called from: wDiff.Diff()
// scheme of blocks, sections, and groups (old block numbers):
// old: 1 2 3D4 5E6 7 8 9 10 11
// | ‾/-/_ X | >|< |
// new: 1 I 3D4 2 E6 5 N 7 10 9 8 11
// section: 0 0 0 1 1 2 2 2
// group: 0 10 111 2 33 4 11 5 6 7 8 9
// fixed: + +++ - ++ - + + - - +
// type: = + =-= = -= = + = = = = =
 
if (wDiff.debug === true) {
wDiff.DetectBlocks = function (text, blocks, groups) {
this.oldText.debugText();
this.newText.debugText();
}
 
// collect identical corresponding ('same') blocks from old text and sort by new text
// WED('text.oldText', wDiff.DebugText(text.oldText));
this.getSameBlocks();
// WED('text.newText', wDiff.DebugText(text.newText));
 
// collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
// collect identical corresponding ('same') blocks from old text and sort by new text
this.getSections();
wDiff.GetSameBlocks(text, blocks);
 
// find groups of continuous old text blocks
// collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
this.getGroups();
var sections = [];
wDiff.GetSections(blocks, sections);
 
// set longest sequence of increasing groups in sections as fixed (not moved)
// find groups of continuous old text blocks
this.setFixed();
wDiff.GetGroups(blocks, groups);
 
// collect deletion ('del') blocks from old text
// set longest sequence of increasing groups in sections as fixed (not moved)
this.getDelBlocks();
wDiff.SetFixed(blocks, groups, sections);
 
// collectposition deletion ('del') blocks frominto oldnew text order
this.positionDelBlocks();
wDiff.GetDelBlocks(text, blocks);
 
// convert groups to insertions/deletions if maximal block length is too short
// position 'del' blocks into new text order
var unlink = 0;
wDiff.PositionDelBlocks(blocks);
if (wDiff.blockMinLength > 0) {
 
// repeat as long as unlinking is possible
// sort blocks by new text token number and update groups
var unlinked = false;
wDiff.SortBlocks(blocks, groups);
do {
 
// convert groups'same' to insertions'ins'/deletions'del' if maximal block length is too shortpairs
unlinked = this.unlinkBlocks();
if (wDiff.blockMinLength > 0) {
var unlinked = wDiff.UnlinkBlocks(text, blocks, groups);
 
// repeatstart from startover after conversion
if (unlinked === true) {
unlink ++;
wDiff.SlideGaps(text.newText, text.oldText);
wDiff this.SlideGapsslideGaps(textthis.oldTextnewText, textthis.newTextoldText);
this.slideGaps(this.oldText, this.newText);
 
// repeat block detection from start
this.getSameBlocks();
wDiff.GetSameBlocks(text, blocks);
this.getSections();
wDiff.GetSections(blocks, sections);
this.getGroups();
wDiff.GetGroups(blocks, groups);
this.setFixed();
wDiff.SetFixed(blocks, groups, sections);
this.getDelBlocks();
wDiff.GetDelBlocks(text, blocks);
this.positionDelBlocks();
wDiff.PositionDelBlocks(blocks);
}
} while (unlinked === true);
}
}
 
// collect insertion ('ins') blocks from new text
this.getInsBlocks();
wDiff.GetInsBlocks(text, blocks);
 
// set group numbers of 'ins' blocks
// sort blocks by new text token number and update groups
this.setInsGroups();
wDiff.SortBlocks(blocks, groups);
 
// mark original positions of moved groups
// set group numbers of 'ins' and 'del' blocks
this.insertMarks();
wDiff.SetInsDelGroups(blocks, groups);
 
if (wDiff.debug === true) {
// mark original positions of moved groups
console.log('Unlinked: ', unlink);
wDiff.MarkMoved(groups);
this.debugGroups('Groups');
this.debugBlocks('Blocks');
}
return;
};
 
// set moved block colors
wDiff.ColorMoved(groups);
 
// TextDiff.getSameBlocks(): collect identical corresponding ('same') blocks from old text and sort by new text
// WED('Groups', wDiff.DebugGroups(groups));
// called from: .detectBlocks()
// WED('Blocks', wDiff.DebugBlocks(blocks));
// calls: .wordCount()
// changes: .blocks
 
this.getSameBlocks = function () {
return;
};
 
var blocks = this.blocks;
 
// clear blocks array
// wDiff.GetSameBlocks: collect identical corresponding ('same') blocks from old text and sort by new text
blocks.splice(0);
// called from: DetectBlocks()
// changes: creates blocks
 
// cycle through old text to find matched (linked) blocks
wDiff.GetSameBlocks = function (text, blocks) {
var j = this.oldText.first;
var i = null;
while (j !== null) {
 
// skip 'del' blocks
// clear blocks array
while ( (j !== null) && (this.oldText.tokens[j].link === null) ) {
blocks.splice(0);
j = this.oldText.tokens[j].next;
}
 
// get 'same' block
// cycle through old text to find matched (linked) blocks
if (j !== null) {
var j = text.oldText.first;
i = this.oldText.tokens[j].link;
var i = null;
var iStart = i;
while (j !== null) {
var jStart = j;
 
// skipdetect 'del'matching blocks ('same')
var count = 0;
while ( (j !== null) && (text.oldText.tokens[j].link === null) ) {
var unique = false;
j = text.oldText.tokens[j].next;
var string = '';
while ( (i !== null) && (j !== null) && (this.oldText.tokens[j].link == i) ) {
var token = this.oldText.tokens[j].token;
count ++;
if (this.newText.tokens[i].unique === true) {
unique = true;
}
string += token;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
 
// save old text 'same' block
blocks.push({
oldBlock: blocks.length,
newBlock: null,
oldNumber: this.oldText.tokens[jStart].number,
newNumber: this.newText.tokens[iStart].number,
oldStart: jStart,
count: count,
unique: unique,
words: this.wordCount(string),
chars: string.length,
type: 'same',
section: null,
group: null,
fixed: null,
moved: null,
string: string
});
}
}
 
// sort blocks by new text token number
// get 'same' block
if blocks.sort(j !==function(a, nullb) {
return a.newNumber - b.newNumber;
i = text.oldText.tokens[j].link;
});
var iStart = i;
var jStart = j;
 
// detect matchingnumber blocks ('same')in new text order
for (var block = 0; block < blocks.length; block ++) {
var count = 0;
var uniqueblocks[block].newBlock = falseblock;
var chars = 0;
var string = '';
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
var token = text.oldText.tokens[j].token;
count ++;
if (text.newText.tokens[i].unique === true) {
unique = true;
}
chars += token.length;
string += token;
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
 
// save old text 'same' block
blocks.push({
oldBlock: blocks.length,
newBlock: null,
oldNumber: text.oldText.tokens[jStart].number,
newNumber: text.newText.tokens[iStart].number,
oldStart: jStart,
count: count,
unique: unique,
words: wDiff.WordCount(string),
chars: chars,
type: 'same',
section: null,
group: null,
fixed: null,
string: string
});
}
return;
}
};
 
// sort blocks by new text token number
blocks.sort(function(a, b) {
return a.newNumber - b.newNumber;
});
 
// TextDiff.getSections(): collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
// number blocks in new text order
// called from: .detectBlocks()
for (var block = 0; block < blocks.length; block ++) {
// changes: creates sections, blocks[].section
blocks[block].newBlock = block;
}
return;
};
 
this.getSections = function () {
 
var blocks = this.blocks;
// wDiff.GetSections: collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
var sections = this.sections;
// called from: DetectBlocks()
// changes: creates sections, blocks[].section
 
wDiff.GetSections // = function (blocks,clear sections) {array
sections.splice(0);
 
// cycle through blocks
// clear sections array
for (var block = 0; block < blocks.length; block ++) {
sections.splice(0);
 
var sectionStart = block;
// cycle through blocks
for ( var blocksectionEnd = 0; block < blocks.length; block ++) {
 
var oldMax = blocks[sectionStart].oldNumber;
var sectionStart = block;
var sectionEndsectionOldMax = blockoldMax;
 
// check right
var oldMax = blocks[sectionStart].oldNumber;
for (var j = sectionStart + 1; j < blocks.length; j ++) {
var sectionOldMax = oldMax;
 
// check rightfor crossing over to the left
if (blocks[j].oldNumber > oldMax) {
for (var j = sectionStart + 1; j < blocks.length; j ++) {
oldMax = blocks[j].oldNumber;
 
}
// check for crossing over to the left
else if (blocks[j].oldNumber >< oldMaxsectionOldMax) {
oldMax sectionEnd = blocks[j].oldNumber;
sectionOldMax = oldMax;
}
}
else if (blocks[j].oldNumber < sectionOldMax) {
sectionEnd = j;
sectionOldMax = oldMax;
}
}
 
// save crossing sections
if (sectionEnd > sectionStart) {
 
// save section to block
for (var i = sectionStart; i <= sectionEnd; i ++) {
blocks[i].section = sections.length;
}
 
// save section
sections.push({
blockStart: sectionStart,
blockEnd: sectionEnd
});
block = sectionEnd;
}
 
// save section
sections.push({
blockStart: sectionStart,
blockEnd: sectionEnd,
deleted: false
});
block = sectionEnd;
}
return;
}
return};
};
 
 
// wDiffTextDiff.GetGroupsgetGroups(): find groups of continuous old text blocks
// called from: DetectBlocks.detectBlocks()
// calls: .wordCount()
// changes: creates .groups, .blocks[].group
 
wDiff this.GetGroupsgetGroups = function (blocks, groups) {
 
var blocks = this.blocks;
// clear groups array
var groups = this.splice(0)groups;
 
// clear groups array
// cycle through blocks
groups.splice(0);
for (var block = 0; block < blocks.length; block ++) {
if (blocks[block].deleted === true) {
continue;
}
var groupStart = block;
var groupEnd = block;
var oldBlock = blocks[groupStart].oldBlock;
 
// cycle through blocks
// get word and char count of block
for (var wordsblock = wDiff.WordCount(blocks[0; block] < blocks.string)length; block ++) {
var maxWordsgroupStart = wordsblock;
var uniquegroupEnd = falseblock;
var charsoldBlock = blocks[blockgroupStart].charsoldBlock;
 
// get word and char count of block
// check right
for ( var iwords = groupEnd + 1; i < this.wordCount(blocks[block].lengthstring); i ++) {
var maxWords = words;
var unique = blocks[block].unique;
var chars = blocks[block].chars;
 
// check for crossing over to the leftright
iffor (blocks[var i].oldBlock != oldBlockgroupEnd + 1; i < blocks.length; i ++) {
break;
}
oldBlock = blocks[i].oldBlock;
 
// getcheck wordfor andcrossing charover countto ofthe blockleft
if (blocks[i].wordsoldBlock >!= maxWordsoldBlock + 1) {
break;
maxWords = blocks[i].words;
}
if oldBlock = (blocks[i].unique === true) {oldBlock;
 
unique = true;
// get word and char count of block
if (blocks[i].words > maxWords) {
maxWords = blocks[i].words;
}
if (blocks[i].unique === true) {
unique = true;
}
words += blocks[i].words;
chars += blocks[i].chars;
groupEnd = i;
}
words += blocks[i].words;
chars += blocks[i].chars;
groupEnd = i;
}
 
// save crossing group
if (groupEnd >= groupStart) {
 
// set groups outside sections as fixed
var fixed = false;
if (blocks[groupStart].section === null) {
fixed = true;
}
 
// save group to block
for (var i = groupStart; i <= groupEnd; i ++) {
blocks[i].group = groups.length;
blocks[i].fixed = fixed;
}
 
// save group
groups.push({
oldNumber: blocks[groupStart].oldNumber,
blockStart: groupStart,
blockEnd: groupEnd,
unique: unique,
maxWords: maxWords,
words: words,
chars: chars,
fixed: fixed,
moved movedFrom: []null,
movedFrom color: null,
});
color: null,
block = groupEnd;
diff: ''
});
block = groupEnd;
}
return;
}
return};
};
 
 
// wDiffTextDiff.SetFixedsetFixed(): set longest sequence of increasing groups in sections as fixed (not moved)
// called from: DetectBlocks.detectBlocks()
// calls: wDiff.FindMaxPathfindMaxPath()
// changes: .groups[].fixed, .blocks[].fixed
 
wDiff this.SetFixedsetFixed = function (blocks, groups, sections) {
 
var blocks = this.blocks;
// cycle through sections
var groups = this.groups;
for (var section = 0; section < sections.length; section ++) {
var blockStartsections = sections[section]this.blockStartsections;
var blockEnd = sections[section].blockEnd;
 
// cycle through sections
var groupStart = blocks[blockStart].group;
for (var section = 0; section < sections.length; section ++) {
var groupEnd = blocks[blockEnd].group;
var blockStart = sections[section].blockStart;
var blockEnd = sections[section].blockEnd;
 
var groupStart = blocks[blockStart].group;
// recusively find path of groups in increasing old group order with longest char length
var groupEnd = blocks[blockEnd].group;
 
// recusively find path of groups in increasing old group order with longest char length
// start at each group of section
var cache = [];
var maxChars = 0;
var maxPath = null;
 
for (var i = groupStart; i <= groupEnd; i ++) {
// start at each group of section
var pathObj = wDiff.FindMaxPath(i, [], 0, cache, groups, groupEnd);
for (var i = groupStart; i <= groupEnd; i ++) {
if (pathObj.chars > maxChars) {
var pathObj = this.findMaxPath(i, groupEnd, cache);
maxPath = pathObj.path;
maxChars =if (pathObj.chars; > maxChars) {
maxPath = pathObj.path;
maxChars = pathObj.chars;
}
}
}
 
// mark fixed groups
for (var i = 0; i < maxPath.length; i ++) {
var group = maxPath[i];
groups[group].fixed = true;
 
// mark fixed blocks
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
blocks[block].fixed = true;
}
}
}
return;
}
return};
};
 
 
// wDiffTextDiff.FindMaxPathfindMaxPath(): recusively find path of groups in increasing old group order with longest char length
// input: start,: path start group;, path,: array of path groups;, chars,: char count of path;, cache,: cached sub-path lengths; groups, groups, group object; groupEnd,: last group
// called from: .setFixed()
// returns: returnObj, contains path and length
// calls: itself recursively
// called from: wDiff.SetFixed()
// returns: returnObj, contains path and length
// calls: itself recursively
 
wDiff this.FindMaxPathfindMaxPath = function (start, path, charsgroupEnd, cache, groups, groupEnd) {
 
var groups = this.groups;
// add current path point
var pathLocal = path.slice();
pathLocal.push(start);
chars = chars + groups[start].chars;
 
// find longest sub-path
// last group, terminate recursion
var maxChars = 0;
var returnObj = { path: pathLocal, chars: chars };
var oldNumber = groups[start].oldNumber;
if (start == groupEnd) {
returnvar returnObj = { path: [], chars: 0};
for (var i = start + 1; i <= groupEnd; i ++) {
}
 
// only in increasing old group order
// find longest sub-path
if (groups[i].oldNumber < oldNumber) {
var maxChars = 0;
continue;
var oldNumber = groups[start].oldNumber;
}
for (var i = start + 1; i <= groupEnd; i ++) {
 
// get longest sub-path from cache (deep copy)
// only in increasing old group order
var pathObj;
if (groups[i].oldNumber < oldNumber) {
if (cache[i] !== undefined) {
continue;
pathObj = { path: cache[i].path.slice(), chars: cache[i].chars };
}
}
 
// get longest sub-path fromby cacherecursion
else {
if (cache[start] !== undefined) {
returnObj pathObj = this.findMaxPath(i, groupEnd, cache[start]);
}
 
// get longest sub-path by recursion
else {
var pathObj = wDiff.FindMaxPath(i, pathLocal, chars, cache, groups, groupEnd);
 
// select longest sub-path
if (pathObj.chars > maxChars) {
maxChars = pathObj.chars;
returnObj = pathObj;
}
}
}
 
// saveadd longestcurrent pathstart to cachepath
returnObj.path.unshift(start);
if (cache[i] === undefined) {
returnObj.chars += groups[start].chars;
cache[start] = returnObj;
}
return returnObj;
};
 
// save path to cache (deep copy)
if (cache[start] === undefined) {
cache[start] = { path: returnObj.path.slice(), chars: returnObj.chars };
}
 
return returnObj;
// wDiff.GetDelBlocks: collect deletion ('del') blocks from old text
};
// called from: DetectBlocks()
// changes: blocks
 
wDiff.GetDelBlocks = function (text, blocks) {
 
// TextDiff.getDelBlocks(): collect deletion ('del') blocks from old text
// cycle through old text to find matched (linked) blocks
// called from: .detectBlocks()
var j = text.oldText.first;
// changes: .blocks
var i = null;
while (j !== null) {
 
this.getDelBlocks = function () {
// collect 'del' blocks
var oldStart = j;
var count = 0;
var string = '';
while ( (j !== null) && (text.oldText.tokens[j].link === null) ) {
count ++;
string += text.oldText.tokens[j].token;
j = text.oldText.tokens[j].next;
}
 
var blocks = this.blocks;
// save old text 'del' block
 
if (count !== 0) {
// cycle through old text to find matched (linked) blocks
blocks.push({
var j = this.oldText.first;
oldBlock: null,
newBlock:var i = null,;
while (j !== null) {
oldNumber: text.oldText.tokens[oldStart].number,
newNumber: null,
oldStart: oldStart,
count: count,
unique: false,
words: null,
chars: null,
type: 'del',
section: null,
group: null,
fixed: null,
string: string
});
}
 
// skipcollect 'samedel' blockblocks
if var (joldStart !== null) {j;
var count = 0;
i = text.oldText.tokens[j].link;
var string = '';
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
iwhile ( (j !== textnull) && (this.newTextoldText.tokens[ij].next;link === null) ) {
count ++;
j = text.oldText.tokens[j].next;
string += this.oldText.tokens[j].token;
j = this.oldText.tokens[j].next;
}
 
// save old text 'del' block
if (count !== 0) {
blocks.push({
oldBlock: null,
newBlock: null,
oldNumber: this.oldText.tokens[oldStart].number,
newNumber: null,
oldStart: oldStart,
count: count,
unique: false,
words: null,
chars: string.length,
type: 'del',
section: null,
group: null,
fixed: null,
moved: null,
string: string
});
}
 
// skip 'same' blocks
if (j !== null) {
i = this.oldText.tokens[j].link;
while ( (i !== null) && (j !== null) && (this.oldText.tokens[j].link == i) ) {
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
}
return;
}
return};
};
 
 
// wDiffTextDiff.PositionDelBlockspositionDelBlocks(): position 'del' blocks into new text order
// called from: DetectBlocks.detectBlocks()
// calls: .sortBlocks()
// changes: .blocks[].section/group/fixed/newNumber
//
// deletion blocks move with fixed neighbor (new number +/- 0.1):
// old: 1 D 2 1 D 2
// / / \ / \ \
// new: 1 D 2 1 D 2
// fixed: * *
// new number: 1 1.1 1.9 2
 
wDiff this.PositionDelBlockspositionDelBlocks = function (blocks) {
 
var blocks = this.blocks;
// sort shallow copy of blocks by oldNumber
var blocksOldgroups = blocksthis.slice()groups;
blocksOld.sort(function(a, b) {
return a.oldNumber - b.oldNumber;
});
 
// cyclesort throughshallow 'del'copy of blocks in old textby orderoldNumber
var blocksOld = blocks.slice();
for (var blockOld = 0; blockOld < blocksOld.length; blockOld ++) {
blocksOld.sort(function(a, b) {
var delBlock = blocksOld[blockOld];
return a.oldNumber - b.oldNumber;
if (delBlock.type != 'del') {
continue});
}
 
// getcycle through blocks in old text prev blockorder
for (var block = 0; block < blocksOld.length; block ++) {
var prevBlock;
var delBlock = blocksOld[block];
if (blockOld > 0) {
prevBlock = blocks[ blocksOld[blockOld - 1].newBlock ];
}
 
// get'del' oldblock text next blockonly
if (delBlock.type != 'del') {
var nextBlock;
continue;
if (blockOld < blocksOld.length - 1) {
}
nextBlock = blocks[ blocksOld[blockOld + 1].newBlock ];
}
 
// moveget afterold text prev block if fixed
var neighborprevBlockNumber;
var prevBlock;
if ( (prevBlock !== undefined) && (prevBlock.fixed === true) ) {
if (block > 0) {
neighbor = prevBlock;
prevBlockNumber = blocksOld[block - 1].newBlock;
delBlock.newNumber = neighbor.newNumber + 0.1;
prevBlock = blocks[prevBlockNumber];
}
}
 
// moveget beforeold text next block if fixed
var nextBlockNumber;
else if ( (nextBlock !== undefined) && (nextBlock.fixed === true) ) {
neighbor =var nextBlock;
if (block < blocksOld.length - 1) {
delBlock.newNumber = neighbor.newNumber - 0.1;
nextBlockNumber = blocksOld[block + 1].newBlock;
}
nextBlock = blocks[nextBlockNumber];
}
 
// move after prev block if existentfixed
var neighbor;
else if (prevBlock !== undefined) {
neighborif ( (prevBlock !== undefined) && (prevBlock;.fixed === true) ) {
neighbor = prevBlock;
delBlock.newNumber = neighbor.newNumber + 0.1;
}
 
// move before next block if fixed
else if ( (nextBlock !== undefined) && (nextBlock.fixed === true) ) {
neighbor = nextBlock;
delBlock.newNumber = neighbor.newNumber - 0.1;
}
 
// move beforeafter firstprev block if not start of group
else if ( (prevBlock !== undefined) && (prevBlockNumber != groups[ prevBlock.group ].blockEnd) ) {
else {
delBlock.newNumber neighbor = -0.1prevBlock;
delBlock.newNumber = neighbor.newNumber + 0.1;
}
}
 
// updatemove 'del'before next block withif not start neighborof datagroup
else if (neighbor (nextBlock !== undefined) && (nextBlockNumber != groups[ nextBlock.group ].blockStart) ) {
neighbor = nextBlock;
delBlock.section = neighbor.section;
delBlock.groupnewNumber = neighbor.groupnewNumber - 0.1;
}
delBlock.fixed = neighbor.fixed;
 
// move after closest previous fixed block
else {
for (var fixed = block; fixed >= 0; fixed --) {
if (blocksOld[fixed].fixed === true) {
neighbor = blocksOld[fixed];
delBlock.newNumber = neighbor.newNumber + 0.1;
break;
}
}
}
 
// move before first block
if (neighbor === undefined) {
delBlock.newNumber = -0.1;
}
 
// update 'del' block data
else {
delBlock.section = neighbor.section;
delBlock.group = neighbor.group;
delBlock.fixed = neighbor.fixed;
}
}
}
return;
};
 
// sort 'del' blocks in and update groups
this.sortBlocks();
 
return;
// wDiff.UnlinkBlocks: convert 'same' blocks in groups into 'ins'/'del' pairs if too short
};
// called from: DetectBlocks()
// changes: text.newText/oldText[].link
// returns: true if text tokens were unlinked
 
wDiff.UnlinkBlocks = function (text, blocks, groups) {
 
// TextDiff.unlinkBlocks(): convert 'same' blocks in groups into 'ins'/'del' pairs if too short
var unlinked = false;
// called from: .detectBlocks()
// calls: .unlinkSingleBlock()
// changes: .newText/oldText[].link
// returns: true if text tokens were unlinked
 
this.unlinkBlocks = function () {
// cycle through groups
for (var group = 0; group < groups.length; group ++) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
 
var blocks = this.blocks;
// no block in group is at least blockMinLength words long
ifvar (groups[group].maxWords <= wDiffthis.blockMinLength) {groups;
 
// cycle through groups
// unlink whole moved group if it contains no unique matched token
var unlinked = false;
if ( (groups[group].fixed === false) && (groups[group].unique === false) ) {
for (var group = 0; group < groups.length; 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 < wDiff.blockMinLength) && (groups[group].unique === false) ) {
for (var block = blockStart; block <= blockEnd; block ++) {
if (blocks[block].type == 'same') {
wDiffthis.UnlinkSingleBlockunlinkSingleBlock(blocks[block], text);
unlinked = true;
}
Line 2,054 ⟶ 2,300:
}
 
// otherwise unlink block flanks
else {
 
// unlink blocks from start if preceded by 'del'
for (var block = blockStart; block <= blockEnd; block ++) {
if ( (block > 0) && (blocks[block - 1].type == 'del') && (blocks[block].type == 'same') ) {
 
// 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;
}
wDiffthis.UnlinkSingleBlockunlinkSingleBlock(blocks[block], text);
unlinked = true;
blockStart = block;
Line 2,071 ⟶ 2,317:
}
 
// unlink blocks from end if followed by 'del'
for (var block = blockEnd; block > blockStart; block --) {
if ( (blockEnd < blocks.length - 1) && (blocks[block + 1].type == 'del') && (blocks[block].type == 'same') ) {
 
// stop unlinking if more than one word or a unique word
Line 2,079 ⟶ 2,325:
break;
}
wDiffthis.UnlinkSingleBlockunlinkSingleBlock(blocks[block], text);
unlinked = true;
}
Line 2,085 ⟶ 2,331:
}
}
return unlinked;
}
};
return unlinked;
};
 
 
// wDiffTextDiff.UnlinkBlockunlinkBlock(): un-linkunlink text tokens of single block, converting them into 'ins'/'del' pairspair
// called from: wDiff.UnlinkBlocksunlinkBlocks()
// changes: text.newText/oldText[].link
 
wDiff this.UnlinkSingleBlockunlinkSingleBlock = function (block, text) {
 
// cycle through old text
var j = block.oldStart;
for (var count = 0; count < block.count; count ++) {
 
// unlink tokens
text this.newText.tokens[ textthis.oldText.tokens[j].link ].link = null;
text this.oldText.tokens[j].link = null;
j = textthis.oldText.tokens[j].next;
}
return;
};
 
 
// wDiffTextDiff.GetInsBlocksgetInsBlocks(): collect insertion ('ins') blocks from new text
// called from: DetectBlocks.detectBlocks()
// changescalls: blocks.sortBlocks()
// changes: .blocks
 
wDiff this.GetInsBlocksgetInsBlocks = function (text, blocks) {
 
var blocks = this.blocks;
// cycle through new text to find insertion blocks
var i = text.newText.first;
while (i !== null) {
 
// cycle through new text to find insertion blocks
// jump over linked (matched) block
whilevar ( (i !== null) && (textthis.newText.tokens[i].link !== null) ) {first;
while (i !== null) {
i = text.newText.tokens[i].next;
}
 
// detectjump insertionover blockslinked ('ins'matched) block
if while ( (i !== null) && (this.newText.tokens[i].link !== null) ) {
i = this.newText.tokens[i].next;
var iStart = i;
var count = 0;
var string = '';
while ( (i !== null) && (text.newText.tokens[i].link === null) ) {
count ++;
string += text.newText.tokens[i].token;
i = text.newText.tokens[i].next;
}
 
// savedetect newinsertion textblocks ('ins' block)
if (i !== null) {
blocks.push({
oldBlock:var iStart null,= i;
newBlock:var count null,= 0;
var string = '';
oldNumber: null,
newNumber:while text( (i !== null) && (this.newText.tokens[iStarti].number,link === null) ) {
oldStart: count null,++;
string += this.newText.tokens[i].token;
count: count,
i = this.newText.tokens[i].next;
unique: false,
}
words: null,
 
chars: null,
type: // save new text 'ins', block
blocks.push({
section: null,
group oldBlock: null,
fixed newBlock: null,
string oldNumber: stringnull,
newNumber: this.newText.tokens[iStart].number,
});
oldStart: null,
count: count,
unique: false,
words: null,
chars: string.length,
type: 'ins',
section: null,
group: null,
fixed: null,
moved: null,
string: string
});
}
}
 
}
// sort 'ins' blocks in and update groups
return;
this.sortBlocks();
};
 
return;
};
 
 
// wDiffTextDiff.SortBlockssortBlocks(): sort blocks by new text token number and update groups
// called from: DetectBlocks.positionDelBlocks(), .getInsBlocks(), .insertMarks()
// changes: .blocks, .groups
 
wDiff this.SortBlockssortBlocks = function (blocks, groups) {
 
var blocks = this.blocks;
// sort by newNumber
var groups = this.groups;
blocks.sort(function(a, b) {
return a.newNumber - b.newNumber;
});
 
// sort by newNumber, then by old number
// cycle through blocks and update groups with new block numbers
blocks.sort(function(a, b) {
var group = null;
var comp = a.newNumber - b.newNumber;
for (var block = 0; block < blocks.length; block ++) {
if (comp === 0) {
var blockGroup = blocks[block].group;
comp = a.oldNumber - b.oldNumber;
if (blockGroup !== null) {
}
if (blockGroup != group) {
return comp;
group = blocks[block].group;
});
groups[group].blockStart = block;
 
groups[group].oldNumber = blocks[block].oldNumber;
// cycle through blocks and update groups with new block numbers
var group = null;
for (var block = 0; block < blocks.length; block ++) {
var blockGroup = blocks[block].group;
if (blockGroup !== null) {
if (blockGroup != group) {
group = blocks[block].group;
groups[group].blockStart = block;
groups[group].oldNumber = blocks[block].oldNumber;
}
groups[blockGroup].blockEnd = block;
}
groups[blockGroup].blockEnd = block;
}
return;
}
return};
};
 
 
// wDiffTextDiff.SetInsDelGroupssetInsGroups: set group numbers of 'ins' and 'del' blocks
// called from: DetectBlocks.detectBlocks()
// changes: .groups, .blocks[].fixed/group
 
wDiff this.SetInsDelGroupssetInsGroups = function (blocks, groups) {
 
var blocks = this.blocks;
// set group numbers of 'ins' and 'del' blocks inside existing groups
for ( var groupgroups = 0; group < this.groups.length; group ++) {
 
var fixed = groups[group].fixed;
// set group numbers of 'ins' blocks inside existing groups
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
iffor (blocks[block].var group === null0; group < groups.length; group ++) {
blocks[block].groupvar fixed = groups[group].fixed;
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
blocks[block].fixed = fixed;
if (blocks[block].group === null) {
blocks[block].group = group;
blocks[block].fixed = fixed;
}
}
}
}
 
// add remaining 'ins' and 'del' blocks to new groups
 
// cycle through blocks
for (var block = 0; block < blocks.length; block ++) {
 
// skip existing groups
if (blocks[block].group === null) {
blocks[block].group = groups.length;
var fixed = blocks[block].fixed;
 
// save new single-block group
groups.push({
oldNumber: blocks[block].oldNumber,
blockStart: block,
blockEnd: block,
unique: falseblocks[block].unique,
maxWords: nullblocks[block].words,
words: nullblocks[block].words,
chars: nullblocks[block].chars,
fixed: blocks[block].fixed,
moved movedFrom: []null,
movedFrom color: null,
});
color: null,
}
diff: ''
});
}
return;
}
return};
};
 
 
// wDiffTextDiff.MarkMovedinsertMarks(): mark original positions of moved groups
// called from: DetectBlocks.detectBlocks()
// changes: .groups[].moved/movedFrom
// moved block marks at original positions relative to fixed groups:
// groups: 3 7
// 1 <| | (no next smaller fixed)
// 5 |< |
// |> 5 |
// | 5 <|
// | >| 5
// | |> 9 (no next larger fixed)
// fixed: * *
// mark direction: groups[.movedGroup].blockStart < .groups[group].blockStart
// group side: groups[.movedGroup].oldNumber < .groups[group].oldNumber
 
wDiff this.MarkMovedinsertMarks = function (groups) {
 
var blocks = this.blocks;
// cycle through groups (moved group)
var groups = this.groups;
for (var movedGroup = 0; movedGroup < groups.length; movedGroup ++) {
var moved = [];
if (groups[movedGroup].fixed !== false) {
var color = 1;
continue;
 
// make shallow copy of blocks
var blocksOld = blocks.slice();
 
// enumerate copy
for (var i = 0; i < blocksOld.length; i ++) {
blocksOld[i].number = i;
}
var movedOldNumber = groups[movedGroup].oldNumber;
 
// findsort closestcopy fixedby groupsoldNumber
blocksOld.sort(function(a, b) {
var nextSmallerNumber = null;
return a.oldNumber - b.oldNumber;
var nextSmallerGroup = null;
});
var nextLargerNumber = null;
var nextLargerGroup = null;
 
// cyclecreate throughlookup groupstable: (original positions)to sorted
var lookupSorted = [];
for (var group = 0; group < groups.length; group ++) {
for (var i = 0; i < blocksOld.length; i ++) {
if ( (groups[group].fixed !== true) || (group == movedGroup) ) {
lookupSorted[ blocksOld[i].number ] = i;
}
 
// cycle through groups (moved group)
for (var moved = 0; moved < groups.length; moved ++) {
var movedGroup = groups[moved];
if (movedGroup.fixed !== false) {
continue;
}
var movedOldNumber = movedGroup.oldNumber;
 
// find closest fixed groupblock with closestto smallerthe oldNumberleft
var oldNumberfixedLeft = groups[group].oldNumbernull;
var leftChars = 0;
if ( (oldNumber < movedOldNumber) && ( (nextSmallerNumber === null) || (oldNumber > nextSmallerNumber) ) ) {
for (var block = lookupSorted[ groups[moved].blockStart ] - 1; block >= 0; block --) {
nextSmallerNumber = oldNumber;
leftChars += blocksOld[block].chars;
nextSmallerGroup = group;
if (blocksOld[block].fixed === true) {
fixedLeft = blocksOld[block];
break;
}
}
 
// find closest fixed groupblock with closestto largerthe oldNumberright
var fixedRight = null;
if ( (oldNumber > movedOldNumber) && ( (nextLargerNumber === null) || (oldNumber < nextLargerNumber) ) ) {
nextLargerNumbervar rightChars = oldNumber0;
for (var block = lookupSorted[ groups[moved].blockEnd ] + 1; block < blocksOld.length; block ++) {
nextLargerGroup = group;
rightChars += blocksOld[block].chars;
if (blocksOld[block].fixed === true) {
fixedLeft = blocksOld[block];
break;
}
}
}
 
// no larger fixed groupblock, moved right
var movedFromfixedBlock = ''null;
if (nextLargerGroupfixedRight === null) {
movedFrom fixedBlock = 'left'fixedLeft;
}
 
// no smaller fixed groupblock, moved rightleft
else if (nextSmallerGroupfixedLeft === null) {
movedFrom fixedBlock = 'right'fixedRight;
}
 
// group moved from between two closest fixed neighbors, moved left or right depending on char distance
else {
var rightChars = 0;
for (var group = nextSmallerGroup + 1; group < movedGroup; group ++) {
rightChars += groups[group].chars;
}
var leftChars = 0;
for (var group = movedGroup + 1; group < nextLargerGroup; group ++) {
leftChars += groups[group].chars;
}
 
// group moved from between two closest fixed neighbors, moved left or right depending on char distance
// moved right
else if (rightChars <= leftChars) {
movedFromfixedBlock = 'left'fixedRight;
}
 
// moved left
else {
movedFromfixedBlock = 'right'fixedLeft;
}
}
 
// from left side of fixed group
// check for null-moves
var newNumber;
if (movedFrom == 'left') {
if (movedOldNumber < fixedBlock.oldNumber) {
if (groups[nextSmallerGroup].blockEnd + 1 != groups[movedGroup].blockStart) {
newNumber = fixedBlock.newNumber - 0.1;
groups[nextSmallerGroup].moved.push(movedGroup);
groups[movedGroup].movedFrom = nextSmallerGroup;
}
 
}
// from right side of fixed group
else if (movedFrom == 'right') {
else {
if (groups[movedGroup].blockEnd + 1 != groups[nextLargerGroup].blockStart) {
newNumber = fixedBlock.newNumber + 0.1;
groups[nextLargerGroup].moved.push(movedGroup);
groups[movedGroup].movedFrom = nextLargerGroup;
}
}
}
 
// insert 'mark' block
// cycle through groups, sort blocks moved from here by old number
blocks.push({
for (var group = 0; group < groups.length; group ++) {
oldBlock: null,
var moved = groups[group].moved;
if newBlock: (moved !== null) {,
oldNumber: movedOldNumber,
moved.sort(function(a, b) {
newNumber: newNumber,
return groups[a].oldNumber - groups[b].oldNumber;
oldStart: null,
count: null,
unique: null,
words: null,
chars: 0,
type: 'mark',
section: null,
group: fixedBlock.group,
fixed: true,
moved: moved,
string: ''
});
}
}
return;
};
 
// set group color
 
movedGroup.color = color;
// wDiff.ColorMoved: set moved block color numbers
movedGroup.movedFrom = fixedBlock.group;
// called from: DetectBlocks()
// changes: groups[].color
 
wDiff.ColorMoved = function (groups) {
 
// cycle through groups
var moved = [];
for (var group = 0; group < groups.length; group ++) {
moved = moved.concat(groups[group].moved);
}
 
// sort moved array by old number
moved.sort(function(a, b) {
return groups[a].oldNumber - groups[b].oldNumber;
});
 
// set color
var color = 0;
for (var i = 0; i < moved.length; i ++) {
var movedGroup = moved[i];
if (wDiff.showBlockMoves === true) {
groups[movedGroup].color = color;
color ++;
}
}
return;
};
 
// sort mark blocks in and update groups
this.sortBlocks();
 
return;
// wDiff.AssembleDiff: process diff data into formatted html text
};
// input: text, object containing text tokens list; blocks, array containing block type; groups, array containing fixed (not moved), color, and moved mark data
// returns: diff html string
// called from: wDiff.Diff()
// calls: wDiff.HtmlCustomize(), wDiff.HtmlFormat()
 
wDiff.AssembleDiff = function (text, blocks, groups) {
 
// TextDiff.assembleDiff(): create html formatted diff text from block and group data
//
// input: version: 'new', 'old', show only one marked-up version
// create group diffs
// returns: diff html string
//
// called from: .diff()
// calls: .htmlCustomize(), .htmlEscape(), .htmlFormatBlock(), .htmlFormat()
 
this.assembleDiff = function (version) {
// cycle through groups
for (var group = 0; group < groups.length; group ++) {
var color = groups[group].color;
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
var diff = '';
 
var blocks = this.blocks;
// check for colored block and move direction
var blockFromgroups = nullthis.groups;
if (color !== null) {
if (groups[ groups[group].movedFrom ].blockStart < blockStart) {
blockFrom = 'left';
}
else {
blockFrom = 'right';
}
}
 
// make shallow copy of groups and sort by blockStart
// add colored block start markup
var groupsSort = groups.slice();
if (blockFrom == 'left') {
groupsSort.sort(function(a, b) {
diff += wDiff.HtmlCustomize(wDiff.htmlBlockLeftStart, color);
return a.blockStart - b.blockStart;
}
});
else if (blockFrom == 'right') {
diff += wDiff.HtmlCustomize(wDiff.htmlBlockRightStart, color);
}
 
//
// cycle through blocks
// create group diffs
for (var block = blockStart; block <= blockEnd; block ++) {
//
var type = blocks[block].type;
var string = blocks[block].string;
 
// htmlcycle escapethrough text stringgroups
var htmlFrags = [];
string = wDiff.HtmlEscape(string);
for (var group = 0; group < groupsSort.length; group ++) {
var color = groupsSort[group].color;
var blockStart = groupsSort[group].blockStart;
var blockEnd = groupsSort[group].blockEnd;
 
// addcheck 'same'for (unchanged)colored textblock and movedmove blockdirection
ifvar (typemoveDir == 'same') {null;
if (color !== null) {
var groupUnSort = blocks[blockStart].group;
diff += wDiff.HtmlFormatBlock(string);
if (groupsSort[group].movedFrom < groupUnSort) {
moveDir = 'left';
}
else {
diffmoveDir += string'right';
}
}
 
// add 'del'colored textblock start markup
else if (typeversion =!= 'delold') {
var html = '';
if (wDiff.regExpBlankBlock.test(string) === true) {
if (moveDir == 'left') {
diff += wDiff.htmlDeleteStartBlank;
html = this.htmlCustomize(wDiff.htmlBlockLeftStart, color);
}
else if (moveDir == 'right') {
diffhtml += this.htmlCustomize(wDiff.htmlDeleteStarthtmlBlockRightStart, color);
}
htmlFrags.push(html);
diff += wDiff.HtmlFormatBlock(string) + wDiff.htmlDeleteEnd;
}
 
// addcycle 'ins'through textblocks
for (var block = blockStart; block <= blockEnd; block ++) {
else if (type == 'ins') {
var html = '';
if (wDiff.regExpBlankBlock.test(string) === true) {
var type = blocks[block].type;
diff += wDiff.htmlInsertStartBlank;
var string = blocks[block].string;
 
// html escape text string
string = this.htmlEscape(string);
 
// add 'same' (unchanged) text and moved block
if (type == 'same') {
if (color !== null) {
if (version != 'old') {
html = this.htmlFormatBlock(string);
}
}
else {
html = string;
}
}
 
else {
// add 'del' text && (blocks[block].fixed == true)
diff += wDiff.htmlInsertStart;
else if ( (type == 'del') && (version != 'new') ) {
 
// for old version skip 'del' inside moved group
if ( (version != 'old') || (color === null) ) {
if (wDiff.regExpBlankBlock.test(string) === true) {
html = wDiff.htmlDeleteStartBlank;
}
else {
html = wDiff.htmlDeleteStart;
}
html += this.htmlFormatBlock(string) + wDiff.htmlDeleteEnd;
}
}
diff += wDiff.HtmlFormatBlock(string) + wDiff.htmlInsertEnd;
}
}
 
// add colored'ins' block end markuptext
else if (blockFrom (type == 'leftins') && (version != 'old') ) {
if (wDiff.regExpBlankBlock.test(string) === true) {
diff += wDiff.htmlBlockLeftEnd;
html = wDiff.htmlInsertStartBlank;
}
}
else if (blockFrom == 'right') {
else {
diff += wDiff.htmlBlockRightEnd;
html = wDiff.htmlInsertStart;
}
}
html += this.htmlFormatBlock(string) + wDiff.htmlInsertEnd;
}
 
// add 'mark' code
groups[group].diff = diff;
else if ( (type == 'mark') && (version != 'new') ) {
}
var moved = blocks[block].moved;
var movedGroup = groups[moved];
var markColor = movedGroup.color;
 
//
// mark original block positions
//
 
// get moved block text ('same' and 'del')
// cycle through groups
var string = '';
for (var group = 0; group < groups.length; group ++) {
for (var mark = movedGroup.blockStart; mark <= movedGroup.blockEnd; mark ++) {
var moved = groups[group].moved;
if ( (blocks[mark].type == 'same') || (blocks[mark].type == 'del') ) {
string += blocks[mark].string;
}
}
 
// display as deletion at original position
// cycle through list of groups moved from here
if ( (wDiff.showBlockMoves === false) || (version == 'old') ) {
var leftMarks = '';
string = this.htmlEscape(string);
var rightMarks = '';
string = this.htmlFormatBlock(string);
for (var i = 0; i < moved.length; i ++) {
if (version == 'old') {
var movedGroup = moved[i];
if (movedGroup.blockStart < groupsSort[group].blockStart) {
var markColor = groups[movedGroup].color;
html = this.htmlCustomize(wDiff.htmlBlockLeftStart, markColor) + string + wDiff.htmlBlockLeftEnd;
var mark;
}
else {
html = this.htmlCustomize(wDiff.htmlBlockRightStart, markColor) + string + wDiff.htmlBlockRightEnd;
}
}
else {
if (wDiff.regExpBlankBlock.test(string) === true) {
html = wDiff.htmlDeleteStartBlank + string + wDiff.htmlDeleteEnd;
}
else {
html = wDiff.htmlDeleteStart + string + wDiff.htmlDeleteEnd;
}
}
}
 
// getdisplay movedas mark, get blockmark textdirection
else {
var string = '';
for if (var block = groups[movedGroup].blockStart; block <= groupsgroupsSort[movedGroupgroup].blockEnd; block ++blockStart) {
html = this.htmlCustomize(wDiff.htmlMarkLeft, markColor, string);
if (blocks[block].type != 'ins') {
}
string += blocks[block].string;
else {
html = this.htmlCustomize(wDiff.htmlMarkRight, markColor, string);
}
}
}
htmlFrags.push(html);
}
 
// add colored block end markup
// display as deletion at original position
if (wDiff.showBlockMovesversion ==!= false'old') {
var html = '';
string = wDiff.HtmlEscape(string);
if (wDiff.regExpBlankBlock.test(string)moveDir === true'left') {
markhtml = wDiff.htmlDeleteStartBlankhtmlBlockLeftEnd;
}
else if (moveDir == 'right') {
markhtml = wDiff.htmlDeleteStarthtmlBlockRightEnd;
}
htmlFrags.push(html);
mark += wDiff.HtmlFormatBlock(string) + wDiff.htmlDeleteEnd;
}
 
// get mark direction
else {
if (groups[movedGroup].blockStart < groups[group].blockStart) {
mark = wDiff.htmlMarkLeft;
}
else {
mark = wDiff.htmlMarkRight;
}
mark = wDiff.HtmlCustomize(mark, markColor, string);
}
 
// get side of group to mark
if (groups[movedGroup].oldNumber < groups[group].oldNumber) {
leftMarks += mark;
}
else {
rightMarks += mark;
}
}
groups[group].diff = leftMarks + groups[group].diff + rightMarks;
}
 
// join fragments
//
this.html = htmlFrags.join('');
// join diffs
//
 
// markup newlines and spaces in blocks
// make shallow copy of groups and sort by blockStart
this.htmlFormat();
var groupsSort = groups.slice();
groupsSort.sort(function(a, b) {
return a.blockStart - b.blockStart;
});
 
return;
// cycle through sorted groups and assemble diffs
};
for (var group = 0; group < groupsSort.length; group ++) {
text.diff += groupsSort[group].diff;
}
 
// WED('Groups', wDiff.DebugGroups(groups));
 
//
// keep newlines and multiple spaces
// TextDiff.htmlCustomize(): customize move indicator html: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
wDiff.HtmlFormat(text);
// input: text (html or css code), number: block number, title: title attribute (popup) text
// returns: customized text
// called from: .assembleDiff()
 
this.htmlCustomize = function (text, number, title) {
// WED('text.diff', text.diff);
 
if (wDiff.coloredBlocks === true) {
return text.diff;
var blockStyle = wDiff.styleBlockColor[number];
};
if (blockStyle === undefined) {
 
blockStyle = '';
 
}
//
var markStyle = wDiff.styleMarkColor[number];
// wDiff.HtmlCustomize: customize move indicator html: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
if (markStyle === undefined) {
// input: text (html or css code)
markStyle = '';
// returns: customized text
}
// called from: wDiff.AssembleDiff()
text = text.replace(/\{block\}/g, ' ' + blockStyle);
 
text = text.replace(/\{mark\}/g, ' ' + markStyle);
wDiff.HtmlCustomize = function (text, number, title) {
text = text.replace(/\{class\}/g, number);
 
if (wDiff.coloredBlocks === true) {
var blockStyle = wDiff.styleBlockColor[number];
if (blockStyle === undefined) {
blockStyle = '';
}
else {
var markStyle = wDiff.styleMarkColor[number];
text = text.replace(/\{block\}|\{mark\}|\{class\}/g, '');
if (markStyle === undefined) {
markStyle = '';
}
text = text.replace(/\{blocknumber\}/g, ' ' + blockStylenumber);
text = text.replace(/\{mark\}/g, ' ' + markStyle);
text = text.replace(/\{class\}/g, number);
}
else {
text = text.replace(/\{block\}|\{mark\}|\{class\}/g, '');
}
text = text.replace(/\{number\}/g, number);
 
// shorten title text, replace {title}
if ( (title !== undefined) && (title !== '') ) {
var max = 512;
var end = 128;
var gapMark = ' [...] ';
if (title.length > max) {
title = title.substr(0, max - gapMark.length - end) + gapMark + title.substr(title.length - end);
}
title = this.htmlEscape(title);
title = title.replace(/\t/g, '&nbsp;&nbsp;');
title = title.replace(/ /g, '&nbsp;&nbsp;');
text = text.replace(/\{title\}/, ' title="' + title + '"');
}
else {
title = wDiff.HtmlEscape(title);
title text = titletext.replace(/\t{title\}/g, '&nbsp;&nbsp;');
}
title = title.replace(/ /g, '&nbsp;&nbsp;');
return text;
text = text.replace(/\{title\}/, ' title="' + title + '"');
};
else {
text = text.replace(/\{title\}/, '');
}
return text;
};
 
 
// wDiffTextDiff.HtmlEscapehtmlEscape(): replace html-sensitive characters in output text with character entities
// input: html text
// returns: escaped html text
// called from: wDiff.Diffdiff(), wDiff.AssembleDiffassembleDiff()
 
wDiff this.HtmlEscapehtmlEscape = function (texthtml) {
 
text html = texthtml.replace(/&/g, '&amp;');
text html = texthtml.replace(/</g, '&lt;');
text html = texthtml.replace(/>/g, '&gt;');
text html = texthtml.replace(/"/g, '&quot;');
return (texthtml);
};
 
 
// wDiffTextDiff.HtmlFormatBlockhtmlFormatBlock(): markup newlines and spaces in blocks
// input: string
// called from: wDiff.Diff(), wDiff.AssembleDiff()
// returns: formatted string
//
// called from: .diff(), .assembleDiff()
 
wDiff this.HtmlFormatBlockhtmlFormatBlock = function (string) {
// spare blanks in tags
string = string.replace(/(<[^>]*>)|( )/g, function (p, p1, p2) {
if (p2 == ' ') {
return wDiff.htmlSpace;
}
return p1;
});
string = string.replace(/\n/g, wDiff.htmlNewline);
return string;
};
 
// spare blanks in tags
string = string.replace(/(<[^>]*>)|( )/g, function (p, p1, p2) {
if (p2 == ' ') {
return wDiff.htmlSpace;
}
return p1;
});
string = string.replace(/\n/g, wDiff.htmlNewline);
return string;
};
 
// wDiff.HtmlFormat: tidy html, join chained markup, markup tabs, add container
// changes: text.diff
// called from: wDiff.Diff(), wDiff.AssembleDiff()
 
// TextDiff.htmlFormat(): markup tabs, add container
wDiff.HtmlFormat = function (text) {
// changes: .diff
// called from: .diff(), .assembleDiff()
 
this.htmlFormat = function () {
text.diff = text.diff.replace(/<\/(\w+)><!--wDiff(Delete|Insert)--><\1\b[^>]*\bclass="wDiff\2"[^>]*>/g, '');
text.diff = text.diff.replace(/\t/g, wDiff.htmlTab);
text.diff = wDiff.htmlContainerStart + wDiff.htmlFragmentStart + text.diff + wDiff.htmlFragmentEnd + wDiff.htmlContainerEnd;
return;
};
 
this.html = this.html.replace(/\t/g, wDiff.htmlTab);
this.html = wDiff.htmlContainerStart + wDiff.htmlFragmentStart + this.html + wDiff.htmlFragmentEnd + wDiff.htmlContainerEnd;
return;
};
 
// wDiff.ShortenOutput: shorten diff html by removing unchanged parts
// input: diff html string from wDiff.Diff()
// returns: shortened html with removed unchanged passages indicated by (...) or separator
 
// TextDiff.shortenOutput(): shorten diff html by removing unchanged sections
wDiff.ShortenOutput = function (html) {
// input: diff html string from .diff()
// returns: shortened html with removed unchanged passages indicated by (...) or separator
 
this.shortenOutput = function () {
var diff = '';
 
var html = this.html;
// wikEd.debugTimer.push(['shorten?', new Date]);
var diff = '';
 
// remove container by non-regExp replace
// empty text
html = html.replace(wDiff.htmlContainerStart, '');
if ( (html === undefined) || (html === '') ) {
html = html.replace(wDiff.htmlFragmentStart, '');
return '';
html = html.replace(wDiff.htmlFragmentEnd, '');
}
html = html.replace(wDiff.htmlContainerEnd, '');
 
// scan for diff html tags
// remove container by non-regExp replace
var regExpDiff = /<\w+\b[^>]*\bclass="[^">]*?\bwDiff(MarkLeft|MarkRight|BlockLeft|BlockRight|Delete|Insert)\b[^">]*"[^>]*>(.|\n)*?<!--wDiff\1-->/g;
html = html.replace(wDiff.htmlContainerStart, '');
var tagsStart = [];
html = html.replace(wDiff.htmlFragmentStart, '');
var tagsEnd = [];
html = html.replace(wDiff.htmlFragmentEnd, '');
var i = 0;
html = html.replace(wDiff.htmlContainerEnd, '');
var regExpMatch;
 
// save tag positions
// scan for diff html tags
while ( (regExpMatch = regExpDiff.exec(html)) !== null ) {
var regExpDiff = /<\w+\b[^>]*\bclass="[^">]*?\bwDiff(MarkLeft|MarkRight|BlockLeft|BlockRight|Delete|Insert)\b[^">]*"[^>]*>(.|\n)*?<!--wDiff\1-->/g;
var tagsStart = [];
var tagsEnd = [];
var i = 0;
var regExpMatch;
 
// combine consecutive diff tags
// save tag positions
if ( (i > 0) && (tagsEnd[i - 1] == regExpMatch.index) ) {
while ( (regExpMatch = regExpDiff.exec(html)) !== null ) {
tagsEnd[i - 1] = regExpMatch.index + regExpMatch[0].length;
}
else {
tagsStart[i] = regExpMatch.index;
tagsEnd[i] = regExpMatch.index + regExpMatch[0].length;
i ++;
}
}
 
// combine consecutiveno diff tags detected
if ( (i > 0) && (tagsEnd[i - 1]tagsStart.length === regExpMatch.index) 0) {
this.html = wDiff.htmlNoChange;
tagsEnd[i - 1] = regExpMatch.index + regExpMatch[0].length;
return;
}
else {
tagsStart[i] = regExpMatch.index;
tagsEnd[i] = regExpMatch.index + regExpMatch[0].length;
i ++;
}
}
 
// define regexps
// no diff tags detected
var regExpLine = /^(\n+|.)|(\n+|.)$|\n+/g;
if (tagsStart.length === 0) {
var regExpHeading = /(^|\n)(<[^>]+>)*(==+.+?==+|\{\||\|\}).*?\n?/g;
return wDiff.htmlNoChange;
var regExpParagraph = /^(\n\n+|.)|(\n\n+|.)$|\n\n+/g;
}
var regExpBlank = /(<[^>]+>)*\s+/g;
 
// get line positions
// define regexps
var regExpMatch;
var regExpLine = /^(\n+|.)|(\n+|.)$|\n+/g;
var lines = [];
var regExpHeading = /(^|\n)(<[^>]+>)*(==+.+?==+|\{\||\|\}).*?\n?/g;
while ( (regExpMatch = regExpLine.exec(html)) !== null) {
var regExpParagraph = /^(\n\n+|.)|(\n\n+|.)$|\n\n+/g;
lines.push(regExpMatch.index);
var regExpBlank = /(<[^>]+>)*\s+/g;
}
 
// get lineheading positions
var regExpMatchheadings = [];
var linesheadingsEnd = [];
while ( (regExpMatch = regExpLineregExpHeading.exec(html)) !== null ) {
lines headings.push(regExpMatch.index);
headingsEnd.push(regExpMatch.index + regExpMatch[0].length);
}
}
 
// get headingparagraph positions
var headingsparagraphs = [];
while ( (regExpMatch = regExpParagraph.exec(html)) !== null ) {
var headingsEnd = [];
paragraphs.push(regExpMatch.index);
while ( (regExpMatch = regExpHeading.exec(html)) !== null ) {
}
headings.push(regExpMatch.index);
headingsEnd.push(regExpMatch.index + regExpMatch[0].length);
}
 
// getdetermine paragraphfragment border positions around diff tags
var paragraphslineMaxBefore = []0;
var headingBefore = 0;
while ( (regExpMatch = regExpParagraph.exec(html)) !== null ) {
var paragraphBefore = 0;
paragraphs.push(regExpMatch.index);
var lineBefore = 0;
}
 
var lineMaxAfter = 0;
// determine fragment border positions around diff tags
var lineMaxBeforeheadingAfter = 0;
var headingBeforeparagraphAfter = 0;
var paragraphBeforelineAfter = 0;
var lineBefore = 0;
 
var lineMaxAfterrangeStart = 0[];
var headingAfterrangeEnd = 0[];
var paragraphAfterrangeStartType = 0[];
var lineAfterrangeEndType = 0[];
 
// cycle through diff tag start positions
var rangeStart = [];
for (var i = 0; i < tagsStart.length; i ++) {
var rangeEnd = [];
var rangeStartTypetagStart = tagsStart[i];
var rangeEndTypetagEnd = tagsEnd[i];
 
// maximal lines to search before diff tag
// cycle through diff tag start positions
var rangeStartMin = 0;
for (var i = 0; i < tagsStart.length; i ++) {
for (var j = lineMaxBefore; j < lines.length - 1; j ++) {
var tagStart = tagsStart[i];
if (tagStart < lines[j + 1]) {
var tagEnd = tagsEnd[i];
if (j - wDiff.linesBeforeMax >= 0) {
rangeStartMin = lines[j - wDiff.linesBeforeMax];
}
lineMaxBefore = j;
break;
}
}
 
// maximalfind lineslast to searchheading before diff tag
if (rangeStart[i] === undefined) {
var rangeStartMin = 0;
for (var j = lineMaxBeforeheadingBefore; j < linesheadings.length - 1; j ++) {
if (tagStart < linesheadings[j] +> 1]tagStart) {
break;
if (j - wDiff.linesBeforeMax >= 0) {
}
rangeStartMin = lines[j - wDiff.linesBeforeMax];
if (headings[j + 1] > tagStart) {
if ( (headings[j] > tagStart - wDiff.headingBefore) && (headings[j] > rangeStartMin) ) {
rangeStart[i] = headings[j];
rangeStartType[i] = 'heading';
headingBefore = j;
}
break;
}
}
lineMaxBefore = j;
break;
}
}
 
// find last headingparagraph before diff tag
if (rangeStart[i] === undefined) {
for (var j = headingBeforeparagraphBefore; j < headingsparagraphs.length - 1; j ++) {
if (headingsparagraphs[j] > tagStart) {
break;
}
if (paragraphs[j + 1] > tagStart - wDiff.paragraphBeforeMin) {
if ( (paragraphs[j] > tagStart - wDiff.paragraphBeforeMax) && (paragraphs[j] > rangeStartMin) ) {
rangeStart[i] = paragraphs[j];
rangeStartType[i] = 'paragraph';
paragraphBefore = j;
}
break;
}
}
}
if (headings[j + 1] > tagStart) {
 
if ( (headings[j] > tagStart - wDiff.headingBefore) && (headings[j] > rangeStartMin) ) {
// find last line break before diff tag
rangeStart[i] = headings[j];
if (rangeStart[i] === undefined) {
rangeStartType[i] = 'heading';
for (var j = lineBefore; j < lines.length - 1; j ++) {
headingBefore = j;
if (lines[j + 1] > tagStart - wDiff.lineBeforeMin) {
if ( (lines[j] > tagStart - wDiff.lineBeforeMax) && (lines[j] > rangeStartMin) ) {
rangeStart[i] = lines[j];
rangeStartType[i] = 'line';
lineBefore = j;
}
break;
}
break;
}
}
}
 
// find last paragraphblank before diff tag
if (rangeStart[i] === undefined) {
var lastPos = tagStart - wDiff.blankBeforeMax;
for (var j = paragraphBefore; j < paragraphs.length - 1; j ++) {
if (paragraphs[j]lastPos >< tagStartrangeStartMin) {
breaklastPos = rangeStartMin;
}
regExpBlank.lastIndex = lastPos;
if (paragraphs[j + 1] > tagStart - wDiff.paragraphBeforeMin) {
while ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
if ( (paragraphs[j] > tagStart - wDiff.paragraphBeforeMax) && (paragraphs[j] > rangeStartMin) ) {
if (regExpMatch.index > tagStart - wDiff.blankBeforeMin) {
rangeStart[i] = paragraphs[j];
rangeStartTyperangeStart[i] = 'paragraph'lastPos;
paragraphBeforerangeStartType[i] = j'blank';
break;
}
lastPos = regExpMatch.index;
break;
}
}
}
 
// findfixed lastnumber lineof breakchars before diff tag
if (rangeStart[i] === undefined) {
if (tagStart - wDiff.charsBefore > rangeStartMin) {
for (var j = lineBefore; j < lines.length - 1; j ++) {
if (lines rangeStart[j + 1i] >= tagStart - wDiff.lineBeforeMin) {charsBefore;
rangeStartType[i] = 'chars';
if ( (lines[j] > tagStart - wDiff.lineBeforeMax) && (lines[j] > rangeStartMin) ) {
rangeStart[i] = lines[j];
rangeStartType[i] = 'line';
lineBefore = j;
}
break;
}
}
}
 
// findfixed lastnumber blankof lines before diff tag
if (rangeStart[i] === undefined) {
rangeStart[i] = rangeStartMin;
var lastPos = tagStart - wDiff.blankBeforeMax;
rangeStartType[i] = 'lines';
if (lastPos < rangeStartMin) {
lastPos = rangeStartMin;
}
 
regExpBlank.lastIndex = lastPos;
// maximal lines to search after diff tag
while ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
var rangeEndMax = html.length;
if (regExpMatch.index > tagStart - wDiff.blankBeforeMin) {
for (var j = lineMaxAfter; j < lines.length; j ++) {
rangeStart[i] = lastPos;
if (lines[j] > tagEnd) {
rangeStartType[i] = 'blank';
if (j + wDiff.linesAfterMax < lines.length) {
rangeEndMax = lines[j + wDiff.linesAfterMax];
}
lineMaxAfter = j;
break;
}
lastPos = regExpMatch.index;
}
}
 
// fixedfind numberfirst ofheading chars beforeafter diff tag
if (rangeStartrangeEnd[i] === undefined) {
for (var j = headingAfter; j < headingsEnd.length; j ++) {
if (tagStart - wDiff.charsBefore > rangeStartMin) {
if (headingsEnd[j] > tagEnd) {
rangeStart[i] = tagStart - wDiff.charsBefore;
if ( (headingsEnd[j] < tagEnd + wDiff.headingAfter) && (headingsEnd[j] < rangeEndMax) ) {
rangeStartType[i] = 'chars';
rangeEnd[i] = headingsEnd[j];
}
rangeEndType[i] = 'heading';
}
paragraphAfter = j;
 
}
// fixed number of lines before diff tag
break;
if (rangeStart[i] === undefined) {
}
rangeStart[i] = rangeStartMin;
rangeStartType[i] = 'lines';
}
 
// maximal lines to search after diff tag
var rangeEndMax = html.length;
for (var j = lineMaxAfter; j < lines.length; j ++) {
if (lines[j] > tagEnd) {
if (j + wDiff.linesAfterMax < lines.length) {
rangeEndMax = lines[j + wDiff.linesAfterMax];
}
lineMaxAfter = j;
break;
}
}
 
// find first headingparagraph after diff tag
if (rangeEnd[i] === undefined) {
for (var j = headingAfterparagraphAfter; j < headingsEndparagraphs.length; j ++) {
if (headingsEndparagraphs[j] > tagEnd + wDiff.paragraphAfterMin) {
if ( (headingsEndparagraphs[j] < tagEnd + wDiff.headingAfterparagraphAfterMax) && (headingsEndparagraphs[j] < rangeEndMax) ) {
rangeEnd[i] = headingsEndparagraphs[j];
rangeEndType[i] = 'headingparagraph';
paragraphAfter = j;
}
break;
}
break;
}
}
}
 
// find first paragraphline break after diff tag
if (rangeEnd[i] === undefined) {
for (var j = paragraphAfterlineAfter; j < paragraphslines.length; j ++) {
if (paragraphslines[j] > tagEnd + wDiff.paragraphAfterMinlineAfterMin) {
if ( (paragraphslines[j] < tagEnd + wDiff.paragraphAfterMaxlineAfterMax) && (paragraphslines[j] < rangeEndMax) ) {
rangeEnd[i] = paragraphslines[j];
rangeEndType[i] = 'paragraphline';
paragraphAfter lineAfter = j;
}
break;
}
break;
}
}
}
 
// find first line breakblank after diff tag
if (rangeEnd[i] === undefined) {
regExpBlank.lastIndex = tagEnd + wDiff.blankAfterMin;
for (var j = lineAfter; j < lines.length; j ++) {
if (lines[j] >(regExpMatch tagEnd= + wDiffregExpBlank.lineAfterMinexec(html)) !== null ) {
if ( (lines[j]regExpMatch.index < tagEnd + wDiff.lineAfterMaxblankAfterMax) && (lines[j]regExpMatch.index < rangeEndMax) ) {
rangeEnd[i] = lines[j]regExpMatch.index;
rangeEndType[i] = 'lineblank';
lineAfter = j;
}
break;
}
}
}
 
// findfixed blanknumber of chars after diff tag
if (rangeEnd[i] === undefined) {
regExpBlank.lastIndex = if (tagEnd + wDiff.blankAfterMin;charsAfter < rangeEndMax) {
rangeEnd[i] = tagEnd + wDiff.charsAfter;
if ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
rangeEndType[i] = 'chars';
if ( (regExpMatch.index < tagEnd + wDiff.blankAfterMax) && (regExpMatch.index < rangeEndMax) ) {
rangeEnd[i] = regExpMatch.index;
rangeEndType[i] = 'blank';
}
}
}
 
// fixed number of charslines after diff tag
if (rangeEnd[i] === undefined) {
if rangeEnd[i] (tagEnd + wDiff.charsAfter <= rangeEndMax) {;
rangeEndrangeEndType[i] = tagEnd + wDiff.charsAfter'lines';
rangeEndType[i] = 'chars';
}
}
 
// remove overlaps, join close fragments
// fixed number of lines after diff tag
var fragmentStart = [];
if (rangeEnd[i] === undefined) {
var fragmentEnd = [];
rangeEnd[i] = rangeEndMax;
var fragmentStartType = [];
rangeEndType[i] = 'lines';
var fragmentEndType = [];
fragmentStart[0] = rangeStart[0];
fragmentEnd[0] = rangeEnd[0];
fragmentStartType[0] = rangeStartType[0];
fragmentEndType[0] = rangeEndType[0];
var j = 1;
for (var i = 1; i < rangeStart.length; i ++) {
 
// get lines between fragments
var lines = 0;
if (fragmentEnd[j - 1] < rangeStart[i]) {
var join = html.substring(fragmentEnd[j - 1], rangeStart[i]);
lines = (join.match(/\n/g) || []).length;
}
 
if ( (rangeStart[i] > fragmentEnd[j - 1] + wDiff.fragmentJoinChars) || (lines > wDiff.fragmentJoinLines) ) {
fragmentStart[j] = rangeStart[i];
fragmentEnd[j] = rangeEnd[i];
fragmentStartType[j] = rangeStartType[i];
fragmentEndType[j] = rangeEndType[i];
j ++;
}
else {
fragmentEnd[j - 1] = rangeEnd[i];
fragmentEndType[j - 1] = rangeEndType[i];
}
}
}
 
// removeassemble overlaps, join closethe fragments
for (var i = 0; i < fragmentStart.length; i ++) {
var fragmentStart = [];
var fragmentEnd = [];
var fragmentStartType = [];
var fragmentEndType = [];
fragmentStart[0] = rangeStart[0];
fragmentEnd[0] = rangeEnd[0];
fragmentStartType[0] = rangeStartType[0];
fragmentEndType[0] = rangeEndType[0];
var j = 1;
for (var i = 1; i < rangeStart.length; i ++) {
 
// get linestext between fragmentsfragment
var fragment = html.substring(fragmentStart[i], fragmentEnd[i]);
var lines = 0;
fragment = fragment.replace(/^\n+|\n+$/g, '');
if (fragmentEnd[j - 1] < rangeStart[i]) {
 
var join = html.substring(fragmentEnd[j - 1], rangeStart[i]);
// add inline marks for omitted chars and words
lines = (join.match(/\n/g) || []).length;
if (fragmentStart[i] > 0) {
if (fragmentStartType[i] == 'chars') {
fragment = wDiff.htmlOmittedChars + fragment;
}
else if (fragmentStartType[i] == 'blank') {
fragment = wDiff.htmlOmittedChars + ' ' + fragment;
}
}
if (fragmentEnd[i] < html.length) {
if (fragmentStartType[i] == 'chars') {
fragment = fragment + wDiff.htmlOmittedChars;
}
else if (fragmentStartType[i] == 'blank') {
fragment = fragment + ' ' + wDiff.htmlOmittedChars;
}
}
 
// remove leading and trailing empty lines
fragment = fragment.replace(/^\n+|\n+$/g, '');
 
// add fragment separator
if (i > 0) {
diff += wDiff.htmlSeparator;
}
 
// add fragment wrapper
diff += wDiff.htmlFragmentStart + fragment + wDiff.htmlFragmentEnd;
}
 
// add diff wrapper
if ( (rangeStart[i] > fragmentEnd[j - 1] + wDiff.fragmentJoinChars) || (lines > wDiff.fragmentJoinLines) ) {
diff = wDiff.htmlContainerStart + diff + wDiff.htmlContainerEnd;
fragmentStart[j] = rangeStart[i];
 
fragmentEnd[j] = rangeEnd[i];
this.html = diff;
fragmentStartType[j] = rangeStartType[i];
return;
fragmentEndType[j] = rangeEndType[i];
};
j ++;
 
 
// wDiff.wordCount(): count words in string
// called from: .getGroups(), .getSameBlocks()
//
 
this.wordCount = function (string) {
 
return (string.match(wDiff.regExpWord) || []).length;
};
 
 
// TextDiff.debugBlocks(): dump blocks object for debugging
// input: text: title, group: block object (optional)
//
 
this.debugBlocks = function (text, blocks) {
 
if (blocks === undefined) {
blocks = this.blocks;
}
var dump = '\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq \twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \tstring\n';
else {
for (var i = 0; i < blocks.length; i ++) {
fragmentEnd[j - 1] = rangeEnd[i];
dump += i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' + blocks[i].oldNumber + ' \t' + (blocks[i].newNumber || 'null').toString().substr(0, 6) + ' \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.debugShortenString(blocks[i].string) + '\n';
fragmentEndType[j - 1] = rangeEndType[i];
}
console.log(text + ':\n' + dump);
}
};
 
// assemble the fragments
for (var i = 0; i < fragmentStart.length; i ++) {
 
// TextDiff.debugGroups(): dump groups object for debugging
// get text fragment
// input: text: title, group: group object (optional)
var fragment = html.substring(fragmentStart[i], fragmentEnd[i]);
//
fragment = fragment.replace(/^\n+|\n+$/g, '');
 
this.debugGroups = function (text, groups) {
// add inline marks for omitted chars and words
 
if (fragmentStart[i] > 0) {
if (fragmentStartType[i]groups === 'chars'undefined) {
groups = this.groups;
fragment = wDiff.htmlOmittedChars + fragment;
}
else if (fragmentStartType[i] == 'blank') {
fragment = wDiff.htmlOmittedChars + ' ' + fragment;
}
}
var dump = '\ni \toldNm \tblSta \tblEnd \tuniq \tmaxWo \twords \tchars \tfixed \toldNm \tmFrom \tcolor\n';
if (fragmentEnd[i] < html.length) {
for (var i = 0; i < groups.length; i ++) {
if (fragmentStartType[i] == 'chars') {
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';
fragment = fragment + wDiff.htmlOmittedChars;
}
else if (fragmentStartType[i] == 'blank') {
fragment = fragment + ' ' + wDiff.htmlOmittedChars;
}
}
console.log(text + ':\n' + dump);
};
 
// remove leading and trailing empty lines
fragment = fragment.replace(/^\n+|\n+$/g, '');
 
// TextDiff.debugShortenString(): shorten string for dumping
// add fragment separator
// called from .debugBlocks, .debugGroups, Text.debugText
if (i > 0) {
//
diff += wDiff.htmlSeparator;
}
 
this.debugShortenString = function (string) {
// encapsulate span errors
diff += wDiff.htmlFragmentStart + fragment + wDiff.htmlFragmentEnd;
}
 
if (typeof string != 'string') {
// add to container
string = string.toString();
diff = wDiff.htmlContainerStart + diff + wDiff.htmlContainerEnd;
}
string = string.replace(/\n/g, '\\n');
string = string.replace(/\t/g, ' ');
var max = 100;
if (string.length > max) {
string = string.substr(0, max - 1 - 30) + '…' + string.substr(string.length - 30);
}
return '"' + string + '"';
};
 
// WED('diff', diff);
 
// initialze text diff object
// wikEd.debugTimer.push(['shorten=', new Date]);
// wikEdthis.DebugTimerinit();
 
return diff;
};
 
 
// wDiff.addScript(): add script to head
//
// called from: wDiff.init()
// wDiff.AddScript: add script to head
//
 
wDiff.AddScriptaddScript = function (code) {
 
var script = document.createElement('script');
Line 3,007 ⟶ 3,318:
 
 
// wDiff.addStyleSheet(): add CSS rules to new style sheet, cross-browser >= IE6
//
// called from: wDiff.init()
// wDiff.AddStyleSheet: add CSS rules to new style sheet, cross-browser >= IE6
//
 
wDiff.AddStyleSheetaddStyleSheet = function (css) {
 
var style = document.createElement('style');
Line 3,023 ⟶ 3,334:
document.getElementsByTagName('head')[0].appendChild(style);
return;
};
 
 
//
// wDiff.WordCount: count words in string
//
 
wDiff.WordCount = function (string) {
 
return (string.match(wDiff.regExpWordCount) || []).length;
};
 
 
//
// wDiff.DebugText: dump text (text.oldText or text.newText) object
//
 
wDiff.DebugText = function (text) {
var dump = 'first: ' + text.first + '\tlast: ' + text.last + '\n';
dump += '\ni \tlink \t(prev \tnext) \t#num \t"token"\n';
var i = text.first;
while ( (i !== null) && (text.tokens[i] !== null) ) {
dump += i + ' \t' + text.tokens[i].link + ' \t(' + text.tokens[i].prev + ' \t' + text.tokens[i].next + ') \t#' + text.tokens[i].number + ' \t' + wDiff.DebugShortenString(text.tokens[i].token) + '\n';
i = text.tokens[i].next;
}
return dump;
};
 
 
//
// wDiff.DebugBlocks: dump blocks object
//
 
wDiff.DebugBlocks = function (blocks) {
var dump = '\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq \twords \tchars \ttype \tsect \tgroup \tfixed \tstring\n';
for (var i = 0; i < blocks.length; 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' + wDiff.DebugShortenString(blocks[i].string) + '\n';
}
return dump;
};
 
 
//
// wDiff.DebugGroups: dump groups object
//
 
wDiff.DebugGroups = function (groups) {
var dump = '\ni \tblSta \tblEnd \tuniq \tmaxWo \twords \tchars \tfixed \toldNm \tmFrom \tcolor \tmoved \tdiff\n';
for (var i = 0; i < groups.length; i ++) {
dump += i + ' \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 + ' \t' + groups[i].moved.toString() + ' \t' + wDiff.DebugShortenString(groups[i].diff) + '\n';
}
return dump;
};
 
 
//
// wDiff.DebugShortenString: shorten string for debugging
//
 
wDiff.DebugShortenString = function (string) {
if (string === null) {
return 'null';
}
string = string.replace(/\n/g, '\\n');
string = string.replace(/\t/g, ' ');
var max = 100;
if (string.length > max) {
string = string.substr(0, max - 1 - 30) + '…' + string.substr(string.length - 30);
}
return '"' + string + '"';
};
 
 
// initialize wDiff
wDiff.Initinit();
 
// </syntaxhighlight>