User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.0.4 (August 26, 2014) sort blocks moved from block by old number
another background that could use some darkmode friendly color
 
(41 intermediate revisions by 3 users not shown)
Line 1:
// <syntaxhighlight lang="JavaScript">
 
// ==UserScript==
// @name wDiff
// @versionname 1.0.4 wikEd diff
// @dateversion August 26, 20141.2.4
// @date October 23, 2014
// @description improved word-based diff library with block move detection
// @homepage https://en.wikipedia.org/wiki/User:Cacycle/diff
Line 9 ⟶ 10:
// @author Cacycle (https://en.wikipedia.org/wiki/User:Cacycle)
// @license released into the public ___domain
// ==/UserScript==
 
/**
* wikEd diff: inline-style difference engine with block move support
*
* Improved JavaScript diff library that returns html/css-formatted new text version with
* highlighted deletions, insertions, and block moves. It is compatible with all browsers and is
* not dependent on external libraries.
*
* WikEdDiff.php and the JavaScript library wikEd diff are synced one-to-one ports. Changes and
* fixes are to be applied to both versions.
*
* JavaScript library (mirror): https://en.wikipedia.org/wiki/User:Cacycle/diff
* JavaScript online tool: http://cacycle.altervista.org/wikEd-diff-tool.html
* MediaWiki extension: https://www.mediawiki.org/wiki/Extension:wikEdDiff
*
* This difference engine applies a word-based algorithm that uses unique words as anchor points
* to identify matching text and moved blocks (Paul Heckel: A technique for isolating differences
* between files. Communications of the ACM 21(4):264 (1978)).
*
* Additional features:
*
* - Visual inline style, changes are shown in a single output text
* - Block move detection and highlighting
* - Resolution down to characters level
* - Unicode and multilingual support
* - Stepwise split (paragraphs, lines, sentences, words, characters)
* - Recursive diff
* - Optimized code for resolving unmatched sequences
* - Minimization of length of moved blocks
* - Alignment of ambiguous unmatched sequences to next line break or word border
* - Clipping of unchanged irrelevant parts from the output (optional)
* - Fully customizable
* - Text split optimized for MediaWiki source texts
* - Well commented and documented code
*
* Datastructures (abbreviations from publication):
*
* class WikEdDiffText: diff text object (new or old version)
* .text text of version
* .words[] word count table
* .first index of first token in tokens list
* .last index of last token in tokens list
*
* .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 WikEdDiff: diff object
* .config[]: configuration settings, see top of code for customization options
* .regExp[]: all regular expressions
* .split regular expressions used for splitting text into tokens
* .htmlCode HTML code fragments used for creating the output
* .msg output messages
* .newText new text
* .oldText old text
* .maxWords word count of longest linked block
* .html diff html
* .error flag: result has not passed unit tests
* .bordersDown[] linked region borders downwards, [new index, old index]
* .bordersUp[] linked region borders upwards, [new index, old index]
* .symbols: symbols table for whole text at all refinement levels
* .token[] hash table of parsed tokens for passes 1 - 3, points to symbol[i]
* .symbol[]: array of objects that hold token counters and pointers:
* .newCount new text token counter (NC)
* .oldCount old text token counter (OC)
* .newToken token index in text.newText.tokens
* .oldToken token index in text.oldText.tokens
* .linked flag: at least one unique token pair has been linked
*
* .blocks[]: array, block data (consecutive text tokens) in new text order
* .oldBlock number of block in old text order
* .newBlock number of block in new text order
* .oldNumber old text token number of first token
* .newNumber new text token number of first token
* .oldStart old text token index of first token
* .count number of tokens
* .unique contains unique linked token
* .words word count
* .chars char length
* .type '=', '-', '+', '|' (same, deletion, insertion, mark)
* .section section number
* .group group number of block
* .fixed belongs to a fixed (not moved) group
* .moved moved block group number corresponding with mark block
* .text text of block tokens
*
* .sections[]: array, block sections with no block move crosses outside a section
* .blockStart first block in section
* .blockEnd last block in section
 
* .groups[]: array, section blocks that are consecutive in old text order
Improved JavaScript diff library that returns html/css-formatted new text version with highlighted deletions, inserts, and block moves.
* .oldNumber first block oldNumber
It is compatible with all browsers and is not dependent on external libraries.
* .blockStart first block index
An implementation of the word-based algorithm from:
* .blockEnd last block index
* .unique contains unique linked token
* .maxWords word count of longest linked block
* .words word count
* .chars char count
* .fixed not moved from original position
* .movedFrom group position this group has been moved from
* .color color number of moved group
*
* .fragments[]: diff fragment list ready for markup, abstraction layer for customization
* .text block or mark text
* .color moved block or mark color number
* .type '=', '-', '+' same, deletion, insertion
* '<', '>' mark left, mark right
* '(<', '(>', ')' block start and end
* '~', ' ~', '~ ' omission indicators
* '[', ']', ',' fragment start and end, fragment separator
* '{', '}' container start and end
*
*/
 
// JSHint options
Communications of the ACM 21(4):264 (1978)
/* jshint -W004, -W100, newcap: true, browser: true, jquery: true, sub: true, bitwise: true,
http://doi.acm.org/10.1145/359460.359467
curly: true, evil: true, forin: true, freeze: true, globalstrict: true, immed: true,
latedef: true, loopfunc: true, quotmark: single, strict: true, undef: true */
/* global console */
 
// Turn on ECMAScript 5 strict mode
Additional features:
'use strict';
* Word (token) types have been optimized for MediaWiki source texts
* Stepwise token size refinement, starting with paragraphs, then sentences, words, and finally characters
* Additional post-pass-5 code for resolving islands caused by common tokens at the border of sequences of common tokens
* Color coding of moved blocks and their marks at the original position
* Block detection minimizes length of moved vs. static blocks
* Optional omission of unchanged irrelevant parts from the output
* Fully customizable
* Well commented and documented code
 
/** Define global objects. */
This code is used by the MediaWiki in-browser text editors [[en:User:Cacycle/editor]] and [[en:User:Cacycle/wikEd]]
var wikEdDiffConfig;
and the enhanced diff view tool wikEdDiff [[en:User:Cacycle/wikEd]].
var WED;
 
Usage:
var diffHtml = wDiff.Diff(oldString, newString);
diffHtml = wDiff.ShortenOutput(diffHtml);
 
/**
Datastructures (abbreviations from publication):
* wikEd diff main class.
*
* @class WikEdDiff
*/
var WikEdDiff = function () {
 
/** @var array config Configuration and customization settings. */
text: objects for text related data
this.config = {
.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
.first: index of first token in tokens list
.last: index of last token in tokens list
.diff: diff html
 
/** Core diff settings (with default values). */
symbols[token]: associative array (hash) of parsed tokens for passes 1 - 3, points to symbol[i]
symbol[]: array of objects that hold token counters and pointers:
.newCount: new text token counter (NC)
.oldCount: old text token counter (OC)
.newToken: token index in text.newText.tokens
.oldToken: token index in text.oldText.tokens
 
/**
blocks[]: array of objects that holds block (consecutive text tokens) data in order of the new text
* @var bool config.fullDiff
.oldBlock: blocks.length, number of block in order of the old text
* Show complete un-clipped diff text (false)
.oldNumber: old text token number of first token in block
*/
.newNumber: new text token number of first token in block
'fullDiff': false,
.chars: char length of block
.type: 'same', 'del', 'ins'
.section: section number of block (for testing)
.group: group number of block
.fixed: block belongs to fixed (not moved) group (for testing)
.string: string of block tokens
 
/**
groups[]: section blocks that are consecutive in old text
* @var bool config.showBlockMoves
oldNumber: first block's oldNumber
* Enable block move layout with highlighted blocks and marks at the original positions (true)
blockStart: first block index of group
*/
blockEnd: last block index of group
'showBlockMoves': true,
maxWords: word count of longest uninterrupted block
words: word count of group
chars: char count of group
fixed: group is set to fixed (not moved)
moved[]: list of groups that have been moved from this position
movedFrom: position this group has been moved from
color: color number of moved group
diff: group diff
 
/**
*/
* @var bool config.charDiff
* Enable character-refined diff (true)
*/
'charDiff': true,
 
/**
// turn on ECMAScript 5 strict mode
* @var bool config.repeatedDiff
'use strict';
* Enable repeated diff to resolve problematic sequences (true)
*/
'repeatedDiff': true,
 
/**
if (typeof wDiff == 'undefined') { window.wDiff = {}; }
* @var bool config.recursiveDiff
* Enable recursive diff to resolve problematic sequences (true)
*/
'recursiveDiff': true,
 
/**
//
* @var int config.recursionMax
// css for core diff
* Maximum recursion depth (10)
//
*/
'recursionMax': 10,
 
/**
if (typeof wDiff.styleContainer == 'undefined') { wDiff.styleContainer = ''; }
* @var bool config.unlinkBlocks
if (typeof wDiff.StyleDelete == 'undefined') { wDiff.styleDelete = 'font-weight: normal; text-decoration: none; color: #fff; background-color: #c33; border-radius: 0.25em; padding: 0.2em 1px;' }
* Reject blocks if they are too short and their words are not unique,
if (typeof wDiff.styleInsert == 'undefined') { wDiff.styleInsert = 'font-weight: normal; text-decoration: none; color: #fff; background-color: #07e; border-radius: 0.25em; padding: 0.2em 1px;'; }
* prevents fragmentated diffs for very different versions (true)
if (typeof wDiff.styleBlockLeft == 'undefined') { wDiff.styleBlockLeft = 'background-color: #d0d0d0; border-radius: 0.25em; padding: 0.25em 1px; margin: 0 1px;'; }
*/
if (typeof wDiff.styleBlockRight == 'undefined') { wDiff.styleBlockRight = 'background-color: #d0d0d0; border-radius: 0.25em; padding: 0.25em 1px; margin: 0 1px;'; }
'unlinkBlocks': true,
if (typeof wDiff.styleBlockColor == 'undefined') { wDiff.styleBlockColor = [
'background-color: #ffff60;',
'background-color: #c0ff60;',
'background-color: #ffd8ff;',
'background-color: #a0ffff;',
'background-color: #ffe840;',
'background-color: #bbccff;',
'background-color: #ffaaff;',
'background-color: #ffbbbb;',
'background-color: #a0e8a0;'
]; }
if (typeof wDiff.styleMarkLeft == 'undefined') { wDiff.styleMarkLeft = 'color: #d0d0d0; background-color: #c33; border-radius: 0.25em; padding: 0.2em 0.2em; margin: 0 1px;'; }
if (typeof wDiff.styleMarkRight == 'undefined') { wDiff.styleMarkRight = 'color: #d0d0d0; background-color: #c33; border-radius: 0.25em; padding: 0.2em 0.2em; margin: 0 1px;'; }
if (typeof wDiff.styleMarkColor == 'undefined') { wDiff.styleMarkColor = [
'color: #ffff60;',
'color: #c0ff60;',
'color: #ffd8ff;',
'color: #a0ffff;',
'color: #ffd840;',
'color: #bbccff;',
'color: #ff99ff;',
'color: #ff9999;',
'color: #90d090;'
]; }
if (typeof wDiff.styleNewline == 'undefined') { wDiff.styleNewline = ''; }
if (typeof wDiff.styleTab == 'undefined') { wDiff.styleTab = ''; }
if (typeof wDiff.stylesheet == 'undefined') { wDiff.stylesheet = '.wDiffTab:before { content: "→"; color: #bbb; font-size: smaller; } .wDiffNewline:before { content: "¶"; color: #ccc; padding: 0.2em; } .wDiffMarkRight:before { content: "▶"; } .wDiffMarkLeft:before { content: "◀"; }' }
 
/**
//
* @var int config.unlinkMax
// css for shorten output
* Maximum number of rejection cycles (5)
//
*/
'unlinkMax': 5,
 
/**
if (typeof wDiff.styleFragment == 'undefined') { wDiff.styleFragment = 'white-space: pre-wrap; background: #fcfcfc; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: inherit; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0;'; }
* @var int config.blockMinLength
if (typeof wDiff.styleNoChange == 'undefined') { wDiff.styleNoChange = 'white-space: pre-wrap; background: #f0f0f0; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: inherit; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0;'; }
* Reject blocks if shorter than this number of real words (3)
if (typeof wDiff.styleSeparator == 'undefined') { wDiff.styleSeparator = 'margin-bottom: 1em;'; }
*/
if (typeof wDiff.styleOmittedChars == 'undefined') { wDiff.styleOmittedChars = ''; }
'blockMinLength': 3,
 
/**
//
* @var bool config.coloredBlocks
// html for core diff,
* Display blocks in differing colors (rainbow color scheme) (false)
//
*/
'coloredBlocks': false,
 
/**
// {block} and {mark} are replaced by block number color style, {title} is replaced by title attribute (popup)
* @var bool config.coloredBlocks
// class plus html comment are required indicators for wDiff.ShortenOutput()
* Do not use UniCode block move marks (legacy browsers) (false)
if (typeof wDiff.htmlContainerStart == 'undefined') { wDiff.htmlContainerStart = '<div class="wDiffContainer" style="' + wDiff.styleContainer + '">'; }
*/
if (typeof wDiff.htmlContainerEnd == 'undefined') { wDiff.htmlContainerEnd = '</div>'; }
'noUnicodeSymbols': false,
 
/**
if (typeof wDiff.htmlDeleteStart == 'undefined') { wDiff.htmlDeleteStart = '<span class="wDiffDelete" style="' + wDiff.styleDelete + '" title="−">'; }
* @var bool config.stripTrailingNewline
if (typeof wDiff.htmlDeleteEnd == 'undefined') { wDiff.htmlDeleteEnd = '</span><!--wDiffDelete-->'; }
* Strip trailing newline off of texts (true in .js, false in .php)
*/
'stripTrailingNewline': true,
 
/**
if (typeof wDiff.htmlInsertStart == 'undefined') { wDiff.htmlInsertStart = '<span class="wDiffInsert" style="' + wDiff.styleInsert + '" title="+">'; }
* @var bool config.debug
if (typeof wDiff.htmlInsertEnd == 'undefined') { wDiff.htmlInsertEnd = '</span><!--wDiffInsert-->'; }
* Show debug infos and stats (block, group, and fragment data) in debug console (false)
*/
'debug': false,
 
/**
if (typeof wDiff.htmlBlockLeftStart == 'undefined') { wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft" style="' + wDiff.styleBlockLeft + ' {block}" title="▶ ▢">'; }
* @var bool config.timer
if (typeof wDiff.htmlBlockLeftEnd == 'undefined') { wDiff.htmlBlockLeftEnd = '</span><!--wDiffBlockLeft-->'; }
* Show timing results in debug console (false)
*/
'timer': false,
 
/**
if (typeof wDiff.htmlBlockRightStart == 'undefined') { wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight" style="' + wDiff.styleBlockRight + ' {block}" title="▭ ◀">'; }
* @var bool config.unitTesting
if (typeof wDiff.htmlBlockRightEnd == 'undefined') { wDiff.htmlBlockRightEnd = '</span><!--wDiffBlockRight-->'; }
* Run unit tests to prove correct working, display results in debug console (false)
*/
'unitTesting': false,
 
/** RegExp character classes. */
if (typeof wDiff.htmlMarkRight == 'undefined') { wDiff.htmlMarkRight = '<span class="wDiffMarkRight" style="' + wDiff.styleMarkRight + ' {mark}"{title}></span><!--wDiffMarkRight-->'; }
if (typeof wDiff.htmlMarkLeft == 'undefined') { wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft" style="' + wDiff.styleMarkLeft + ' {mark}"{title}></span><!--wDiffMarkLeft-->'; }
 
// UniCode letter support for regexps
if (typeof wDiff.htmlNewline == 'undefined') { wDiff.htmlNewline = '<span class="wDiffNewline" style="' + wDiff.styleNewline + '"></span>\n'; }
// From http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
if (typeof wDiff.htmlTab == 'undefined') { wDiff.htmlTab = '<span class="wDiffTab" style="' + wDiff.styleTab + '">\t</span>'; }
'regExpLetters':
'a-zA-Z0-9' + (
'00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-' +
'037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA' +
'05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-' +
'07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D' +
'09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE' +
'09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A39' +
'0A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE0' +
'0AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B83' +
'0B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C' +
'0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-' +
'0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-' +
'0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E87' +
'0E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC4' +
'0EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066' +
'106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D' +
'1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-1315' +
'1318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-' +
'17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C' +
'1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA0' +
'1BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F15' +
'1F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC4' +
'1FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-2113' +
'21152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-' +
'2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE' +
'2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-' +
'3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCC' +
'A000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-' +
'A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3' +
'A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-' +
'AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-' +
'AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9' +
'FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3D' +
'FD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-' +
'FFCFFFD2-FFD7FFDA-FFDC'
).replace( /(\w{4})/g, '\\u$1' ),
 
// New line characters without and with \n and \r
//
'regExpNewLines': '\\u0085\\u2028',
// html for shorten output
'regExpNewLinesAll': '\\n\\r\\u0085\\u2028',
//
 
// Breaking white space characters without \n, \r, and \f
if (typeof wDiff.htmlFragmentStart == 'undefined') { wDiff.htmlFragmentStart = '<pre class="wDiffFragment" style="' + wDiff.styleFragment + '">'; }
'regExpBlanks': ' \\t\\x0b\\u2000-\\u200b\\u202f\\u205f\\u3000',
if (typeof wDiff.htmlFragmentEnd == 'undefined') { wDiff.htmlFragmentEnd = '</pre>'; }
 
// Full stops without '.'
if (typeof wDiff.htmlNoChange == 'undefined') { wDiff.htmlNoChange = '<pre class="wDiffFragment" style="' + wDiff.styleNoChange + '" title="="></pre>'; }
'regExpFullStops':
if (typeof wDiff.htmlSeparator == 'undefined') { wDiff.htmlSeparator = '<div class="wDiffStyleSeparator" style="' + wDiff.styleSeparator + '"></div>'; }
'\\u0589\\u06D4\\u0701\\u0702\\u0964\\u0DF4\\u1362\\u166E\\u1803\\u1809' +
if (typeof wDiff.htmlOmittedChars == 'undefined') { wDiff.htmlOmittedChars = '<span class="wDiffOmittedChars" style="' + wDiff.styleOmittedChars + '">…</span>'; }
'\\u2CF9\\u2CFE\\u2E3C\\u3002\\uA4FF\\uA60E\\uA6F3\\uFE52\\uFF0E\\uFF61',
 
// New paragraph characters without \n and \r
//
'regExpNewParagraph': '\\f\\u2029',
// core diff settings
//
 
// Exclamation marks without '!'
// enable block move layout with color coded blocks and marks at their original position
'regExpExclamationMarks':
if (typeof wDiff.showBlockMoves == 'undefined') { wDiff.showBlockMoves = true; }
'\\u01C3\\u01C3\\u01C3\\u055C\\u055C\\u07F9\\u1944\\u1944' +
'\\u203C\\u203C\\u2048\\u2048\\uFE15\\uFE57\\uFF01',
 
// Question marks without '?'
// minimal number of real words for a moved block (0 for always showing color coded blocks)
'regExpQuestionMarks':
if (typeof wDiff.blockMinLength == 'undefined') { wDiff.blockMinLength = 3; }
'\\u037E\\u055E\\u061F\\u1367\\u1945\\u2047\\u2049' +
'\\u2CFA\\u2CFB\\u2E2E\\uA60F\\uA6F7\\uFE56\\uFF1F',
 
/** Clip settings. */
// further resolve replacements character-wise from start and end
if (typeof wDiff.charDiff == 'undefined') { wDiff.charDiff = true; }
 
// Find clip position: characters from right
// enable recursive diff to resolve problematic sequences
'clipHeadingLeft': 1500,
if (typeof wDiff.recursiveDiff == 'undefined') { wDiff.recursiveDiff = true; }
'clipParagraphLeftMax': 1500,
'clipParagraphLeftMin': 500,
'clipLineLeftMax': 1000,
'clipLineLeftMin': 500,
'clipBlankLeftMax': 1000,
'clipBlankLeftMin': 500,
'clipCharsLeft': 500,
 
// Find clip position: characters from right
// UniCode letter support for regexps, from http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
'clipHeadingRight': 1500,
if (typeof wDiff.letters == 'undefined') { wDiff.letters = 'a-zA-Z0-9' + '00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCCA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC'.replace(/(\w{4})/g, '\\u$1'); }
'clipParagraphRightMax': 1500,
'clipParagraphRightMin': 500,
'clipLineRightMax': 1000,
'clipLineRightMin': 500,
'clipBlankRightMax': 1000,
'clipBlankRightMin': 500,
'clipCharsRight': 500,
 
// Maximum number of lines to search for clip position
// regExp for splitting into paragraphs after newline
'clipLinesRightMax': 10,
if (typeof wDiff.regExpParagraph == 'undefined') { wDiff.regExpParagraph = new RegExp('(.|\\n)+?(\\n|$)', 'g'); }
'clipLinesLeftMax': 10,
 
// Skip clipping if ranges are too close
// regExp for splitting into sentences after .spaces or before newline
'clipSkipLines': 5,
if (typeof wDiff.regExpSentence == 'undefined') { wDiff.regExpSentence = new RegExp('\\n|.*?\\.( +|(?=\\n))|.+?(?=\\n)', 'g'); }
'clipSkipChars': 1000,
 
// Css stylesheet
// regExp for splitting into words, multi-char markup, and chars
'cssMarkLeft': '◀',
if (typeof wDiff.regExpWord == 'undefined') { wDiff.regExpWord = new RegExp('([' + wDiff.letters + '])+|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.', 'g'); }
'cssMarkRight': '▶',
'stylesheet':
 
// Insert
// regExp for splitting into chars
'.wikEdDiffInsert {' +
if (typeof wDiff.regExpChar == 'undefined') { wDiff.regExpChar = new RegExp('[' + wDiff.letters + ']', 'g'); }
'font-weight: bold; background-color: #bbddff; ' +
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'} ' +
'.wikEdDiffInsertBlank { background-color: #66bbff; } ' +
'.wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; } ' +
 
// Delete
// regExps for bubbling up gaps
'.wikEdDiffDelete {' +
if (typeof wDiff.regExpBubbleStop == 'undefined') { wDiff.regExpBubbleStop = /\n$/; }
'font-weight: bold; background-color: #ffe49c; ' +
if (typeof wDiff.regExpBubbleClosing == 'undefined') { wDiff.regExpBubbleClosing = /^[\s)\]}>\-–—.,:;?!’\/\\]/; }
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'} ' +
'.wikEdDiffDeleteBlank { background-color: #ffd064; } ' +
'.wikEdDiffFragment:hover .wikEdDiffDeleteBlank { background-color: #ffe49c; } ' +
 
// Block
'.wikEdDiffBlock {' +
'font-weight: bold; background-color: #e8e8e8; ' +
'border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; ' +
'} ' +
'.wikEdDiffBlock { color: #000; } ' +
'.wikEdDiffBlock0 { background-color: #ffff80; } ' +
'.wikEdDiffBlock1 { background-color: #d0ff80; } ' +
'.wikEdDiffBlock2 { background-color: #ffd8f0; } ' +
'.wikEdDiffBlock3 { background-color: #c0ffff; } ' +
'.wikEdDiffBlock4 { background-color: #fff888; } ' +
'.wikEdDiffBlock5 { background-color: #bbccff; } ' +
'.wikEdDiffBlock6 { background-color: #e8c8ff; } ' +
'.wikEdDiffBlock7 { background-color: #ffbbbb; } ' +
'.wikEdDiffBlock8 { background-color: #a0e8a0; } ' +
'.wikEdDiffBlockHighlight {' +
'background-color: #777; color: #fff; ' +
'border: solid #777; border-width: 1px 0; ' +
'} ' +
 
// Mark
//
'.wikEdDiffMarkLeft, .wikEdDiffMarkRight {' +
// shorten output settings
'font-weight: bold; background-color: #ffe49c; ' +
//
'color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; ' +
'} ' +
'.wikEdDiffMarkLeft:before { content: "{cssMarkLeft}"; } ' +
'.wikEdDiffMarkRight:before { content: "{cssMarkRight}"; } ' +
'.wikEdDiffMarkLeft.wikEdDiffNoUnicode:before { content: "<"; } ' +
'.wikEdDiffMarkRight.wikEdDiffNoUnicode:before { content: ">"; } ' +
'.wikEdDiffMark { background-color: #e8e8e8; color: #666; } ' +
'.wikEdDiffMark0 { background-color: #ffff60; } ' +
'.wikEdDiffMark1 { background-color: #c8f880; } ' +
'.wikEdDiffMark2 { background-color: #ffd0f0; } ' +
'.wikEdDiffMark3 { background-color: #a0ffff; } ' +
'.wikEdDiffMark4 { background-color: #fff860; } ' +
'.wikEdDiffMark5 { background-color: #b0c0ff; } ' +
'.wikEdDiffMark6 { background-color: #e0c0ff; } ' +
'.wikEdDiffMark7 { background-color: #ffa8a8; } ' +
'.wikEdDiffMark8 { background-color: #98e898; } ' +
'.wikEdDiffMarkHighlight { background-color: #777; color: #fff; } ' +
 
// Wrappers
// characters before diff tag to search for previous heading, paragraph, line break, cut characters
'.wikEdDiffContainer { } ' +
if (typeof wDiff.headingBefore == 'undefined') { wDiff.headingBefore = 1500; }
'.wikEdDiffFragment {' +
if (typeof wDiff.paragraphBefore == 'undefined') { wDiff.paragraphBefore = 1500; }
'white-space: pre-wrap; background-color: var(--background-color-base, #fff); border: #bbb solid; ' +
if (typeof wDiff.lineBeforeMax == 'undefined') { wDiff.lineBeforeMax = 1000; }
'border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; ' +
if (typeof wDiff.lineBeforeMin == 'undefined') { wDiff.lineBeforeMin = 500; }
'font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; ' +
if (typeof wDiff.blankBeforeMax == 'undefined') { wDiff.blankBeforeMax = 1000; }
'} ' +
if (typeof wDiff.blankBeforeMin == 'undefined') { wDiff.blankBeforeMin = 500; }
'.wikEdDiffNoChange { background: var(--background-color-interactive, #eaecf0); border: 1px #bbb solid; border-radius: 0.5em; ' +
if (typeof wDiff.charsBefore == 'undefined') { wDiff.charsBefore = 500; }
'line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; ' +
'text-align: center; ' +
'} ' +
'.wikEdDiffSeparator { margin-bottom: 1em; } ' +
'.wikEdDiffOmittedChars { } ' +
 
// Newline
// characters after diff tag to search for next heading, paragraph, line break, or characters
'.wikEdDiffNewline:before { content: "¶"; color: transparent; } ' +
if (typeof wDiff.headingAfter == 'undefined') { wDiff.headingAfter = 1500; }
'.wikEdDiffBlock:hover .wikEdDiffNewline:before { color: #aaa; } ' +
if (typeof wDiff.paragraphAfter == 'undefined') { wDiff.paragraphAfter = 1500; }
'.wikEdDiffBlockHighlight .wikEdDiffNewline:before { color: transparent; } ' +
if (typeof wDiff.lineAfterMax == 'undefined') { wDiff.lineAfterMax = 1000; }
'.wikEdDiffBlockHighlight:hover .wikEdDiffNewline:before { color: #ccc; } ' +
if (typeof wDiff.lineAfterMin == 'undefined') { wDiff.lineAfterMin = 500; }
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffNewline:before, ' +
if (typeof wDiff.blankAfterMax == 'undefined') { wDiff.blankAfterMax = 1000; }
'.wikEdDiffInsert:hover .wikEdDiffNewline:before' +
if (typeof wDiff.blankAfterMin == 'undefined') { wDiff.blankAfterMin = 500; }
'{ color: #999; } ' +
if (typeof wDiff.charsAfter == 'undefined') { wDiff.charsAfter = 500; }
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffNewline:before, ' +
'.wikEdDiffDelete:hover .wikEdDiffNewline:before' +
'{ color: #aaa; } ' +
 
// Tab
// maximal fragment distance to join close fragments
'.wikEdDiffTab { position: relative; } ' +
if (typeof wDiff.fragmentJoin == 'undefined') { wDiff.fragmentJoin = 1000; }
'.wikEdDiffTabSymbol { position: absolute; top: -0.2em; } ' +
'.wikEdDiffTabSymbol:before { content: "→"; font-size: smaller; color: #ccc; } ' +
'.wikEdDiffBlock .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffInsert .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffDelete .wikEdDiffTabSymbol:before { color: #bbb; } ' +
 
// Space
'.wikEdDiffSpace { position: relative; } ' +
'.wikEdDiffSpaceSymbol { position: absolute; top: -0.2em; left: -0.05em; } ' +
'.wikEdDiffSpaceSymbol:before { content: "·"; color: transparent; } ' +
'.wikEdDiffBlock:hover .wikEdDiffSpaceSymbol:before { color: #999; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffSpaceSymbol:before { color: transparent; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffSpaceSymbol:before { color: #ddd; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffInsert:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #888; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffDelete:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #999; } ' +
 
// Error
//
'.wikEdDiffError .wikEdDiffFragment,' +
// wDiff.Init: initialize wDiff
'.wikEdDiffError .wikEdDiffNoChange' +
// called from: on code load
'{ background: #faa; }'
// calls: wDiff.AddStyleSheet()
};
 
/** Add regular expressions to configuration settings. */
wDiff.Init = function() {
 
this.config.regExp = {
// compatibility fixes for old names of functions
window.StringDiff = wDiff.Diff;
window.WDiffString = wDiff.Diff;
window.WDiffShortenOutput = wDiff.ShortenOutput;
 
// RegExps for splitting text
// shortcut to wikEd.Debug()
'split': {
if (typeof WED != 'function') {
 
if (typeof console == 'object') {
// Split into paragraphs, after double newlines
window.WED = console.log;
'paragraph': new RegExp(
'(\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
']',
'g'
),
 
// Split into lines
'line': new RegExp(
'\\r\\n|\\n|\\r|[' +
this.config.regExpNewLinesAll +
']',
'g'
),
 
// Split into sentences /[^ ].*?[.!?:;]+(?= |$)/
'sentence': new RegExp(
'[^' +
this.config.regExpBlanks +
'].*?[.!?:;' +
this.config.regExpFullStops +
this.config.regExpExclamationMarks +
this.config.regExpQuestionMarks +
']+(?=[' +
this.config.regExpBlanks +
']|$)',
'g'
),
 
// Split into inline chunks
'chunk': new RegExp(
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
 
// Split into words, multi-char markup, and chars
// regExpLetters speed-up: \\w+
'word': new RegExp(
'(\\w+|[_' +
this.config.regExpLetters +
'])+([\'’][_' +
this.config.regExpLetters +
']*)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.',
'g'
),
 
// Split into chars
'character': /./g
},
 
// RegExp to detect blank tokens
'blankOnlyToken': new RegExp(
'[^' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']'
),
 
// RegExps for sliding gaps: newlines and space/word breaks
'slideStop': new RegExp(
'[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']$'
),
'slideBorder': new RegExp(
'[' +
this.config.regExpBlanks +
']$'
),
 
// RegExps for counting words
'countWords': new RegExp(
'(\\w+|[_' +
this.config.regExpLetters +
'])+([\'’][_' +
this.config.regExpLetters +
']*)*',
'g'
),
'countChunks': new RegExp(
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
 
// RegExp detecting blank-only and single-char blocks
'blankBlock': /^([^\t\S]+|[^\t])$/,
 
// RegExps for clipping
'clipLine': new RegExp(
'[' + this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g'
),
'clipHeading': new RegExp(
'( ^|\\n)(==+.+?==+|\\{\\||\\|\\}).*?(?=\\n|$)', 'g' ),
'clipParagraph': new RegExp(
'( (\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
'])+',
'g'
),
'clipBlank': new RegExp(
'[' +
this.config.regExpBlanks + ']+',
'g'
),
'clipTrimNewLinesLeft': new RegExp(
'[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimNewLinesRight': new RegExp(
'^[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g'
),
'clipTrimBlanksLeft': new RegExp(
'[' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimBlanksRight': new RegExp(
'^[' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g'
)
};
 
/** Add messages to configuration settings. */
 
this.config.msg = {
'wiked-diff-empty': '(No difference)',
'wiked-diff-same': '=',
'wiked-diff-ins': '+',
'wiked-diff-del': '-',
'wiked-diff-block-left': '◀',
'wiked-diff-block-right': '▶',
'wiked-diff-block-left-nounicode': '<',
'wiked-diff-block-right-nounicode': '>',
'wiked-diff-error': 'Error: diff not consistent with versions!'
};
 
/**
* Add output html fragments to configuration settings.
* Dynamic replacements:
* {number}: class/color/block/mark/id number
* {title}: title attribute (popup)
* {nounicode}: noUnicodeSymbols fallback
*/
this.config.htmlCode = {
'noChangeStart':
'<div class="wikEdDiffNoChange" title="' +
this.config.msg['wiked-diff-same'] +
'">',
'noChangeEnd': '</div>',
 
'containerStart': '<div class="wikEdDiffContainer" id="wikEdDiffContainer">',
'containerEnd': '</div>',
 
'fragmentStart': '<pre class="wikEdDiffFragment" style="white-space: pre-wrap;">',
'fragmentEnd': '</pre>',
'separator': '<div class="wikEdDiffSeparator"></div>',
 
'insertStart':
'<span class="wikEdDiffInsert" title="' +
this.config.msg['wiked-diff-ins'] +
'">',
'insertStartBlank':
'<span class="wikEdDiffInsert wikEdDiffInsertBlank" title="' +
this.config.msg['wiked-diff-ins'] +
'">',
'insertEnd': '</span>',
 
'deleteStart':
'<span class="wikEdDiffDelete" title="' +
this.config.msg['wiked-diff-del'] +
'">',
'deleteStartBlank':
'<span class="wikEdDiffDelete wikEdDiffDeleteBlank" title="' +
this.config.msg['wiked-diff-del'] +
'">',
'deleteEnd': '</span>',
 
'blockStart':
'<span class="wikEdDiffBlock"' +
'title="{title}" id="wikEdDiffBlock{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
'blockColoredStart':
'<span class="wikEdDiffBlock wikEdDiffBlock wikEdDiffBlock{number}"' +
'title="{title}" id="wikEdDiffBlock{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
'blockEnd': '</span>',
 
'markLeft':
'<span class="wikEdDiffMarkLeft{nounicode}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
'markLeftColored':
'<span class="wikEdDiffMarkLeft{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
 
'markRight':
'<span class="wikEdDiffMarkRight{nounicode}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
'markRightColored':
'<span class="wikEdDiffMarkRight{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
 
'newline': '<span class="wikEdDiffNewline">\n</span>',
'tab': '<span class="wikEdDiffTab"><span class="wikEdDiffTabSymbol"></span>\t</span>',
'space': '<span class="wikEdDiffSpace"><span class="wikEdDiffSpaceSymbol"></span> </span>',
 
'omittedChars': '<span class="wikEdDiffOmittedChars">…</span>',
 
'errorStart': '<div class="wikEdDiffError" title="Error: diff not consistent with versions!">',
'errorEnd': '</div>'
};
 
/*
* Add JavaScript event handler function to configuration settings
* Highlights corresponding block and mark elements on hover and jumps between them on click
* Code for use in non-jQuery environments and legacy browsers (at least IE 8 compatible)
*
* @option Event|undefined event Browser event if available
* @option element Node DOM node
* @option type string Event type
*/
this.config.blockHandler = function ( event, element, type ) {
 
// IE compatibility
if ( event === undefined && window.event !== undefined ) {
event = window.event;
}
 
else {
// Get mark/block elements
window.WED = alert;
var number = element.id.replace( /\D/g, '' );
var block = document.getElementById( 'wikEdDiffBlock' + number );
var mark = document.getElementById( 'wikEdDiffMark' + number );
if ( block === null || mark === null ) {
return;
}
}
 
// Highlight corresponding mark/block pairs
// add styles to head
if ( type === 'mouseover' ) {
wDiff.AddStyleSheet(wDiff.stylesheet);
element.onmouseover = null;
element.onmouseout = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'mouseout' );
};
element.onclick = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'click' );
};
block.className += ' wikEdDiffBlockHighlight';
mark.className += ' wikEdDiffMarkHighlight';
}
 
// Remove mark/block highlighting
return;
if ( type === 'mouseout' || type === 'click' ) {
};
element.onmouseout = null;
element.onmouseover = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'mouseover' );
};
 
// Reset, allow outside container (e.g. legend)
if ( type !== 'click' ) {
block.className = block.className.replace( / wikEdDiffBlockHighlight/g, '' );
mark.className = mark.className.replace( / wikEdDiffMarkHighlight/g, '' );
 
// GetElementsByClassName
// wDiff.Diff: main method
var container = document.getElementById( 'wikEdDiffContainer' );
// input: oldString, newString, strings containing the texts to be diffed
if ( container !== null ) {
// called from: user code
var spans = container.getElementsByTagName( 'span' );
// calls: wDiff.Split(), wDiff.SplitRefine(), wDiff.CalculateDiff(), wDiff.DetectBlocks(), wDiff.AssembleDiff()
var spansLength = spans.length;
// returns: diff html code, call wDiff.ShortenOutput() for shortening this output
for ( var i = 0; i < spansLength; i ++ ) {
if ( spans[i] !== block && spans[i] !== mark ) {
if ( spans[i].className.indexOf( ' wikEdDiffBlockHighlight' ) !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffBlockHighlight/g, '' );
}
else if ( spans[i].className.indexOf( ' wikEdDiffMarkHighlight') !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffMarkHighlight/g, '' );
}
}
}
}
}
}
 
// Scroll to corresponding mark/block element
wDiff.Diff = function(oldString, newString) {
if ( type === 'click' ) {
 
// Get corresponding element
var diff = '';
var corrElement;
if ( element === block ) {
corrElement = mark;
}
else {
corrElement = block;
}
 
// Get element height (getOffsetTop)
// IE / Mac fix
var corrElementPos = 0;
oldString = oldString.replace(/\r\n?/g, '\n');
var node = corrElement;
newString = newString.replace(/\r\n?/g, '\n');
do {
corrElementPos += node.offsetTop;
} while ( ( node = node.offsetParent ) !== null );
 
// Get scroll height
// prepare text data object
var text = {top;
if ( window.pageYOffset !== undefined ) {
newText: {
top = window.pageYOffset;
string: newString,
tokens: [],}
first:else null,{
top = document.documentElement.scrollTop;
last: null
},
 
oldText: {
// Get cursor pos
string: oldString,
tokens:var [],cursor;
if ( event.pageY !== undefined ) {
first: null,
cursor = event.pageY;
last: null
},
else if ( event.clientY !== undefined ) {
diff: ''
cursor = event.clientY + top;
}
 
// Get line height
var line = 12;
if ( window.getComputedStyle !== undefined ) {
line = parseInt( window.getComputedStyle( corrElement ).getPropertyValue( 'line-height' ) );
}
 
// Scroll element under mouse cursor
window.scroll( 0, corrElementPos + top - cursor + line / 2 );
}
return;
};
 
/** Internal data structures. */
// trap trivial changes: no change
if (oldString == newString) {
text.diff = wDiff.HtmlEscape(newString)
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var WikEdDiffText newText New text version object with text and token list */
// trap trivial changes: old text deleted
this.newText = null;
if ( (oldString == null) || (oldString.length == 0) ) {
text.diff = wDiff.htmlInsertStart + wDiff.HtmlEscape(newString) + wDiff.htmlInsertEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var WikEdDiffText oldText Old text version object with text and token list */
// trap trivial changes: new text deleted
this.oldText = null;
if ( (newString == null) || (newString.length == 0) ) {
text.diff = wDiff.htmlDeleteStart + wDiff.HtmlEscape(oldString) + wDiff.htmlDeleteEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var object symbols Symbols table for whole text at all refinement levels */
// split new and old text into paragraps
this.symbols = {
wDiff.Split(text.newText, wDiff.regExpParagraph);
token: [],
wDiff.Split(text.oldText, wDiff.regExpParagraph);
hashTable: {},
linked: false
};
 
/** @var array bordersDown Matched region borders downwards */
// calculate diff
this.bordersDown = [];
wDiff.CalculateDiff(text);
 
/** @var array bordersUp Matched region borders upwards */
// refine different paragraphs into sentences
this.bordersUp = [];
wDiff.SplitRefine(text.newText, wDiff.regExpSentence);
wDiff.SplitRefine(text.oldText, wDiff.regExpSentence);
 
/** @var array blocks Block data (consecutive text tokens) in new text order */
// calculate refined diff
this.blocks = [];
wDiff.CalculateDiff(text);
 
/** @var int maxWords Maximal detected word count of all linked blocks */
// refine different sentences into words
this.maxWords = 0;
wDiff.SplitRefine(text.newText, wDiff.regExpWord);
wDiff.SplitRefine(text.oldText, wDiff.regExpWord);
 
/** @var array groups Section blocks that are consecutive in old text order */
// calculate refined diff information with recursion for unresolved gaps
this.groups = [];
wDiff.CalculateDiff(text, true);
 
/** @var array sections Block sections with no block move crosses outside a section */
// bubble up gaps
this.sections = [];
wDiff.BubbleUpGaps(text.newText, text.oldText);
wDiff.BubbleUpGaps(text.oldText, text.newText);
 
/** @var object timer Debug timer array: string 'label' => float milliseconds. */
// split tokens into chars in selected unresolved gaps
this.timer = {};
if (wDiff.charDiff == true) {
wDiff.SplitRefineChars(text);
 
/** @var array recursionTimer Count time spent in recursion level in milliseconds. */
// calculate refined diff information with recursion for unresolved gaps
this.recursionTimer = [];
wDiff.CalculateDiff(text, true);
}
 
//** bubbleOutput updata. gaps*/
wDiff.BubbleUpGaps(text.newText, text.oldText);
wDiff.BubbleUpGaps(text.oldText, text.newText);
 
/** @var bool error Unit tests have detected a diff error */
// enumerate tokens lists
this.error = false;
wDiff.EnumerateTokens(text.newText);
wDiff.EnumerateTokens(text.oldText);
 
/** @var array fragments Diff fragment list for markup, abstraction layer for customization */
// detect moved blocks
var blocksthis.fragments = [];
var groups = [];
wDiff.DetectBlocks(text, blocks, groups);
 
/** @var string html Html code of diff */
// assemble diff blocks into formatted html text
this.html = '';
diff = wDiff.AssembleDiff(text, blocks, groups);
 
return diff;
};
 
/**
* Constructor, initialize settings, load js and css.
*
* @param[in] object wikEdDiffConfig Custom customization settings
* @param[out] object config Settings
*/
 
this.init = function () {
// wDiff.Split: 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
// changes: text (text.newText or text.oldText): text.tokens list, text.first, text.last
// called from: wDiff.Diff()
 
// Import customizations from wikEdDiffConfig{}
wDiff.Split = function(text, regExp, token) {
if ( typeof wikEdDiffConfig === 'object' ) {
this.deepCopy( wikEdDiffConfig, this.config );
}
 
// Add CSS stylescheet
var prev = null;
this.addStyleSheet( this.config.stylesheet );
var next = null;
var current = text.tokens.length;
var first = current;
var string = '';
 
// Load block handler script
// split full text or specified token
if (token this.config.showBlockMoves === true null) {
string = text.string;
}
else {
prev = text.tokens[token].prev;
next = text.tokens[token].next;
string = text.tokens[token].token;
}
 
// Add block handler to head if running under Greasemonkey
// split text into tokens
if ( typeof GM_info === 'object' ) {
var number = 0;
var script = 'var wikEdDiffBlockHandler = ' + this.config.blockHandler.toString() + ';';
var regExpMatch;
this.addScript( script );
while ( (regExpMatch = regExp.exec(string)) != null) {
}
else {
window.wikEdDiffBlockHandler = this.config.blockHandler;
}
}
return;
};
 
// insert current item, link to previous
text.tokens[current] = {
token: regExpMatch[0],
prev: prev,
next: null,
link: null,
number: null,
parsed: false,
};
number ++;
 
/**
// link previous item to current
* Main diff method.
if (prev != null) {
*
text.tokens[prev].next = current;
* @param string oldString Old text version
* @param string newString New text version
* @param[out] array fragment
* Diff fragment list ready for markup, abstraction layer for customized diffs
* @param[out] string html Html code of diff
* @return string Html code of diff
*/
this.diff = function ( oldString, newString ) {
 
// Start total timer
if ( this.config.timer === true ) {
this.time( 'total' );
}
prev = current;
current ++;
}
 
// Start diff timer
// connect last new item and existing next item
if ( (numberthis.config.timer > 0) && (token !=== null)true ) {
this.time( 'diff' );
if (prev != null) {
text.tokens[prev].next = next;
}
 
if (next != null) {
// Reset error flag
text.tokens[next].prev = prev;
this.error = false;
 
// Strip trailing newline (.js only)
if ( this.config.stripTrailingNewline === true ) {
if ( newString.substr( -1 ) === '\n' && oldString.substr( -1 === '\n' ) ) {
newString = newString.substr( 0, newString.length - 1 );
oldString = oldString.substr( 0, oldString.length - 1 );
}
}
}
 
// Load version strings into WikEdDiffText objects
// set text first and last token index
this.newText = new WikEdDiff.WikEdDiffText( newString, this );
if (number > 0) {
this.oldText = new WikEdDiff.WikEdDiffText( oldString, this );
 
// Trap trivial changes: no change
// initial text split
if (token this.newText.text === this.oldText.text null) {
textthis.firsthtml = 0;
this.config.htmlCode.containerStart +
text.last = prev;
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd +
this.config.htmlCode.containerEnd;
return this.html;
}
 
// firstTrap ortrivial lastchanges: tokenold hastext been splitdeleted
elseif {(
this.oldText.text === '' || (
if (token == text.first) {
textthis.firstoldText.text === '\n' first;&&
( this.newText.text.charAt( this.newText.text.length - 1 ) === '\n' )
}
)
if (token == text.last) {
) {
text.last = prev;
}this.html =
this.config.htmlCode.containerStart +
this.config.htmlCode.fragmentStart +
this.config.htmlCode.insertStart +
this.htmlEscape( this.newText.text ) +
this.config.htmlCode.insertEnd +
this.config.htmlCode.fragmentEnd +
this.config.htmlCode.containerEnd;
return this.html;
}
}
return;
};
 
// Trap trivial changes: new text deleted
if (
this.newText.text === '' || (
this.newText.text === '\n' &&
( this.oldText.text.charAt( this.oldText.text.length - 1 ) === '\n' )
)
) {
this.html =
this.config.htmlCode.containerStart +
this.config.htmlCode.fragmentStart +
this.config.htmlCode.deleteStart +
this.htmlEscape( this.oldText.text ) +
this.config.htmlCode.deleteEnd +
this.config.htmlCode.fragmentEnd +
this.config.htmlCode.containerEnd;
return this.html;
}
 
// Split new and old text into paragraps
// wDiff.SplitRefine: split unique unmatched tokens into smaller tokens
if ( this.config.timer === true ) {
// changes: text (text.newText or text.oldText) .tokens list
this.time( 'paragraph split' );
// called from: wDiff.Diff()
}
// calls: wDiff.Split()
this.newText.splitText( 'paragraph' );
this.oldText.splitText( 'paragraph' );
if ( this.config.timer === true ) {
this.timeEnd( 'paragraph split' );
}
 
// Calculate diff
wDiff.SplitRefine = function(text, regExp) {
this.calculateDiff( 'line' );
 
// Refine different paragraphs into lines
// cycle through tokens list
if ( this.config.timer === true ) {
var i = text.first;
this.time( 'line split' );
while ( (i != null) && (text.tokens[i] != null) ) {
}
this.newText.splitRefine( 'line' );
this.oldText.splitRefine( 'line' );
if ( this.config.timer === true ) {
this.timeEnd( 'line split' );
}
 
// Calculate refined diff
// refine unique unmatched tokens into smaller tokens
this.calculateDiff( 'line' );
if (text.tokens[i].link == null) {
 
wDiff.Split(text, regExp, i);
// Refine different lines into sentences
if ( this.config.timer === true ) {
this.time( 'sentence split' );
}
this.newText.splitRefine( 'sentence' );
this.oldText.splitRefine( 'sentence' );
if ( this.config.timer === true ) {
this.timeEnd( 'sentence split' );
}
i = text.tokens[i].next;
}
return;
};
 
// Calculate refined diff
this.calculateDiff( 'sentence' );
 
// Refine different sentences into chunks
// wDiff.SplitRefineChars: split tokens into chars in the following unresolved regions (gaps):
if ( this.config.timer === true ) {
// - one token became separated by space, dash, or any string
this.time( 'chunk split' );
// - same number of tokens in gap and strong similarity of all tokens:
}
// - addition or deletion of flanking strings in tokens
this.newText.splitRefine( 'chunk' );
// - addition or deletion of internal string in tokens
this.oldText.splitRefine( 'chunk' );
// - same length and at least 50 % identity
if ( this.config.timer === true ) {
// - same start or end, same text longer than different text
this.timeEnd( 'chunk split' );
// - 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: wDiff.Diff()
// calls: wDiff.Split()
// steps:
// find corresponding gaps
// select gaps of identical token number and strong similarity in all tokens
// refine words into chars in selected gaps
 
// Calculate refined diff
wDiff.SplitRefineChars = function(text) {
this.calculateDiff( 'chunk' );
 
// Refine different chunks into words
//
if ( this.config.timer === true ) {
// find corresponding gaps
this.time( 'word split' );
//
}
this.newText.splitRefine( 'word' );
this.oldText.splitRefine( 'word' );
if ( this.config.timer === true ) {
this.timeEnd( 'word split' );
}
 
// Calculate refined diff information with recursion for unresolved gaps
// cycle trough new text tokens list
this.calculateDiff( 'word', true );
var gaps = [];
var gap = null;
var i = text.newText.first;
var j = text.oldText.first;
while ( (i != null) && (text.newText.tokens[i] != null) ) {
 
// Slide gaps
// get list item properties
if ( this.config.timer === true ) {
var newLink = text.newText.tokens[i].link;
this.time( 'word slide' );
var oldLink = null;
}
if (j != null) {
oldLinkthis.slideGaps( =this.newText, textthis.oldText.tokens[j].link );
this.slideGaps( this.oldText, this.newText );
if ( this.config.timer === true ) {
this.timeEnd( 'word slide' );
}
 
// startSplit oftokens gapinto in new and oldchars
if ( this.config.charDiff === true ) {
if ( (gap == null) && (newLink == null) && (oldLink == null) ) {
 
gap = gaps.length;
// Split tokens into chars in selected unresolved gaps
gaps.push({
if ( this.config.timer === true ) {
newFirst: i,
this.time( 'character split' );
newLast: i,
}
newTokens: 1,
this.splitRefineChars();
oldFirst: j,
if ( this.config.timer === true ) {
oldLast: j,
this.timeEnd( 'character split' );
oldTokens: null,
}
charSplit: null
 
});
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( 'character', true );
 
// Slide gaps
if ( this.config.timer === true ) {
this.time( 'character slide' );
}
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
if ( this.config.timer === true ) {
this.timeEnd( 'character slide' );
}
}
 
// Free memory
// count chars and tokens in gap
this.symbols = undefined;
else if ( (gap != null) && (newLink == null) ) {
this.bordersDown = undefined;
gaps[gap].newLast = i;
this.bordersUp = undefined;
gaps[gap].newTokens ++;
this.newText.words = undefined;
this.oldText.words = undefined;
 
// Enumerate token lists
this.newText.enumerateTokens();
this.oldText.enumerateTokens();
 
// Detect moved blocks
if ( this.config.timer === true ) {
this.time( 'blocks' );
}
this.detectBlocks();
if ( this.config.timer === true ) {
this.timeEnd( 'blocks' );
}
 
// gapFree endedmemory
this.newText.tokens = undefined;
else if ( (gap != null) && (newLink != null) ) {
this.oldText.tokens = undefined;
gap = null;
 
// Assemble blocks into fragment table
this.getDiffFragments();
 
// Free memory
this.blocks = undefined;
this.groups = undefined;
this.sections = undefined;
 
// Stop diff timer
if ( this.config.timer === true ) {
this.timeEnd( 'diff' );
}
 
// nextUnit list elementstests
if (newLink !this.config.unitTesting === true null) {
 
j = text.oldText.tokens[newLink].next;
// Test diff to test consistency between input and output
if ( this.config.timer === true ) {
this.time( 'unit tests' );
}
this.unitTests();
if ( this.config.timer === true ) {
this.timeEnd( 'unit tests' );
}
}
i = text.newText.tokens[i].next;
}
 
// Clipping
// cycle trough gaps and add old text gap data
if ( this.config.fullDiff === false ) {
for (var gap = 0; gap < gaps.length; gap ++) {
 
// Clipping unchanged sections from unmoved block text
// cycle trough old text tokens list
if ( this.config.timer === true ) {
var j = gaps[gap].oldFirst;
this.time( 'clip' );
while ( (j != null) && (text.oldText.tokens[j] != null) && (text.oldText.tokens[j].link == null) ) {
}
this.clipDiffFragments();
if ( this.config.timer === true ) {
this.timeEnd( 'clip' );
}
}
 
// Create html formatted diff code from diff fragments
// count old chars and tokens in gap
if ( this.config.timer === true ) {
gaps[gap].oldLast = j;
this.time( 'html' );
gaps[gap].oldTokens ++;
}
this.getDiffHtml();
if ( this.config.timer === true ) {
this.timeEnd( 'html' );
}
 
// No change
j = text.oldText.tokens[j].next;
if ( this.html === '' ) {
this.html =
this.config.htmlCode.containerStart +
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd +
this.config.htmlCode.containerEnd;
}
}
 
// Add error indicator
//
if ( this.error === true ) {
// select gaps of identical token number and strong similarity of all tokens
this.html = this.config.htmlCode.errorStart + this.html + this.config.htmlCode.errorEnd;
//
}
 
// Stop total timer
for (var gap = 0; gap < gaps.length; gap ++) {
varif charSplit( this.config.timer === true; ) {
this.timeEnd( 'total' );
}
 
return this.html;
// not same gap length
};
if (gaps[gap].newTokens != gaps[gap].oldTokens) {
 
 
// one word became separated by space, dash, or any string
/**
if ( (gaps[gap].newTokens == 1) && (gaps[gap].oldTokens == 3) ) {
* Split tokens into chars in the following unresolved regions (gaps):
if (text.newText.tokens[ gaps[gap].newFirst ].token != text.oldText.tokens[ gaps[gap].oldFirst ].token + text.oldText.tokens[ gaps[gap].oldLast ].token ) {
* - One token became connected or separated by space or dash (or any token)
continue;
* - 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
* Identical tokens including space separators will be linked,
* resulting in word-wise char-level diffs
*
* @param[in/out] WikEdDiffText newText, oldText Text object tokens list
*/
this.splitRefineChars = function () {
 
/** Find corresponding gaps. */
 
// Cycle through new text tokens list
var gaps = [];
var gap = null;
var i = this.newText.first;
var j = this.oldText.first;
while ( 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;
}
 
// cycleCycle troughthrough newgaps textand tokensadd listold andtext setgap charSplitdata
var igapsLength = gaps[gap].newFirstlength;
for ( var jgap = gaps[0; gap].oldFirst < gapsLength; gap ++ ) {
while (i != null) {
var newToken = text.newText.tokens[i].token;
var oldToken = text.oldText.tokens[j].token;
 
// getCycle shorterthrough andold longertext tokentokens list
var shorterTokenj = gaps[gap].oldFirst;
while (
var longerToken;
j !== null &&
if (newToken.length < oldToken.length) {
this.oldText.tokens[j] !== null &&
shorterToken = newToken;
this.oldText.tokens[j].link === null
longerToken = oldToken;
}) {
 
else {
// Count old chars and tokens in gap
shorterToken = oldToken;
longerTokengaps[gap].oldLast = newTokenj;
gaps[gap].oldTokens ++;
 
j = this.oldText.tokens[j].next;
}
}
 
/** Select gaps of identical token number and strong similarity of all tokens. */
// not same token length
if (newToken.length != oldToken.length) {
 
var gapsLength = gaps.length;
// test for addition or deletion of internal string in tokens
for ( var gap = 0; gap < gapsLength; gap ++ ) {
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;
var token = this.newText.tokens[ gaps[gap].newFirst ].token;
var tokenFirst = this.oldText.tokens[ gaps[gap].oldFirst ].token;
var tokenLast = this.oldText.tokens[ gaps[gap].oldLast ].token;
if (
token.indexOf( tokenFirst ) !== 0 ||
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
left ++;
}
else if ( gaps[gap].oldTokens === 1 && gaps[gap].newTokens === 3 ) {
 
var token = this.oldText.tokens[ gaps[gap].oldFirst ].token;
// find number of identical chars from right
var tokenFirst = this.newText.tokens[ gaps[gap].newFirst ].token;
var right = 0;
var tokenLast = this.newText.tokens[ gaps[gap].newLast ].token;
while (right < shorterToken.length) {
if (
if (newToken.charAt(newToken.length - 1 - right) != oldToken.charAt(oldToken.length - 1 - right)) {
token.indexOf( tokenFirst ) !== 0 ||
break;
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
right ++;
}
else {
continue;
}
gaps[gap].charSplit = true;
}
 
// Cycle through new text tokens list and set charSplit
// no simple insertion or deletion of internal string
else {
if (left + right != shorterToken.length) {
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;
 
// 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 text at start or end shorter than differenttoken textlength
if ( (left < shorterTokennewToken.length /!== 2) && (right < shorterTokenoldToken.length / 2) ) {
 
// doTest notfor splitaddition intoor charsdeletion thisof gapinternal string in tokens
 
// Find number of identical chars from left
var left = 0;
while ( left < shorterToken.length ) {
if ( newToken.charAt( left ) !== oldToken.charAt( left ) ) {
break;
}
left ++;
}
 
// Find number of identical chars from right
var right = 0;
while ( right < shorterToken.length ) {
if (
newToken.charAt( newToken.length - 1 - right ) !==
oldToken.charAt( oldToken.length - 1 - right )
) {
break;
}
right ++;
}
 
// No simple insertion or deletion of internal string
if ( left + right !== shorterToken.length ) {
 
// Not addition or deletion of flanking strings in tokens
// Smaller token not part of larger token
if ( longerToken.indexOf( shorterToken ) === -1 ) {
 
// Same text at start or end shorter than different text
if ( left < shorterToken.length / 2 && (right < shorterToken.length / 2) ) {
 
// Do not split into chars in this gap
charSplit = false;
break;
}
}
}
}
 
// Same token length
else if ( newToken !== oldToken ) {
 
// Tokens less than 50 % identical
var ident = 0;
var tokenLength = shorterToken.length;
for ( var pos = 0; pos < tokenLength; pos ++ ) {
if ( shorterToken.charAt( pos ) === longerToken.charAt( pos ) ) {
ident ++;
}
}
if ( ident / shorterToken.length < 0.49 ) {
 
// Do not split into chars this gap
charSplit = false;
break;
}
}
 
// Next list elements
if ( i === gaps[gap].newLast ) {
break;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
gaps[gap].charSplit = charSplit;
}
}
 
/** Refine words into chars in selected gaps. */
// same token length
else if (newToken != oldToken) {
 
var gapsLength = gaps.length;
// tokens less than 50 % identical
for ( var identgap = 0; gap < gapsLength; gap ++ ) {
if ( gaps[gap].charSplit === true ) {
for (var pos = 0; pos < shorterToken.length; pos ++) {
 
if (shorterToken.charAt(pos) == longerToken.charAt(pos)) {
// Cycle through new text tokens list, link spaces, and split into chars
ident ++;
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
var newGapLength = i - gaps[gap].newLast;
var oldGapLength = j - gaps[gap].oldLast;
while ( i !== null || j !== null ) {
 
// Link identical tokens (spaces) to keep char refinement to words
if (
newGapLength === oldGapLength &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
}
if (ident/shorterToken.length < 0.49) {
 
// doRefine not splitwords into chars this gap
charSplitelse = false;{
if ( i !== null ) {
break
this.newText.splitText( 'character', i );
}
if ( j !== null ) {
this.oldText.splitText( 'character', j );
}
}
 
// Next list elements
if ( i === gaps[gap].newLast ) {
i = null;
}
if ( j === gaps[gap].oldLast ) {
j = null;
}
if ( i !== null ) {
i = this.newText.tokens[i].next;
}
if ( j !== null ) {
j = this.oldText.tokens[j].next;
}
}
}
}
return;
};
 
 
// next list elements
/**
if (i == gaps[gap].newLast) {
* Move gaps with ambiguous identical fronts to last newline border or otherwise last word border.
break;
*
* @param[in/out] wikEdDiffText text, textLinked These two are newText and oldText
*/
this.slideGaps = function ( text, textLinked ) {
 
var regExpSlideBorder = this.config.regExp.slideBorder;
var regExpSlideStop = this.config.regExp.slideStop;
 
// Cycle through tokens list
var i = text.first;
var gapStart = null;
while ( i !== null ) {
 
// Remember gap start
if ( gapStart === null && text.tokens[i].link === null ) {
gapStart = i;
}
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
gaps[gap].charSplit = charSplit;
}
 
// Find gap end
//
else if ( gapStart !== null && text.tokens[i].link !== null ) {
// refine words into chars in selected gaps
var gapFront = gapStart;
//
var gapBack = text.tokens[i].prev;
 
// Slide down as deep as possible
for (var gap = 0; gap < gaps.length; gap ++) {
var front = gapFront;
if (gaps[gap].charSplit == true) {
var back = text.tokens[gapBack].next;
if (
front !== null &&
back !== null &&
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;
 
// gapFront cycle trough new= text .tokens list[gapFront].next;
var i gapBack = gapstext.tokens[gapgapBack].newFirstnext;
var j = gaps[gap].oldFirst;
while (i != null) {
var newToken = text.newText.tokens[i].token;
var oldToken = text.oldText.tokens[j].token;
 
// front link identical= text.tokens (spaces)[front].next;
back = text.tokens[back].next;
if (newToken == oldToken) {
text.newText.tokens[i].link = j;
text.oldText.tokens[j].link = i;
}
 
// Test slide up, remember last line break or word border
// refine different words into chars
var front = text.tokens[gapFront].prev;
else {
var back = gapBack;
wDiff.Split(text.newText, wDiff.regExpChar, i);
var gapFrontBlankTest = regExpSlideBorder.test( text.tokens[gapFront].token );
wDiff.Split(text.oldText, wDiff.regExpChar, j);
var frontStop = front;
if ( text.tokens[back].link === null ) {
while (
front !== null &&
back !== null &&
text.tokens[front].link !== null &&
text.tokens[front].token === text.tokens[back].token
) {
if ( front !== null ) {
 
// Stop at line break
if ( regExpSlideStop.test( text.tokens[front].token ) === true ) {
frontStop = front;
break;
}
 
// Stop at first word border (blank/word or word/blank)
if (
regExpSlideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
frontStop = front;
}
}
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
}
 
// nextActually listslide elementsup to stop
ifvar (ifront == gapstext.tokens[gapgapFront].newLast) {prev;
breakvar back = gapBack;
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;
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
i = text.tokens[i].next;
}
return;
}
};
 
// WED('Gap', wDiff.DebugGaps(gaps));
 
/**
return;
* Calculate diff information, can be called repeatedly during refining.
};
* 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 matching tokens
* Pass 4: connect adjacent identical tokens downwards
* Pass 5: connect adjacent identical tokens upwards
* Repeat with empty symbol table (against crossed-over gaps)
* Recursively diff still unresolved regions downwards with empty symbol table
* Recursively diff still unresolved regions upwards with empty symbol table
*
* @param array symbols Symbol table object
* @param string level Split level: 'paragraph', 'line', 'sentence', 'chunk', 'word', 'character'
*
* Optionally for recursive or repeated calls:
* @param bool repeating Currently repeating with empty symbol table
* @param bool recurse Enable recursion
* @param int newStart, newEnd, oldStart, oldEnd Text object tokens indices
* @param int recursionLevel Recursion level
* @param[in/out] WikEdDiffText newText, oldText Text object, tokens list link property
*/
this.calculateDiff = function (
level,
recurse,
repeating,
newStart,
oldStart,
up,
recursionLevel
) {
 
// Set defaults
if ( repeating === undefined ) { repeating = false; }
if ( recurse === undefined ) { recurse = false; }
if ( newStart === undefined ) { newStart = this.newText.first; }
if ( oldStart === undefined ) { oldStart = this.oldText.first; }
if ( up === undefined ) { up = false; }
if ( recursionLevel === undefined ) { recursionLevel = 0; }
 
// Start timers
// wDiff.BubbleUpGaps: move gaps with ambiguous identical fronts and backs up
if ( this.config.timer === true && repeating === false && recursionLevel === 0 ) {
// start ambiguous gap borders after line breaks and text section closing characters
this.time( level );
// changes: text (text.newText or text.oldText) .tokens list
}
// called from: wDiff.Diff()
if ( this.config.timer === true && repeating === false ) {
this.time( level + recursionLevel );
}
 
// Get object symbols table and linked region borders
wDiff.BubbleUpGaps = function(text, textLinked) {
var symbols;
var bordersDown;
var bordersUp;
if ( recursionLevel === 0 && repeating === false ) {
symbols = this.symbols;
bordersDown = this.bordersDown;
bordersUp = this.bordersUp;
}
 
// Create empty local symbols table and linked region borders arrays
// cycle through tokens list
else {
var i = text.first;
symbols = {
var gapStart = null;
token: [],
while ( (i != null) && (text.tokens[i] != null) ) {
hashTable: {},
linked: false
};
bordersDown = [];
bordersUp = [];
}
 
 
// remember gap start
// Updated versions of linked region borders
if ( (gapStart == null) && (text.tokens[i].link == null) ) {
gapStartvar bordersUpNext = i[];
var bordersDownNext = [];
 
/**
* Pass 1: parse new text into symbol table.
*/
 
// Cycle through new text tokens list
var i = newStart;
while ( i !== null ) {
if ( this.newText.tokens[i].link === null ) {
 
// Add new entry to symbol table
var token = this.newText.tokens[i].token;
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
symbols.hashTable[token] = symbols.token.length;
symbols.token.push( {
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
} );
}
 
// Or update existing entry
else {
 
// Increment token counter for new text
var hashToArray = symbols.hashTable[token];
symbols.token[hashToArray].newCount ++;
}
}
 
// Stop after gap if recursing
else if ( recursionLevel > 0 ) {
break;
}
 
// Get next token
if ( up === false ) {
i = this.newText.tokens[i].next;
}
else {
i = this.newText.tokens[i].prev;
}
}
// find gap end
else if ( (gapStart != null) && (text.tokens[i].link != null) ) {
 
/**
// bubble up, stop at line breaks
* Pass 2: parse old text into symbol table.
var front = text.tokens[gapStart].prev;
*/
var back = text.tokens[i].prev;
 
while (
// Cycle through old text tokens list
(front != null) && (back != null) && (wDiff.regExpBubbleStop.test(text.tokens[front].token) == false) &&
var j = oldStart;
(text.tokens[front].link != null) && (text.tokens[back].link == null) &&
while ( j !== null ) {
(text.tokens[front].token == text.tokens[back].token)
if ( this.oldText.tokens[j].link === null ) {
) {
 
text.tokens[back].link = text.tokens[front].link;
// Add new entry to symbol table
textLinked.tokens[ text.tokens[back].link ].link = back;
textvar token = this.oldText.tokens[frontj].link = nulltoken;
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
front = text.tokens[front].prev;
symbols.hashTable[token] = symbols.token.length;
back = text.tokens[back].prev;
symbols.token.push( {
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
} );
}
 
// Or update existing entry
else {
 
// Increment token counter for old text
var hashToArray = symbols.hashTable[token];
symbols.token[hashToArray].oldCount ++;
 
// Add token number for old text
symbols.token[hashToArray].oldToken = j;
}
}
 
// Stop after gap if recursing
// do not start gap with spaces or other closing characters, roll back (bubble down)
else if ( (back != null) && (frontrecursionLevel !=> null)0 ) {
break;
front = text.tokens[front].next;
back = text.tokens[back].next;
}
 
while (
// Get next token
(back != null) && (front != null) && (wDiff.regExpBubbleClosing.test(text.tokens[front].token) == true) &&
if ( up === false ) {
(text.tokens[front].link == null) && (text.tokens[back].link != null) &&
(text.tokens[front].tokenj == textthis.oldText.tokens[backj].token)next;
) {}
else {
text.tokens[front].link = text.tokens[back].link;
textLinked.tokens[j text= this.oldText.tokens[frontj].link ].link = frontprev;
text.tokens[back].link = null;
front = text.tokens[front].next;
back = text.tokens[back].next;
}
gapStart = null;
}
i = text.tokens[i].next;
}
return;
};
 
/**
* Pass 3: connect unique tokens.
*/
 
// Cycle through symbol array
// wDiff.EnumerateTokens: enumerate text token list
var symbolsLength = symbols.token.length;
// changes: text (text.newText or text.oldText) .tokens list
for ( var i = 0; i < symbolsLength; i ++ ) {
// called from: wDiff.Diff()
 
// Find tokens in the symbol table that occur only once in both versions
wDiff.EnumerateTokens = function(text) {
if ( symbols.token[i].newCount === 1 && symbols.token[i].oldCount === 1 ) {
var newToken = symbols.token[i].newToken;
var oldToken = symbols.token[i].oldToken;
var newTokenObj = this.newText.tokens[newToken];
var oldTokenObj = this.oldText.tokens[oldToken];
 
// Connect from new to old and from old to new
// enumerate tokens list
if ( newTokenObj.link === null ) {
var number = 0;
var i = text.first;
while ( (i != null) && (text.tokens[i] != null) ) {
text.tokens[i].number = number;
number ++;
i = text.tokens[i].next;
}
return;
};
 
// Do not use spaces as unique markers
if (
this.config.regExp.blankOnlyToken.test( newTokenObj.token ) === true
) {
 
// Link new and old tokens
// wDiff.CalculateDiff: calculate diff information, can be called repeatedly during refining
newTokenObj.link = oldToken;
// input: text, object containing text data and tokens
oldTokenObj.link = newToken;
// optionally for recursive calls: newStart, newEnd, oldStart, oldEnd (tokens list indexes), recursionLevel
symbols.linked = true;
// 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 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
 
// Save linked region borders
wDiff.CalculateDiff = function(text, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
bordersDown.push( [newToken, oldToken] );
bordersUp.push( [newToken, oldToken] );
 
// symbolCheck if (token) datacontains unique word
if ( recursionLevel === 0 ) {
var symbol = [];
var symbolsunique = {}false;
if ( level === 'character' ) {
unique = true;
}
else {
var token = newTokenObj.token;
var words =
( token.match( this.config.regExp.countWords ) || [] ).concat(
( token.match( this.config.regExp.countChunks ) || [] )
);
 
// Unique if longer than min block length
// set defaults
var wordsLength = words.length;
if (typeof newStart == 'undefined') { newStart = text.newText.first; }
if ( wordsLength >= this.config.blockMinLength ) {
if (typeof newEnd == 'undefined') { newEnd = text.newText.last; }
unique = true;
if (typeof oldStart == 'undefined') { oldStart = text.oldText.first; }
}
if (typeof oldEnd == 'undefined') { oldEnd = text.oldText.last; }
if (typeof recursionLevel == 'undefined') { recursionLevel = 0; }
 
// Unique if it contains at least one unique word
// limit recursion depth
else {
if (recursionLevel > 10) {
for ( var i = 0;i < wordsLength; i ++ ) {
return;
var word = words[i];
}
if (
this.oldText.words[word] === 1 &&
this.newText.words[word] === 1 &&
Object.prototype.hasOwnProperty.call( this.oldText.words, word ) === true &&
Object.prototype.hasOwnProperty.call( this.newText.words, word ) === true
) {
unique = true;
break;
}
}
}
}
 
// Set unique
//
if ( unique === true ) {
// pass 1: parse new text into symbol table
newTokenObj.unique = true;
//
oldTokenObj.unique = true;
}
}
}
}
}
}
 
// Continue passes only if unique tokens have been linked previously
// cycle trough new text tokens list
if ( symbols.linked === true ) {
var i = newStart;
while ( (i != null) && (text.newText.tokens[i] != null) ) {
 
/**
// parse token only once during split refinement
* Pass 4: connect adjacent identical tokens downwards.
if ( (text.newText.tokens[i].parsed == false) || (recursionLevel > 0) ) {
*/
text.newText.tokens[i].parsed = true;
 
// addCycle newthrough entrylist toof linked new symboltext tabletokens
var tokenbordersLength = textbordersDown.newText.tokens[i].tokenlength;
for ( var match = 0; match < bordersLength; match ++ ) {
if (Object.prototype.hasOwnProperty.call(symbols, token) == false) {
var currenti = symbol.lengthbordersDown[match][0];
var j = bordersDown[match][1];
symbols[token] = current;
 
symbol[current] = {
newCount:// 1,Next down
var iMatch = i;
oldCount: 0,
var jMatch = j;
newToken: i,
i = this.newText.tokens[i].next;
oldToken: null
j = this.oldText.tokens[j].next;
};
 
// Cycle through new text list gap region downwards
while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
 
// Connect if same token
if ( this.newText.tokens[i].token === this.oldText.tokens[j].token ) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
 
// Not a match yet, maybe in next refinement level
else {
bordersDownNext.push( [iMatch, jMatch] );
break;
}
 
// Next token down
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
 
/**
// or update existing entry
* Pass 5: connect adjacent identical tokens upwards.
else {
*/
 
// incrementCycle tokenthrough counterlist forof connected new text tokens
var hashToArraybordersLength = symbols[token]bordersUp.length;
for ( var match = 0; match < bordersLength; match ++ ) {
symbol[hashToArray].newCount ++;
var i = bordersUp[match][0];
var j = bordersUp[match][1];
 
// Next up
var iMatch = i;
var jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
 
// Cycle through new text gap region upwards
while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
 
// Connect if same token
if ( this.newText.tokens[i].token === this.oldText.tokens[j].token ) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
 
// Not a match yet, maybe in next refinement level
else {
bordersUpNext.push( [iMatch, jMatch] );
break;
}
 
// Next token up
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
}
}
 
/**
// next list element
* Connect adjacent identical tokens downwards from text start.
if (i == newEnd) {
* Treat boundary as connected, stop after first connected token.
break;
} */
i = text.newText.tokens[i].next;
}
 
// Only for full text diff
//
if ( recursionLevel === 0 && repeating === false ) {
// pass 2: parse old text into symbol table
//
 
// From start
// cycle trough old text tokens list
var ji = oldStartthis.newText.first;
while var ( (j != null) && (textthis.oldText.tokens[j] != null) ) {first;
var iMatch = null;
var jMatch = null;
 
// Cycle through old text tokens down
// parse token only once during split refinement
// Connect identical tokens, stop after first connected token
if ( (text.oldText.tokens[j].parsed == false) || (recursionLevel > 0) ) {
while (
text.oldText.tokens[j].parsed = true;
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
if ( iMatch !== null ) {
bordersDownNext.push( [iMatch, jMatch] );
}
 
// From end
// add new entry to symbol table
var token i = text.oldTextthis.tokens[j]newText.tokenlast;
j = this.oldText.last;
if (Object.prototype.hasOwnProperty.call(symbols, token) == false) {
var currentiMatch = symbol.lengthnull;
symbols[token]jMatch = currentnull;
 
symbol[current] = {
// Cycle through old text tokens up
newCount: 0,
// Connect identical tokens, stop after first connected token
oldCount: 1,
newToken:while null,(
oldToken:i j!== null &&
}; j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
if ( iMatch !== null ) {
bordersUpNext.push( [iMatch, jMatch] );
}
}
 
// Save updated linked region borders to object
// or update existing entry
if ( recursionLevel === 0 && repeating === false ) {
this.bordersDown = bordersDownNext;
this.bordersUp = bordersUpNext;
}
 
// Merge local updated linked region borders into object
else {
this.bordersDown = this.bordersDown.concat( bordersDownNext );
this.bordersUp = this.bordersUp.concat( bordersUpNext );
}
 
// increment token counter for old text
var hashToArray = symbols[token];
symbol[hashToArray].oldCount ++;
 
/**
// add token number for old text
* Repeat once with empty symbol table to link hidden unresolved common tokens in cross-overs.
symbol[hashToArray].oldToken = j;
* ("and" in "and this a and b that" -> "and this a and b that")
*/
 
if ( repeating === false && this.config.repeatedDiff === true ) {
var repeat = true;
this.calculateDiff( level, recurse, repeat, newStart, oldStart, up, recursionLevel );
}
 
/**
* Refine by recursively diffing not linked regions with new symbol table.
* At word and character level only.
* Helps against gaps caused by addition of common tokens around sequences of common tokens.
*/
 
if (
recurse === true &&
this.config['recursiveDiff'] === true &&
recursionLevel < this.config.recursionMax
) {
 
/**
* Recursively diff gap downwards.
*/
 
// Cycle through list of linked region borders
var bordersLength = bordersDownNext.length;
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersDownNext[match][0];
var j = bordersDownNext[match][1];
 
// Next token down
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
 
// Start recursion at first gap token pair
if (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
var repeat = false;
var dirUp = false;
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
}
}
 
/**
* Recursively diff gap upwards.
*/
 
// Cycle through list of linked region borders
var bordersLength = bordersUpNext.length;
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersUpNext[match][0];
var j = bordersUpNext[match][1];
 
// Next token up
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
 
// Start recursion at first gap token pair
if (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
var repeat = false;
var dirUp = true;
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
}
}
}
}
 
// nextStop list elementtimers
if ( this.config.timer === true && repeating === false ) {
if (j == oldEnd) {
if ( this.recursionTimer[recursionLevel] === undefined ) {
break;
this.recursionTimer[recursionLevel] = 0;
}
this.recursionTimer[recursionLevel] += this.timeEnd( level + recursionLevel, true );
}
if ( this.config.timer === true && repeating === false && recursionLevel === 0 ) {
this.timeRecursionEnd( level );
this.timeEnd( level );
}
j = text.oldText.tokens[j].next;
}
 
return;
//
};
// pass 3: connect unique tokens
//
 
// cycle trough symbol array
for (var i = 0; i < symbol.length; i ++) {
 
/**
// find tokens in the symbol table that occur only once in both versions
* Main method for processing raw diff data, extracting deleted, inserted, and moved blocks.
if ( (symbol[i].newCount == 1) && (symbol[i].oldCount == 1) ) {
*
var newToken = symbol[i].newToken;
* Scheme of blocks, sections, and groups (old block numbers):
var oldToken = symbol[i].oldToken;
* 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: = . =-= = -= = . = = = = =
*
* @param[out] array groups Groups table object
* @param[out] array blocks Blocks table object
* @param[in/out] WikEdDiffText newText, oldText Text object tokens list
*/
this.detectBlocks = function () {
 
// Debug log
// do not use spaces as unique markers
if (/^\s+$/ this.test(textconfig.newText.tokens[newToken].token)debug === true false) {
this.oldText.debugText( 'Old text' );
this.newText.debugText( 'New text' );
}
 
// connectCollect fromidentical newcorresponding to('=') old andblocks from old totext and sort by new text
this.getSameBlocks();
if (text.newText.tokens[newToken].link == null) {
 
text.newText.tokens[newToken].link = oldToken;
// Collect independent block sections with no block move crosses outside a section
text.oldText.tokens[oldToken].link = newToken;
this.getSections();
 
// Find groups of continuous old text blocks
this.getGroups();
 
// Set longest sequence of increasing groups in sections as fixed (not moved)
this.setFixed();
 
// Convert groups to insertions/deletions if maximum block length is too short
// Only for more complex texts that actually have blocks of minimum block length
var unlinkCount = 0;
if (
this.config.unlinkBlocks === true &&
this.config.blockMinLength > 0 &&
this.maxWords >= this.config.blockMinLength
) {
if ( this.config.timer === true ) {
this.time( 'total unlinking' );
}
 
// Repeat as long as unlinking is possible
var unlinked = true;
while ( unlinked === true && unlinkCount < this.config.unlinkMax ) {
 
// Convert '=' to '+'/'-' pairs
unlinked = this.unlinkBlocks();
 
// Start over after conversion
if ( unlinked === true ) {
unlinkCount ++;
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
 
// Repeat block detection from start
this.maxWords = 0;
this.getSameBlocks();
this.getSections();
this.getGroups();
this.setFixed();
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'total unlinking' );
}
}
}
 
// Collect deletion ('-') blocks from old text
//
this.getDelBlocks();
// pass 4: connect adjacent identical tokens downwards
//
 
// cyclePosition trough'-' blocks into new text tokens listorder
this.positionDelBlocks();
var i = text.newText.first;
while ( (i != null) && (text.newText.tokens[i] != null) ) {
var iNext = text.newText.tokens[i].next;
 
// Collect insertion ('+') blocks from new text
// find already connected pairs
this.getInsBlocks();
var j = text.newText.tokens[i].link;
if (j != null) {
var jNext = text.oldText.tokens[j].next;
 
// Set group numbers of '+' blocks
// check if the following tokens are not yet connected
this.setInsGroups();
if ( (iNext != null) && (jNext != null) ) {
if ( (text.newText.tokens[iNext].link == null) && (text.oldText.tokens[jNext].link == null) ) {
 
// Mark original positions of moved groups
// connect if the following tokens are the same
this.insertMarks();
if (text.newText.tokens[iNext].token == text.oldText.tokens[jNext].token) {
 
text.newText.tokens[iNext].link = jNext;
// Debug log
text.oldText.tokens[jNext].link = iNext;
if ( this.config.timer === true || this.config.debug === true ) {
console.log( 'Unlink count: ', unlinkCount );
}
if ( this.config.debug === true ) {
this.debugGroups( 'Groups' );
this.debugBlocks( 'Blocks' );
}
return;
};
 
 
/**
* Collect identical corresponding matching ('=') blocks from old text and sort by new text.
*
* @param[in] WikEdDiffText newText, oldText Text objects
* @param[in/out] array blocks Blocks table object
*/
this.getSameBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'getSameBlocks' );
}
 
var blocks = this.blocks;
 
// Clear blocks array
blocks.splice( 0 );
 
// Cycle through old text to find connected (linked, matched) blocks
var j = this.oldText.first;
var i = null;
while ( j !== null ) {
 
// Skip '-' blocks
while ( j !== null && this.oldText.tokens[j].link === null ) {
j = this.oldText.tokens[j].next;
}
 
// Get '=' block
if ( j !== null ) {
i = this.oldText.tokens[j].link;
var iStart = i;
var jStart = j;
 
// Detect matching blocks ('=')
var count = 0;
var unique = false;
var text = '';
while ( i !== null && j !== null && this.oldText.tokens[j].link === i ) {
text += this.oldText.tokens[j].token;
count ++;
if ( this.newText.tokens[i].unique === true ) {
unique = true;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
 
// Save old text '=' 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( text ),
chars: text.length,
type: '=',
section: null,
group: null,
fixed: null,
moved: null,
text: text
} );
}
}
i = iNext;
}
 
// Sort blocks by new text token number
//
blocks.sort( function( a, b ) {
// pass 5: connect adjacent identical tokens upwards
return a.newNumber - b.newNumber;
//
} );
 
// cycleNumber troughblocks in new text tokens listorder
var iblocksLength = textblocks.newText.lastlength;
for ( var block = 0; block < blocksLength; block ++ ) {
while ( (i != null) && (text.newText.tokens[i] != null) ) {
blocks[block].newBlock = block;
var iNext = text.newText.tokens[i].prev;
}
 
if ( this.config.timer === true ) {
// find already connected pairs
this.timeEnd( 'getSameBlocks' );
var j = text.newText.tokens[i].link;
}
if (j != null) {
return;
var jNext = text.oldText.tokens[j].prev;
};
 
// check if the preceeding tokens are not yet connected
if ( (iNext != null) && (jNext != null) ) {
if ( (text.newText.tokens[iNext].link == null) && (text.oldText.tokens[jNext].link == null) ) {
 
/**
// connect if the preceeding tokens are the same
* Collect independent block sections with no block move crosses
if (text.newText.tokens[iNext].token == text.oldText.tokens[jNext].token) {
* outside a section for per-section determination of non-moving fixed groups.
text.newText.tokens[iNext].link = jNext;
*
text.oldText.tokens[jNext].link = iNext;
* @param[out] array sections Sections table object
}
* @param[in/out] array blocks Blocks table object, section property
*/
this.getSections = function () {
 
if ( this.config.timer === true ) {
this.time( 'getSections' );
}
 
var blocks = this.blocks;
var sections = this.sections;
 
// Clear sections array
sections.splice( 0 );
 
// Cycle through blocks
var blocksLength = blocks.length;
for ( var block = 0; block < blocksLength; block ++ ) {
 
var sectionStart = block;
var sectionEnd = block;
 
var oldMax = blocks[sectionStart].oldNumber;
var sectionOldMax = oldMax;
 
// Check right
for ( var j = sectionStart + 1; j < blocksLength; j ++ ) {
 
// Check for crossing over to the left
if ( blocks[j].oldNumber > oldMax ) {
oldMax = blocks[j].oldNumber;
}
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;
}
}
if ( this.config.timer === true ) {
i = iNext;
this.timeEnd( 'getSections' );
}
}
return;
};
 
// refine by recursively diffing unresolved regions caused by addition of common tokens around sequences of common tokens, only at word level split
if ( (recurse == true) && (wDiff.recursiveDiff == true) ) {
 
/**
//
* Find groups of continuous old text blocks.
// recursively diff still unresolved regions downwards
// *
* @param[out] array groups Groups table object
* @param[in/out] array blocks Blocks table object, group property
*/
this.getGroups = function () {
 
if ( this.config.timer === true ) {
// cycle trough new text tokens list
this.time( 'getGroups' );
var i = newStart;
}
var j = oldStart;
 
var blocks = this.blocks;
while ( (i != null) && (text.newText.tokens[i] != null) ) {
var groups = this.groups;
 
// Clear groups array
// get j from previous tokens match
groups.splice( 0 );
var iPrev = text.newText.tokens[i].prev;
 
if (iPrev != null) {
// Cycle through blocks
var jPrev = text.newText.tokens[iPrev].link;
var blocksLength = blocks.length;
if (jPrev != null) {
for ( var block = 0; block < blocksLength; block ++ ) {
j = text.oldText.tokens[jPrev].next;
var groupStart = block;
var groupEnd = block;
var oldBlock = blocks[groupStart].oldBlock;
 
// Get word and char count of block
var words = this.wordCount( blocks[block].text );
var maxWords = words;
var unique = blocks[block].unique;
var chars = blocks[block].chars;
 
// Check right
for ( var i = groupEnd + 1; i < blocksLength; i ++ ) {
 
// Check for crossing over to the left
if ( blocks[i].oldBlock !== oldBlock + 1 ) {
break;
}
oldBlock = blocks[i].oldBlock;
 
// 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;
}
 
// Save crossing group
// check for the start of an unresolved sequence
if ( groupEnd >= groupStart ) {
if ( (j != null) && (text.oldText.tokens[j] != null) && (text.newText.tokens[i].link == null) && (text.oldText.tokens[j].link == null) ) {
 
// Set groups outside sections as fixed
// determine the limits of of the unresolved new sequence
var iStartfixed = ifalse;
varif iEnd( blocks[groupStart].section === null; ) {
var iLength fixed = 0true;
var iNext = i;
while ( (iNext != null) && (text.newText.tokens[iNext].link == null) ) {
iEnd = iNext;
iLength ++;
if (iEnd == newEnd) {
break;
}
iNext = text.newText.tokens[iNext].next;
}
 
// Save group to block
// determine the limits of of the unresolved old sequence
for ( var jStarti = jgroupStart; i <= groupEnd; i ++ ) {
var jEnd blocks[i].group = nullgroups.length;
var jLength blocks[i].fixed = 0fixed;
var jNext = j;
while ( (jNext != null) && (text.oldText.tokens[jNext].link == null) ) {
jEnd = jNext;
jLength ++;
if (jEnd == oldEnd) {
break;
}
jNext = text.oldText.tokens[jNext].next;
}
 
// Save group
// recursively diff the unresolved sequence
groups.push( {
if ( (iLength > 0) && (jLength > 0) ) {
oldNumber: blocks[groupStart].oldNumber,
if ( (iLength > 1) || (jLength > 1) ) {
blockStart: groupStart,
if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) {
blockEnd: groupEnd,
wDiff.CalculateDiff(text, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
}unique: unique,
maxWords: maxWords,
}
words: words,
chars: chars,
fixed: fixed,
movedFrom: null,
color: null
} );
block = groupEnd;
 
// Set global word count of longest linked block
if ( maxWords > this.maxWords ) {
this.maxWords = maxWords;
}
i = iEnd;
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getGroups' );
}
return;
};
 
 
// next list element
/**
if (i == newEnd) {
* Set longest sequence of increasing groups in sections as fixed (not moved).
break;
} *
* @param[in] array sections Sections table object
i = text.newText.tokens[i].next;
* @param[in/out] array groups Groups table object, fixed property
* @param[in/out] array blocks Blocks table object, fixed property
*/
this.setFixed = function () {
 
if ( this.config.timer === true ) {
this.time( 'setFixed' );
}
 
var blocks = this.blocks;
//
var groups = this.groups;
// recursively diff still unresolved regions upwards
var sections = this.sections;
//
 
// Cycle through sections
// cycle trough new text tokens list
var isectionsLength = newEndsections.length;
for ( var section = 0; section < sectionsLength; section ++ ) {
var j = oldEnd;
var blockStart = sections[section].blockStart;
while ( (i != null) && (text.newText.tokens[i] != null) ) {
var blockEnd = sections[section].blockEnd;
 
var groupStart = blocks[blockStart].group;
// get j from next matched tokens
var iPrevgroupEnd = text.newText.tokensblocks[iblockEnd].nextgroup;
 
if (iPrev != null) {
// Recusively find path of groups in increasing old group order with longest char length
var jPrev = text.newText.tokens[iPrev].link;
ifvar (jPrevcache != null) {[];
var maxChars = 0;
j = text.oldText.tokens[jPrev].prev;
var maxPath = null;
 
// Start at each group of section
for ( var i = groupStart; i <= groupEnd; i ++ ) {
var pathObj = this.findMaxPath( i, groupEnd, cache );
if ( pathObj.chars > maxChars ) {
maxPath = pathObj.path;
maxChars = pathObj.chars;
}
}
 
// Mark fixed groups
// check for the start of an unresolved sequence
var maxPathLength = maxPath.length;
if ( (j != null) && (text.oldText.tokens[j] != null) && (text.newText.tokens[i].link == null) && (text.oldText.tokens[j].link == null) ) {
for ( var i = 0; i < maxPathLength; i ++ ) {
var group = maxPath[i];
groups[group].fixed = true;
 
// Mark fixed blocks
// determine the limits of of the unresolved new sequence
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
var iStart = null;
var iEnd blocks[block].fixed = itrue;
}
var iLength = 0;
}
var iNext = i;
}
while ( (iNext != null) && (text.newText.tokens[iNext].link == null) ) {
if ( this.config.timer === true ) {
iStart = iNext;
this.timeEnd( 'setFixed' );
iLength ++;
}
if (iStart == newStart) {
breakreturn;
};
 
 
/**
* Recusively find path of groups in increasing old group order with longest char length.
*
* @param int start Path start group
* @param int groupEnd Path last group
* @param array cache Cache object, contains returnObj for start
* @return array returnObj Contains path and char length
*/
this.findMaxPath = function ( start, groupEnd, cache ) {
 
var groups = this.groups;
 
// Find longest sub-path
var maxChars = 0;
var oldNumber = groups[start].oldNumber;
var returnObj = { path: [], chars: 0};
for ( var i = start + 1; i <= groupEnd; i ++ ) {
 
// Only in increasing old group order
if ( groups[i].oldNumber < oldNumber ) {
continue;
}
 
// Get longest sub-path from cache (deep copy)
var pathObj;
if ( cache[i] !== undefined ) {
pathObj = { path: cache[i].path.slice(), chars: cache[i].chars };
}
 
// Get longest sub-path by recursion
else {
pathObj = this.findMaxPath( i, groupEnd, cache );
}
 
// Select longest sub-path
if ( pathObj.chars > maxChars ) {
maxChars = pathObj.chars;
returnObj = pathObj;
}
}
 
// Add current start to path
returnObj.path.unshift( start );
returnObj.chars += groups[start].chars;
 
// Save path to cache (deep copy)
if ( cache[start] === undefined ) {
cache[start] = { path: returnObj.path.slice(), chars: returnObj.chars };
}
 
return returnObj;
};
 
 
/**
* Convert matching '=' blocks in groups into insertion/deletion ('+'/'-') pairs
* if too short and too common.
* Prevents fragmentated diffs for very different versions.
*
* @param[in] array blocks Blocks table object
* @param[in/out] WikEdDiffText newText, oldText Text object, linked property
* @param[in/out] array groups Groups table object
* @return bool True if text tokens were unlinked
*/
this.unlinkBlocks = function () {
 
var blocks = this.blocks;
var groups = this.groups;
 
// Cycle through groups
var unlinked = false;
var groupsLength = groups.length;
for ( var group = 0; group < groupsLength; group ++ ) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
 
// Unlink whole group if no block is at least blockMinLength words long and unique
if ( groups[group].maxWords < this.config.blockMinLength && groups[group].unique === false ) {
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === '=' ) {
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
iNext = text.newText.tokens[iNext].prev;
}
}
 
// Otherwise unlink block flanks
// determine the limits of of the unresolved old sequence
else {
var jStart = null;
 
var jEnd = j;
var// jLengthUnlink =blocks 0;from start
for ( var block = blockStart; block <= blockEnd; block ++ ) {
var jNext = j;
while if ( (jNext != null) && (text.oldText.tokensblocks[jNextblock].linktype === null)'=' ) {
 
jStart = jNext;
// Stop unlinking if more than one word or a unique word
jLength ++;
if (jStart blocks[block].words > 1 || blocks[block].unique === true oldStart) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
blockStart = block;
}
jNext = text.oldText.tokens[jNext].prev;
}
 
// Unlink blocks from end
// recursively diff the unresolved sequence
iffor ( (iLengthvar >block 0)= &&blockEnd; (jLengthblock > 0)blockStart; block -- ) {
if ( (iLengthblocks[block].type >=== 1) || (jLength > 1)'=' ) {
 
if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) {
// Stop unlinking if more than one word or a unique word
wDiff.CalculateDiff(text, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
if (
blocks[block].words > 1 ||
( blocks[block].words === 1 && blocks[block].unique === true )
) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
i = iStart;
}
 
// next list element
if (i == newStart) {
break;
}
i = text.newText.tokens[i].prev;
}
return unlinked;
}
return};
};
 
 
/**
// wDiff.DetectBlocks: extract block data for inserted, deleted, or moved blocks from diff data in text object
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs.
// input:
*
// text: object containing text tokens list
// * @param[in] array blocks: empty array forBlocks blocktable dataobject
* @param[out] WikEdDiffText newText, oldText Text objects, link property
// groups: empty array for group data
*/
// changes: blocks, groups
this.unlinkSingleBlock = function ( block ) {
// called from: wDiff.Diff()
// steps:
// collect identical corresponding ('same') blocks from old text
// sort blocks by new text token number
// collect groups of continuous old text blocks
// collect independent block sections (no old/new crosses outside section)
// find groups of continuous old text blocks
// set longest sequence of increasing groups in sections as fixed (not moved)
// collect insertion ('ins') blocks from new text
// collect deletion ('del') blocks from old text
// position 'del' blocks into new text order
// re-sort blocks by new text token number and update groups
// set group numbers of 'ins' and 'del' blocks inside existing groups
// add remaining 'ins' and 'del' blocks to groups
// mark original positions of moved groups
// set moved block colors
//
// 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: = + =-= = -= = + = = = = =
 
// Cycle through old text
wDiff.DetectBlocks = function(text, blocks, groups) {
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
 
// Unlink tokens
// WED('text.oldText', wDiff.DebugText(text.oldText));
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
// WED('text.newText', wDiff.DebugText(text.newText));
this.oldText.tokens[j].link = null;
j = this.oldText.tokens[j].next;
}
return;
};
 
//
// collect identical corresponding ('same') blocks from old text
//
 
/**
// cycle through old text to find matched (linked) blocks
* Collect deletion ('-') blocks from old text.
var j = text.oldText.first;
*
var i = null;
* @param[in] WikEdDiffText oldText Old Text object
var deletions = [];
* @param[out] array blocks Blocks table object
while (j != null) {
*/
this.getDelBlocks = function () {
 
if ( this.config.timer === true ) {
// detect 'del' blocks and remember for later
this.time( 'getDelBlocks' );
var delStart = j;
var delEnd = null;
var string = '';
while ( (j != null) && (text.oldText.tokens[j].link == null) ) {
string += text.oldText.tokens[j].token;
delEnd = j;
j = text.oldText.tokens[j].next;
}
 
var blocks = this.blocks;
// save old text 'del' block data
if (delEnd != null) {
deletions.push({
oldStart: delStart,
oldBlock: blocks.length,
string: string
});
}
 
// Cycle through old text to find connected (linked, matched) blocks
// get 'same' block
ifvar (j != null) {this.oldText.first;
var i = null;
i = text.oldText.tokens[j].link;
while ( j !== null ) {
var iStart = i;
var jStart = j;
 
// detectCollect matching'-' blocks ('same')
var charsoldStart = 0j;
var stringcount = ''0;
var text = '';
while ( (i != null) && (j != null) && (text.oldText.tokens[j].link == i) ) {
varwhile token( j !== textnull && this.oldText.tokens[j].token;link === null ) {
charscount += token.length+;
stringtext += this.oldText.tokens[j].token;
ij = textthis.newTextoldText.tokens[ij].next;
j = text.oldText.tokens[j].next;
}
 
// saveSave old text 'same-' block
if ( count !== 0 ) {
blocks.push({
oldBlock: blocks.length,push( {
oldBlock: null,
oldNumber: text.oldText.tokens[jStart].number,
newBlock: null,
newNumber: text.newText.tokens[iStart].number,
oldNumber: this.oldText.tokens[oldStart].number,
chars: chars,
type newNumber: 'same'null,
section oldStart: nulloldStart,
group count: nullcount,
fixed unique: nullfalse,
string words: string null,
chars: text.length,
});
type: '-',
section: null,
group: null,
fixed: null,
moved: null,
text: text
} );
}
 
// Skip '=' 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;
}
}
}
if ( this.config.timer === true ) {
}
this.timeEnd( 'getDelBlocks' );
}
return;
};
 
//
// sort blocks by new text token number
//
 
/**
blocks.sort(function(a, b) {
* Position deletion '-' blocks into new text order.
return a.newNumber - b.newNumber;
* Deletion blocks move with fixed reference:
});
* Old: 1 D 2 1 D 2
* / \ / \ \
* New: 1 D 2 1 D 2
* Fixed: * *
* newNumber: 1 1 2 2
*
* Marks '|' and deletions '-' get newNumber of reference block
* and are sorted around it by old text number.
*
* @param[in/out] array blocks Blocks table, newNumber, section, group, and fixed properties
*
*/
this.positionDelBlocks = function () {
 
if ( this.config.timer === true ) {
//
this.time( 'positionDelBlocks' );
// collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
}
//
 
var sectionsblocks = []this.blocks;
var groups = this.groups;
 
// cycleSort throughshallow copy of blocks by oldNumber
var blocksOld = blocks.slice();
var nextSectionStart = 0;
blocksOld.sort( function( a, b ) {
for (var block = 0; block < blocks.length; block ++) {
return a.oldNumber - b.oldNumber;
} );
 
// Cycle through blocks in old text order
var sectionStart = block;
var sectionEndblocksOldLength = blockblocksOld.length;
for ( var block = 0; block < blocksOldLength; block ++ ) {
var delBlock = blocksOld[block];
 
// '-' block only
var oldMax = blocks[sectionStart].oldNumber;
if ( delBlock.type !== '-' ) {
var sectionOldMax = oldMax;
continue;
}
 
// Find fixed '=' reference block from original block position to position '-' block
// check right
// Similar to position marks '|' code
for (var j = sectionStart + 1; j < blocks.length; j ++) {
 
// checkGet forold crossingtext overprev to the leftblock
var prevBlockNumber = null;
if (blocks[j].oldNumber > oldMax) {
var prevBlock = null;
oldMax = blocks[j].oldNumber;
if ( block > 0 ) {
prevBlockNumber = blocksOld[block - 1].newBlock;
prevBlock = blocks[prevBlockNumber];
}
 
else if (blocks[j].oldNumber < sectionOldMax) {
// Get old text next block
sectionEnd = j;
sectionOldMaxvar nextBlockNumber = oldMaxnull;
var nextBlock = null;
if ( block < blocksOld.length - 1 ) {
nextBlockNumber = blocksOld[block + 1].newBlock;
nextBlock = blocks[nextBlockNumber];
}
}
 
// Move after prev block if fixed
// save crossing sections
var refBlock = null;
if (sectionEnd > sectionStart) {
if ( prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
refBlock = prevBlock;
}
 
// saveMove sectionbefore tonext block if fixed
else if ( nextBlock !== null && nextBlock.type === '=' && nextBlock.fixed === true ) {
for (var i = sectionStart; i <= sectionEnd; i ++) {
refBlock = nextBlock;
blocks[i].section = sections.length;
}
 
// Move after prev block if not start of group
// save section
sections.pushelse if ({
prevBlock !== null &&
blockStart: sectionStart,
prevBlock.type === '=' &&
blockEnd: sectionEnd,
prevBlockNumber !== groups[ prevBlock.group ].blockEnd
});
) {
block = sectionEnd;
refBlock = prevBlock;
}
 
// Move before next block if not start of group
else if (
nextBlock !== null &&
nextBlock.type === '=' &&
nextBlockNumber !== groups[ nextBlock.group ].blockStart
) {
refBlock = nextBlock;
}
 
// Move after closest previous fixed block
else {
for ( var fixed = block; fixed >= 0; fixed -- ) {
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
refBlock = blocksOld[fixed];
break;
}
}
}
 
// Move before first block
if ( refBlock === null ) {
delBlock.newNumber = -1;
}
 
// Update '-' block data
else {
delBlock.newNumber = refBlock.newNumber;
delBlock.section = refBlock.section;
delBlock.group = refBlock.group;
delBlock.fixed = refBlock.fixed;
}
}
}
 
// Sort '-' blocks in and update groups
//
this.sortBlocks();
// find groups of continuous old text blocks
//
 
if ( this.config.timer === true ) {
var regExpWordCount = new RegExp('(^|[^' + wDiff.letters + '])[' + wDiff.letters + '][' + wDiff.letters + '_\'’]*', 'g');
this.timeEnd( 'positionDelBlocks' );
}
return;
};
 
// cycle through blocks
for (var block = 0; block < blocks.length; block ++) {
var groupStart = null;
var groupEnd = null;
 
/**
// get word and char count of block
* Collect insertion ('+') blocks from new text.
var words = (blocks[block].string.match(regExpWordCount) || []).length;
*
var maxWords = words;
* @param[in] WikEdDiffText newText New Text object
var chars = blocks[block].chars;
* @param[out] array blocks Blocks table object
*/
this.getInsBlocks = function () {
 
if ( this.config.timer === true ) {
groupStart = block;
this.time( 'getInsBlocks' );
groupEnd = block;
}
var oldBlock = blocks[groupStart].oldBlock;
 
var blocks = this.blocks;
// check right
for (var i = groupEnd + 1; i < blocks.length; i ++) {
 
// checkCycle forthrough crossingnew overtext to thefind leftinsertion blocks
var i = this.newText.first;
if (blocks[i].oldBlock != oldBlock + 1) {
while ( i !== null ) {
break;
 
// Jump over linked (matched) block
while ( i !== null && this.newText.tokens[i].link !== null ) {
i = this.newText.tokens[i].next;
}
oldBlock = blocks[i].oldBlock;
 
// Detect insertion blocks ('+')
// get word and char count of block
if ( i !== null ) {
var blockWords = (blocks[i].string.match(regExpWordCount) || []).length;
var iStart = i;
if (blockWords > maxWords) {
maxWordsvar count = blockWords0;
var text = '';
while ( i !== null && this.newText.tokens[i].link === null ) {
count ++;
text += this.newText.tokens[i].token;
i = this.newText.tokens[i].next;
}
 
// Save new text '+' block
blocks.push( {
oldBlock: null,
newBlock: null,
oldNumber: null,
newNumber: this.newText.tokens[iStart].number,
oldStart: null,
count: count,
unique: false,
words: null,
chars: text.length,
type: '+',
section: null,
group: null,
fixed: null,
moved: null,
text: text
} );
}
}
words += blockWords;
chars += blocks[i].chars;
 
// Sort '+' blocks in and update groups
// skip trailing 'del'
this.sortBlocks();
groupEnd = i;
 
if ( this.config.timer === true ) {
this.timeEnd( 'getInsBlocks' );
}
return;
};
 
// save crossing groups
if ( (groupStart != null) && (groupEnd != null) ) {
 
/**
// set groups outside sections as fixed
* Sort blocks by new text token number and update groups.
var fixed = false;
*
if (blocks[groupStart].section == null) {
* @param[in/out] array groups Groups table object
fixed = true;
* @param[in/out] array blocks Blocks table object
}
*/
this.sortBlocks = function () {
 
var blocks = this.blocks;
// save group to block
var groups = this.groups;
for (var i = groupStart; i <= groupEnd; i ++) {
 
blocks[i].group = groups.length;
// Sort by newNumber, then by old number
blocks[i].fixed = fixed;
blocks.sort( function( a, b ) {
var comp = a.newNumber - b.newNumber;
if ( comp === 0 ) {
comp = a.oldNumber - b.oldNumber;
}
return comp;
} );
 
// Cycle through blocks and update groups with new block numbers
// save group
var group = null;
groups.push({
var blocksLength = blocks.length;
oldNumber: blocks[groupStart].oldNumber,
for ( var block = 0; block < blocksLength; block ++ ) {
blockStart: groupStart,
var blockGroup = blocks[block].group;
blockEnd: groupEnd,
words:if ( blockGroup !== null ) words,{
if ( blockGroup !== group ) {
maxWords: maxWords,
group = blocks[block].group;
chars: chars,
groups[group].blockStart = block;
fixed: fixed,
groups[group].oldNumber = blocks[block].oldNumber;
moved: [],
}
movedFrom: null,
groups[blockGroup].blockEnd = block;
color: null,
}
diff: ''
});
block = groupEnd;
}
return;
}
};
 
//
// set longest sequence of increasing groups in sections as fixed (not moved)
//
 
/**
// cycle through sections
* Set group numbers of insertion '+' blocks.
for (var section = 0; section < sections.length; section ++) {
*
var blockStart = sections[section].blockStart;
* @param[in/out] array groups Groups table object
var blockEnd = sections[section].blockEnd;
* @param[in/out] array blocks Blocks table object, fixed and group properties
*/
this.setInsGroups = function () {
 
if ( this.config.timer === true ) {
var groupStart = blocks[blockStart].group;
this.time( 'setInsGroups' );
var groupEnd = blocks[blockEnd].group;
}
 
var blocks = this.blocks;
// recusively find path of groups in increasing old group order with longest char length
var groups = this.groups;
 
// start at eachSet group numbers of section'+' blocks inside existing groups
var cachegroupsLength = []groups.length;
for ( var group = 0; group < groupsLength; group ++ ) {
var maxChars = 0;
var maxPathfixed = nullgroups[group].fixed;
for ( var iblock = groupStartgroups[group].blockStart; iblock <= groupEndgroups[group].blockEnd; iblock ++) ) {
if ( blocks[block].group === null ) {
var pathObj = wDiff.FindMaxPath(i, [], 0, cache, groups, groupEnd);
blocks[block].group = group;
if (pathObj.chars > maxChars) {
blocks[block].fixed = fixed;
maxPath = pathObj.path;
}
maxChars = pathObj.chars
}
}
 
// markAdd fixedremaining '+' blocks to new groups
for (var i = 0; i < maxPath.length; i ++) {
var group = maxPath[i];
groups[group].fixed = true
 
// markCycle fixedthrough blocks
var blocksLength = blocks.length;
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
for ( var block = 0; block < blocksLength; block ++ ) {
blocks[block].fixed = true;
 
// Skip existing groups
if ( blocks[block].group === null ) {
blocks[block].group = groups.length;
 
// Save new single-block group
groups.push( {
oldNumber: blocks[block].oldNumber,
blockStart: block,
blockEnd: block,
unique: blocks[block].unique,
maxWords: blocks[block].words,
words: blocks[block].words,
chars: blocks[block].chars,
fixed: blocks[block].fixed,
movedFrom: null,
color: null
} );
}
}
if ( this.config.timer === true ) {
}
this.timeEnd( 'setInsGroups' );
}
return;
};
 
//
// collect insertion ('ins') blocks from new text
//
 
/**
// cycle through new text to find insertion blocks
* Mark original positions of moved groups.
var i = text.newText.first;
* Scheme: moved block marks at original positions relative to fixed groups:
while (i != null) {
* 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
*
* Marks '|' and deletions '-' get newNumber of reference block
* and are sorted around it by old text number.
*
* @param[in/out] array groups Groups table object, movedFrom property
* @param[in/out] array blocks Blocks table object
*/
this.insertMarks = function () {
 
if ( this.config.timer === true ) {
// jump over linked (matched) block
this.time( 'insertMarks' );
while ( (i != null) && (text.newText.tokens[i].link != null) ) {
i = text.newText.tokens[i].next;
}
 
//var detectblocks insertion= this.blocks ('ins');
ifvar (igroups != null) {this.groups;
var iStartmoved = i[];
var stringcolor = ''1;
 
while ( (i != null) && (text.newText.tokens[i].link == null) ) {
// Make shallow copy of blocks
string += text.newText.tokens[i].token;
var blocksOld = blocks.slice();
i = text.newText.tokens[i].next;
 
// Enumerate copy
var blocksOldLength = blocksOld.length;
for ( var i = 0; i < blocksOldLength; i ++ ) {
blocksOld[i].number = i;
}
 
// Sort copy by oldNumber
blocksOld.sort( function( a, b ) {
var comp = a.oldNumber - b.oldNumber;
if ( comp === 0 ) {
comp = a.newNumber - b.newNumber;
}
return comp;
} );
 
// Create lookup table: original to sorted
// save new text 'ins' block
var lookupSorted = [];
blocks.push({
for ( var i = 0; i < blocksOldLength; i ++ ) {
oldBlock: null,
lookupSorted[ blocksOld[i].number ] = i;
oldNumber: null,
newNumber: text.newText.tokens[iStart].number,
chars: null,
type: 'ins',
section: null,
group: null,
fixed: null,
string: string
});
}
}
 
// Cycle through groups (moved group)
//
var groupsLength = groups.length;
// collect deletion ('del') blocks from old text
for ( var moved = 0; moved < groupsLength; moved ++ ) {
//
var movedGroup = groups[moved];
if ( movedGroup.fixed !== false ) {
continue;
}
var movedOldNumber = movedGroup.oldNumber;
 
// Find fixed '=' reference block from original block position to position '|' block
// cycle through 'del' blocks and hash oldBlock indexes
// Similar to position deletions '-' code
var oldBlocks = [];
for (var block = 0; block < blocks.length; block ++) {
oldBlocks[ blocks[block].oldBlock ] = block;
}
 
// Get old text prev block
// cycle through deletions detected earlier
var prevBlock = null;
for (var del = 0; del < deletions.length; del ++) {
var block = lookupSorted[ movedGroup.blockStart ];
var newNumber = 0;
if ( block > 0 ) {
var oldBlock = deletions[del].oldBlock;
prevBlock = blocksOld[block - 1];
}
 
// getGet old text next block
var nextBlock = oldBlocks[oldBlock]null;
var block = lookupSorted[ movedGroup.blockEnd ];
if ( block < blocksOld.length - 1 ) {
nextBlock = blocksOld[block + 1];
}
 
// getMove old textafter prev block if fixed
var prevBlockrefBlock = null;
if ( prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
if (oldBlock > 0) {
refBlock = prevBlock;
prevBlock = oldBlocks[oldBlock - 1];
}
 
// Move before next block if fixed
//
else if ( nextBlock !== null && nextBlock.type === '=' && nextBlock.fixed === true ) {
// position 'del' blocks into new text order
refBlock = nextBlock;
//
}
 
// deletionFind blocks move withclosest fixed neighborblock (newto number +/-the 0.3):left
else {
// old: 1 D 2 1 D 2
for ( var fixed = lookupSorted[ movedGroup.blockStart ] - 1; fixed >= 0; fixed -- ) {
// / / \ ‾/-/_
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
// new: 1 D 2 D 2 1
refBlock = blocksOld[fixed];
// fixed: * *
break;
// new number: 1 1.3 1.7 2
}
}
}
 
// Get position of new mark block
// move direction important for general del-ins order
var newNumber;
var markGroup;
 
// moveNo aftersmaller prevfixed block, ifmoved fixedright from before first block
var if neighbor( refBlock === null; ) {
newNumber = -1;
if ( (prevBlock != null) && (blocks[prevBlock].fixed == true) ) {
neighbor markGroup = blocks[prevBlock]groups.length;
newNumber = neighbor.newNumber + 0.3;
}
 
// moveSave before nextnew single-mark-block if fixedgroup
groups.push( {
else if ( (nextBlock != null) && (blocks[nextBlock].fixed == true) ) {
oldNumber: 0,
neighbor = blocks[nextBlock];
blockStart: blocks.length,
newNumber = neighbor.newNumber - 0.3;
blockEnd: blocks.length,
}
unique: false,
maxWords: null,
words: null,
chars: 0,
fixed: null,
movedFrom: null,
color: null
} );
}
else {
newNumber = refBlock.newNumber;
markGroup = refBlock.group;
}
 
// moveInsert after prev'|' block if existent
blocks.push( {
else if (prevBlock != null) {
oldBlock: null,
neighbor = blocks[prevBlock];
newBlock: null,
newNumber = neighbor.newNumber + 0.3;
oldNumber: movedOldNumber,
}
newNumber: newNumber,
oldStart: null,
count: null,
unique: null,
words: null,
chars: 0,
type: '|',
section: null,
group: markGroup,
fixed: true,
moved: moved,
text: ''
} );
 
// moveSet beforegroup next blockcolor
movedGroup.color = color;
else if (nextBlock != null) {
movedGroup.movedFrom = markGroup;
neighbor = blocks[nextBlock];
color ++;
newNumber = neighbor.newNumber - 0.3;
}
 
// Sort '|' blocks in and update groups
// move before first block
this.sortBlocks();
else {
newNumber = -0.3;
}
 
if ( this.config.timer === true ) {
// get neighbor data
this.timeEnd( 'insertMarks' );
var section = null;
var group = null;
var fixed = null;
if (neighbor != null) {
section = neighbor.section;
group = neighbor.group;
fixed = neighbor.fixed;
}
return;
};
 
// save old text 'del' block
blocks.push({
oldBlock: null,
oldNumber: text.oldText.tokens[ deletions[del].oldStart ].number,
newNumber: newNumber,
chars: null,
type: 'del',
section: section,
group: group,
fixed: fixed,
string: deletions[del].string
});
}
 
//**
* Collect diff fragment list for markup, create abstraction layer for customized diffs.
// re-sort blocks by new text token number and update groups
* Adds the following fagment types:
//
* '=', '-', '+' same, deletion, insertion
* '<', '>' mark left, mark right
* '(<', '(>', ')' block start and end
* '[', ']' fragment start and end
* '{', '}' container start and end
*
* @param[in] array groups Groups table object
* @param[in] array blocks Blocks table object
* @param[out] array fragments Fragments array, abstraction layer for diff code
*/
this.getDiffFragments = function () {
 
var blocks = this.blocks;
// sort by newNumber
var groups = this.groups;
blocks.sort(function(a, b) {
var fragments = this.fragments;
return a.newNumber - b.newNumber;
});
 
// cycleMake throughshallow blockscopy and updateof groups withand newsort blockby numbersblockStart
var groupgroupsSort = nullgroups.slice();
groupsSort.sort( function( a, b ) {
for (var block = 0; block < blocks.length; block ++) {
return a.blockStart - b.blockStart;
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
}
}
 
// Cycle through groups
//
var groupsSortLength = groupsSort.length;
// set group numbers of 'ins' and 'del' blocks inside existing groups
for ( var group = 0; group < groupsSortLength; group ++ ) {
//
var blockStart = groupsSort[group].blockStart;
var blockEnd = groupsSort[group].blockEnd;
 
// Add moved block start
for (var group = 0; group < groups.length; group ++) {
var fixedcolor = groupsgroupsSort[group].fixedcolor;
if ( color !== null ) {
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
var type;
if (blocks[block].group == null) {
if ( groupsSort[group].movedFrom < blocks[block blockStart ].group =) group;{
blocks[block].fixed type = fixed'(<';
}
else {
type = '(>';
}
fragments.push( {
text: '',
type: type,
color: color
} );
}
}
}
 
// Cycle through blocks
//
for ( var block = blockStart; block <= blockEnd; block ++ ) {
// add remaining 'ins' and 'del' blocks to groups
var type = blocks[block].type;
//
 
// Add '=' unchanged text and moved block
// cycle through blocks
if ( type === '=' || type === '-' || type === '+' ) {
for (var block = 0; block < blocks.length; block ++) {
fragments.push( {
text: blocks[block].text,
type: type,
color: color
} );
}
 
// skipAdd existing'<' groupsand '>' marks
else if (blocks[block].group type === '|' null) {
var movedGroup = groups[ blocks[block].groupmoved = groups.length];
var fixed = blocks[block].fixed;
 
// saveGet groupmark text
var markText = '';
groups.push({
for (
oldNumber: blocks[block].oldNumber,
var movedBlock = movedGroup.blockStart;
blockStart: block,
movedBlock <= movedGroup.blockEnd;
blockEnd: block,
maxWords: movedBlock null,++
words: ) null,{
if ( blocks[movedBlock].type === '=' || blocks[movedBlock].type === '-' ) {
chars: null,
markText += blocks[movedBlock].text;
fixed: fixed,
moved: [], }
}
movedFrom: null,
color: null,
diff: ''
});
}
}
 
// Get mark direction
//
var markType;
// mark original positions of moved groups
if ( movedGroup.blockStart < blockStart ) {
//
markType = '<';
}
else {
markType = '>';
}
 
// Add mark
// moved block marks at original positions relative to fixed groups:
fragments.push( {
// groups: 3 7
text: markText,
// 1 <| | (no next smaller fixed)
type: markType,
// 5 |< |
color: movedGroup.color
// |> 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
 
// Add moved block end
// cycle through groups (moved group)
if ( color !== null ) {
for (var movedGroup = 0; movedGroup < groups.length; movedGroup ++) {
fragments.push( {
if (groups[movedGroup].fixed != false) {
text: '',
continue;
type: ' )',
color: color
} );
}
}
var movedOldNumber = groups[movedGroup].oldNumber;
 
// Cycle through fragments, join consecutive fragments of same type (i.e. '-' blocks)
// find closest fixed groups
var nextSmallerNumberfragmentsLength = nullfragments.length;
for ( var fragment = 1; fragment < fragmentsLength; fragment ++ ) {
var nextSmallerGroup = null;
var nextLargerNumber = null;
var nextLargerGroup = null;
 
// Check if joinable
// cycle through groups (original positions)
if (
for (var group = 0; group < groups.length; group ++) {
fragments[fragment].type === fragments[fragment - 1].type &&
if ( (groups[group].fixed != true) || (group == movedGroup) ) {
fragments[fragment].color === fragments[fragment - 1].color &&
continue;
fragments[fragment].text !== '' && fragments[fragment - 1].text !== ''
}
) {
 
// Join and splice
// find fixed group with closest smaller oldNumber
fragments[fragment - 1].text += fragments[fragment].text;
var oldNumber = groups[group].oldNumber;
fragments.splice( fragment, 1 );
if ( (oldNumber < movedOldNumber) && ( (nextSmallerNumber == null) || (oldNumber > nextSmallerNumber) ) ) {
fragment --;
nextSmallerNumber = oldNumber;
nextSmallerGroup = group;
}
}
 
// Enclose in containers
// find fixed group with closest larger oldNumber
fragments.unshift( { text: '', type: '{', color: null }, { text: '', type: '[', color: null } );
if ( (oldNumber > movedOldNumber) && ( (nextLargerNumber == null) || (oldNumber < nextLargerNumber) ) ) {
fragments.push( { text: '', type: ']', color: null }, { text: '', type: '}', color: null } );
nextLargerNumber = oldNumber;
 
nextLargerGroup = group;
}return;
};
 
 
/**
* Clip unchanged sections from unmoved block text.
* Adds the following fagment types:
* '~', ' ~', '~ ' omission indicators
* '[', ']', ',' fragment start and end, fragment separator
*
* @param[in/out] array fragments Fragments array, abstraction layer for diff code
*/
this.clipDiffFragments = function () {
 
var fragments = this.fragments;
 
// Skip if only one fragment in containers, no change
if ( fragments.length === 5 ) {
return;
}
 
// noMin largerlength fixedfor group, movedclipping right
var movedFromminRight = ''this.config.clipHeadingRight;
if ( this.config.clipParagraphRightMin < minRight ) {
if (nextLargerGroup == null) {
minRight = this.config.clipParagraphRightMin;
movedFrom = 'left';
}
if ( this.config.clipLineRightMin < minRight ) {
minRight = this.config.clipLineRightMin;
}
if ( this.config.clipBlankRightMin < minRight ) {
minRight = this.config.clipBlankRightMin;
}
if ( this.config.clipCharsRight < minRight ) {
minRight = this.config.clipCharsRight;
}
 
// noMin smallerlength fixedfor group,clipping moved rightleft
var minLeft = this.config.clipHeadingLeft;
else if (nextSmallerGroup == null) {
if ( this.config.clipParagraphLeftMin < minLeft ) {
movedFrom = 'right';
minLeft = this.config.clipParagraphLeftMin;
}
if ( this.config.clipLineLeftMin < minLeft ) {
minLeft = this.config.clipLineLeftMin;
}
if ( this.config.clipBlankLeftMin < minLeft ) {
minLeft = this.config.clipBlankLeftMin;
}
if ( this.config.clipCharsLeft < minLeft ) {
minLeft = this.config.clipCharsLeft;
}
 
// Cycle through fragments
// group moved from between two closest fixed neighbors, moved left or right depending on char distance
var fragmentsLength = fragments.length;
else {
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
var rightChars = 0;
 
for (var group = nextSmallerGroup + 1; group < movedGroup; group ++) {
// Skip if not an unmoved and unchanged block
rightChars += groups[group].chars;
var type = fragments[fragment].type;
var color = fragments[fragment].color;
if ( type !== '=' || color !== null ) {
continue;
}
 
var leftChars = 0;
// Skip if too short for clipping
for (var group = movedGroup + 1; group < nextLargerGroup; group ++) {
leftCharsvar text += groupsfragments[groupfragment].charstext;
var textLength = text.length;
if ( textLength < minRight && textLength < minLeft ) {
continue;
}
 
// Get line positions including start and end
// moved right
ifvar (rightCharslines <= leftChars) {[];
movedFromvar lastIndex = 'left'null;
var regExpMatch;
while ( ( regExpMatch = this.config.regExp.clipLine.exec( text ) ) !== null ) {
lines.push( regExpMatch.index );
lastIndex = this.config.regExp.clipLine.lastIndex;
}
if ( lines[0] !== 0 ) {
lines.unshift( 0 );
}
if ( lastIndex !== textLength ) {
lines.push( textLength );
}
 
// movedGet leftheading positions
var headings = [];
else {
movedFromvar headingsEnd = 'right'[];
while ( ( regExpMatch = this.config.regExp.clipHeading.exec( text ) ) !== null ) {
headings.push( regExpMatch.index );
headingsEnd.push( regExpMatch.index + regExpMatch[0].length );
}
}
 
// Get paragraph positions including start and end
// check for null-moves
var paragraphs = [];
if (movedFrom == 'left') {
var lastIndex = null;
if (groups[nextSmallerGroup].blockEnd + 1 != groups[movedGroup].blockStart) {
while ( ( regExpMatch = this.config.regExp.clipParagraph.exec( text ) ) !== null ) {
groups[nextSmallerGroup].moved.push(movedGroup);
paragraphs.push( regExpMatch.index );
groups[movedGroup].movedFrom = nextSmallerGroup;
lastIndex = this.config.regExp.clipParagraph.lastIndex;
}
if ( paragraphs[0] !== 0 ) {
}
paragraphs.unshift( 0 );
else if (movedFrom == 'right') {
}
if (groups[movedGroup].blockEnd + 1 != groups[nextLargerGroup].blockStart) {
if ( lastIndex !== textLength ) {
groups[nextLargerGroup].moved.push(movedGroup);
paragraphs.push( textLength );
groups[movedGroup].movedFrom = nextLargerGroup;
}
}
}
 
// Determine ranges to keep on left and right side
// cycle through groups, sort blocks moved from here by old number
var rangeRight = null;
for (var group = 0; group < groups.length; group ++) {
var movedrangeLeft = groups[group].movednull;
var rangeRightType = '';
if (moved != null) {
var rangeLeftType = '';
moved.sort(function(a, b) {
return groups[a].oldNumber - groups[b].oldNumber;
});
}
}
 
// Find clip pos from left, skip for first non-container block
//
if ( fragment !== 2 ) {
// set moved block colors
//
 
// Maximum lines to search from left
// cycle through groups
var movedrangeLeftMax = []textLength;
for if (var group = 0; groupthis.config.clipLinesLeftMax < groupslines.length; group ++) {
rangeLeftMax = lines[this.config.clipLinesLeftMax];
moved = moved.concat(groups[group].moved);
}
}
 
// Find first heading from left
// sort moved array by old number
if ( rangeLeft === null ) {
moved.sort(function(a, b) {
var headingsLength = headingsEnd.length;
return groups[a].oldNumber - groups[b].oldNumber;
for ( var j = 0; j < headingsLength; j ++ ) {
});
if ( headingsEnd[j] > this.config.clipHeadingLeft || headingsEnd[j] > rangeLeftMax ) {
break;
}
rangeLeft = headingsEnd[j];
rangeLeftType = 'heading';
break;
}
}
 
// Find first paragraph from left
// set color
if ( rangeLeft === null ) {
var color = 0;
for ( var iparagraphsLength = 0; i < movedparagraphs.length; i ++) {
for ( var j = 0; j < paragraphsLength; j ++ ) {
var movedGroup = moved[i];
if (
if ( (groups[movedGroup].maxWords >= wDiff.blockMinLength) && (wDiff.showBlockMoves == true) ) {
paragraphs[j] > this.config.clipParagraphLeftMax ||
groups[movedGroup].color = color;
paragraphs[j] > rangeLeftMax
color ++;
} ) {
break;
}
}
if ( paragraphs[j] > this.config.clipParagraphLeftMin ) {
rangeLeft = paragraphs[j];
rangeLeftType = 'paragraph';
break;
}
}
}
 
// Find first line break from left
// WED('Deletions', wDiff.DebugDeletions(deletions));
if ( rangeLeft === null ) {
// WED('Groups', wDiff.DebugGroups(groups));
var linesLength = lines.length;
// WED('Blocks', wDiff.DebugBlocks(blocks));
for ( var j = 0; j < linesLength; j ++ ) {
if ( lines[j] > this.config.clipLineLeftMax || lines[j] > rangeLeftMax ) {
break;
}
if ( lines[j] > this.config.clipLineLeftMin ) {
rangeLeft = lines[j];
rangeLeftType = 'line';
break;
}
}
}
 
// Find first blank from left
return;
if ( rangeLeft === null ) {
};
this.config.regExp.clipBlank.lastIndex = this.config.clipBlankLeftMin;
if ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if (
regExpMatch.index < this.config.clipBlankLeftMax &&
regExpMatch.index < rangeLeftMax
) {
rangeLeft = regExpMatch.index;
rangeLeftType = 'blank';
}
}
}
 
// Fixed number of chars from left
if ( rangeLeft === null ) {
if ( this.config.clipCharsLeft < rangeLeftMax ) {
rangeLeft = this.config.clipCharsLeft;
rangeLeftType = 'chars';
}
}
 
// Fixed number of lines from left
// wDiff.FindMaxPath: recusively find path of groups in increasing old group order with longest char length
if ( rangeLeft === null ) {
// 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
rangeLeft = rangeLeftMax;
// returns: returnObj, contains path and length
rangeLeftType = 'fixed';
// called from: wDiff.DetectBlocks()
}
// calls: itself recursively
}
 
// Find clip pos from right, skip for last non-container block
wDiff.FindMaxPath = function(start, path, chars, cache, groups, groupEnd) {
if ( fragment !== fragments.length - 3 ) {
 
// Maximum lines to search from right
// add current path point
var pathLocalrangeRightMin = path.slice()0;
if ( lines.length >= this.config.clipLinesRightMax ) {
pathLocal.push(start);
rangeRightMin = lines[lines.length - this.config.clipLinesRightMax];
chars = chars + groups[start].chars;
}
 
// Find last group,heading terminatefrom recursionright
if ( rangeRight === null ) {
var returnObj = { path: pathLocal, chars: chars };
for ( var j = headings.length - 1; j >= 0; j -- ) {
if (i == groupEnd) {
if (
return returnObj;
headings[j] < textLength - this.config.clipHeadingRight ||
}
headings[j] < rangeRightMin
) {
break;
}
rangeRight = headings[j];
rangeRightType = 'heading';
break;
}
}
 
// Find last paragraph from right
// find longest sub-path
if ( rangeRight === null ) {
var maxChars = 0;
for ( var j = paragraphs.length - 1; j >= 0 ; j -- ) {
var oldNumber = groups[start].oldNumber;
if (
for (var i = start + 1; i <= groupEnd; i ++) {
paragraphs[j] < textLength - this.config.clipParagraphRightMax ||
paragraphs[j] < rangeRightMin
) {
break;
}
if ( paragraphs[j] < textLength - this.config.clipParagraphRightMin ) {
rangeRight = paragraphs[j];
rangeRightType = 'paragraph';
break;
}
}
}
 
// onlyFind inlast increasingline oldbreak groupfrom orderright
if ( rangeRight === null ) {
if (groups[i].oldNumber < oldNumber) {
for ( var j = lines.length - 1; j >= 0; j -- ) {
continue;
if (
}
lines[j] < textLength - this.config.clipLineRightMax ||
lines[j] < rangeRightMin
) {
break;
}
if ( lines[j] < textLength - this.config.clipLineRightMin ) {
rangeRight = lines[j];
rangeRightType = 'line';
break;
}
}
}
 
// getFind longestlast sub-pathblank from cacheright
if (cache[start] !rangeRight === null ) {
var startPos = textLength - this.config.clipBlankRightMax;
returnObj = cache[start];
if ( startPos < rangeRightMin ) {
}
startPos = rangeRightMin;
}
this.config.regExp.clipBlank.lastIndex = startPos;
var lastPos = null;
while ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if ( regExpMatch.index > textLength - this.config.clipBlankRightMin ) {
if ( lastPos !== null ) {
rangeRight = lastPos;
rangeRightType = 'blank';
}
break;
}
lastPos = regExpMatch.index;
}
}
 
// Fixed number of chars from right
// get longest sub-path by recursion
if ( rangeRight === null ) {
else {
if ( textLength - this.config.clipCharsRight > rangeRightMin ) {
var pathObj = wDiff.FindMaxPath(i, pathLocal, chars, cache, groups, groupEnd);
rangeRight = textLength - this.config.clipCharsRight;
rangeRightType = 'chars';
}
}
 
// Fixed number of lines from right
// select longest sub-path
if (pathObj.chars >rangeRight === null maxChars) {
returnObj rangeRight = pathObjrangeRightMin;
rangeRightType = 'fixed';
}
}
}
}
 
// Check if we skip clipping if ranges are close together
// save longest path to cache
if (cache[i] rangeLeft !== null && rangeRight !== null ) {
cache[start] = returnObj;
}
return returnObj;
};
 
// Skip if overlapping ranges
if ( rangeLeft > rangeRight ) {
continue;
}
 
// Skip if chars too close
// wDiff.AssembleDiff: process diff data into formatted html text
var skipChars = rangeRight - rangeLeft;
// input: text, object containing text tokens list; blocks, array containing block type; groups, array containing fixed (not moved), color, and moved mark data
if ( skipChars < this.config.clipSkipChars ) {
// returns: diff html string
continue;
// called from: wDiff.Diff()
}
// calls: wDiff.HtmlCustomize(), wDiff.HtmlFormat()
 
// Skip if lines too close
wDiff.AssembleDiff = function(text, blocks, groups) {
var skipLines = 0;
var linesLength = lines.length;
for ( var j = 0; j < linesLength; j ++ ) {
if ( lines[j] > rangeRight || skipLines > this.config.clipSkipLines ) {
break;
}
if ( lines[j] > rangeLeft ) {
skipLines ++;
}
}
if ( skipLines < this.config.clipSkipLines ) {
continue;
}
}
 
// Skip if nothing to clip
//
if ( rangeLeft === null && rangeRight === null ) {
// create group diffs
continue;
//
}
 
// Split left text
// cycle through groups
var textLeft = null;
for (var group = 0; group < groups.length; group ++) {
var fixedomittedLeft = groups[group].fixednull;
if ( rangeLeft !== null ) {
var color = groups[group].color;
textLeft = text.slice( 0, rangeLeft );
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
var diff = '';
 
// Remove trailing empty lines
// check for colored block and move direction
textLeft = textLeft.replace( this.config.regExp.clipTrimNewLinesLeft, '' );
var blockFrom = null;
 
if ( (fixed == false) && (color != null) ) {
// Get omission indicators, remove trailing blanks
if (groups[ groups[group].movedFrom ].blockStart < blockStart) {
blockFromif ( rangeLeftType === 'leftchars'; ) {
omittedLeft = '~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
else if ( rangeLeftType === 'blank' ) {
omittedLeft = ' ~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
}
 
else {
blockFrom// =Split 'right'; text
var textRight = null;
var omittedRight = null;
if ( rangeRight !== null ) {
textRight = text.slice( rangeRight );
 
// Remove leading empty lines
textRight = textRight.replace( this.config.regExp.clipTrimNewLinesRight, '' );
 
// Get omission indicators, remove leading blanks
if ( rangeRightType === 'chars' ) {
omittedRight = '~';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
else if ( rangeRightType === 'blank' ) {
omittedRight = '~ ';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
}
 
// Remove split element
fragments.splice( fragment, 1 );
fragmentsLength --;
 
// Add left text to fragments list
if ( rangeLeft !== null ) {
fragments.splice( fragment ++, 0, { text: textLeft, type: '=', color: null } );
fragmentsLength ++;
if ( omittedLeft !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedLeft, color: null } );
fragmentsLength ++;
}
}
 
// Add fragment container and separator to list
if ( rangeLeft !== null && rangeRight !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: ']', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: ',', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: '[', color: null } );
fragmentsLength += 3;
}
 
// Add right text to fragments list
if ( rangeRight !== null ) {
if ( omittedRight !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedRight, color: null } );
fragmentsLength ++;
}
fragments.splice( fragment ++, 0, { text: textRight, type: '=', color: null } );
fragmentsLength ++;
}
}
 
// Debug log
// add colored block start markup
if (blockFrom this.config.debug === true 'left') {
this.debugFragments( 'Fragments' );
diff += wDiff.HtmlCustomize(wDiff.htmlBlockLeftStart, color);
}
 
else if (blockFrom == 'right') {
return;
diff += wDiff.HtmlCustomize(wDiff.htmlBlockRightStart, color);
};
 
 
/**
* Create html formatted diff code from diff fragments.
*
* @param[in] array fragments Fragments array, abstraction layer for diff code
* @param string|undefined version
* Output version: 'new' or 'old': only text from new or old version, used for unit tests
* @param[out] string html Html code of diff
*/
this.getDiffHtml = function ( version ) {
 
var fragments = this.fragments;
 
// No change, only one unchanged block in containers
if ( fragments.length === 5 && fragments[2].type === '=' ) {
this.html = '';
return;
}
 
// cycleCycle through blocksfragments
var htmlFragments = [];
for (var block = blockStart; block <= blockEnd; block ++) {
var typefragmentsLength = blocks[block]fragments.typelength;
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
var string = blocks[block].string;
var text = fragments[fragment].text;
var type = fragments[fragment].type;
var color = fragments[fragment].color;
var html = '';
 
// htmlTest escapeif text stringis blanks-only or a single character
var blank = false;
string = wDiff.HtmlEscape(string);
if ( text !== '' ) {
blank = this.config.regExp.blankBlock.test( text );
}
 
// Add container start markup
// moved block too small, make it an insertion and place it as a deletion at its original position
if ( type === '{' ) {
if ( ( (groups[group].maxWords < wDiff.blockMinLength) || (wDiff.showBlockMoves == false) ) && (fixed == false) ) {
html = this.config.htmlCode.containerStart;
if (type != 'del') {
string = string.replace(/\n/g, wDiff.htmlNewline);
diff += wDiff.htmlInsertStart + string + wDiff.htmlInsertEnd;
}
}
 
// addAdd 'same'container (unchanged)end textmarkup
else if ( type === 'same}' ) {
html = this.config.htmlCode.containerEnd;
diff += string;
}
 
// addAdd 'del'fragment textstart markup
else if ( type === 'del[' ) {
html = this.config.htmlCode.fragmentStart;
string = string.replace(/\n/g, wDiff.htmlNewline);
diff += wDiff.htmlDeleteStart + string + wDiff.htmlDeleteEnd;
}
 
// addAdd 'ins'fragment textend markup
else if ( type === 'ins]' ) {
html = this.config.htmlCode.fragmentEnd;
string = string.replace(/\n/g, wDiff.htmlNewline);
diff += wDiff.htmlInsertStart + string + wDiff.htmlInsertEnd;
}
}
 
// addAdd coloredfragment block endseparator markup
else if (blockFrom type === 'left,' ) {
html = this.config.htmlCode.separator;
diff += wDiff.htmlBlockLeftEnd;
}
else if (blockFrom == 'right') {
diff += wDiff.htmlBlockRightEnd;
}
 
// Add omission markup
groups[group].diff = diff;
if ( type === '~' ) {
}
html = this.config.htmlCode.omittedChars;
}
 
// Add omission markup
//
if ( type === ' ~' ) {
// mark original block positions
html = ' ' + this.config.htmlCode.omittedChars;
//
}
 
// Add omission markup
// cycle through groups
if ( type === '~ ' ) {
for (var group = 0; group < groups.length; group ++) {
html = this.config.htmlCode.omittedChars + ' ';
var moved = groups[group].moved;
}
 
// Add colored left-pointing block start markup
// cycle through list of groups moved from here
var else leftMarksif ( type === '(<'; ) {
var if rightMarks( version !== 'old'; ) {
for (var i = 0; i < moved.length; i ++) {
var movedGroup = moved[i];
var markColor = groups[movedGroup].color
var mark = '';
 
// getGet moved block texttitle
var movedText = ''title;
if ( this.config.noUnicodeSymbols === true ) {
for (var block = groups[movedGroup].blockStart; block <= groups[movedGroup].blockEnd; block ++) {
title = this.config.msg['wiked-diff-block-left-nounicode'];
if (blocks[block].type != 'ins') {
}
movedText += blocks[block].string;
else {
title = this.config.msg['wiked-diff-block-left'];
}
 
// Get html
if ( this.config.coloredBlocks === true ) {
html = this.config.htmlCode.blockColoredStart;
}
else {
html = this.config.htmlCode.blockStart;
}
html = this.htmlCustomize( html, color, title );
}
}
 
// Add colored right-pointing block start markup
// moved block too small, make it a deletion at its original position
else if ( type === '(>' ) {
if ( (groups[movedGroup].words < wDiff.blockMinLength) || (wDiff.showBlockMoves == false) ) {
if ( version !== 'old' ) {
mark = wDiff.htmlDeleteStart + wDiff.HtmlEscape(movedText) + wDiff.htmlDeleteEnd;
 
// Get title
var title;
if ( this.config.noUnicodeSymbols === true ) {
title = this.config.msg['wiked-diff-block-right-nounicode'];
}
else {
title = this.config.msg['wiked-diff-block-right'];
}
 
// Get html
if ( this.config.coloredBlocks === true ) {
html = this.config.htmlCode.blockColoredStart;
}
else {
html = this.config.htmlCode.blockStart;
}
html = this.htmlCustomize( html, color, title );
}
}
 
// getAdd markcolored directionblock end markup
else if ( type === ' )' ) {
if ( version !== 'old' ) {
if (groups[movedGroup].blockStart < groups[group].blockStart) {
markhtml = wDiffthis.htmlMarkLeftconfig.htmlCode.blockEnd;
}
}
 
// Add '=' (unchanged) text and moved block
if ( type === '=' ) {
text = this.htmlEscape( text );
if ( color !== null ) {
if ( version !== 'old' ) {
html = this.markupBlanks( text, true );
}
}
else {
markhtml = wDiffthis.htmlMarkRightmarkupBlanks( text );
}
mark = wDiff.HtmlCustomize(mark, markColor, movedText);
}
 
// Add '-' text
else if ( type === '-' ) {
if ( version !== 'new' ) {
 
// getFor sideold ofversion groupskip to'-' markinside moved group
if ( version !== 'old' || color === null ) {
if (groups[movedGroup].oldNumber < groups[group].oldNumber) {
text = this.htmlEscape( text );
leftMarks += mark;
text = this.markupBlanks( text, true );
if ( blank === true ) {
html = this.config.htmlCode.deleteStartBlank;
}
else {
html = this.config.htmlCode.deleteStart;
}
html += text + this.config.htmlCode.deleteEnd;
}
}
}
 
else {
rightMarks// Add '+=' mark;text
else if ( type === '+' ) {
if ( version !== 'old' ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
if ( blank === true ) {
html = this.config.htmlCode.insertStartBlank;
}
else {
html = this.config.htmlCode.insertStart;
}
html += text + this.config.htmlCode.insertEnd;
}
}
 
// Add '<' and '>' code
else if ( type === '<' || type === '>' ) {
if ( version !== 'new' ) {
 
// Display as deletion at original position
if ( this.config.showBlockMoves === false || version === 'old' ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
if ( version === 'old' ) {
if ( this.config.coloredBlocks === true ) {
html =
this.htmlCustomize( this.config.htmlCode.blockColoredStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
else {
html =
this.htmlCustomize( this.config.htmlCode.blockStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
}
else {
if ( blank === true ) {
html =
this.config.htmlCode.deleteStartBlank +
text +
this.config.htmlCode.deleteEnd;
}
else {
html = this.config.htmlCode.deleteStart + text + this.config.htmlCode.deleteEnd;
}
}
}
 
// Display as mark
else {
if ( type === '<' ) {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markLeftColored, color, text );
}
else {
html = this.htmlCustomize( this.config.htmlCode.markLeft, color, text );
}
}
else {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markRightColored, color, text );
}
else {
html = this.htmlCustomize( this.config.htmlCode.markRight, color, text );
}
}
}
}
}
htmlFragments.push( html );
}
groups[group].diff = leftMarks + groups[group].diff + rightMarks;
}
 
// Join fragments
//
this.html = htmlFragments.join( '' );
// join diffs
//
 
return;
// make shallow copy of groups and sort by blockStart
};
var groupsSort = groups.slice();
groupsSort.sort(function(a, b) {
return a.blockStart - b.blockStart;
});
 
// 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));
* Customize html code fragments.
* Replaces:
* {number}: class/color/block/mark/id number
* {title}: title attribute (popup)
* {nounicode}: noUnicodeSymbols fallback
* input: html, number: block number, title: title attribute (popup) text
*
* @param string html Html code to be customized
* @return string Customized html code
*/
this.htmlCustomize = function ( html, number, title ) {
 
// Replace {number} with class/color/block/mark/id number
// keep newlines and multiple spaces
html = html.replace( /\{number\}/g, number);
wDiff.HtmlFormat(text);
 
// Replace {nounicode} with wikEdDiffNoUnicode class name
// WED('text.diff', text.diff);
if ( this.config.noUnicodeSymbols === true ) {
html = html.replace( /\{nounicode\}/g, ' wikEdDiffNoUnicode');
}
else {
html = html.replace( /\{nounicode\}/g, '');
}
 
// Shorten title text, replace {title}
return text.diff;
if ( title !== undefined ) {
};
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;');
html = html.replace( /\{title\}/, title);
}
return html;
};
 
 
//**
* Replace html-sensitive characters in output text with character entities.
// wDiff.HtmlCustomize: customize move indicator html: replace {block} with block style, {mark} with mark style, and {title} with title attribute
*
// input: text (html or css code)
* @param string html Html code to be escaped
// returns: customized text
* @return string Escaped html code
// called from: wDiff.AssembleDiff()
*/
this.htmlEscape = function ( html ) {
 
html = html.replace( /&/g, '&amp;');
wDiff.HtmlCustomize = function(text, number, title) {
html = html.replace( /</g, '&lt;');
html = html.replace( />/g, '&gt;');
html = html.replace( /"/g, '&quot;');
return html;
};
 
text = text.replace(/\{block\}/, wDiff.styleBlockColor[number] || '');
text = text.replace(/\{mark\}/, wDiff.styleMarkColor[number] || '');
 
/**
// shorten title text, replace {title}
* Markup tabs, newlines, and spaces in diff fragment text.
if ( (title != null) && (title != '') ) {
*
var max = 512;
* @param bool highlight Highlight newlines and spaces in addition to tabs
var end = 128;
* @param string html Text code to be marked-up
var gapMark = ' [...] ';
* @return string Marked-up text
if (title.length > max) {
*/
title = title.substr(0, max - gapMark.length - end) + gapMark + title.substr(title.length - end);
this.markupBlanks = function ( html, highlight ) {
 
if ( highlight === true ) {
html = html.replace( / /g, this.config.htmlCode.space);
html = html.replace( /\n/g, this.config.htmlCode.newline);
}
html = html.replace( /\t/g, this.config.htmlCode.tab);
title = wDiff.HtmlEscape(title);
return html;
title = title.replace(/\t/g, '&nbsp;&nbsp;');
};
title = title.replace(/ /g, '&nbsp;&nbsp;');
text = text.replace(/\{title\}/, ' title="' + title + '"');
}
return text;
};
 
 
//**
* Count real words in text.
// wDiff.HtmlEscape: replace html-sensitive characters in output text with character entities
*
// input: text
* @param string text Text for word counting
// returns: escaped text
* @return int Number of words in text
// called from: wDiff.Diff(), wDiff.AssembleDiff()
*/
this.wordCount = function ( text ) {
 
return ( text.match( this.config.regExp.countWords ) || [] ).length;
wDiff.HtmlEscape = function(text) {
};
 
text = text.replace(/&/g, '&amp;');
text = text.replace(/</g, '&lt;');
text = text.replace(/>/g, '&gt;');
text = text.replace(/"/g, '&quot;');
return (text);
};
 
/**
* Test diff code for consistency with input versions.
* Prints results to debug console.
*
* @param[in] WikEdDiffText newText, oldText Text objects
*/
this.unitTests = function () {
 
// Check if output is consistent with new text
//
this.getDiffHtml( 'new' );
// wDiff.HtmlFormat: tidy html, keep newlines and multiple spaces, add container
var diff = this.html.replace( /<[^>]*>/g, '');
// changes: text.diff
var text = this.htmlEscape( this.newText.text );
// called from: wDiff.Diff(), wDiff.AssembleDiff()
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with new text version!'
);
this.error = true;
console.log( 'new text:\n', text );
console.log( 'new diff:\n', diff );
}
else {
console.log( 'OK: wikEdDiff unit test passed: diff consistent with new text.' );
}
 
// Check if output is consistent with old text
wDiff.HtmlFormat = function(text) {
this.getDiffHtml( 'old' );
var diff = this.html.replace( /<[^>]*>/g, '');
var text = this.htmlEscape( this.oldText.text );
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with old text version!'
);
this.error = true;
console.log( 'old text:\n', text );
console.log( 'old diff:\n', diff );
}
else {
console.log( 'OK: wikEdDiff unit test passed: diff consistent with old text.' );
}
 
return;
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 + text.diff + wDiff.htmlContainerEnd;
return;
};
 
 
/**
// wDiff.ShortenOutput: shorten diff html by removing unchanged parts
* Dump blocks object to browser console.
// input: diff html string from wDiff.Diff()
*
// returns: shortened html with removed unchanged passages indicated by (...) or separator
* @param string name Block name
* @param[in] array blocks Blocks table object
*/
this.debugBlocks = function ( name, blocks ) {
 
if ( blocks === undefined ) {
wDiff.ShortenOutput = function(html) {
blocks = this.blocks;
}
var dump =
'\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq' +
'\twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \ttext\n';
var blocksLength = blocks.length;
for ( var i = 0; i < blocksLength; i ++ ) {
dump +=
i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' +
blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \t' + blocks[i].oldStart + ' \t' +
blocks[i].count + ' \t' + blocks[i].unique + ' \t' + blocks[i].words + ' \t' +
blocks[i].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' +
blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + blocks[i].moved + ' \t' +
this.debugShortenText( blocks[i].text ) + '\n';
}
console.log( name + ':\n' + dump );
};
 
var diff = '';
 
/**
// empty text
* Dump groups object to browser console.
if ( (html == null) || (html == '') ) {
*
return '';
* @param string name Group name
}
* @param[in] array groups Groups table object
*/
this.debugGroups = function ( name, groups ) {
 
if ( groups === undefined ) {
// remove container by non-regExp replace
groups = this.groups;
diff = diff.replace(wDiff.htmlContainerStart, '');
}
diff = diff.replace(wDiff.htmlContainerEnd, '')
var dump =
'\ni \toldNm \tblSta \tblEnd \tuniq \tmaxWo' +
'\twords \tchars \tfixed \toldNm \tmFrom \tcolor\n';
var groupsLength = groupsLength;
for ( var i = 0; i < groups.length; i ++ ) {
dump +=
i + ' \t' + groups[i].oldNumber + ' \t' + groups[i].blockStart + ' \t' +
groups[i].blockEnd + ' \t' + groups[i].unique + ' \t' + groups[i].maxWords + ' \t' +
groups[i].words + ' \t' + groups[i].chars + ' \t' + groups[i].fixed + ' \t' +
groups[i].oldNumber + ' \t' + groups[i].movedFrom + ' \t' + groups[i].color + '\n';
}
console.log( name + ':\n' + dump );
};
 
// scan for diff html tags
var regExpDiff = /<\w+\b[^>]*\bclass="wDiff(MarkLeft|MarkRight|BlockLeft|BlockRight|Delete|Insert)"[^>]*>(.|\n)*?<!--wDiff\1-->/g;
var tagStart = [];
var tagEnd = [];
var i = 0;
var regExpMatch;
 
/**
// save tag positions
* Dump fragments array to browser console.
while ( (regExpMatch = regExpDiff.exec(html)) != null ) {
*
* @param string name Fragments name
* @param[in] array fragments Fragments array
*/
this.debugFragments = function ( name ) {
 
var fragments = this.fragments;
// combine consecutive diff tags
var dump = '\ni \ttype \tcolor \ttext\n';
if ( (i > 0) && (tagEnd[i - 1] == regExpMatch.index) ) {
var fragmentsLength = fragments.length;
tagEnd[i - 1] = regExpMatch.index + regExpMatch[0].length;
for ( var i = 0; i < fragmentsLength; i ++ ) {
dump +=
i + ' \t"' + fragments[i].type + '" \t' + fragments[i].color + ' \t' +
this.debugShortenText( fragments[i].text, 120, 40 ) + '\n';
}
console.log( name + ':\n' + dump );
else {
};
tagStart[i] = regExpMatch.index;
tagEnd[i] = regExpMatch.index + regExpMatch[0].length;
i ++;
}
}
 
// no diff tags detected
if (tagStart.length == 0) {
return wDiff.htmlNoChange;
}
 
/**
// define regexps
* Dump borders array to browser console.
var regExpHeading = /\n=+.+?=+ *\n|\n\{\||\n\|\}/g;
*
var regExpParagraph = /\n\n+/g;
* @param string name Arrays name
var regExpLine = /\n+/g;
* @param[in] array border Match border array
var regExpBlank = /(<[^>]+>)*\s+/g;
*/
this.debugBorders = function ( name, borders ) {
 
var dump = '\ni \t[ new \told ]\n';
// determine fragment border positions around diff tags
var rangeStartbordersLength = []borders.length;
for ( var i = 0; i < bordersLength; i ++ ) {
var rangeEnd = [];
dump += i + ' \t[ ' + borders[i][0] + ' \t' + borders[i][1] + ' ]\n';
var rangeStartType = [];
}
var rangeEndType = [];
console.log( name, dump );
};
 
// cycle through diff tag start positions
for (var i = 0; i < tagStart.length; i ++) {
var regExpMatch;
 
/**
// find last heading before diff tag
* Shorten text for dumping.
var lastPos = tagStart[i] - wDiff.headingBefore;
*
if (lastPos < 0) {
* @param string text Text to be shortened
lastPos = 0;
* @param int max Max length of (shortened) text
* @param int end Length of trailing fragment of shortened text
* @return string Shortened text
*/
this.debugShortenText = function ( text, max, end ) {
 
if ( typeof text !== 'string' ) {
text = text.toString();
}
text = text.replace( /\n/g, '\\n');
regExpHeading.lastIndex = lastPos;
text = text.replace( /\t/g, ' ');
while ( (regExpMatch = regExpHeading.exec(html)) != null ) {
if ( max === undefined ) {
if (regExpMatch.index > tagStart[i]) {
breakmax = 50;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'heading';
}
if ( end === undefined ) {
 
end = 15;
// find last paragraph before diff tag
if (rangeStart[i] == null) {
lastPos = tagStart[i] - wDiff.paragraphBefore;
if (lastPos < 0) {
lastPos = 0;
}
regExpParagraph.lastIndex = lastPos;
while ( (regExpMatch = regExpParagraph.exec(html)) != null) {
if (regExpMatch.index > tagStart[i]) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'paragraph';
}
}
if ( text.length > max ) {
text = text.substr( 0, max - 1 - end ) + '…' + text.substr( text.length - end );
}
return '"' + text + '"';
};
 
 
// find last line break before diff tag
/**
if (rangeStart[i] == null) {
* Start timer 'label', analogous to JavaScript console timer.
lastPos = tagStart[i] - wDiff.lineBeforeMax;
* Usage: this.time( 'label' );
if (lastPos < 0) {
*
lastPos = 0;
* @param string label Timer label
}
* @param[out] array timer Current time in milliseconds (float)
regExpLine.lastIndex = lastPos;
*/
while ( (regExpMatch = regExpLine.exec(html)) != null ) {
this.time = function ( label ) {
if (regExpMatch.index > tagStart[i] - wDiff.lineBeforeMin) {
 
break;
this.timer[label] = new Date().getTime();
}
return;
rangeStart[i] = regExpMatch.index;
};
rangeStartType[i] = 'line';
 
 
/**
* Stop timer 'label', analogous to JavaScript console timer.
* Logs time in milliseconds since start to browser console.
* Usage: this.timeEnd( 'label' );
*
* @param string label Timer label
* @param bool noLog Do not log result
* @return float Time in milliseconds
*/
this.timeEnd = function ( label, noLog ) {
 
var diff = 0;
if ( this.timer[label] !== undefined ) {
var start = this.timer[label];
var stop = new Date().getTime();
diff = stop - start;
this.timer[label] = undefined;
if ( noLog !== true ) {
console.log( label + ': ' + diff.toFixed( 2 ) + ' ms' );
}
}
return diff;
};
 
 
// find last blank before diff tag
/**
if (rangeStart[i] == null) {
* Log recursion timer results to browser console.
lastPos = tagStart[i] - wDiff.blankBeforeMax;
* Usage: this.timeRecursionEnd();
if (lastPos < 0) {
*
lastPos = 0;
* @param string text Text label for output
* @param[in] array recursionTimer Accumulated recursion times
*/
this.timeRecursionEnd = function ( text ) {
 
if ( this.recursionTimer.length > 1 ) {
 
// Subtract times spent in deeper recursions
var timerEnd = this.recursionTimer.length - 1;
for ( var i = 0; i < timerEnd; i ++ ) {
this.recursionTimer[i] -= this.recursionTimer[i + 1];
}
regExpBlank.lastIndex = lastPos;
while ( (regExpMatch = regExpBlank.exec(html)) != null ) {
if (regExpMatch.index > tagStart[i] - wDiff.blankBeforeMin) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'blank';
}
}
 
// Log recursion times
// fixed number of chars before diff tag
var timerLength = this.recursionTimer.length;
if (rangeStart[i] == null) {
for ( var i = 0; i < timerLength; i ++ ) {
rangeStart[i] = tagStart[i] - wDiff.charsBefore;
console.log( text + ' recursion ' + i + ': ' + this.recursionTimer[i].toFixed( 2 ) + ' ms' );
rangeStartType[i] = 'chars';
if (rangeStart[i] < 0) {
rangeStart[i] = 0;
}
}
this.recursionTimer = [];
return;
};
 
 
// find first heading after diff tag
/**
regExpHeading.lastIndex = tagEnd[i];
* Log variable values to debug console.
if ( (regExpMatch = regExpHeading.exec(html)) != null ) {
* Usage: this.debug( 'var', var );
if (regExpMatch.index < tagEnd[i] + wDiff.headingAfter) {
*
rangeEnd[i] = regExpMatch.index + regExpMatch[0].length;
* @param string name Object identifier
rangeEndType[i] = 'heading';
* @param mixed|undefined name Object to be logged
}
*/
this.debug = function ( name, object ) {
 
if ( object === undefined ) {
console.log( name );
}
else {
console.log( name + ': ' + object );
}
return;
};
 
 
// find first paragraph after diff tag
/**
if (rangeEnd[i] == null) {
* Add script to document head.
regExpParagraph.lastIndex = tagEnd[i];
*
if ( (regExpMatch = regExpParagraph.exec(html)) != null ) {
* @param string code JavaScript code
if (regExpMatch.index < tagEnd[i] + wDiff.paragraphAfter) {
*/
rangeEnd[i] = regExpMatch.index;
this.addScript = function ( code ) {
rangeEndType[i] = 'paragraph';
 
}
if ( document.getElementById( 'wikEdDiffBlockHandler' ) === null ) {
var script = document.createElement( 'script' );
script.id = 'wikEdDiffBlockHandler';
if ( script.innerText !== undefined ) {
script.innerText = code;
}
else {
script.textContent = code;
}
document.getElementsByTagName( 'head' )[0].appendChild( script );
}
return;
};
 
 
// find first line break after diff tag
/**
if (rangeEnd[i] == null) {
* Add stylesheet to document head, cross-browser >= IE6.
regExpLine.lastIndex = tagEnd[i] + wDiff.lineAfterMin;
*
if ( (regExpMatch = regExpLine.exec(html)) != null ) {
* @param string css CSS code
if (regExpMatch.index < tagEnd[i] + wDiff.lineAfterMax) {
*/
rangeEnd[i] = regExpMatch.index;
this.addStyleSheet = function ( css ) {
rangeEndType[i] = 'break';
 
}
if ( document.getElementById( 'wikEdDiffStyles' ) === null ) {
 
// Replace mark symbols
css = css.replace( /\{cssMarkLeft\}/g, this.config.cssMarkLeft);
css = css.replace( /\{cssMarkRight\}/g, this.config.cssMarkRight);
 
var style = document.createElement( 'style' );
style.id = 'wikEdDiffStyles';
style.type = 'text/css';
if ( style.styleSheet !== undefined ) {
style.styleSheet.cssText = css;
}
else {
style.appendChild( document.createTextNode( css ) );
}
document.getElementsByTagName( 'head' )[0].appendChild( style );
}
return;
};
 
 
/**
* Recursive deep copy from target over source for customization import.
*
* @param object source Source object
* @param object target Target object
*/
this.deepCopy = function ( source, target ) {
 
for ( var key in source ) {
// find blank after diff tag
if ( Object.prototype.hasOwnProperty.call( source, key ) === true ) {
if (rangeEnd[i] == null) {
if ( typeof source[key] === 'object' ) {
regExpBlank.lastIndex = tagEnd[i] + wDiff.blankAfterMin;
this.deepCopy( source[key], target[key] );
if ( (regExpMatch = regExpBlank.exec(html)) != null ) {
}
if (regExpMatch.index < tagEnd[i] + wDiff.blankAfterMax) {
else {
rangeEnd[i] = regExpMatch.index;
rangeEndTypetarget[ikey] = 'blank'source[key];
}
}
}
return;
};
 
// Initialze WikEdDiff object
// fixed number of chars after diff tag
this.init();
if (rangeEnd[i] == null) {
};
rangeEnd[i] = tagEnd[i] + wDiff.charsAfter;
 
if (rangeEnd[i] > html.length) {
 
rangeEnd[i] = html.length;
/**
rangeEndType[i] = 'chars';
* Data and methods for single text version (old or new one).
}
*
* @class WikEdDiffText
*/
WikEdDiff.WikEdDiffText = function ( text, parent ) {
 
/** @var WikEdDiff parent Parent object for configuration settings and debugging methods */
this.parent = parent;
 
/** @var string text Text of this version */
this.text = null;
 
/** @var array tokens Tokens list */
this.tokens = [];
 
/** @var int first, last First and last index of tokens list */
this.first = null;
this.last = null;
 
/** @var array words Word counts for version text */
this.words = {};
 
 
/**
* Constructor, initialize text object.
*
* @param string text Text of version
* @param WikEdDiff parent Parent, for configuration settings and debugging methods
*/
this.init = function () {
 
if ( typeof text !== 'string' ) {
text = text.toString();
}
}
 
// IE / Mac fix
// remove overlaps, join close fragments
this.text = text.replace( /\r\n?/g, '\n');
var fragmentStart = [];
 
var fragmentEnd = [];
// Parse and count words and chunks for identification of unique real words
var fragmentStartType = [];
if ( this.parent.config.timer === true ) {
var fragmentEndType = [];
this.parent.time( 'wordParse' );
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 ++) {
if (rangeStart[i] > fragmentEnd[j - 1] + wDiff.fragmentJoin) {
fragmentStart[j] = rangeStart[i];
fragmentEnd[j] = rangeEnd[i];
fragmentStartType[j] = rangeStartType[i];
fragmentEndType[j] = rangeEndType[i];
j ++;
}
this.wordParse( this.parent.config.regExp.countWords );
else {
this.wordParse( this.parent.config.regExp.countChunks );
fragmentEnd[j - 1] = rangeEnd[i];
if ( this.parent.config.timer === true ) {
fragmentEndType[j - 1] = rangeEndType[i];
this.parent.timeEnd( 'wordParse' );
}
return;
}
};
 
// assemble the fragments
for (var i = 0; i < fragmentStart.length; i ++) {
 
/**
// get text fragment
* Parse and count words and chunks for identification of unique words.
var fragment = html.substring(fragmentStart[i], fragmentEnd[i]);
*
var fragment = fragment.replace(/^\n+|\n+$/g, '');
* @param string regExp Regular expression for counting words
* @param[in] string text Text of version
* @param[out] array words Number of word occurrences
*/
this.wordParse = function ( regExp ) {
 
var regExpMatch = this.text.match( regExp );
// add inline marks for omitted chars and words
if (fragmentStart[i] >regExpMatch !== null 0) {
var matchLength = regExpMatch.length;
if (fragmentStartType[i] == 'chars') {
for (var i = 0; i < matchLength; i ++) {
fragment = wDiff.htmlOmittedChars + fragment;
var word = regExpMatch[i];
}
if ( Object.prototype.hasOwnProperty.call( this.words, word ) === false ) {
else if (fragmentStartType[i] == 'blank') {
this.words[word] = 1;
fragment = wDiff.htmlOmittedChars + ' ' + fragment;
}
else {
}
this.words[word] ++;
if (fragmentEnd[i] < html.length) {
}
if (fragmentStartType[i] == 'chars') {
fragment = fragment + wDiff.htmlOmittedChars;
}
else if (fragmentStartType[i] == 'blank') {
fragment = fragment + ' ' + wDiff.htmlOmittedChars;
}
}
return;
};
 
// remove leading and trailing empty lines
fragment = fragment.replace(/^\n+|\n+$/g, '');
 
/**
// add fragment separator
* Split text into paragraph, line, sentence, chunk, word, or character tokens.
if (i > 0) {
*
diff += wDiff.htmlSeparator;
* @param string level Level of splitting: paragraph, line, sentence, chunk, word, or character
}
* @param int|null token Index of token to be split, otherwise uses full text
* @param[in] string text Full text to be split
* @param[out] array tokens Tokens list
* @param[out] int first, last First and last index of tokens list
*/
this.splitText = function ( level, token ) {
 
var prev = null;
// encapsulate span errors
var next = null;
diff += wDiff.htmlFragmentStart + fragment + wDiff.htmlFragmentEnd;
var current = this.tokens.length;
}
var first = current;
var text = '';
 
// Split full text or specified token
// add to container
if ( token === undefined ) {
diff = wDiff.htmlContainerStart + diff + wDiff.htmlContainerEnd;
text = this.text;
}
else {
prev = this.tokens[token].prev;
next = this.tokens[token].next;
text = this.tokens[token].token;
}
 
// Split text into tokens, regExp match as separator
// WED('diff', diff);
var number = 0;
var split = [];
var regExpMatch;
var lastIndex = 0;
var regExp = this.parent.config.regExp.split[level];
while ( ( regExpMatch = regExp.exec( text ) ) !== null ) {
if ( regExpMatch.index > lastIndex ) {
split.push( text.substring( lastIndex, regExpMatch.index ) );
}
split.push( regExpMatch[0] );
lastIndex = regExp.lastIndex;
}
if ( lastIndex < text.length ) {
split.push( text.substring( lastIndex ) );
}
 
// Cycle through new tokens
return diff;
var splitLength = split.length;
};
for ( var i = 0; i < splitLength; i ++ ) {
 
// Insert current item, link to previous
this.tokens.push( {
token: split[i],
prev: prev,
next: null,
link: null,
number: null,
unique: false
} );
number ++;
 
// Link previous item to current
//
if ( prev !== null ) {
// wDiff.AddStyleSheet: add CSS rules to new style sheet, cross-browser >= IE6
this.tokens[prev].next = current;
//
}
prev = current;
current ++;
}
 
// Connect last new item and existing next item
wDiff.AddStyleSheet = function(css) {
if ( number > 0 && token !== undefined ) {
if ( prev !== null ) {
this.tokens[prev].next = next;
}
if ( next !== null ) {
this.tokens[next].prev = prev;
}
}
 
// Set text first and last token index
var style = document.createElement('style');
if ( number > 0 ) {
style.type = 'text/css';
if (style.styleSheet != null) {
style.styleSheet.cssText = css;
}
else {
style.appendChild( document.createTextNode(css) );
}
document.getElementsByTagName('head')[0].appendChild(style);
return;
};
 
// Initial text split
if ( token === undefined ) {
this.first = 0;
this.last = prev;
}
 
// First or last token has been split
//
else {
// wDiff.DebugText: dump text (text.oldText or text.newText) object
if ( token === this.first ) {
//
this.first = first;
}
if ( token === this.last ) {
this.last = prev;
}
}
}
return;
};
 
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;
};
 
/**
* Split unique unmatched tokens into smaller tokens.
*
* @param string level Level of splitting: line, sentence, chunk, or word
* @param[in] array tokens Tokens list
*/
this.splitRefine = function ( regExp ) {
 
// Cycle through tokens list
//
var i = this.first;
// wDiff.DebugBlocks: dump blocks object
while ( i !== null ) {
//
 
// Refine unique unmatched tokens into smaller tokens
wDiff.DebugBlocks = function(blocks) {
if ( this.tokens[i].link === null ) {
var dump = '\ni \toldBl \toldNm \tnewNm \tchars \ttype \tsect \tgroup \tfixed \tstring\n';
this.splitText( regExp, i );
for (var i = 0; i < blocks.length; i ++) {
}
dump += i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \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';
i = this.tokens[i].next;
}
}
return dump;
return;
};
};
 
 
//**
* Enumerate text token list before detecting blocks.
// wDiff.DebugGroups: dump groups object
*
//
* @param[out] array tokens Tokens list
*/
this.enumerateTokens = function () {
 
// Enumerate tokens list
wDiff.DebugGroups = function(groups) {
var number = 0;
var dump = '\ni \tblSta \tblEnd \tmWord \twords \tchars \tfixed \oldNm \tmoved \tmFrom \tcolor \tdiff\n';
for ( var i = 0; i < groupsthis.lengthfirst; i ++) {
while ( i !== null ) {
dump += i + ' \t' + groups[i].blockStart + ' \t' + groups[i].blockEnd + ' \t' + groups[i].maxWords + ' \t' + groups[i].words + ' \t' + groups[i].chars + ' \t' + groups[i].fixed + ' \t' + groups[i].oldNumber + ' \t' + groups[i].moved.toString() + ' \t' + groups[i].movedFrom + ' \t' + groups[i].color + ' \t' + wDiff.DebugShortenString(groups[i].diff) + '\n';
this.tokens[i].number = number;
}
number ++;
return dump;
i = this.tokens[i].next;
};
}
return;
};
 
 
//**
* Dump tokens object to browser console.
// wDiff.DebugGaps: dump gaps object
*
//
* @param string name Text name
* @param[in] int first, last First and last index of tokens list
* @param[in] array tokens Tokens list
*/
this.debugText = function ( name ) {
 
var tokens = this.tokens;
wDiff.DebugGaps = function(gaps) {
var dump = '\nifirst: \tnFirs' \tnLast+ \tnTokthis.first \toFirs+ '\toLasttlast: \toTok' \tcharSplit+ this.last + '\n';
dump += '\ni \tlink \t(prev \tnext) \tuniq \t#num \t"token"\n';
for (var i = 0; i < gaps.length; i ++) {
var i = this.first;
dump += i + ' \t' + gaps[i].newFirst + ' \t' + gaps[i].newLast + ' \t' + gaps[i].newTokens + ' \t' + gaps[i].oldFirst + ' \t' + gaps[i].oldLast + ' \t' + gaps[i].oldTokens + ' \t' + gaps[i].charSplit + '\n';
while ( i !== null ) {
}
return dump; +=
i + ' \t' + tokens[i].link + ' \t(' + tokens[i].prev + ' \t' + tokens[i].next + ') \t' +
};
tokens[i].unique + ' \t#' + tokens[i].number + ' \t' +
parent.debugShortenText( tokens[i].token ) + '\n';
i = tokens[i].next;
}
console.log( name + ':\n' + dump );
return;
};
 
 
// Initialize WikEdDiffText object
//
this.init();
// wDiff.DebugShortenString: shorten string for debugging
//
 
wDiff.DebugShortenString = function(string) {
if (string == null) {
return 'null';
}
string = string.replace(/\n/g, '\\n');
var max = 100;
if (string.length > max) {
string = string.substr(0, max - 1 - 30) + '…' + string.substr(string.length - 30);
}
return '"' + string + '"';
};
 
 
// initialize wDiff
wDiff.Init();
 
// </syntaxhighlight>