User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.0.8 (September 02, 2014) css styles to classes, disable colors, add scrolling between block and its mark, add bloack and mark highlighting on hovering
another background that could use some darkmode friendly color
 
(36 intermediate revisions by 3 users not shown)
Line 2:
 
// ==UserScript==
// @name wDiffwikEd diff
// @version 1.02.84
// @date SeptemberOctober 0223, 2014
// @description improved word-based diff library with block move detection
// @homepage https://en.wikipedia.org/wiki/User:Cacycle/diff
Line 12:
// ==/UserScript==
 
/**
* wikEd diff: inline-style difference engine with block move support
*
* Improved JavaScript diff library that returns html/css-formatted new text version with
* highlighted deletions, insertions, and block moves. It is compatible with all browsers and is
* not dependent on external libraries.
*
* WikEdDiff.php and the JavaScript library wikEd diff are synced one-to-one ports. Changes and
* 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: number of block in old text order
* Show complete un-clipped diff text (false)
.newBlock: number of block in new text order
*/
.oldNumber: old text token number of first token in block
'fullDiff': false,
.newNumber: new text token number of first token in block
.oldStart: old text token index of first token in block
.count number of token in block
.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 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,
 
/**
// JSHint options: W004: is already defined, W097: Use the function form of "use strict", W100: This character may get silently deleted by one or more browsers
* @var bool config.repeatedDiff
/* jshint -W004, -W097, -W100, newcap: false, browser: true, jquery: true, sub: true, bitwise: true, curly: false, evil: true, forin: true, freeze: true, immed: true, latedef: true, loopfunc: true, quotmark: single, undef: true */
* Enable repeated diff to resolve problematic sequences (true)
/* global console */
*/
'repeatedDiff': true,
 
/**
// turn on ECMAScript 5 strict mode
* @var bool config.recursiveDiff
'use strict';
* Enable recursive diff to resolve problematic sequences (true)
*/
'recursiveDiff': true,
 
/**
// define global object
* @var int config.recursionMax
var wDiff; if (wDiff === undefined) { wDiff = {}; }
* Maximum recursion depth (10)
var WED;
*/
'recursionMax': 10,
 
/**
//
* @var bool config.unlinkBlocks
// core diff settings
* Reject blocks if they are too short and their words are not unique,
//
* prevents fragmentated diffs for very different versions (true)
*/
'unlinkBlocks': true,
 
/**
// enable block move layout with color coded blocks and marks at their original position
* @var int config.unlinkMax
if (wDiff.showBlockMoves === undefined) { wDiff.showBlockMoves = true; }
* Maximum number of rejection cycles (5)
*/
'unlinkMax': 5,
 
/**
// minimal number of real words for a moved block (0 for always showing color coded blocks)
if (wDiff.blockMinLength* ===@var undefined)int { wDiffconfig.blockMinLength = 3; }
* Reject blocks if shorter than this number of real words (3)
*/
'blockMinLength': 3,
 
/**
// further resolve replacements character-wise from start and end
* @var bool config.coloredBlocks
if (wDiff.charDiff === undefined) { wDiff.charDiff = true; }
* Display blocks in differing colors (rainbow color scheme) (false)
*/
'coloredBlocks': false,
 
/**
// enable recursive diff to resolve problematic sequences
* @var bool config.coloredBlocks
if (wDiff.recursiveDiff === undefined) { wDiff.recursiveDiff = true; }
* Do not use UniCode block move marks (legacy browsers) (false)
*/
'noUnicodeSymbols': false,
 
/**
// display blocks in different colors
* @var bool config.stripTrailingNewline
if (wDiff.coloredBlocks === undefined) { wDiff.coloredBlocks = false; }
* Strip trailing newline off of texts (true in .js, false in .php)
*/
'stripTrailingNewline': true,
 
/**
// UniCode letter support for regexps, from http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
* @var bool config.debug
if (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'); }
* Show debug infos and stats (block, group, and fragment data) in debug console (false)
*/
'debug': false,
 
/**
// regExp for splitting into paragraphs after newline
* @var bool config.timer
if (wDiff.regExpParagraph === undefined) { wDiff.regExpParagraph = new RegExp('(.|\\n)+?(\\n|$)', 'g'); }
* Show timing results in debug console (false)
*/
'timer': false,
 
/**
// regExp for splitting into sentences after .spaces or before newline
* @var bool config.unitTesting
if (wDiff.regExpSentence === undefined) { wDiff.regExpSentence = new RegExp('\\n|.*?\\.( +|(?=\\n))|.+?(?=\\n)', 'g'); }
* Run unit tests to prove correct working, display results in debug console (false)
*/
'unitTesting': false,
 
/** RegExp character classes. */
// regExp for splitting into words, multi-char markup, and chars
if (wDiff.regExpWord === undefined) { wDiff.regExpWord = new RegExp('([' + wDiff.letters + '])+|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.', 'g'); }
 
// UniCode letter support for regexps
// regExp for splitting into chars
// From http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
if (wDiff.regExpChar === undefined) { wDiff.regExpChar = new RegExp('.', 'g'); }
'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
// regExps for bubbling up gaps
'regExpNewLines': '\\u0085\\u2028',
if (wDiff.regExpBubbleStop === undefined) { wDiff.regExpBubbleStop = /\n$/; }
'regExpNewLinesAll': '\\n\\r\\u0085\\u2028',
if (wDiff.regExpBubbleClosing === undefined) { wDiff.regExpBubbleClosing = /^[\s)\]}>\-–—.,:;?!’\/\\=+]/; }
 
// Breaking white space characters without \n, \r, and \f
// regExp for counting words
'regExpBlanks': ' \\t\\x0b\\u2000-\\u200b\\u202f\\u205f\\u3000',
if (wDiff.regExpWordCount === undefined) { wDiff.regExpWordCount = new RegExp('(^|[^' + wDiff.letters + '])[' + wDiff.letters + '][' + wDiff.letters + '_\'’]*', 'g'); }
 
// Full stops without '.'
//
'regExpFullStops':
// shorten output settings
'\\u0589\\u06D4\\u0701\\u0702\\u0964\\u0DF4\\u1362\\u166E\\u1803\\u1809' +
//
'\\u2CF9\\u2CFE\\u2E3C\\u3002\\uA4FF\\uA60E\\uA6F3\\uFE52\\uFF0E\\uFF61',
 
// New paragraph characters without \n and \r
// characters before diff tag to search for previous heading, paragraph, line break, cut characters
'regExpNewParagraph': '\\f\\u2029',
if (wDiff.headingBefore === undefined) { wDiff.headingBefore = 1500; }
if (wDiff.paragraphBefore === undefined) { wDiff.paragraphBefore = 1500; }
if (wDiff.lineBeforeMax === undefined) { wDiff.lineBeforeMax = 1000; }
if (wDiff.lineBeforeMin === undefined) { wDiff.lineBeforeMin = 500; }
if (wDiff.blankBeforeMax === undefined) { wDiff.blankBeforeMax = 1000; }
if (wDiff.blankBeforeMin === undefined) { wDiff.blankBeforeMin = 500; }
if (wDiff.charsBefore === undefined) { wDiff.charsBefore = 500; }
 
// Exclamation marks without '!'
// characters after diff tag to search for next heading, paragraph, line break, or characters
'regExpExclamationMarks':
if (wDiff.headingAfter === undefined) { wDiff.headingAfter = 1500; }
'\\u01C3\\u01C3\\u01C3\\u055C\\u055C\\u07F9\\u1944\\u1944' +
if (wDiff.paragraphAfter === undefined) { wDiff.paragraphAfter = 1500; }
'\\u203C\\u203C\\u2048\\u2048\\uFE15\\uFE57\\uFF01',
if (wDiff.lineAfterMax === undefined) { wDiff.lineAfterMax = 1000; }
if (wDiff.lineAfterMin === undefined) { wDiff.lineAfterMin = 500; }
if (wDiff.blankAfterMax === undefined) { wDiff.blankAfterMax = 1000; }
if (wDiff.blankAfterMin === undefined) { wDiff.blankAfterMin = 500; }
if (wDiff.charsAfter === undefined) { wDiff.charsAfter = 500; }
 
// Question marks without '?'
// lines before and after diff tag to search for previous heading, paragraph, line break, cut characters
'regExpQuestionMarks':
if (wDiff.linesBeforeMax === undefined) { wDiff.linesBeforeMax = 10; }
'\\u037E\\u055E\\u061F\\u1367\\u1945\\u2047\\u2049' +
if (wDiff.linesAfterMax === undefined) { wDiff.linesAfterMax = 10; }
'\\u2CFA\\u2CFB\\u2E2E\\uA60F\\uA6F7\\uFE56\\uFF1F',
 
/** Clip settings. */
// maximal fragment distance to join close fragments
if (wDiff.fragmentJoinLines === undefined) { wDiff.fragmentJoinLines = 10; }
if (wDiff.fragmentJoinChars === undefined) { wDiff.fragmentJoinChars = 1000; }
 
// Find clip position: characters from right
//
'clipHeadingLeft': 1500,
// css classes
'clipParagraphLeftMax': 1500,
//
'clipParagraphLeftMin': 500,
'clipLineLeftMax': 1000,
'clipLineLeftMin': 500,
'clipBlankLeftMax': 1000,
'clipBlankLeftMin': 500,
'clipCharsLeft': 500,
 
// Find clip position: characters from right
if (wDiff.stylesheet === undefined) {
'clipHeadingRight': 1500,
wDiff.stylesheet =
'clipParagraphRightMax': 1500,
'.wDiffTab:before { content: "→"; color: #bbb; font-size: smaller; }' +
'clipParagraphRightMin': 500,
'.wDiffNewline:before { content: "¶"; color: #ccc; padding: 0 0.2em 0 1px; }' +
'clipLineRightMax': 1000,
'.wDiffMarkRight:before { content: "▶"; }' +
'clipLineRightMin': 500,
'.wDiffMarkLeft:before { content: "◀"; }' +
'clipBlankRightMax': 1000,
'.wDiffDelete { font-weight: normal; text-decoration: none; color: #fff; background-color: #c33; border-radius: 0.25em; padding: 0.2em 1px; }' +
'clipBlankRightMin': 500,
'.wDiffInsert { font-weight: normal; text-decoration: none; color: #fff; background-color: #07e; border-radius: 0.25em; padding: 0.2em 1px; }' +
'clipCharsRight': 500,
'.wDiffBlockLeft { background-color: #ddd; border-radius: 0.25em; padding: 0.25em 1px; margin: 0 1px; }' +
'.wDiffBlockRight { background-color: #ddd; border-radius: 0.25em; padding: 0.25em 1px; margin: 0 1px; }' +
'.wDiffMarkLeft { color: #ddd; background-color: #c33; border-radius: 0.25em; padding: 0.2em 0.2em; margin: 0 1px; }' +
'.wDiffMarkRight { color: #ddd; background-color: #c33; border-radius: 0.25em; padding: 0.2em 0.2em; margin: 0 1px; }' +
'.wDiffFragment { 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; }' +
'.wDiffNoChange { 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; }' +
'.wDiffSeparator { margin-bottom: 1em; }' +
'.wDiffSeparator { }' +
'.wDiffBlock { background-color: #ddd; }' +
'.wDiffBlock0 { background-color: #ffff60; }' +
'.wDiffBlock1 { background-color: #c0ff60; }' +
'.wDiffBlock2 { background-color: #ffd8ff; }' +
'.wDiffBlock3 { background-color: #a0ffff; }' +
'.wDiffBlock4 { background-color: #ffe840; }' +
'.wDiffBlock5 { background-color: #bbccff; }' +
'.wDiffBlock6 { background-color: #ffaaff; }' +
'.wDiffBlock7 { background-color: #ffbbbb; }' +
'.wDiffBlock8 { background-color: #a0e8a0; }' +
'.wDiffMark { color: #ddd; }' +
'.wDiffMark0 { color: #ffff60; }' +
'.wDiffMark1 { color: #c0ff60; }' +
'.wDiffMark2 { color: #ffd8ff; }' +
'.wDiffMark3 { color: #a0ffff; }' +
'.wDiffMark4 { color: #ffd840; }' +
'.wDiffMark5 { color: #bbccff; }' +
'.wDiffMark6 { color: #ff99ff; }' +
'.wDiffMark7 { color: #ff9999; }' +
'.wDiffMark8 { color: #90d090; }' +
'.wDiffBlockHighlight { background-color: #333; color: #fff; }' +
'.wDiffMarkHighlight { background-color: #333; color: #fff; }';
}
 
// Maximum number of lines to search for clip position
//
'clipLinesRightMax': 10,
// css styles
'clipLinesLeftMax': 10,
//
 
// Skip clipping if ranges are too close
if (wDiff.styleContainer === undefined) { wDiff.styleContainer = ''; }
'clipSkipLines': 5,
if (wDiff.StyleDelete === undefined) { wDiff.styleDelete = ''; }
'clipSkipChars': 1000,
if (wDiff.styleInsert === undefined) { wDiff.styleInsert = ''; }
if (wDiff.styleBlockLeft === undefined) { wDiff.styleBlockLeft = ''; }
if (wDiff.styleBlockRight === undefined) { wDiff.styleBlockRight = ''; }
if (wDiff.styleBlockHighlight === undefined) { wDiff.styleBlockHighlight = ''; }
if (wDiff.styleBlockColor === undefined) { wDiff.styleBlockColor = []; }
if (wDiff.styleMarkLeft === undefined) { wDiff.styleMarkLeft = ''; }
if (wDiff.styleMarkRight === undefined) { wDiff.styleMarkRight = ''; }
if (wDiff.styleMarkColor === undefined) { wDiff.styleMarkColor = []; }
if (wDiff.styleNewline === undefined) { wDiff.styleNewline = ''; }
if (wDiff.styleTab === undefined) { wDiff.styleTab = ''; }
if (wDiff.styleFragment === undefined) { wDiff.styleFragment = ''; }
if (wDiff.styleNoChange === undefined) { wDiff.styleNoChange = ''; }
if (wDiff.styleSeparator === undefined) { wDiff.styleSeparator = ''; }
if (wDiff.styleOmittedChars === undefined) { wDiff.styleOmittedChars = ''; }
 
// Css stylesheet
//
'cssMarkLeft': '◀',
// html for core diff
'cssMarkRight': '▶',
//
'stylesheet':
 
// Insert
// dynamic replacements: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
'.wikEdDiffInsert {' +
// class plus html comment are required indicators for wDiff.ShortenOutput()
'font-weight: bold; background-color: #bbddff; ' +
if (wDiff.blockEvent === undefined) { wDiff.blockEvent = ' onmouseover="wDiff.BlockHandler(null, this);"'; }
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'} ' +
'.wikEdDiffInsertBlank { background-color: #66bbff; } ' +
'.wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; } ' +
 
// Delete
if (wDiff.htmlContainerStart === undefined) { wDiff.htmlContainerStart = '<div class="wDiffContainer" style="' + wDiff.styleContainer + '">'; }
'.wikEdDiffDelete {' +
if (wDiff.htmlContainerEnd === undefined) { wDiff.htmlContainerEnd = '</div>'; }
'font-weight: bold; background-color: #ffe49c; ' +
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'} ' +
'.wikEdDiffDeleteBlank { background-color: #ffd064; } ' +
'.wikEdDiffFragment:hover .wikEdDiffDeleteBlank { background-color: #ffe49c; } ' +
 
// Block
if (wDiff.htmlDeleteStart === undefined) { wDiff.htmlDeleteStart = '<span class="wDiffDelete" style="' + wDiff.styleDelete + '" title="−">'; }
'.wikEdDiffBlock {' +
if (wDiff.htmlDeleteEnd === undefined) { wDiff.htmlDeleteEnd = '</span><!--wDiffDelete-->'; }
'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
if (wDiff.htmlInsertStart === undefined) { wDiff.htmlInsertStart = '<span class="wDiffInsert" style="' + wDiff.styleInsert + '" title="+">'; }
'.wikEdDiffMarkLeft, .wikEdDiffMarkRight {' +
if (wDiff.htmlInsertEnd === undefined) { wDiff.htmlInsertEnd = '</span><!--wDiffInsert-->'; }
'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
if (wDiff.htmlBlockLeftStart === undefined) { wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft wDiffBlock{class}" style="' + wDiff.styleBlockLeft + '{block}" title="▶ ▢" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
'.wikEdDiffContainer { } ' +
if (wDiff.htmlBlockLeftEnd === undefined) { wDiff.htmlBlockLeftEnd = '</span><!--wDiffBlockLeft-->'; }
'.wikEdDiffFragment {' +
'white-space: pre-wrap; background-color: var(--background-color-base, #fff); border: #bbb solid; ' +
'border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; ' +
'font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; ' +
'} ' +
'.wikEdDiffNoChange { background: var(--background-color-interactive, #eaecf0); border: 1px #bbb solid; border-radius: 0.5em; ' +
'line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; ' +
'text-align: center; ' +
'} ' +
'.wikEdDiffSeparator { margin-bottom: 1em; } ' +
'.wikEdDiffOmittedChars { } ' +
 
// Newline
if (wDiff.htmlBlockRightStart === undefined) { wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight wDiffBlock{class}" style="' + wDiff.styleBlockRight + '{block}" title="▭ ◀" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
'.wikEdDiffNewline:before { content: "¶"; color: transparent; } ' +
if (wDiff.htmlBlockRightEnd === undefined) { wDiff.htmlBlockRightEnd = '</span><!--wDiffBlockRight-->'; }
'.wikEdDiffBlock:hover .wikEdDiffNewline:before { color: #aaa; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffNewline:before { color: transparent; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffNewline:before { color: #ccc; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffNewline:before, ' +
'.wikEdDiffInsert:hover .wikEdDiffNewline:before' +
'{ color: #999; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffNewline:before, ' +
'.wikEdDiffDelete:hover .wikEdDiffNewline:before' +
'{ color: #aaa; } ' +
 
// Tab
if (wDiff.htmlMarkRight === undefined) { wDiff.htmlMarkRight = '<span class="wDiffMarkRight wDiffMark{class}" style="' + wDiff.styleMarkRight + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->'; }
'.wikEdDiffTab { position: relative; } ' +
if (wDiff.htmlMarkLeft === undefined) { wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft wDiffMark{class}" style="' + wDiff.styleMarkLeft + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->'; }
'.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
if (wDiff.htmlNewline === undefined) { wDiff.htmlNewline = '<span class="wDiffNewline" style="' + wDiff.styleNewline + '"></span>\n'; }
'.wikEdDiffSpace { position: relative; } ' +
if (wDiff.htmlTab === undefined) { wDiff.htmlTab = '<span class="wDiffTab" style="' + wDiff.styleTab + '">\t</span>'; }
'.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,' +
// html for shorten output
'.wikEdDiffError .wikEdDiffNoChange' +
//
'{ background: #faa; }'
};
 
/** Add regular expressions to configuration settings. */
if (wDiff.htmlFragmentStart === undefined) { wDiff.htmlFragmentStart = '<pre class="wDiffFragment" style="' + wDiff.styleFragment + '">'; }
if (wDiff.htmlFragmentEnd === undefined) { wDiff.htmlFragmentEnd = '</pre>'; }
 
this.config.regExp = {
if (wDiff.htmlNoChange === undefined) { wDiff.htmlNoChange = '<pre class="wDiffFragment" style="' + wDiff.styleNoChange + '" title="="></pre>'; }
if (wDiff.htmlSeparator === undefined) { wDiff.htmlSeparator = '<div class="wDiffStyleSeparator" style="' + wDiff.styleSeparator + '"></div>'; }
if (wDiff.htmlOmittedChars === undefined) { wDiff.htmlOmittedChars = '<span class="wDiffOmittedChars" style="' + wDiff.styleOmittedChars + '">…</span>'; }
 
// RegExps for splitting text
'split': {
 
// Split into paragraphs, after double newlines
//
'paragraph': new RegExp(
// javascript handler for output code
'(\\r\\n|\\n|\\r){2,}|[' +
//
this.config.regExpNewParagraph +
']',
'g'
),
 
// Split into lines
// wDiff.BlockHandler: event handler for block and mark elements
'line': new RegExp(
if (wDiff.BlockHandler === undefined) { wDiff.BlockHandler = function (event, element) {
'\\r\\n|\\n|\\r|[' +
this.config.regExpNewLinesAll +
']',
'g'
),
 
// Split into sentences /[^ ].*?[.!?:;]+(?= |$)/
// get event data
'sentence': new RegExp(
var type;
'[^' +
if (event !== null) {
this.config.regExpBlanks +
element = event.currentTarget;
'].*?[.!?:;' +
type = event.type;
this.config.regExpFullStops +
event.stopPropagation();
this.config.regExpExclamationMarks +
}
this.config.regExpQuestionMarks +
']+(?=[' +
this.config.regExpBlanks +
']|$)',
'g'
),
 
// Split into inline chunks
// get mark/block elements
'chunk': new RegExp(
var number = element.id.replace(/\D/g, '');
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
var block = document.getElementById('wDiffBlock' + number);
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
var mark = document.getElementById('wDiffMark' + number);
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
switch (type) {
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
 
// Split into words, multi-char markup, and chars
// highlight corresponding mark/block pairs
// regExpLetters speed-up: \\w+
case undefined:
'word': new RegExp(
case 'mouseover':
'(\\w+|[_' +
if (element.addEventListener !== undefined) {
this.config.regExpLetters +
element.addEventListener('mouseout', wDiff.BlockHandler, false);
'])+([\'’][_' +
element.addEventListener('click', wDiff.BlockHandler, false);
this.config.regExpLetters +
}
']*)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.',
else if (element.attachEvent !== undefined) {
'g'
element.attachEvent('onmouseout', wDiff.BlockHandler);
),
element.attachEvent('onclick', wDiff.BlockHandler);
}
else {
return;
}
block.className += ' wDiffBlockHighlight';
mark.className += ' wDiffMarkHighlight';
break;
 
// Split into chars
// remove mark/block highlighting
case 'mouseoutcharacter': /./g
},
var highlighted = document.getElementsByClassName('wDiffBlockHighlight');
for (var i = 0; i < highlighted.length; i ++) {
highlighted[i].className = highlighted[i].className.replace(/ wDiffBlockHighlight/g, '');
}
var highlighted = document.getElementsByClassName('wDiffMarkHighlight');
for (var i = 0; i < highlighted.length; i ++) {
highlighted[i].className = highlighted[i].className.replace(/ wDiffMarkHighlight/g, '');
}
break;
 
// scrollRegExp to correspondingdetect mark/blockblank elementtokens
'blankOnlyToken': new RegExp(
case 'click':
'[^' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']'
),
 
// RegExps for sliding gaps: newlines and space/word breaks
// remove element click handler
'slideStop': new RegExp(
if (element.removeEventListener !== undefined) {
'[' +
element.removeEventListener('mouseout', wDiff.BlockHandler, false);
this.config.regExpNewLinesAll +
}
this.config.regExpNewParagraph +
else {
']$'
element.detachEvent('onmouseout', wDiff.BlockHandler);
),
'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;
}
 
// Get mark/block elements
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
if ( type === 'mouseover' ) {
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
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
var container = document.getElementById( 'wikEdDiffContainer' );
if ( container !== null ) {
var spans = container.getElementsByTagName( 'span' );
var spansLength = spans.length;
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
if ( type === 'click' ) {
 
// getGet corresponding element
var corrElement;
if ( element === block ) {
corrElement = mark;
}
Line 350 ⟶ 794:
}
 
// Get element height (getOffsetTop)
// get offsetTop
var corrElementPos = 0;
var node = corrElement;
do {
corrElementPos += node.offsetTop;
} while ( ( node = node.offsetParent ) !== null );
 
// Get scroll element under mouse cursorheight
var top = window.pageYOffset;
if ( window.pageYOffset !== undefined ) {
var cursor = event.pageY;
top = window.pageYOffset;
var line = parseInt(window.getComputedStyle(corrElement).getPropertyValue('line-height'));
}
window.scroll(0, corrElementPos + top - cursor + line / 2);
break;else {
top = document.documentElement.scrollTop;
}
}
return;
}; }
 
// Get cursor pos
//
var cursor;
// start of diff code
if ( event.pageY !== undefined ) {
//
cursor = event.pageY;
}
else if ( event.clientY !== undefined ) {
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
// wDiff.Init: initialize wDiff
window.scroll( 0, corrElementPos + top - cursor + line / 2 );
// called from: on code load
}
// calls: wDiff.AddStyleSheet()
return;
};
 
/** Internal data structures. */
wDiff.Init = function () {
 
/** @var WikEdDiffText newText New text version object with text and token list */
// compatibility fixes for old names of functions
this.newText = null;
window.StringDiff = wDiff.Diff;
window.WDiffString = wDiff.Diff;
window.WDiffShortenOutput = wDiff.ShortenOutput;
 
/** @var WikEdDiffText oldText Old text version object with text and token list */
// shortcut to wikEd.Debug()
this.oldText = null;
if (WED === undefined) {
if (typeof console == 'object') {
WED = console.log;
}
else {
WED = window.alert;
}
}
 
/** @var object symbols Symbols table for whole text at all refinement levels */
// add styles to head
this.symbols = {
wDiff.AddStyleSheet(wDiff.stylesheet);
token: [],
hashTable: {},
linked: false
};
 
/** @var array bordersDown Matched region borders downwards */
// add block handler to head if running under Greasemonkey
this.bordersDown = [];
if (typeof GM_info == 'object') {
var script = 'var wDiff; if (wDiff === undefined) { wDiff = {}; } wDiff.BlockHandler = ' + wDiff.BlockHandler.toString();
wDiff.AddScript(script);
}
return;
};
 
/** @var array bordersUp Matched region borders upwards */
this.bordersUp = [];
 
/** @var array blocks Block data (consecutive text tokens) in new text order */
// wDiff.Diff: main method
this.blocks = [];
// input: oldString, newString, strings containing the texts to be diffed
// called from: user code
// calls: wDiff.Split(), wDiff.SplitRefine(), wDiff.CalculateDiff(), wDiff.DetectBlocks(), wDiff.AssembleDiff()
// returns: diff html code, call wDiff.ShortenOutput() for shortening this output
 
/** @var int maxWords Maximal detected word count of all linked blocks */
wDiff.Diff = function (oldString, newString) {
this.maxWords = 0;
 
/** @var array groups Section blocks that are consecutive in old text order */
var diff = '';
this.groups = [];
 
/** @var array sections Block sections with no block move crosses outside a section */
// IE / Mac fix
this.sections = [];
oldString = oldString.replace(/\r\n?/g, '\n');
newString = newString.replace(/\r\n?/g, '\n');
 
/** @var object timer Debug timer array: string 'label' => float milliseconds. */
// prepare text data object
var textthis.timer = {};
newText: {
string: newString,
tokens: [],
first: null,
last: null
},
oldText: {
string: oldString,
tokens: [],
first: null,
last: null
},
diff: ''
};
 
/** @var array recursionTimer Count time spent in recursion level in milliseconds. */
// trap trivial changes: no change
this.recursionTimer = [];
if (oldString == newString) {
text.diff = wDiff.HtmlEscape(newString);
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** Output data. */
// trap trivial changes: old text deleted
if ( (oldString === null) || (oldString.length === 0) ) {
text.diff = wDiff.htmlInsertStart + wDiff.HtmlEscape(newString) + wDiff.htmlInsertEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var bool error Unit tests have detected a diff error */
// trap trivial changes: new text deleted
this.error = false;
if ( (newString === null) || (newString.length === 0) ) {
text.diff = wDiff.htmlDeleteStart + wDiff.HtmlEscape(oldString) + wDiff.htmlDeleteEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var array fragments Diff fragment list for markup, abstraction layer for customization */
// split new and old text into paragraps
this.fragments = [];
wDiff.Split(text.newText, wDiff.regExpParagraph);
wDiff.Split(text.oldText, wDiff.regExpParagraph);
 
/** @var string html Html code of diff */
// calculate diff
this.html = '';
wDiff.CalculateDiff(text);
 
// refine different paragraphs into sentences
wDiff.SplitRefine(text.newText, wDiff.regExpSentence);
wDiff.SplitRefine(text.oldText, wDiff.regExpSentence);
 
/**
// calculate refined diff
* Constructor, initialize settings, load js and css.
wDiff.CalculateDiff(text);
*
* @param[in] object wikEdDiffConfig Custom customization settings
* @param[out] object config Settings
*/
 
this.init = function () {
// refine different sentences into words
wDiff.SplitRefine(text.newText, wDiff.regExpWord);
wDiff.SplitRefine(text.oldText, wDiff.regExpWord);
 
// Import customizations from wikEdDiffConfig{}
// calculate refined diff information with recursion for unresolved gaps
if ( typeof wikEdDiffConfig === 'object' ) {
wDiff.CalculateDiff(text, true);
this.deepCopy( wikEdDiffConfig, this.config );
}
 
// Add CSS stylescheet
// bubble up gaps
this.addStyleSheet( this.config.stylesheet );
wDiff.BubbleUpGaps(text.newText, text.oldText);
wDiff.BubbleUpGaps(text.oldText, text.newText);
 
// Load block handler script
// split tokens into chars in selected unresolved gaps
if (wDiff this.charDiffconfig.showBlockMoves === true ) {
wDiff.SplitRefineChars(text);
 
// Add block handler to head if running under Greasemonkey
// calculate refined diff information with recursion for unresolved gaps
if ( typeof GM_info === 'object' ) {
wDiff.CalculateDiff(text, true);
var script = 'var wikEdDiffBlockHandler = ' + this.config.blockHandler.toString() + ';';
}
this.addScript( script );
}
else {
window.wikEdDiffBlockHandler = this.config.blockHandler;
}
}
return;
};
 
// bubble up gaps
wDiff.BubbleUpGaps(text.newText, text.oldText);
wDiff.BubbleUpGaps(text.oldText, text.newText);
 
/**
// enumerate tokens lists
* Main diff method.
wDiff.EnumerateTokens(text.newText);
*
wDiff.EnumerateTokens(text.oldText);
* @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 ) {
 
// detectStart movedtotal blockstimer
if ( this.config.timer === true ) {
var blocks = [];
this.time( 'total' );
var groups = [];
}
wDiff.DetectBlocks(text, blocks, groups);
 
// Start diff timer
// assemble diff blocks into formatted html text
if ( this.config.timer === true ) {
diff = wDiff.AssembleDiff(text, blocks, groups);
this.time( 'diff' );
}
 
// Reset error flag
return diff;
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
// wDiff.Split: split text into paragraph, sentence, or word tokens
this.newText = new WikEdDiff.WikEdDiffText( newString, this );
// 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
this.oldText = new WikEdDiff.WikEdDiffText( oldString, this );
// changes: text (text.newText or text.oldText): text.tokens list, text.first, text.last
// called from: wDiff.Diff()
 
// Trap trivial changes: no change
wDiff.Split = function (text, regExp, token) {
if ( this.newText.text === this.oldText.text ) {
this.html =
this.config.htmlCode.containerStart +
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd +
this.config.htmlCode.containerEnd;
return this.html;
}
 
// Trap trivial changes: old text deleted
var prev = null;
if (
var next = null;
this.oldText.text === '' || (
var current = text.tokens.length;
this.oldText.text === '\n' &&
var first = current;
( this.newText.text.charAt( this.newText.text.length - 1 ) === '\n' )
var string = '';
)
) {
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;
}
 
// Trap trivial changes: new text deleted
// split full text or specified token
if (
if (token === undefined) {
this.newText.text === '' || (
string = text.string;
this.newText.text === '\n' &&
}
( this.oldText.text.charAt( this.oldText.text.length - 1 ) === '\n' )
else {
)
prev = text.tokens[token].prev;
) {
next = text.tokens[token].next;
this.html =
string = text.tokens[token].token;
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;
}
 
// splitSplit new and old text into tokensparagraps
if ( this.config.timer === true ) {
var number = 0;
this.time( 'paragraph split' );
var regExpMatch;
}
while ( (regExpMatch = regExp.exec(string)) !== null) {
this.newText.splitText( 'paragraph' );
this.oldText.splitText( 'paragraph' );
if ( this.config.timer === true ) {
this.timeEnd( 'paragraph split' );
}
 
// Calculate diff
// insert current item, link to previous
this.calculateDiff( 'line' );
text.tokens[current] = {
token: regExpMatch[0],
prev: prev,
next: null,
link: null,
number: null,
parsed: false
};
number ++;
 
// Refine different paragraphs into lines
// link previous item to current
if (prev !this.config.timer === true null) {
this.time( 'line split' );
text.tokens[prev].next = current;
}
this.newText.splitRefine( 'line' );
prev = current;
this.oldText.splitRefine( 'line' );
current ++;
if ( this.config.timer === true ) {
}
this.timeEnd( 'line split' );
}
 
// Calculate refined diff
this.calculateDiff( 'line' );
 
// Refine different lines into sentences
// connect last new item and existing next item
if ( (numberthis.config.timer > 0) && (token !=== undefined)true ) {
this.time( 'sentence split' );
if (prev !== null) {
text.tokens[prev].next = next;
}
this.newText.splitRefine( 'sentence' );
if (next !== null) {
this.oldText.splitRefine( 'sentence' );
text.tokens[next].prev = prev;
if ( this.config.timer === true ) {
this.timeEnd( 'sentence split' );
}
}
 
// Calculate refined diff
// set text first and last token index
this.calculateDiff( 'sentence' );
if (number > 0) {
 
// Refine different sentences into chunks
// initial text split
if (token this.config.timer === undefinedtrue ) {
textthis.firsttime( ='chunk 0split' );
}
text.last = prev;
this.newText.splitRefine( 'chunk' );
this.oldText.splitRefine( 'chunk' );
if ( this.config.timer === true ) {
this.timeEnd( 'chunk split' );
}
 
// Calculate refined diff
// first or last token has been split
this.calculateDiff( 'chunk' );
else {
 
if (token == text.first) {
// Refine different chunks into words
text.first = first;
if ( this.config.timer === true ) {
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
this.calculateDiff( 'word', true );
 
// Slide gaps
if ( this.config.timer === true ) {
this.time( 'word slide' );
}
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
if ( this.config.timer === true ) {
this.timeEnd( 'word slide' );
}
 
// Split tokens into chars
if ( this.config.charDiff === true ) {
 
// Split tokens into chars in selected unresolved gaps
if ( this.config.timer === true ) {
this.time( 'character split' );
}
this.splitRefineChars();
if (token == text.last) {
if ( this.config.timer === true ) {
text.last = prev;
this.timeEnd( 'character split' );
}
}
}
return;
};
 
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( 'character', true );
 
// Slide gaps
// 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( 'character slide' );
// called from: wDiff.Diff()
}
// calls: wDiff.Split()
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
if ( this.config.timer === true ) {
this.timeEnd( 'character slide' );
}
}
 
// Free memory
wDiff.SplitRefine = function (text, regExp) {
this.symbols = undefined;
this.bordersDown = undefined;
this.bordersUp = undefined;
this.newText.words = undefined;
this.oldText.words = undefined;
 
// Enumerate token lists
// cycle through tokens list
this.newText.enumerateTokens();
var i = text.first;
this.oldText.enumerateTokens();
while ( (i !== null) && (text.tokens[i] !== null) ) {
 
// Detect moved blocks
// refine unique unmatched tokens into smaller tokens
if (text this.tokens[i]config.linktimer === nulltrue ) {
wDiffthis.Splittime(text, regExp,'blocks' i);
}
this.detectBlocks();
if ( this.config.timer === true ) {
this.timeEnd( 'blocks' );
}
i = text.tokens[i].next;
}
return;
};
 
// Free memory
this.newText.tokens = undefined;
this.oldText.tokens = undefined;
 
// Assemble blocks into fragment table
// wDiff.SplitRefineChars: split tokens into chars in the following unresolved regions (gaps):
this.getDiffFragments();
// - one token became separated by space, dash, or any string
// - same number of tokens in gap and strong similarity of all tokens:
// - addition or deletion of flanking strings in tokens
// - addition or deletion of internal string in tokens
// - same length and at least 50 % identity
// - same start or end, same text longer than different text
// - same length and at least 50 % identity
// identical tokens including space separators will be linked, resulting in word-wise char-level diffs
// changes: text (text.newText or text.oldText) .tokens list
// called from: 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
 
// Free memory
wDiff.SplitRefineChars = function (text) {
this.blocks = undefined;
this.groups = undefined;
this.sections = undefined;
 
// Stop diff timer
//
if ( this.config.timer === true ) {
// find corresponding gaps
this.timeEnd( 'diff' );
//
}
 
// Unit tests
// cycle trough new text tokens list
if ( this.config.unitTesting === true ) {
var gaps = [];
var gap = null;
var i = text.newText.first;
var j = text.oldText.first;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
 
// Test diff to test consistency between input and output
// get token links
if ( this.config.timer === true ) {
var newLink = text.newText.tokens[i].link;
this.time( 'unit tests' );
var oldLink = null;
}
if (j !== null) {
this.unitTests();
oldLink = text.oldText.tokens[j].link;
if ( this.config.timer === true ) {
this.timeEnd( 'unit tests' );
}
}
 
// Clipping
// start of gap in new and old
if ( this.config.fullDiff === false ) {
if ( (gap === null) && (newLink === null) && (oldLink === null) ) {
 
gap = gaps.length;
// Clipping unchanged sections from unmoved block text
gaps.push({
if ( this.config.timer === true ) {
newFirst: i,
newLast:this.time( 'clip' i,);
}
newTokens: 1,
this.clipDiffFragments();
oldFirst: j,
if ( this.config.timer === true ) {
oldLast: j,
this.timeEnd( 'clip' );
oldTokens: null,
}
charSplit: null
});
}
 
// Create html formatted diff code from diff fragments
// count chars and tokens in gap
else if ( (gap !== null) && (newLinkthis.config.timer === null)true ) {
gaps[gap]this.newLasttime( ='html' i);
}
gaps[gap].newTokens ++;
this.getDiffHtml();
if ( this.config.timer === true ) {
this.timeEnd( 'html' );
}
 
// gapNo endedchange
else if ( (gapthis.html !== null) && (newLink !== null)'' ) {
gapthis.html = null;
this.config.htmlCode.containerStart +
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd +
this.config.htmlCode.containerEnd;
}
 
// nextAdd listerror elementsindicator
if (newLink !this.error === true null) {
this.html = this.config.htmlCode.errorStart + this.html + this.config.htmlCode.errorEnd;
j = text.oldText.tokens[newLink].next;
}
i = text.newText.tokens[i].next;
}
 
// Stop total timer
// cycle trough gaps and add old text gap data
if ( this.config.timer === true ) {
for (var gap = 0; gap < gaps.length; gap ++) {
this.timeEnd( 'total' );
}
 
return this.html;
// cycle trough old text tokens list
};
var j = gaps[gap].oldFirst;
while ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.oldText.tokens[j].link === null) ) {
 
// count old chars and tokens in gap
gaps[gap].oldLast = j;
gaps[gap].oldTokens ++;
 
/**
j = text.oldText.tokens[j].next;
* Split tokens into chars in the following unresolved regions (gaps):
}
* - One token became connected or separated by space or dash (or any token)
}
* - 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. */
//
// select gaps of identical token number and strong similarity of all tokens
//
 
// Cycle through new text tokens list
for (var gap = 0; gap < gaps.length; gap ++) {
var charSplitgaps = true[];
var gap = null;
var i = this.newText.first;
var j = this.oldText.first;
while ( i !== null ) {
 
// notGet sametoken gap lengthlinks
var newLink = this.newText.tokens[i].link;
if (gaps[gap].newTokens != gaps[gap].oldTokens) {
var oldLink = null;
if ( j !== null ) {
oldLink = this.oldText.tokens[j].link;
}
 
// Start of gap in new and old
// one word became separated by space, dash, or any string
if ( (gaps[gap].newTokens === 1)null && (gaps[gap].oldTokensnewLink === null && oldLink === 3)null ) {
gap = gaps.length;
if (text.newText.tokens[ gaps[gap].newFirst ].token != text.oldText.tokens[ gaps[gap].oldFirst ].token + text.oldText.tokens[ gaps[gap].oldLast ].token ) {
continue;gaps.push( {
} newFirst: i,
newLast: i,
newTokens: 1,
oldFirst: j,
oldLast: j,
oldTokens: null,
charSplit: null
} );
}
 
else if ( (gaps[gap].oldTokens == 1) && (gaps[gap].newTokens == 3) ) {
// Count chars and tokens in gap
if (text.oldText.tokens[ gaps[gap].oldFirst ].token != text.newText.tokens[ gaps[gap].newFirst ].token + text.newText.tokens[ gaps[gap].newLast ].token ) {
else if ( gap !== null && newLink === null ) {
continue;
gaps[gap].newLast = i;
}
gaps[gap].newTokens ++;
}
 
else {
// Gap ended
continue;
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;
}
}
}
 
// next list elements
if (i == gaps[gap].newLast) {
break;
}
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
return;
gaps[gap].charSplit = charSplit;
};
 
//
// refine words into chars in selected gaps
//
 
/**
for (var gap = 0; gap < gaps.length; gap ++) {
* Move gaps with ambiguous identical fronts to last newline border or otherwise last word border.
if (gaps[gap].charSplit === true) {
*
* @param[in/out] wikEdDiffText text, textLinked These two are newText and oldText
*/
this.slideGaps = function ( text, textLinked ) {
 
var regExpSlideBorder = this.config.regExp.slideBorder;
// cycle trough new text tokens list
var regExpSlideStop = this.config.regExp.slideStop;
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
while (i !== null) {
var newToken = text.newText.tokens[i].token;
var oldToken = text.oldText.tokens[j].token;
 
// linkCycle identicalthrough tokens (spaces)list
var i = text.first;
if (newToken == oldToken) {
var gapStart = null;
text.newText.tokens[i].link = j;
while ( i !== null ) {
text.oldText.tokens[j].link = i;
}
 
// Remember gap start
// refine different words into chars
if ( gapStart === null && text.tokens[i].link === null ) {
else {
gapStart = i;
wDiff.Split(text.newText, wDiff.regExpChar, i);
wDiff.Split(text.oldText, wDiff.regExpChar, j);
}
 
// next list elements
if (i == gaps[gap].newLast) {
break;
}
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
}
}
 
// Find gap end
// WED('Gap', wDiff.DebugGaps(gaps));
else if ( gapStart !== null && text.tokens[i].link !== null ) {
var gapFront = gapStart;
var gapBack = text.tokens[i].prev;
 
// Slide down as deep as possible
return;
var front = gapFront;
};
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 = text.tokens[gapFront].next;
gapBack = text.tokens[gapBack].next;
 
front = text.tokens[front].next;
// wDiff.BubbleUpGaps: move gaps with ambiguous identical fronts and backs up
back = text.tokens[back].next;
// start ambiguous gap borders after line breaks and text section closing characters
}
// changes: text (text.newText or text.oldText) .tokens list
// called from: wDiff.Diff()
 
// Test slide up, remember last line break or word border
wDiff.BubbleUpGaps = function (text, textLinked) {
var front = text.tokens[gapFront].prev;
var back = gapBack;
var gapFrontBlankTest = regExpSlideBorder.test( text.tokens[gapFront].token );
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
// cycle through tokens list
if ( regExpSlideStop.test( text.tokens[front].token ) === true ) {
var i = text.first;
frontStop = front;
var gapStart = null;
break;
while ( (i !== null) && (text.tokens[i] !== null) ) {
}
 
// Stop at first word border (blank/word or word/blank)
// remember gap start
if (
if ( (gapStart === null) && (text.tokens[i].link === null) ) {
regExpSlideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
gapStart = i;
frontStop = front;
}
}
}
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
}
 
// findActually gapslide endup to stop
else var iffront ( (gapStart !== null) && (text.tokens[igapFront].link !== null) ) {prev;
var 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;
// bubble up, stop at line breaks
var front back = text.tokens[gapStartback].prev;
}
var back = text.tokens[i].prev;
gapStart = null;
while (
(front !== null) && (back !== null) && (wDiff.regExpBubbleStop.test(text.tokens[front].token) === false) &&
(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;
}
i = text.tokens[i].next;
 
// do not start gap with spaces or other closing characters, roll back (bubble down)
if ( (back !== null) && (front !== null) ) {
front = text.tokens[front].next;
back = text.tokens[back].next;
}
while (
(back !== null) && (front !== null) && (wDiff.regExpBubbleClosing.test(text.tokens[front].token) === true) &&
(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;
front = text.tokens[front].next;
back = text.tokens[back].next;
}
gapStart = null;
}
return;
i = text.tokens[i].next;
};
return;
};
 
 
/**
// wDiff.EnumerateTokens: enumerate text token list
* Calculate diff information, can be called repeatedly during refining.
// changes: text (text.newText or text.oldText) .tokens list
* Links corresponding tokens from old and new text.
// called from: wDiff.Diff()
* 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
wDiff.EnumerateTokens = function (text) {
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
// enumerate tokens list
if ( this.config.timer === true && repeating === false && recursionLevel === 0 ) {
var number = 0;
this.time( level );
var i = text.first;
}
while ( (i !== null) && (text.tokens[i] !== null) ) {
if ( this.config.timer === true && repeating === false ) {
text.tokens[i].number = number;
this.time( level + recursionLevel );
number ++;
}
i = text.tokens[i].next;
}
return;
};
 
// Get object symbols table and linked region borders
var symbols;
var bordersDown;
var bordersUp;
if ( recursionLevel === 0 && repeating === false ) {
symbols = this.symbols;
bordersDown = this.bordersDown;
bordersUp = this.bordersUp;
}
 
// Create empty local symbols table and linked region borders arrays
// wDiff.CalculateDiff: calculate diff information, can be called repeatedly during refining
else {
// input: text, object containing text data and tokens
symbols = {
// optionally for recursive calls: newStart, newEnd, oldStart, oldEnd (tokens list indexes), recursionLevel
token: [],
// changes: text.oldText/newText.tokens[].link, links corresponding tokens from old and new text
hashTable: {},
// steps:
linked: false
// pass 1: parse new text into symbol table
};
// pass 2: parse old text into symbol table
bordersDown = [];
// pass 3: connect unique tokens
bordersUp = [];
// pass 4: connect adjacent identical tokens downwards
}
// pass 5: connect adjacent identical tokens upwards
// recursively diff still unresolved regions downwards
// recursively diff still unresolved regions upwards
 
wDiff.CalculateDiff = function (text, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
 
// Updated versions of linked region borders
// symbol (token) data
var symbolbordersUpNext = [];
var symbolsbordersDownNext = {}[];
 
/**
// set defaults
* Pass 1: parse new text into symbol table.
if (newStart === undefined) { newStart = text.newText.first; }
*/
if (newEnd === undefined) { newEnd = text.newText.last; }
if (oldStart === undefined) { oldStart = text.oldText.first; }
if (oldEnd === undefined) { oldEnd = text.oldText.last; }
if (recursionLevel === undefined) { recursionLevel = 0; }
 
// Cycle through new text tokens list
// limit recursion depth
var i = newStart;
if (recursionLevel > 10) {
while ( i !== null ) {
return;
if ( this.newText.tokens[i].link === null ) {
}
 
// Add new entry to symbol table
//
var token = this.newText.tokens[i].token;
// pass 1: parse new text into symbol table
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
// cycle trough new text tokens list
else {
var i = newStart;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
 
// parseIncrement token onlycounter oncefor duringnew split refinementtext
var hashToArray = symbols.hashTable[token];
if ( (text.newText.tokens[i].parsed === false) || (recursionLevel > 0) ) {
symbols.token[hashToArray].newCount ++;
text.newText.tokens[i].parsed = true;
}
}
 
// addStop newafter entrygap toif symbol tablerecursing
else if ( recursionLevel > 0 ) {
var token = text.newText.tokens[i].token;
break;
if (Object.prototype.hasOwnProperty.call(symbols, token) === false) {
var current = symbol.length;
symbols[token] = current;
symbol[current] = {
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
};
}
 
// orGet updatenext existing entrytoken
if ( up === false ) {
i = this.newText.tokens[i].next;
}
else {
i = this.newText.tokens[i].prev;
 
// increment token counter for new text
var hashToArray = symbols[token];
symbol[hashToArray].newCount ++;
}
}
 
/**
// next list element
* Pass 2: parse old text into symbol table.
if (i == newEnd) {
break; */
}
i = text.newText.tokens[i].next;
}
 
// Cycle through old text tokens list
//
var j = oldStart;
// pass 2: parse old text into symbol table
while ( j !== null ) {
//
if ( this.oldText.tokens[j].link === null ) {
 
// Add new entry to symbol table
// cycle trough old text tokens list
var token = this.oldText.tokens[j].token;
var j = oldStart;
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
while ( (j !== null) && (text.oldText.tokens[j] !== null) ) {
symbols.hashTable[token] = symbols.token.length;
symbols.token.push( {
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
} );
}
 
// Or update existing entry
// parse token only once during split refinement
else {
if ( (text.oldText.tokens[j].parsed === false) || (recursionLevel > 0) ) {
text.oldText.tokens[j].parsed = true;
 
// addIncrement newtoken entrycounter tofor symbolold tabletext
var tokenhashToArray = textsymbols.oldText.tokenshashTable[jtoken].token;
symbols.token[hashToArray].oldCount ++;
if (Object.prototype.hasOwnProperty.call(symbols, token) === false) {
 
var current = symbol.length;
symbols[ // Add token] =number for old current;text
symbols.token[hashToArray].oldToken = j;
symbol[current] = {
newCount: 0,}
oldCount: 1,
newToken: null,
oldToken: j
};
}
 
// orStop updateafter existinggap entryif recursing
else if ( recursionLevel > 0 ) {
break;
}
 
// incrementGet next token counter for old text
if ( up === false ) {
var hashToArray = symbols[token];
j = this.oldText.tokens[j].next;
symbol[hashToArray].oldCount ++;
}
 
else {
// add token number for old text
j = this.oldText.tokens[j].prev;
symbol[hashToArray].oldToken = j;
}
}
 
/**
// next list element
* Pass 3: connect unique tokens.
if (j === oldEnd) {
break; */
}
j = text.oldText.tokens[j].next;
}
 
// Cycle through symbol array
//
var symbolsLength = symbols.token.length;
// pass 3: connect unique tokens
for ( var i = 0; i < symbolsLength; i ++ ) {
//
 
// Find tokens in the symbol table that occur only once in both versions
// cycle trough symbol array
if ( symbols.token[i].newCount === 1 && symbols.token[i].oldCount === 1 ) {
for (var i = 0; i < symbol.length; i ++) {
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
// find tokens in the symbol table that occur only once in both versions
if ( (symbol[i]newTokenObj.newCountlink == 1) && (symbol[i].oldCount == 1)null ) {
var newToken = symbol[i].newToken;
var oldToken = symbol[i].oldToken;
 
// doDo not use spaces as unique markers
if (
if (/^\s+$/.test(text.newText.tokens[newToken].token) === false) {
this.config.regExp.blankOnlyToken.test( newTokenObj.token ) === true
) {
 
// connect fromLink new to old and from old to newtokens
newTokenObj.link = oldToken;
if (text.newText.tokens[newToken].link === null) {
text.newText.tokens[newToken] oldTokenObj.link = oldTokennewToken;
symbols.linked = true;
text.oldText.tokens[oldToken].link = newToken;
}
}
}
}
 
// Save linked region borders
//
bordersDown.push( [newToken, oldToken] );
// pass 4: connect adjacent identical tokens downwards
bordersUp.push( [newToken, oldToken] );
//
 
// Check if token contains unique word
// cycle trough new text tokens list
if ( recursionLevel === 0 ) {
var i = text.newText.first;
var unique = false;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
if ( level === 'character' ) {
var iNext = text.newText.tokens[i].next;
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
// find already connected pairs
var wordsLength = words.length;
var j = text.newText.tokens[i].link;
if (j !=wordsLength >= nullthis.config.blockMinLength ) {
unique = true;
var jNext = text.oldText.tokens[j].next;
}
 
// checkUnique if theit followingcontains tokensat areleast notone yetunique connectedword
else {
if ( (iNext !== null) && (jNext !== null) ) {
for ( var i = 0;i < wordsLength; i ++ ) {
if ( (text.newText.tokens[iNext].link === null) && (text.oldText.tokens[jNext].link === null) ) {
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
// connect if the following tokens are the same
if ( unique === true ) {
if (text.newText.tokens[iNext].token == text.oldText.tokens[jNext].token) {
newTokenObj.unique = true;
text.newText.tokens[iNext].link = jNext;
oldTokenObj.unique = true;
text.oldText.tokens[jNext].link = iNext;
}
}
}
}
}
}
i = iNext;
}
 
// Continue passes only if unique tokens have been linked previously
//
if ( symbols.linked === true ) {
// pass 5: connect adjacent identical tokens upwards
//
 
/**
// cycle trough new text tokens list
* Pass 4: connect adjacent identical tokens downwards.
var i = text.newText.last;
*/
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
var iNext = text.newText.tokens[i].prev;
 
// Cycle through list of linked new text tokens
// find already connected pairs
var bordersLength = bordersDown.length;
var j = text.newText.tokens[i].link;
for ( var match = 0; match < bordersLength; match ++ ) {
if (j !== null) {
var jNexti = text.oldText.tokensbordersDown[jmatch][0].prev;
var j = bordersDown[match][1];
 
// Next down
// check if the preceeding tokens are not yet connected
var iMatch = i;
if ( (iNext !== null) && (jNext !== null) ) {
var jMatch = j;
if ( (text.newText.tokens[iNext].link === null) && (text.oldText.tokens[jNext].link === null) ) {
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
 
// connectCycle ifthrough thenew preceedingtext tokenslist aregap theregion samedownwards
while (
if (text.newText.tokens[iNext].token == text.oldText.tokens[jNext].token) {
i !== null &&
text.newText.tokens[iNext].link = jNext;
j !== null &&
text.oldText.tokens[jNext].link = iNext;
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;
}
}
}
i = iNext;
}
 
/**
// refine by recursively diffing unresolved regions caused by addition of common tokens around sequences of common tokens, only at word level split
* Pass 5: connect adjacent identical tokens upwards.
if ( (recurse === true) && (wDiff.recursiveDiff === true) ) {
*/
 
// Cycle through list of connected new text tokens
//
var bordersLength = bordersUp.length;
// recursively diff still unresolved regions downwards
for ( var match = 0; match < bordersLength; match ++ ) {
//
var i = bordersUp[match][0];
var j = bordersUp[match][1];
 
// Next up
// cycle trough new text tokens list
var iiMatch = newStarti;
var jjMatch = oldStartj;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
 
// Cycle through new text gap region upwards
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
 
// Connect if same token
// get j from previous tokens match
var if iPrev( = textthis.newText.tokens[i].prev;token === this.oldText.tokens[j].token ) {
this.newText.tokens[i].link = j;
if (iPrev !== null) {
var jPrev = text this.newTextoldText.tokens[iPrevj].link = i;
}
if (jPrev !== null) {
j = text.oldText.tokens[jPrev].next;
}
}
 
// check for the start of an unresolved sequence
if ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) ) {
 
// determineNot thea limitsmatch ofyet, ofmaybe thein unresolvednext newrefinement sequencelevel
var else iStart = i;{
bordersUpNext.push( [iMatch, jMatch] );
var iEnd = null;
var iLength = 0;
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;
// Next token up
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
}
 
/**
// determine the limits of of the unresolved old sequence
* Connect adjacent identical tokens downwards from text start.
var jStart = j;
* Treat boundary as connected, stop after first connected token.
var jEnd = null;
*/
var jLength = 0;
 
var jNext = j;
// Only for full text diff
while ( (jNext !== null) && (text.oldText.tokens[jNext].link === null) ) {
if ( recursionLevel === 0 && repeating === false ) {
jEnd = jNext;
 
jLength ++;
if// (jEndFrom == oldEnd) {start
var i = this.newText.first;
break;
var j = this.oldText.first;
}
var iMatch = null;
jNext = text.oldText.tokens[jNext].next;
var jMatch = null;
 
// Cycle through old text tokens down
// Connect identical tokens, stop after first connected token
while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
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
// recursively diff the unresolved sequence
i = this.newText.last;
if ( (iLength > 0) && (jLength > 0) ) {
j = this.oldText.last;
if ( (iLength > 1) || (jLength > 1) ) {
iMatch = null;
if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) {
jMatch = null;
wDiff.CalculateDiff(text, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
 
}
// Cycle through old text tokens up
}
// Connect identical tokens, stop after first connected token
while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
if ( iMatch !== null ) {
bordersUpNext.push( [iMatch, jMatch] );
}
i = iEnd;
}
 
// Save updated linked region borders to object
// next list element
if (i recursionLevel === 0 && repeating === false newEnd) {
this.bordersDown = bordersDownNext;
break;
this.bordersUp = bordersUpNext;
}
i = text.newText.tokens[i].next;
}
 
// Merge local updated linked region borders into object
//
else {
// recursively diff still unresolved regions upwards
this.bordersDown = this.bordersDown.concat( bordersDownNext );
//
this.bordersUp = this.bordersUp.concat( bordersUpNext );
}
 
// cycle trough new text tokens list
var i = newEnd;
var j = oldEnd;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
 
/**
// get j from next matched tokens
* Repeat once with empty symbol table to link hidden unresolved common tokens in cross-overs.
var iPrev = text.newText.tokens[i].next;
* ("and" in "and this a and b that" -> "and this a and b that")
if (iPrev !== null) {
*/
var jPrev = text.newText.tokens[iPrev].link;
 
if (jPrev !== null) {
if ( repeating === false && this.config.repeatedDiff === true ) {
j = text.oldText.tokens[jPrev].prev;
var repeat = true;
}
this.calculateDiff( level, recurse, repeat, newStart, oldStart, up, recursionLevel );
}
 
/**
// check for the start of an unresolved sequence
* Refine by recursively diffing not linked regions with new symbol table.
if ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) ) {
* At word and character level only.
* Helps against gaps caused by addition of common tokens around sequences of common tokens.
*/
 
if (
// determine the limits of of the unresolved new sequence
var iStartrecurse === true null;&&
this.config['recursiveDiff'] === true &&
var iEnd = i;
recursionLevel < this.config.recursionMax
var iLength = 0;
var) iNext = i;{
while ( (iNext !== null) && (text.newText.tokens[iNext].link === null) ) {
iStart = iNext;
iLength ++;
if (iStart == newStart) {
break;
}
iNext = text.newText.tokens[iNext].prev;
}
 
/**
// determine the limits of of the unresolved old sequence
* Recursively diff gap downwards.
var jStart = null;
var jEnd = j;*/
 
var jLength = 0;
// Cycle through list of linked region borders
var jNext = j;
var bordersLength = bordersDownNext.length;
while ( (jNext !== null) && (text.oldText.tokens[jNext].link === null) ) {
for ( match = 0; match < bordersLength; match ++ ) {
jStart = jNext;
var i = bordersDownNext[match][0];
jLength ++;
var j = bordersDownNext[match][1];
if (jStart == oldStart) {
 
break;
// 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 );
}
jNext = text.oldText.tokens[jNext].prev;
}
 
/**
// recursively diff the unresolved sequence
* Recursively diff gap upwards.
if ( (iLength > 0) && (jLength > 0) ) {
*/
if ( (iLength > 1) || (jLength > 1) ) {
 
if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) {
// Cycle through list of linked region borders
wDiff.CalculateDiff(text, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
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 );
}
}
i = iStart;
}
}
 
// nextStop list elementtimers
if ( this.config.timer === true && repeating === false ) {
if (i == newStart) {
if ( this.recursionTimer[recursionLevel] === undefined ) {
break;
this.recursionTimer[recursionLevel] = 0;
}
this.recursionTimer[recursionLevel] += this.timeEnd( level + recursionLevel, true );
i = text.newText.tokens[i].prev;
}
if ( this.config.timer === true && repeating === false && recursionLevel === 0 ) {
this.timeRecursionEnd( level );
this.timeEnd( level );
}
}
return;
};
 
return;
};
 
// wDiff.DetectBlocks: extract block data for inserted, deleted, or moved blocks from diff data in text object
// input:
// text: object containing text tokens list
// blocks: empty array for block data
// groups: empty array for group data
// changes: text, blocks, groups
// called from: wDiff.Diff()
// scheme of blocks, sections, and groups (old block numbers):
// old: 1 2 3D4 5E6 7 8 9 10 11
// | ‾/-/_ X | >|< |
// new: 1 I 3D4 2 E6 5 N 7 10 9 8 11
// section: 0 0 0 1 1 2 2 2
// group: 0 10 111 2 33 4 11 5 6 7 8 9
// fixed: + +++ - ++ - + + - - +
// type: = + =-= = -= = + = = = = =
 
/**
wDiff.DetectBlocks = function (text, blocks, groups) {
* Main method for processing raw diff data, extracting deleted, inserted, and moved blocks.
*
* 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: = . =-= = -= = . = = = = =
*
* @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
// WED('text.oldText', wDiff.DebugText(text.oldText));
if ( this.config.debug === true ) {
// WED('text.newText', wDiff.DebugText(text.newText));
this.oldText.debugText( 'Old text' );
this.newText.debugText( 'New text' );
}
 
// collectCollect identical corresponding ('same=') blocks from old text and sort by new text
this.getSameBlocks();
wDiff.GetSameBlocks(text, blocks);
 
// collectCollect independent block sections (with no old/newblock move crosses outside section)a for per-section determination of non-moving (fixed) groups
this.getSections();
var sections = [];
wDiff.GetSections(blocks, sections);
 
// findFind groups of continuous old text blocks
this.getGroups();
wDiff.GetGroups(blocks, groups);
 
// Set longest sequence of increasing groups in sections as fixed (not moved)
// convert groups to insertions/deletions if maximal block length is too short
this.setFixed();
if ( (wDiff.blockMinLength > 0) && (wDiff.UnlinkBlocks(text, blocks, groups) === true) ) {
 
// repeatConvert from start after conversiongroups to insertions/deletions if maximum block length is too short
// Only for more complex texts that actually have blocks of minimum block length
wDiff.GetSameBlocks(text, blocks);
var unlinkCount = 0;
wDiff.GetSections(blocks, sections);
if (
wDiff.GetGroups(blocks, groups);
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
// set longest sequence of increasing groups in sections as fixed (not moved)
var unlinked = true;
wDiff.SetFixed(blocks, groups, sections);
while ( unlinked === true && unlinkCount < this.config.unlinkMax ) {
 
// Convert '=' to '+'/'-' pairs
// collect deletion ('del') blocks from old text
unlinked = this.unlinkBlocks();
wDiff.GetDelBlocks(text, blocks);
 
// Start over after conversion
// position 'del' blocks into new text order
if ( unlinked === true ) {
wDiff.PositionDelBlocks(blocks);
unlinkCount ++;
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
 
// collectRepeat insertionblock ('ins') blocksdetection from new textstart
this.maxWords = 0;
wDiff.GetInsBlocks(text, blocks);
this.getSameBlocks();
this.getSections();
this.getGroups();
this.setFixed();
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'total unlinking' );
}
}
 
// sortCollect deletion ('-') blocks byfrom newold text token number and update groups
this.getDelBlocks();
wDiff.SortBlocks(blocks, groups);
 
// Position '-' blocks into new text order
// set group numbers of 'ins' and 'del' blocks
this.positionDelBlocks();
wDiff.SetInsDelGroups(blocks, groups);
 
// Collect insertion ('+') blocks from new text
// mark original positions of moved groups
this.getInsBlocks();
wDiff.MarkMoved(groups);
 
// Set group numbers of '+' blocks
// set moved block colors
this.setInsGroups();
wDiff.ColorMoved(groups);
 
// Mark original positions of moved groups
// WED('Groups', wDiff.DebugGroups(groups));
this.insertMarks();
// WED('Blocks', wDiff.DebugBlocks(blocks));
 
// Debug log
return;
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;
};
 
 
/**
// wDiff.GetSameBlocks: collect identical corresponding ('same') blocks from old text and sort by new text
* Collect identical corresponding matching ('=') blocks from old text and sort by new text.
// called from: DetectBlocks()
*
// changes: creates blocks
* @param[in] WikEdDiffText newText, oldText Text objects
* @param[in/out] array blocks Blocks table object
*/
this.getSameBlocks = function () {
 
if ( this.config.timer === true ) {
wDiff.GetSameBlocks = function (text, blocks) {
this.time( 'getSameBlocks' );
}
 
// clear var blocks array= this.blocks;
blocks.splice(0);
 
// Clear blocks array
// cycle through old text to find matched (linked) blocks
blocks.splice( 0 );
var j = text.oldText.first;
var i = null;
while (j !== null) {
 
// Cycle through old text to find connected (linked, matched) blocks
// skip 'del' blocks
whilevar ( (j !== null) && (textthis.oldText.tokens[j].link === null) ) {first;
var i = null;
j = text.oldText.tokens[j].next;
while ( j !== null ) {
}
 
// getSkip 'same-' blockblocks
if while ( j !== null && this.oldText.tokens[j].link === null ) {
i j = textthis.oldText.tokens[j].linknext;
}
var iStart = i;
var jStart = j;
 
// detectGet matching blocks ('same=') block
varif count( j !== null ) 0;{
i = this.oldText.tokens[j].link;
var chars = 0;
var stringiStart = ''i;
var jStart = j;
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
 
var token = text.oldText.tokens[j].token;
// Detect matching blocks ('=')
count ++;
charsvar count += token.length0;
stringvar unique += tokenfalse;
var text = '';
i = text.newText.tokens[i].next;
while ( i !== null && j !== null && textthis.oldText.tokens[j].next;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
} );
}
}
 
// saveSort oldblocks by new text 'same'token blocknumber
blocks.pushsort( function( a, b ) {
return a.newNumber - b.newNumber;
oldBlock: blocks.length,
} );
newBlock: null,
 
oldNumber: text.oldText.tokens[jStart].number,
// Number blocks in new text order
newNumber: text.newText.tokens[iStart].number,
var blocksLength = blocks.length;
oldStart: jStart,
for ( var block = 0; block < blocksLength; block ++ ) {
count: count,
blocks[block].newBlock = block;
chars: chars,
type: 'same',
section: null,
group: null,
fixed: null,
string: string
});
}
}
 
if ( this.config.timer === true ) {
// sort blocks by new text token number
this.timeEnd( 'getSameBlocks' );
blocks.sort(function(a, b) {
}
return a.newNumber - b.newNumber;
return;
});
};
 
// number blocks in new text order
for (var block = 0; block < blocks.length; block ++) {
blocks[block].newBlock = block;
}
return;
};
 
/**
* Collect independent block sections with no block move crosses
* outside a section for per-section determination of non-moving fixed groups.
*
* @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 ) {
// wDiff.GetSections: collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
this.time( 'getSections' );
// called from: DetectBlocks()
}
// changes: creates sections, blocks[].section
 
var blocks = this.blocks;
wDiff.GetSections = function (blocks, sections) {
var sections = this.sections;
 
// clearClear sections array
sections.splice( 0 );
 
// cycleCycle through blocks
for ( var blockblocksLength = 0; block < blocks.length; block ++) {
for ( var block = 0; block < blocksLength; block ++ ) {
 
var sectionStart = block;
var sectionEnd = block;
 
var oldMax = blocks[sectionStart].oldNumber;
var sectionOldMax = oldMax;
 
// checkCheck right
for ( var j = sectionStart + 1; j < blocks.lengthblocksLength; j ++ ) {
 
// checkCheck 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;
}
}
 
else if (blocks[j].oldNumber < sectionOldMax) {
// Save crossing sections
sectionEnd = j;
if ( sectionEnd > sectionStart ) {
sectionOldMax = oldMax;
 
// 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 ) {
this.timeEnd( 'getSections' );
}
return;
};
 
// save crossing sections
if (sectionEnd > sectionStart) {
 
/**
// save section to block
* Find groups of continuous old text blocks.
for (var i = sectionStart; i <= sectionEnd; i ++) {
*
blocks[i].section = sections.length;
* @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 ) {
// save section
this.time( 'getGroups' );
sections.push({
blockStart: sectionStart,
blockEnd: sectionEnd,
deleted: false
});
block = sectionEnd;
}
}
return;
};
 
var blocks = this.blocks;
var groups = this.groups;
 
// Clear groups array
// wDiff.GetGroups: find groups of continuous old text blocks
groups.splice( 0 );
// called from: DetectBlocks()
// changes: creates groups, blocks[].group
 
// Cycle through blocks
wDiff.GetGroups = function (blocks, groups) {
var blocksLength = blocks.length;
for ( var block = 0; block < blocksLength; block ++ ) {
var groupStart = block;
var groupEnd = block;
var oldBlock = blocks[groupStart].oldBlock;
 
// Get word and char count of block
// clear groups array
var words = this.wordCount( blocks[block].text );
groups.splice(0);
var maxWords = words;
var unique = blocks[block].unique;
var chars = blocks[block].chars;
 
// Check right
// cycle through blocks
for ( var blocki = 0groupEnd + 1; blocki < blocks.lengthblocksLength; blocki ++ ) {
 
if (blocks[block].deleted === true) {
// Check for crossing over to the left
continue;
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
if ( groupEnd >= groupStart ) {
 
// Set groups outside sections as fixed
var fixed = false;
if ( blocks[groupStart].section === null ) {
fixed = true;
}
 
// Save group to block
for ( var i = groupStart; i <= groupEnd; i ++ ) {
blocks[i].group = groups.length;
blocks[i].fixed = fixed;
}
 
// Save group
groups.push( {
oldNumber: blocks[groupStart].oldNumber,
blockStart: groupStart,
blockEnd: groupEnd,
unique: unique,
maxWords: maxWords,
words: words,
chars: chars,
fixed: fixed,
movedFrom: null,
color: null
} );
block = groupEnd;
 
// Set global word count of longest linked block
if ( maxWords > this.maxWords ) {
this.maxWords = maxWords;
}
}
}
if ( this.config.timer === true ) {
var groupStart = block;
this.timeEnd( 'getGroups' );
var groupEnd = block;
}
var oldBlock = blocks[groupStart].oldBlock;
return;
};
 
// get word and char count of block
var words = wDiff.WordCount(blocks[block].string);
var maxWords = words;
var chars = blocks[block].chars;
 
/**
// check right
* Set longest sequence of increasing groups in sections as fixed (not moved).
for (var i = groupEnd + 1; i < blocks.length; i ++) {
*
* @param[in] array sections Sections table object
* @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 ) {
// check for crossing over to the left
this.time( 'setFixed' );
if (blocks[i].oldBlock != oldBlock + 1) {
}
break;
 
var blocks = this.blocks;
var groups = this.groups;
var sections = this.sections;
 
// Cycle through sections
var sectionsLength = sections.length;
for ( var section = 0; section < sectionsLength; section ++ ) {
var blockStart = sections[section].blockStart;
var blockEnd = sections[section].blockEnd;
 
var groupStart = blocks[blockStart].group;
var groupEnd = blocks[blockEnd].group;
 
// Recusively find path of groups in increasing old group order with longest char length
var cache = [];
var maxChars = 0;
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;
}
}
oldBlock = blocks[i].oldBlock;
 
// Mark fixed groups
// get word and char count of block
var blockWordsmaxPathLength = wDiffmaxPath.WordCount(blocks[i].string)length;
for ( var i = 0; i < maxPathLength; i ++ ) {
if (blockWords > maxWords) {
maxWordsvar group = blockWordsmaxPath[i];
groups[group].fixed = true;
 
// Mark fixed blocks
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
blocks[block].fixed = true;
}
}
words += blockWords;
chars += blocks[i].chars;
groupEnd = i;
}
if ( this.config.timer === true ) {
this.timeEnd( 'setFixed' );
}
return;
};
 
// save crossing group
if (groupEnd >= groupStart) {
 
/**
// set groups outside sections as fixed
* Recusively find path of groups in increasing old group order with longest char length.
var fixed = false;
*
if (blocks[groupStart].section === null) {
* @param int start Path start group
fixed = true;
* @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)
// save group to block
var pathObj;
for (var i = groupStart; i <= groupEnd; i ++) {
if ( cache[i] !== undefined ) {
blocks[i].group = groups.length;
pathObj = { path: cache[i].path.slice(), chars: cache[i].chars };
blocks[i].fixed = fixed;
}
 
// Get longest sub-path by recursion
// save group
groups.push(else {
pathObj = this.findMaxPath( i, groupEnd, cache );
oldNumber: blocks[groupStart].oldNumber,
}
blockStart: groupStart,
 
blockEnd: groupEnd,
// Select longest sub-path
maxWords: maxWords,
if ( pathObj.chars > maxChars ) {
words: words,
chars: maxChars = pathObj.chars,;
fixed:returnObj = fixed,pathObj;
}
moved: [],
movedFrom: null,
color: null,
diff: ''
});
block = groupEnd;
}
}
return;
};
 
// 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;
};
 
// wDiff.UnlinkBlocks: remove 'same' blocks in groups of continuous old text blocks if too short
// called from: DetectBlocks()
// changes: text.newText/oldText[].link
// returns: true if text tokens were unlinked
 
/**
wDiff.UnlinkBlocks = function (text, blocks, groups) {
* 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 unlinkedblocks = falsethis.blocks;
var groups = this.groups;
 
// cycleCycle through groups
var unlinked = false;
for (var group = 0; group < groups.length; group ++) {
var groupsLength = groups.length;
if ( (groups[group].maxWords < wDiff.blockMinLength) && (groups[group].fixed === false) ) {
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
// cycle through blocks
if ( groups[group].maxWords < this.config.blockMinLength && groups[group].unique === false ) {
for (var block = blockStart; block <= blockEnd; block ++) {
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === '=' ) {
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
 
// cycleOtherwise throughunlink oldblock textflanks
else {
var j = blocks[block].oldStart;
for (var count = 0; count < blocks[block].count; count ++) {
 
// unlinkUnlink tokensblocks from start
for ( var block = blockStart; block <= blockEnd; block ++ ) {
text.newText.tokens[ text.oldText.tokens[j].link ].link = null;
text.oldText.tokensif ( blocks[jblock].linktype === '=' ) null;{
 
j = text.oldText.tokens[j].next;
// Stop unlinking if more than one word or a unique word
if ( blocks[block].words > 1 || blocks[block].unique === true ) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
blockStart = block;
}
}
 
// Unlink blocks from end
for ( var block = blockEnd; block > blockStart; block -- ) {
if ( blocks[block].type === '=' ) {
 
// Stop unlinking if more than one word or a unique word
if (
blocks[block].words > 1 ||
( blocks[block].words === 1 && blocks[block].unique === true )
) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
unlinked = true;
}
}
return unlinked;
}
};
return unlinked;
};
 
 
/**
// wDiff.SetFixed: set longest sequence of increasing groups in sections as fixed (not moved)
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs.
// called from: DetectBlocks()
*
// calls: wDiff.FindMaxPath()
* @param[in] array blocks Blocks table object
// changes: groups[].fixed, blocks[].fixed
* @param[out] WikEdDiffText newText, oldText Text objects, link property
*/
this.unlinkSingleBlock = function ( block ) {
 
// Cycle through old text
wDiff.SetFixed = function (blocks, groups, sections) {
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
 
// Unlink tokens
// cycle through sections
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
for (var section = 0; section < sections.length; section ++) {
this.oldText.tokens[j].link = null;
var blockStart = sections[section].blockStart;
j = this.oldText.tokens[j].next;
var blockEnd = sections[section].blockEnd;
}
return;
};
 
var groupStart = blocks[blockStart].group;
var groupEnd = blocks[blockEnd].group;
 
/**
// recusively find path of groups in increasing old group order with longest char length
* Collect deletion ('-') blocks from old text.
*
* @param[in] WikEdDiffText oldText Old Text object
* @param[out] array blocks Blocks table object
*/
this.getDelBlocks = function () {
 
if ( this.config.timer === true ) {
// start at each group of section
this.time( 'getDelBlocks' );
var cache = [];
var maxChars = 0;
var maxPath = null;
for (var i = groupStart; i <= groupEnd; i ++) {
var pathObj = wDiff.FindMaxPath(i, [], 0, cache, groups, groupEnd);
if (pathObj.chars > maxChars) {
maxPath = pathObj.path;
maxChars = pathObj.chars;
}
}
 
var blocks = this.blocks;
// mark fixed groups
for (var i = 0; i < maxPath.length; i ++) {
var group = maxPath[i];
groups[group].fixed = true;
 
// Cycle through old text to find connected (linked, matched) blocks
// mark fixed blocks
var j = this.oldText.first;
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
var i = null;
blocks[block].fixed = true;
while ( j !== null ) {
 
// Collect '-' blocks
var oldStart = j;
var count = 0;
var text = '';
while ( j !== null && this.oldText.tokens[j].link === null ) {
count ++;
text += this.oldText.tokens[j].token;
j = this.oldText.tokens[j].next;
}
 
// Save old text '-' block
if ( count !== 0 ) {
blocks.push( {
oldBlock: null,
newBlock: null,
oldNumber: this.oldText.tokens[oldStart].number,
newNumber: null,
oldStart: oldStart,
count: count,
unique: false,
words: null,
chars: 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;
};
return;
};
 
 
/**
// wDiff.FindMaxPath: recusively find path of groups in increasing old group order with longest char length
* Position deletion '-' blocks into new text order.
// 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
* Deletion blocks move with fixed reference:
// returns: returnObj, contains path and length
* Old: 1 D 2 1 D 2
// called from: wDiff.SetFixed()
* / \ / \ \
// calls: itself recursively
* 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 ) {
wDiff.FindMaxPath = function (start, path, chars, cache, groups, groupEnd) {
this.time( 'positionDelBlocks' );
}
 
var blocks = this.blocks;
// add current path point
var pathLocalgroups = paththis.slice()groups;
pathLocal.push(start);
chars = chars + groups[start].chars;
 
// Sort shallow copy of blocks by oldNumber
// last group, terminate recursion
var blocksOld = blocks.slice();
var returnObj = { path: pathLocal, chars: chars };
blocksOld.sort( function( a, b ) {
if (start == groupEnd) {
return returnObja.oldNumber - b.oldNumber;
} );
}
 
// Cycle through blocks in old text order
// find longest sub-path
var blocksOldLength = blocksOld.length;
var maxChars = 0;
for ( var block = 0; block < blocksOldLength; block ++ ) {
var oldNumber = groups[start].oldNumber;
var delBlock = blocksOld[block];
for (var i = start + 1; i <= groupEnd; i ++) {
 
// '-' block only
// only in increasing old group order
if (groups[i] delBlock.oldNumbertype !== <'-' oldNumber) {
continue;
}
 
// Find fixed '=' reference block from original block position to position '-' block
// get longest sub-path from cache
// Similar to position marks '|' code
if (cache[start] !== undefined) {
returnObj = cache[start];
}
 
// getGet longestold sub-pathtext byprev recursionblock
var prevBlockNumber = null;
else {
var prevBlock = null;
var pathObj = wDiff.FindMaxPath(i, pathLocal, chars, cache, groups, groupEnd);
if ( block > 0 ) {
prevBlockNumber = blocksOld[block - 1].newBlock;
prevBlock = blocks[prevBlockNumber];
}
 
// selectGet longestold sub-pathtext next block
var nextBlockNumber = null;
if (pathObj.chars > maxChars) {
returnObjvar nextBlock = pathObjnull;
if ( block < blocksOld.length - 1 ) {
nextBlockNumber = blocksOld[block + 1].newBlock;
nextBlock = blocks[nextBlockNumber];
}
}
}
 
// Move after prev block if fixed
// save longest path to cache
var refBlock = null;
if (cache[i] === undefined) {
if ( prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
cache[start] = returnObj;
refBlock = prevBlock;
}
}
return returnObj;
};
 
// Move before next block if fixed
else if ( nextBlock !== null && nextBlock.type === '=' && nextBlock.fixed === true ) {
refBlock = nextBlock;
}
 
// Move after prev block if not start of group
// wDiff.GetDelBlocks: collect deletion ('del') blocks from old text
else if (
// called from: DetectBlocks()
prevBlock !== null &&
// changes: blocks
prevBlock.type === '=' &&
prevBlockNumber !== groups[ prevBlock.group ].blockEnd
) {
refBlock = prevBlock;
}
 
// Move before next block if not start of group
wDiff.GetDelBlocks = function (text, blocks) {
else if (
nextBlock !== null &&
nextBlock.type === '=' &&
nextBlockNumber !== groups[ nextBlock.group ].blockStart
) {
refBlock = nextBlock;
}
 
// Move after closest previous fixed block
// cycle through old text to find matched (linked) blocks
else {
var j = text.oldText.first;
for ( var fixed = block; fixed >= 0; fixed -- ) {
var i = null;
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
while (j !== null) {
refBlock = blocksOld[fixed];
break;
}
}
}
 
// collectMove 'del'before blocksfirst block
if ( refBlock === null ) {
var oldStart = j;
var count delBlock.newNumber = 0 -1;
}
var string = '';
 
while ( (j !== null) && (text.oldText.tokens[j].link === null) ) {
// Update '-' block data
count ++;
else {
string += text.oldText.tokens[j].token;
delBlock.newNumber = refBlock.newNumber;
j = text.oldText.tokens[j].next;
delBlock.section = refBlock.section;
delBlock.group = refBlock.group;
delBlock.fixed = refBlock.fixed;
}
}
 
// Sort '-' blocks in and update groups
// save old text 'del' block
this.sortBlocks();
if (count !== 0) {
 
blocks.push({
if ( this.config.timer === true ) {
oldBlock: null,
this.timeEnd( 'positionDelBlocks' );
newBlock: null,
oldNumber: text.oldText.tokens[oldStart].number,
newNumber: null,
oldStart: oldStart,
count: count,
chars: null,
type: 'del',
section: null,
group: null,
fixed: null,
string: string
});
}
return;
};
 
 
// skip 'same' block
/**
if (j !== null) {
* Collect insertion ('+') blocks from new text.
i = text.oldText.tokens[j].link;
*
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
* @param[in] WikEdDiffText newText New Text object
i = text.newText.tokens[i].next;
* @param[out] array blocks Blocks table object
j = text.oldText.tokens[j].next;
} */
this.getInsBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'getInsBlocks' );
}
}
return;
};
 
var blocks = this.blocks;
 
// Cycle through new text to find insertion blocks
// wDiff.PositionDelBlocks: position 'del' blocks into new text order
var i = this.newText.first;
// called from: DetectBlocks()
while ( i !== null ) {
// changes: blocks[].section/group/fixed/newNumber
//
// deletion blocks move with fixed neighbor (new number +/- 0.1):
// old: 1 D 2 1 D 2
// / / \ / \ \
// new: 1 D 2 1 D 2
// fixed: * *
// new number: 1 1.1 1.9 2
 
// Jump over linked (matched) block
wDiff.PositionDelBlocks = function (blocks) {
while ( i !== null && this.newText.tokens[i].link !== null ) {
i = this.newText.tokens[i].next;
}
 
// sortDetect shallow copy ofinsertion blocks by oldNumber('+')
if ( i !== null ) {
var blocksOld = blocks.slice();
var iStart = i;
blocksOld.sort(function(a, b) {
var count = 0;
return a.oldNumber - b.oldNumber;
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
// cycle through 'del' blocks in old text order
blocks.push( {
for (var blockOld = 0; blockOld < blocksOld.length; blockOld ++) {
oldBlock: null,
var delBlock = blocksOld[blockOld];
newBlock: null,
if (delBlock.type != 'del') {
oldNumber: null,
continue;
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
} );
}
}
 
// getSort old'+' textblocks previn blockand update groups
this.sortBlocks();
var prevBlock;
 
if (blockOld > 0) {
if ( this.config.timer === true ) {
prevBlock = blocks[ blocksOld[blockOld - 1].newBlock ];
this.timeEnd( 'getInsBlocks' );
}
return;
};
 
 
// get old text next block
/**
var nextBlock;
* Sort blocks by new text token number and update groups.
if (blockOld < blocksOld.length - 1) {
*
nextBlock = blocks[ blocksOld[blockOld + 1].newBlock ];
* @param[in/out] array groups Groups table object
* @param[in/out] array blocks Blocks table object
*/
this.sortBlocks = function () {
 
var blocks = this.blocks;
var groups = this.groups;
 
// Sort by newNumber, then by old number
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
var group = null;
var blocksLength = blocks.length;
for ( var block = 0; block < blocksLength; block ++ ) {
var blockGroup = blocks[block].group;
if ( blockGroup !== null ) {
if ( blockGroup !== group ) {
group = blocks[block].group;
groups[group].blockStart = block;
groups[group].oldNumber = blocks[block].oldNumber;
}
groups[blockGroup].blockEnd = block;
}
}
return;
};
 
 
// move after prev block if fixed
/**
var neighbor;
* Set group numbers of insertion '+' blocks.
if ( (prevBlock !== undefined) && (prevBlock.fixed === true) ) {
*
neighbor = prevBlock;
* @param[in/out] array groups Groups table object
delBlock.newNumber = neighbor.newNumber + 0.1;
* @param[in/out] array blocks Blocks table object, fixed and group properties
*/
this.setInsGroups = function () {
 
if ( this.config.timer === true ) {
this.time( 'setInsGroups' );
}
 
var blocks = this.blocks;
// move before next block if fixed
var groups = this.groups;
else if ( (nextBlock !== undefined) && (nextBlock.fixed === true) ) {
 
neighbor = nextBlock;
// Set group numbers of '+' blocks inside existing groups
delBlock.newNumber = neighbor.newNumber - 0.1;
var groupsLength = groups.length;
for ( var group = 0; group < groupsLength; group ++ ) {
var fixed = groups[group].fixed;
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
if ( blocks[block].group === null ) {
blocks[block].group = group;
blocks[block].fixed = fixed;
}
}
}
 
// moveAdd afterremaining prev'+' blockblocks ifto existentnew groups
 
else if (prevBlock !== undefined) {
// Cycle through blocks
neighbor = prevBlock;
var blocksLength = blocks.length;
delBlock.newNumber = neighbor.newNumber + 0.1;
for ( var block = 0; block < blocksLength; block ++ ) {
 
// 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;
};
 
 
// move before next block
/**
else if (nextBlock !== undefined) {
* Mark original positions of moved groups.
neighbor = nextBlock;
* Scheme: moved block marks at original positions relative to fixed groups:
delBlock.newNumber = neighbor.newNumber - 0.1;
* 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 ) {
this.time( 'insertMarks' );
}
 
var blocks = this.blocks;
// move before first block
var groups = this.groups;
else {
delBlock.newNumbervar moved = -0.1[];
var color = 1;
 
// Make shallow copy of blocks
var blocksOld = blocks.slice();
 
// Enumerate copy
var blocksOldLength = blocksOld.length;
for ( var i = 0; i < blocksOldLength; i ++ ) {
blocksOld[i].number = i;
}
 
// Sort copy by oldNumber
// update 'del' block with neighbor data
blocksOld.sort( function( a, b ) {
if (neighbor !== undefined) {
var comp = a.oldNumber - b.oldNumber;
delBlock.section = neighbor.section;
if ( comp === 0 ) {
delBlock.group = neighbor.group;
comp = a.newNumber - b.newNumber;
delBlock.fixed = neighbor.fixed;
}
return comp;
} );
 
// Create lookup table: original to sorted
var lookupSorted = [];
for ( var i = 0; i < blocksOldLength; i ++ ) {
lookupSorted[ blocksOld[i].number ] = i;
}
}
return;
};
 
// Cycle through groups (moved group)
var groupsLength = groups.length;
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
// wDiff.GetInsBlocks: collect insertion ('ins') blocks from new text
// Similar to position deletions '-' code
// called from: DetectBlocks()
// changes: blocks
 
// Get old text prev block
wDiff.GetInsBlocks = function (text, blocks) {
var prevBlock = null;
var block = lookupSorted[ movedGroup.blockStart ];
if ( block > 0 ) {
prevBlock = blocksOld[block - 1];
}
 
// cycleGet through newold text to find insertionnext blocksblock
var inextBlock = text.newText.firstnull;
var block = lookupSorted[ movedGroup.blockEnd ];
while (i !== null) {
if ( block < blocksOld.length - 1 ) {
nextBlock = blocksOld[block + 1];
}
 
// jumpMove overafter linked (matched)prev block if fixed
var refBlock = null;
while ( (i !== null) && (text.newText.tokens[i].link !== null) ) {
if ( prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
i = text.newText.tokens[i].next;
refBlock = prevBlock;
}
}
 
// Move before next block if fixed
// detect insertion blocks ('ins')
else if (i nextBlock !== null && nextBlock.type === '=' && nextBlock.fixed === true ) {
var iStart refBlock = inextBlock;
var count = 0;
var string = '';
while ( (i !== null) && (text.newText.tokens[i].link === null) ) {
count ++;
string += text.newText.tokens[i].token;
i = text.newText.tokens[i].next;
}
 
// saveFind newclosest text 'ins'fixed block to the left
blocks.push(else {
for ( var fixed = lookupSorted[ movedGroup.blockStart ] - 1; fixed >= 0; fixed -- ) {
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
refBlock = blocksOld[fixed];
break;
}
}
}
 
// Get position of new mark block
var newNumber;
var markGroup;
 
// No smaller fixed block, moved right from before first block
if ( refBlock === null ) {
newNumber = -1;
markGroup = groups.length;
 
// Save new single-mark-block group
groups.push( {
oldNumber: 0,
blockStart: blocks.length,
blockEnd: blocks.length,
unique: false,
maxWords: null,
words: null,
chars: 0,
fixed: null,
movedFrom: null,
color: null
} );
}
else {
newNumber = refBlock.newNumber;
markGroup = refBlock.group;
}
 
// Insert '|' block
blocks.push( {
oldBlock: null,
newBlock: null,
oldNumber: nullmovedOldNumber,
newNumber: text.newText.tokens[iStart].numbernewNumber,
oldStart: null,
count: countnull,
charsunique: null,
typewords: 'ins'null,
chars: 0,
type: '|',
section: null,
group: nullmarkGroup,
fixed: nulltrue,
stringmoved: string moved,
text: ''
});
} );
 
// Set group color
movedGroup.color = color;
movedGroup.movedFrom = markGroup;
color ++;
}
}
return;
};
 
// Sort '|' blocks in and update groups
this.sortBlocks();
 
if ( this.config.timer === true ) {
// wDiff.SortBlocks: sort blocks by new text token number and update groups
this.timeEnd( 'insertMarks' );
// called from: DetectBlocks()
}
// changes: blocks
return;
};
 
wDiff.SortBlocks = function (blocks, groups) {
 
/**
// sort by newNumber
* Collect diff fragment list for markup, create abstraction layer for customized diffs.
blocks.sort(function(a, b) {
* Adds the following fagment types:
return a.newNumber - b.newNumber;
* '=', '-', '+' 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;
// cycle through blocks and update groups with new block numbers
var groupgroups = nullthis.groups;
var fragments = this.fragments;
for (var block = 0; block < blocks.length; block ++) {
 
var blockGroup = blocks[block].group;
// Make shallow copy of groups and sort by blockStart
if (blockGroup !== null) {
var groupsSort = groups.slice();
if (blockGroup != group) {
groupsSort.sort( function( a, b ) {
group = blocks[block].group;
groups[group]return a.blockStart =- blockb.blockStart;
} );
groups[group].oldNumber = blocks[block].oldNumber;
 
// Cycle through groups
var groupsSortLength = groupsSort.length;
for ( var group = 0; group < groupsSortLength; group ++ ) {
var blockStart = groupsSort[group].blockStart;
var blockEnd = groupsSort[group].blockEnd;
 
// Add moved block start
var color = groupsSort[group].color;
if ( color !== null ) {
var type;
if ( groupsSort[group].movedFrom < blocks[ blockStart ].group ) {
type = '(<';
}
else {
type = '(>';
}
fragments.push( {
text: '',
type: type,
color: color
} );
}
groups[blockGroup].blockEnd = block;
}
}
return;
};
 
// Cycle through blocks
for ( var block = blockStart; block <= blockEnd; block ++ ) {
var type = blocks[block].type;
 
// Add '=' unchanged text and moved block
// wDiff.SetInsDelGroups: set group numbers of 'ins' and 'del' blocks
if ( type === '=' || type === '-' || type === '+' ) {
// called from: DetectBlocks()
fragments.push( {
// changes: groups, blocks[].fixed/group
text: blocks[block].text,
type: type,
color: color
} );
}
 
// Add '<' and '>' marks
wDiff.SetInsDelGroups = function (blocks, groups) {
else if ( type === '|' ) {
var movedGroup = groups[ blocks[block].moved ];
 
// Get mark text
var markText = '';
for (
var movedBlock = movedGroup.blockStart;
movedBlock <= movedGroup.blockEnd;
movedBlock ++
) {
if ( blocks[movedBlock].type === '=' || blocks[movedBlock].type === '-' ) {
markText += blocks[movedBlock].text;
}
}
 
// Get mark direction
var markType;
if ( movedGroup.blockStart < blockStart ) {
markType = '<';
}
else {
markType = '>';
}
 
// Add mark
fragments.push( {
text: markText,
type: markType,
color: movedGroup.color
} );
}
}
 
// Add moved block end
// set group numbers of 'ins' and 'del' blocks inside existing groups
if ( color !== null ) {
for (var group = 0; group < groups.length; group ++) {
fragments.push( {
var fixed = groups[group].fixed;
text: '',
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
type: ' )',
if (blocks[block].group === null) {
color: color
blocks[block].group = group;
} );
blocks[block].fixed = fixed;
}
}
}
 
// Cycle through fragments, join consecutive fragments of same type (i.e. '-' blocks)
// add remaining 'ins' and 'del' blocks to groups
var fragmentsLength = fragments.length;
for ( var fragment = 1; fragment < fragmentsLength; fragment ++ ) {
 
// Check if joinable
// cycle through blocks
if (
for (var block = 0; block < blocks.length; block ++) {
fragments[fragment].type === fragments[fragment - 1].type &&
fragments[fragment].color === fragments[fragment - 1].color &&
fragments[fragment].text !== '' && fragments[fragment - 1].text !== ''
) {
 
// skipJoin existingand groupssplice
fragments[fragment - 1].text += fragments[fragment].text;
if (blocks[block].group === null) {
fragments.splice( fragment, 1 );
blocks[block].group = groups.length;
fragment --;
var fixed = blocks[block].fixed;
}
 
// save group
groups.push({
oldNumber: blocks[block].oldNumber,
blockStart: block,
blockEnd: block,
maxWords: null,
words: null,
chars: null,
fixed: fixed,
moved: [],
movedFrom: null,
color: null,
diff: ''
});
}
}
return;
};
 
// Enclose in containers
fragments.unshift( { text: '', type: '{', color: null }, { text: '', type: '[', color: null } );
fragments.push( { text: '', type: ']', color: null }, { text: '', type: '}', color: null } );
 
return;
// wDiff.MarkMoved: mark original positions of moved groups
};
// called from: DetectBlocks()
// changes: groups[].moved/movedFrom
// moved block marks at original positions relative to fixed groups:
// groups: 3 7
// 1 <| | (no next smaller fixed)
// 5 |< |
// |> 5 |
// | 5 <|
// | >| 5
// | |> 9 (no next larger fixed)
// fixed: * *
// mark direction: groups[movedGroup].blockStart < groups[group].blockStart
// group side: groups[movedGroup].oldNumber < groups[group].oldNumber
 
wDiff.MarkMoved = function (groups) {
 
/**
// cycle through groups (moved group)
* Clip unchanged sections from unmoved block text.
for (var movedGroup = 0; movedGroup < groups.length; movedGroup ++) {
* Adds the following fagment types:
if (groups[movedGroup].fixed !== false) {
* '~', ' ~', '~ ' omission indicators
continue;
* '[', ']', ',' 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;
}
 
// Min length for clipping right
var minRight = this.config.clipHeadingRight;
if ( this.config.clipParagraphRightMin < minRight ) {
minRight = this.config.clipParagraphRightMin;
}
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;
}
 
// Min length for clipping left
var minLeft = this.config.clipHeadingLeft;
if ( this.config.clipParagraphLeftMin < minLeft ) {
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;
}
var movedOldNumber = groups[movedGroup].oldNumber;
 
// findCycle closestthrough fixed groupsfragments
var nextSmallerNumberfragmentsLength = nullfragments.length;
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
var nextSmallerGroup = null;
var nextLargerNumber = null;
var nextLargerGroup = null;
 
// Skip if not an unmoved and unchanged block
// cycle through groups (original positions)
var type = fragments[fragment].type;
for (var group = 0; group < groups.length; group ++) {
var color = fragments[fragment].color;
if ( (groups[group].fixed !== true) || (group == movedGroup) ) {
if ( type !== '=' || color !== null ) {
continue;
}
 
// findSkip fixedif grouptoo withshort closestfor smaller oldNumberclipping
var oldNumbertext = groupsfragments[groupfragment].oldNumbertext;
var textLength = text.length;
if ( (oldNumber < movedOldNumber) && ( (nextSmallerNumber === null) || (oldNumber > nextSmallerNumber) ) ) {
if ( textLength < minRight && textLength < minLeft ) {
nextSmallerNumber = oldNumber;
continue;
nextSmallerGroup = group;
}
 
// Get line positions including start and end
// find fixed group with closest larger oldNumber
var lines = [];
if ( (oldNumber > movedOldNumber) && ( (nextLargerNumber === null) || (oldNumber < nextLargerNumber) ) ) {
nextLargerNumbervar lastIndex = oldNumbernull;
var regExpMatch;
nextLargerGroup = group;
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 );
}
}
 
// Get heading positions
// no larger fixed group, moved right
var movedFromheadings = ''[];
var headingsEnd = [];
if (nextLargerGroup === null) {
while ( ( regExpMatch = this.config.regExp.clipHeading.exec( text ) ) !== null ) {
movedFrom = 'left';
headings.push( regExpMatch.index );
}
headingsEnd.push( regExpMatch.index + regExpMatch[0].length );
}
 
// Get paragraph positions including start and end
// no smaller fixed group, moved right
var paragraphs = [];
else if (nextSmallerGroup === null) {
movedFromvar lastIndex = 'right'null;
while ( ( regExpMatch = this.config.regExp.clipParagraph.exec( text ) ) !== null ) {
}
paragraphs.push( regExpMatch.index );
 
lastIndex = this.config.regExp.clipParagraph.lastIndex;
// group moved from between two closest fixed neighbors, moved left or right depending on char distance
else {
var rightChars = 0;
for (var group = nextSmallerGroup + 1; group < movedGroup; group ++) {
rightChars += groups[group].chars;
}
if ( paragraphs[0] !== 0 ) {
var leftChars = 0;
paragraphs.unshift( 0 );
for (var group = movedGroup + 1; group < nextLargerGroup; group ++) {
leftChars += groups[group].chars;
}
if ( lastIndex !== textLength ) {
 
paragraphs.push( textLength );
// moved right
if (rightChars <= leftChars) {
movedFrom = 'left';
}
 
// movedDetermine ranges to keep on left and right side
var rangeRight = null;
else {
movedFromvar rangeLeft = 'right'null;
var rangeRightType = '';
var rangeLeftType = '';
 
// Find clip pos from left, skip for first non-container block
if ( fragment !== 2 ) {
 
// Maximum lines to search from left
var rangeLeftMax = textLength;
if ( this.config.clipLinesLeftMax < lines.length ) {
rangeLeftMax = lines[this.config.clipLinesLeftMax];
}
 
// Find first heading from left
if ( rangeLeft === null ) {
var headingsLength = headingsEnd.length;
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
if ( rangeLeft === null ) {
var paragraphsLength = paragraphs.length;
for ( var j = 0; j < paragraphsLength; j ++ ) {
if (
paragraphs[j] > this.config.clipParagraphLeftMax ||
paragraphs[j] > rangeLeftMax
) {
break;
}
if ( paragraphs[j] > this.config.clipParagraphLeftMin ) {
rangeLeft = paragraphs[j];
rangeLeftType = 'paragraph';
break;
}
}
}
 
// Find first line break from left
if ( rangeLeft === null ) {
var linesLength = lines.length;
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
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
if ( rangeLeft === null ) {
rangeLeft = rangeLeftMax;
rangeLeftType = 'fixed';
}
}
}
 
// Find clip pos from right, skip for last non-container block
// check for null-moves
if (movedFrom fragment !== 'left'fragments.length - 3 ) {
 
if (groups[nextSmallerGroup].blockEnd + 1 != groups[movedGroup].blockStart) {
// Maximum lines to search from right
groups[nextSmallerGroup].moved.push(movedGroup);
var rangeRightMin = 0;
groups[movedGroup].movedFrom = nextSmallerGroup;
if ( lines.length >= this.config.clipLinesRightMax ) {
rangeRightMin = lines[lines.length - this.config.clipLinesRightMax];
}
 
// Find last heading from right
if ( rangeRight === null ) {
for ( var j = headings.length - 1; j >= 0; j -- ) {
if (
headings[j] < textLength - this.config.clipHeadingRight ||
headings[j] < rangeRightMin
) {
break;
}
rangeRight = headings[j];
rangeRightType = 'heading';
break;
}
}
 
// Find last paragraph from right
if ( rangeRight === null ) {
for ( var j = paragraphs.length - 1; j >= 0 ; j -- ) {
if (
paragraphs[j] < textLength - this.config.clipParagraphRightMax ||
paragraphs[j] < rangeRightMin
) {
break;
}
if ( paragraphs[j] < textLength - this.config.clipParagraphRightMin ) {
rangeRight = paragraphs[j];
rangeRightType = 'paragraph';
break;
}
}
}
 
// Find last line break from right
if ( rangeRight === null ) {
for ( var j = lines.length - 1; j >= 0; j -- ) {
if (
lines[j] < textLength - this.config.clipLineRightMax ||
lines[j] < rangeRightMin
) {
break;
}
if ( lines[j] < textLength - this.config.clipLineRightMin ) {
rangeRight = lines[j];
rangeRightType = 'line';
break;
}
}
}
 
// Find last blank from right
if ( rangeRight === null ) {
var startPos = textLength - this.config.clipBlankRightMax;
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
if ( rangeRight === null ) {
if ( textLength - this.config.clipCharsRight > rangeRightMin ) {
rangeRight = textLength - this.config.clipCharsRight;
rangeRightType = 'chars';
}
}
 
// Fixed number of lines from right
if ( rangeRight === null ) {
rangeRight = rangeRightMin;
rangeRightType = 'fixed';
}
}
}
else if (movedFrom == 'right') {
if (groups[movedGroup].blockEnd + 1 != groups[nextLargerGroup].blockStart) {
groups[nextLargerGroup].moved.push(movedGroup);
groups[movedGroup].movedFrom = nextLargerGroup;
}
}
}
 
// Check if we skip clipping if ranges are close together
// cycle through groups, sort blocks moved from here by old number
if ( rangeLeft !== null && rangeRight !== null ) {
for (var group = 0; group < groups.length; group ++) {
var moved = groups[group].moved;
if (moved !== null) {
moved.sort(function(a, b) {
return groups[a].oldNumber - groups[b].oldNumber;
});
}
}
return;
};
 
// Skip if overlapping ranges
if ( rangeLeft > rangeRight ) {
continue;
}
 
// Skip if chars too close
// wDiff.ColorMoved: set moved block color numbers
var skipChars = rangeRight - rangeLeft;
// called from: DetectBlocks()
if ( skipChars < this.config.clipSkipChars ) {
// changes: groups[].color
continue;
}
 
// Skip if lines too close
wDiff.ColorMoved = function (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
// cycle through groups
if ( rangeLeft === null && rangeRight === null ) {
var moved = [];
continue;
for (var group = 0; group < groups.length; group ++) {
}
moved = moved.concat(groups[group].moved);
}
 
// Split left text
// sort moved array by old number
var textLeft = null;
moved.sort(function(a, b) {
var omittedLeft = null;
return groups[a].oldNumber - groups[b].oldNumber;
if ( rangeLeft !== null ) {
});
textLeft = text.slice( 0, rangeLeft );
 
// Remove trailing empty lines
// set color
textLeft = textLeft.replace( this.config.regExp.clipTrimNewLinesLeft, '' );
var color = 0;
for (var i = 0; i < moved.length; i ++) {
var movedGroup = moved[i];
if (wDiff.showBlockMoves === true) {
groups[movedGroup].color = color;
color ++;
}
}
return;
};
 
// Get omission indicators, remove trailing blanks
if ( rangeLeftType === 'chars' ) {
omittedLeft = '~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
else if ( rangeLeftType === 'blank' ) {
omittedLeft = ' ~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
}
 
// Split right text
// wDiff.AssembleDiff: process diff data into formatted html text
var textRight = null;
// input: text, object containing text tokens list; blocks, array containing block type; groups, array containing fixed (not moved), color, and moved mark data
var omittedRight = null;
// returns: diff html string
if ( rangeRight !== null ) {
// called from: wDiff.Diff()
textRight = text.slice( rangeRight );
// calls: wDiff.HtmlCustomize(), wDiff.HtmlFormat()
 
// Remove leading empty lines
wDiff.AssembleDiff = function (text, blocks, groups) {
textRight = textRight.replace( this.config.regExp.clipTrimNewLinesRight, '' );
 
// Get omission indicators, remove leading blanks
//
if ( rangeRightType === 'chars' ) {
// create group diffs
omittedRight = '~';
//
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
else if ( rangeRightType === 'blank' ) {
omittedRight = '~ ';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
}
 
// Remove split element
// cycle through groups
fragments.splice( fragment, 1 );
for (var group = 0; group < groups.length; group ++) {
fragmentsLength --;
var fixed = groups[group].fixed;
var color = groups[group].color;
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
var diff = '';
 
// Add left text to fragments list
// check for colored block and move direction
var if blockFrom( rangeLeft !== null; ) {
fragments.splice( fragment ++, 0, { text: textLeft, type: '=', color: null } );
if ( (fixed === false) && (color !== null) ) {
fragmentsLength ++;
if (groups[ groups[group].movedFrom ].blockStart < blockStart) {
if ( omittedLeft !== null ) {
blockFrom = 'left';
fragments.splice( fragment ++, 0, { text: '', type: omittedLeft, color: null } );
fragmentsLength ++;
}
}
 
else {
// Add fragment container and separator to list
blockFrom = 'right';
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 );
}
 
// addAdd 'same'container (unchanged)start textmarkup
if ( type === 'same{' ) {
html = this.config.htmlCode.containerStart;
diff += string;
}
 
// addAdd 'del'container textend markup
else if ( type === 'del}' ) {
html = this.config.htmlCode.containerEnd;
string = string.replace(/\n/g, wDiff.htmlNewline);
diff += wDiff.htmlDeleteStart + string + wDiff.htmlDeleteEnd;
}
 
// addAdd 'ins'fragment textstart markup
else if ( type === 'ins[' ) {
html = this.config.htmlCode.fragmentStart;
string = string.replace(/\n/g, wDiff.htmlNewline);
diff += wDiff.htmlInsertStart + string + wDiff.htmlInsertEnd;
}
}
 
// addAdd colored blockfragment end markup
else if (blockFrom type === 'left]' ) {
html = this.config.htmlCode.fragmentEnd;
diff += wDiff.htmlBlockLeftEnd;
}
else if (blockFrom == 'right') {
diff += wDiff.htmlBlockRightEnd;
}
 
// Add fragment separator markup
groups[group].diff = diff;
else if ( type === ',' ) {
}
html = this.config.htmlCode.separator;
}
 
// 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 omission markup
// cycle through list of groups moved from here
var if leftMarks( type === '~ '; ) {
html = this.config.htmlCode.omittedChars + ' ';
var rightMarks = '';
}
for (var i = 0; i < moved.length; i ++) {
var movedGroup = moved[i];
var markColor = groups[movedGroup].color;
var mark = '';
 
// getAdd movedcolored left-pointing block textstart markup
varelse movedTextif ( type === '(<'; ) {
if ( version !== 'old' ) {
for (var block = groups[movedGroup].blockStart; block <= groups[movedGroup].blockEnd; block ++) {
 
if (blocks[block].type != 'ins') {
// Get title
movedText += blocks[block].string;
var title;
if ( this.config.noUnicodeSymbols === true ) {
title = this.config.msg['wiked-diff-block-left-nounicode'];
}
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
// display as deletion at original position
else if (wDiff.showBlockMoves type === 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;
}
}
}
groups[group].diff = leftMarks + groups[group].diff + rightMarks;
}
 
// Add '<' and '>' code
//
else if ( type === '<' || type === '>' ) {
// join diffs
if ( version !== 'new' ) {
//
 
// Display as deletion at original position
// make shallow copy of groups and sort by blockStart
if ( this.config.showBlockMoves === false || version === 'old' ) {
var groupsSort = groups.slice();
text = this.htmlEscape( text );
groupsSort.sort(function(a, b) {
text = this.markupBlanks( text, true );
return a.blockStart - b.blockStart;
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
// cycle through sorted groups and assemble diffs
else {
for (var group = 0; group < groupsSort.length; group ++) {
if ( type === '<' ) {
text.diff += groupsSort[group].diff;
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 );
}
 
// Join fragments
// WED('Groups', wDiff.DebugGroups(groups));
this.html = htmlFragments.join( '' );
 
return;
// keep newlines and multiple spaces
};
wDiff.HtmlFormat(text);
 
// WED('text.diff', text.diff);
 
/**
return text.diff;
* 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
html = html.replace( /\{number\}/g, number);
 
// Replace {nounicode} with wikEdDiffNoUnicode class name
//
if ( this.config.noUnicodeSymbols === true ) {
// wDiff.HtmlCustomize: customize move indicator html: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
html = html.replace( /\{nounicode\}/g, ' wikEdDiffNoUnicode');
// input: text (html or css code)
// returns: customized text
// called from: wDiff.AssembleDiff()
 
wDiff.HtmlCustomize = function (text, number, title) {
 
if (wDiff.coloredBlocks === true) {
var blockStyle = wDiff.styleBlockColor[number];
if (blockStyle === undefined) {
blockStyle = '';
}
else {
var markStyle = wDiff.styleMarkColor[number];
html = html.replace( /\{nounicode\}/g, '');
if (markStyle === undefined) {
markStyle = '';
}
text = text.replace(/\{block\}/g, ' ' + blockStyle);
text = text.replace(/\{mark\}/g, ' ' + markStyle);
text = text.replace(/\{class\}/g, number);
}
else {
text = text.replace(/\{block\}|\{mark\}|\{class\}/g, '');
}
text = text.replace(/\{number\}/g, number);
 
// shortenShorten title text, replace {title}
if ( (title !== undefined) && (title !== '') ) {
var max = 512;
var end = 128;
var gapMark = ' [...] ';
if ( title.length > max ) {
title =
title = title.substr(0, max - gapMark.length - end) + gapMark + title.substr(title.length - end);
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;
title = wDiff.HtmlEscape(title);
};
title = title.replace(/\t/g, '&nbsp;&nbsp;');
title = title.replace(/ /g, '&nbsp;&nbsp;');
text = text.replace(/\{title\}/, ' title="' + title + '"');
}
else {
text = text.replace(/\{title\}/, '');
}
return text;
};
 
 
//**
// wDiff.HtmlEscape:* replaceReplace html-sensitive characters in output text with character entities.
*
// input: text
* @param string html Html code to be escaped
// returns: escaped text
* @return string Escaped html code
// called from: wDiff.Diff(), wDiff.AssembleDiff()
*/
this.htmlEscape = function ( html ) {
 
html = html.replace( /&/g, '&amp;');
wDiff.HtmlEscape = function (text) {
html = html.replace( /</g, '&lt;');
html = html.replace( />/g, '&gt;');
html = html.replace( /"/g, '&quot;');
return html;
};
 
text = text.replace(/&/g, '&amp;');
text = text.replace(/</g, '&lt;');
text = text.replace(/>/g, '&gt;');
text = text.replace(/"/g, '&quot;');
return (text);
};
 
/**
* Markup tabs, newlines, and spaces in diff fragment text.
*
* @param bool highlight Highlight newlines and spaces in addition to tabs
* @param string html Text code to be marked-up
* @return string Marked-up text
*/
this.markupBlanks = function ( html, highlight ) {
 
if ( highlight === true ) {
//
html = html.replace( / /g, this.config.htmlCode.space);
// wDiff.HtmlFormat: tidy html, keep newlines and multiple spaces, add container
html = html.replace( /\n/g, this.config.htmlCode.newline);
// changes: text.diff
}
// called from: wDiff.Diff(), wDiff.AssembleDiff()
html = html.replace( /\t/g, this.config.htmlCode.tab);
return html;
};
 
wDiff.HtmlFormat = function (text) {
 
/**
text.diff = text.diff.replace(/<\/(\w+)><!--wDiff(Delete|Insert)--><\1\b[^>]*\bclass="wDiff\2"[^>]*>/g, '');
* Count real words in text.
text.diff = text.diff.replace(/\t/g, wDiff.htmlTab);
*
text.diff = wDiff.htmlContainerStart + wDiff.htmlFragmentStart + text.diff + wDiff.htmlFragmentEnd + wDiff.htmlContainerEnd;
* @param string text Text for word counting
return;
* @return int Number of words in text
};
*/
this.wordCount = function ( text ) {
 
return ( text.match( this.config.regExp.countWords ) || [] ).length;
};
 
// wDiff.ShortenOutput: shorten diff html by removing unchanged parts
// input: diff html string from wDiff.Diff()
// returns: shortened html with removed unchanged passages indicated by (...) or separator
 
/**
wDiff.ShortenOutput = function (html) {
* 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
var diff = '';
this.getDiffHtml( 'new' );
var diff = this.html.replace( /<[^>]*>/g, '');
var text = this.htmlEscape( this.newText.text );
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with new text version!'
);
this.error = true;
console.log( 'new text:\n', text );
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
// empty text
this.getDiffHtml( 'old' );
if ( (html === undefined) || (html === '') ) {
var diff = this.html.replace( /<[^>]*>/g, '');
return '';
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;
// remove container by non-regExp replace
};
html = html.replace(wDiff.htmlContainerStart, '');
html = html.replace(wDiff.htmlFragmentStart, '');
html = html.replace(wDiff.htmlFragmentEnd, '');
html = html.replace(wDiff.htmlContainerEnd, '');
 
// scan for diff html tags
var regExpDiff = /<\w+\b[^>]*\bclass="[^">]*?\bwDiff(MarkLeft|MarkRight|BlockLeft|BlockRight|Delete|Insert)\b[^">]*"[^>]*>(.|\n)*?<!--wDiff\1-->/g;
var tagStart = [];
var tagEnd = [];
var i = 0;
var regExpMatch;
 
/**
// save tag positions
* Dump blocks object to browser console.
while ( (regExpMatch = regExpDiff.exec(html)) !== null ) {
*
* @param string name Block name
* @param[in] array blocks Blocks table object
*/
this.debugBlocks = function ( name, blocks ) {
 
if ( blocks === undefined ) {
// combine consecutive diff tags
blocks = this.blocks;
if ( (i > 0) && (tagEnd[i - 1] == regExpMatch.index) ) {
tagEnd[i - 1] = regExpMatch.index + regExpMatch[0].length;
}
elsevar {dump =
'\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq' +
tagStart[i] = regExpMatch.index;
'\twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \ttext\n';
tagEnd[i] = regExpMatch.index + regExpMatch[0].length;
var blocksLength = blocks.length;
i ++;
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 );
}
};
 
// no diff tags detected
if (tagStart.length === 0) {
return wDiff.htmlNoChange;
}
 
/**
// define regexps
* Dump groups object to browser console.
var regExpHeading = /\n=+.+?=+ *\n|\n\{\||\n\|\}/g;
*
var regExpParagraph = /\n\n+/g;
* @param string name Group name
var regExpLine = /\n+/g;
* @param[in] array groups Groups table object
var regExpBlank = /(<[^>]+>)*\s+/g;
*/
this.debugGroups = function ( name, groups ) {
 
if ( groups === undefined ) {
// determine fragment border positions around diff tags
groups = this.groups;
var rangeStart = [];
}
var rangeEnd = [];
var rangeStartTypedump = [];
'\ni \toldNm \tblSta \tblEnd \tuniq \tmaxWo' +
var rangeEndType = [];
'\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 );
};
 
// get line break positions
var lineBreaks = [];
var pos = 0;
do {
lineBreaks.push(pos);
pos = html.indexOf('\n', pos + 1);
} while (pos != -1);
lineBreaks.push(html.length);
 
/**
// cycle through diff tag start positions
* Dump fragments array to browser console.
for (var i = 0; i < tagStart.length; i ++) {
*
var regExpMatch;
* @param string name Fragments name
* @param[in] array fragments Fragments array
*/
this.debugFragments = function ( name ) {
 
var fragments = this.fragments;
// maximal lines to search before diff tag
var dump = '\ni \ttype \tcolor \ttext\n';
var rangeStartMin = 0;
for (var jfragmentsLength = 0; j < lineBreaksfragments.length - 1; j ++) {
iffor (tagStart[ var i = 0; i] < lineBreaks[jfragmentsLength; i ++ 1]) {
dump +=
if (j >= wDiff.linesBeforeMax) {
i + ' \t"' + fragments[i].type + '" \t' + fragments[i].color + ' \t' +
rangeStartMin = lineBreaks[j - wDiff.linesBeforeMax];
this.debugShortenText( fragments[i].text, 120, 40 ) + '\n';
}
break;
}
}
console.log( name + ':\n' + dump );
};
 
// find last heading before diff tag
var lastPos = tagStart[i] - wDiff.headingBefore;
if (lastPos < rangeStartMin) {
lastPos = rangeStartMin;
}
regExpHeading.lastIndex = lastPos;
while ( (regExpMatch = regExpHeading.exec(html)) !== null ) {
if (regExpMatch.index > tagStart[i]) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'heading';
}
 
/**
// find last paragraph before diff tag
* Dump borders array to browser console.
if (rangeStart[i] === undefined) {
*
lastPos = tagStart[i] - wDiff.paragraphBefore;
* @param string name Arrays name
if (lastPos < rangeStartMin) {
* @param[in] array border Match border array
lastPos = rangeStartMin;
} */
this.debugBorders = function ( name, borders ) {
regExpParagraph.lastIndex = lastPos;
while ( (regExpMatch = regExpParagraph.exec(html)) !== null) {
if (regExpMatch.index > tagStart[i]) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'paragraph';
}
}
 
var dump = '\ni \t[ new \told ]\n';
// find last line break before diff tag
var bordersLength = borders.length;
if (rangeStart[i] === undefined) {
for ( var i = 0; i < bordersLength; i ++ ) {
lastPos = tagStart[i] - wDiff.lineBeforeMax;
dump += i + ' \t[ ' + borders[i][0] + ' \t' + borders[i][1] + ' ]\n';
if (lastPos < rangeStartMin) {
lastPos = rangeStartMin;
}
regExpLine.lastIndex = lastPos;
while ( (regExpMatch = regExpLine.exec(html)) !== null ) {
if (regExpMatch.index > tagStart[i] - wDiff.lineBeforeMin) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'line';
}
}
console.log( name, dump );
};
 
// find last blank before diff tag
if (rangeStart[i] === undefined) {
lastPos = tagStart[i] - wDiff.blankBeforeMax;
if (lastPos < rangeStartMin) {
lastPos = rangeStartMin;
}
regExpBlank.lastIndex = lastPos;
while ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
if (regExpMatch.index > tagStart[i] - wDiff.blankBeforeMin) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'blank';
}
}
 
/**
// fixed number of chars before diff tag
* Shorten text for dumping.
if (rangeStart[i] === undefined) {
*
if (rangeStart[i] > rangeStartMin) {
* @param string text Text to be shortened
rangeStart[i] = tagStart[i] - wDiff.charsBefore;
* @param int max Max length of (shortened) text
rangeStartType[i] = 'chars';
* @param int end Length of trailing fragment of shortened text
}
* @return string Shortened text
}
*/
this.debugShortenText = function ( text, max, end ) {
 
if ( typeof text !== 'string' ) {
// fixed number of lines before diff tag
text = text.toString();
if (rangeStart[i] === undefined) {
rangeStart[i] = rangeStartMin;
rangeStartType[i] = 'lines';
}
text = text.replace( /\n/g, '\\n');
 
text = text.replace( /\t/g, ' ');
// maximal lines to search after diff tag
if ( max === undefined ) {
var rangeEndMax = html.length;
var pos max = tagEnd[i]50;
for (var j = 0; j < wDiff.linesAfterMax; j ++) {
pos = html.indexOf('\n', pos + 1);
if (pos == -1) {
rangeEndMax = html.length;
break;
}
rangeEndMax = pos;
}
if ( end === undefined ) {
 
end = 15;
// find first heading after diff tag
regExpHeading.lastIndex = tagEnd[i];
if ( (regExpMatch = regExpHeading.exec(html)) !== null ) {
if ( (regExpMatch.index < tagEnd[i] + wDiff.headingAfter) && (regExpMatch.index < rangeEndMax) ) {
rangeEnd[i] = regExpMatch.index + regExpMatch[0].length;
rangeEndType[i] = 'heading';
}
}
if ( text.length > max ) {
 
text = text.substr( 0, max - 1 - end ) + '…' + text.substr( text.length - end );
// find first paragraph after diff tag
if (rangeEnd[i] === undefined) {
regExpParagraph.lastIndex = tagEnd[i];
if ( (regExpMatch = regExpParagraph.exec(html)) !== null ) {
if ( (regExpMatch.index < tagEnd[i] + wDiff.paragraphAfter) && (regExpMatch.index < rangeEndMax) ) {
rangeEnd[i] = regExpMatch.index;
rangeEndType[i] = 'paragraph';
}
}
}
return '"' + text + '"';
};
 
 
// find first line break after diff tag
/**
if (rangeEnd[i] === undefined) {
* Start timer 'label', analogous to JavaScript console timer.
regExpLine.lastIndex = tagEnd[i] + wDiff.lineAfterMin;
* Usage: this.time( 'label' );
if ( (regExpMatch = regExpLine.exec(html)) !== null ) {
*
if ( (regExpMatch.index < tagEnd[i] + wDiff.lineAfterMax) && (regExpMatch.index < rangeEndMax) ) {
* @param string label Timer label
rangeEnd[i] = regExpMatch.index;
* @param[out] array timer Current time in milliseconds (float)
rangeEndType[i] = 'break';
*/
}
this.time = function ( label ) {
 
this.timer[label] = new Date().getTime();
return;
};
 
 
/**
* 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 blank after diff tag
* Log recursion timer results to browser console.
if (rangeEnd[i] === undefined) {
* Usage: this.timeRecursionEnd();
regExpBlank.lastIndex = tagEnd[i] + wDiff.blankAfterMin;
*
if ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
* @param string text Text label for output
if ( (regExpMatch.index < tagEnd[i] + wDiff.blankAfterMax) && (regExpMatch.index < rangeEndMax) ) {
* @param[in] array recursionTimer Accumulated recursion times
rangeEnd[i] = regExpMatch.index;
*/
rangeEndType[i] = 'blank';
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];
}
}
 
// Log recursion times
// fixed number of chars after diff tag
var timerLength = this.recursionTimer.length;
if (rangeEnd[i] === undefined) {
iffor (rangeEnd[ var i = 0; i] < rangeEndMaxtimerLength; i ++ ) {
console.log( text + ' recursion ' + i + ': ' + this.recursionTimer[i].toFixed( 2 ) + ' ms' );
rangeEnd[i] = tagEnd[i] + wDiff.charsAfter;
rangeEndType[i] = 'chars';
}
}
this.recursionTimer = [];
return;
};
 
// fixed number of lines after diff tag
if (rangeEnd[i] === undefined) {
rangeEnd[i] = rangeEndMax;
rangeEndType[i] = 'lines';
}
}
 
/**
// remove overlaps, join close fragments
* Log variable values to debug console.
var fragmentStart = [];
* Usage: this.debug( 'var', var );
var fragmentEnd = [];
*
var fragmentStartType = [];
* @param string name Object identifier
var fragmentEndType = [];
* @param mixed|undefined name Object to be logged
fragmentStart[0] = rangeStart[0];
*/
fragmentEnd[0] = rangeEnd[0];
this.debug = function ( name, object ) {
fragmentStartType[0] = rangeStartType[0];
fragmentEndType[0] = rangeEndType[0];
var j = 1;
for (var i = 1; i < rangeStart.length; i ++) {
 
if ( object === undefined ) {
// get lines between fragments
console.log( name );
var lines = 0;
if (fragmentEnd[j - 1] < rangeStart[i]) {
var join = html.substring(fragmentEnd[j - 1], rangeStart[i]);
lines = (join.match(/\n/g) || []).length;
}
 
if ( (rangeStart[i] > fragmentEnd[j - 1] + wDiff.fragmentJoinChars) || (lines > wDiff.fragmentJoinLines) ) {
fragmentStart[j] = rangeStart[i];
fragmentEnd[j] = rangeEnd[i];
fragmentStartType[j] = rangeStartType[i];
fragmentEndType[j] = rangeEndType[i];
j ++;
}
else {
console.log( name + ': ' + object );
fragmentEnd[j - 1] = rangeEnd[i];
fragmentEndType[j - 1] = rangeEndType[i];
}
return;
}
};
 
// assemble the fragments
for (var i = 0; i < fragmentStart.length; i ++) {
 
/**
// get text fragment
* Add script to document head.
var fragment = html.substring(fragmentStart[i], fragmentEnd[i]);
*
fragment = fragment.replace(/^\n+|\n+$/g, '');
* @param string code JavaScript code
*/
this.addScript = function ( code ) {
 
if ( document.getElementById( 'wikEdDiffBlockHandler' ) === null ) {
// add inline marks for omitted chars and words
var script = document.createElement( 'script' );
if (fragmentStart[i] > 0) {
script.id = 'wikEdDiffBlockHandler';
if (fragmentStartType[i] == 'chars') {
if ( script.innerText !== undefined ) {
fragment = wDiff.htmlOmittedChars + fragment;
script.innerText = code;
}
else {
else if (fragmentStartType[i] == 'blank') {
script.textContent = code;
fragment = wDiff.htmlOmittedChars + ' ' + fragment;
}
document.getElementsByTagName( 'head' )[0].appendChild( script );
}
return;
if (fragmentEnd[i] < html.length) {
};
if (fragmentStartType[i] == 'chars') {
 
fragment = fragment + wDiff.htmlOmittedChars;
 
/**
* Add stylesheet to document head, cross-browser >= IE6.
*
* @param string css CSS code
*/
this.addStyleSheet = function ( css ) {
 
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 {
else if (fragmentStartType[i] == 'blank') {
style.appendChild( document.createTextNode( css ) );
fragment = fragment + ' ' + wDiff.htmlOmittedChars;
}
document.getElementsByTagName( 'head' )[0].appendChild( style );
}
return;
};
 
// remove leading and trailing empty lines
fragment = fragment.replace(/^\n+|\n+$/g, '');
 
/**
// add fragment separator
* Recursive deep copy from target over source for customization import.
if (i > 0) {
*
diff += wDiff.htmlSeparator;
* @param object source Source object
* @param object target Target object
*/
this.deepCopy = function ( source, target ) {
 
for ( var key in source ) {
if ( Object.prototype.hasOwnProperty.call( source, key ) === true ) {
if ( typeof source[key] === 'object' ) {
this.deepCopy( source[key], target[key] );
}
else {
target[key] = source[key];
}
}
}
return;
};
 
// Initialze WikEdDiff object
// encapsulate span errors
this.init();
diff += wDiff.htmlFragmentStart + fragment + wDiff.htmlFragmentEnd;
};
 
// add to container
diff = wDiff.htmlContainerStart + diff + wDiff.htmlContainerEnd;
 
/**
// WED('diff', diff);
* 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 */
return diff;
this.parent = parent;
};
 
/** @var string text Text of this version */
this.text = null;
 
/** @var array tokens Tokens list */
//
this.tokens = [];
// wDiff.AddScript: add script to head
//
 
/** @var int first, last First and last index of tokens list */
wDiff.AddScript = function (code) {
this.first = null;
this.last = null;
 
/** @var array words Word counts for version text */
var script = document.createElement('script');
this.words = {};
script.id = 'wDiffBlockHandler';
if (script.innerText !== undefined) {
script.innerText = code;
}
else {
script.textContent = code;
}
wikEd.head.appendChild(script);
return;
};
 
 
//**
* Constructor, initialize text object.
// wDiff.AddStyleSheet: add CSS rules to new style sheet, cross-browser >= IE6
*
//
* @param string text Text of version
* @param WikEdDiff parent Parent, for configuration settings and debugging methods
*/
this.init = function () {
 
if ( typeof text !== 'string' ) {
wDiff.AddStyleSheet = function (css) {
text = text.toString();
}
 
// IE / Mac fix
var style = document.createElement('style');
style this.typetext = 'text.replace( /css\r\n?/g, '\n');
if (style.styleSheet !== undefined) {
style.styleSheet.cssText = css;
}
else {
style.appendChild( document.createTextNode(css) );
}
document.getElementsByTagName('head')[0].appendChild(style);
return;
};
 
// Parse and count words and chunks for identification of unique real words
if ( this.parent.config.timer === true ) {
this.parent.time( 'wordParse' );
}
this.wordParse( this.parent.config.regExp.countWords );
this.wordParse( this.parent.config.regExp.countChunks );
if ( this.parent.config.timer === true ) {
this.parent.timeEnd( 'wordParse' );
}
return;
};
 
//
// wDiff.WordCount: count words in string
//
 
/**
wDiff.WordCount = function (string) {
* Parse and count words and chunks for identification of unique words.
*
* @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 );
return (string.match(wDiff.regExpWordCount) || []).length;
if ( regExpMatch !== null ) {
};
var matchLength = regExpMatch.length;
for (var i = 0; i < matchLength; i ++) {
var word = regExpMatch[i];
if ( Object.prototype.hasOwnProperty.call( this.words, word ) === false ) {
this.words[word] = 1;
}
else {
this.words[word] ++;
}
}
}
return;
};
 
 
//**
* Split text into paragraph, line, sentence, chunk, word, or character tokens.
// wDiff.DebugText: dump text (text.oldText or text.newText) object
*
//
* @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;
wDiff.DebugText = function (text) {
var next = null;
var dump = 'first: ' + text.first + '\tlast: ' + text.last + '\n';
var current = this.tokens.length;
dump += '\ni \tlink \t(prev \tnext) \t#num \t"token"\n';
var ifirst = text.firstcurrent;
var text = '';
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 full text or specified token
if ( token === undefined ) {
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
//
var number = 0;
// wDiff.DebugBlocks: dump blocks object
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
wDiff.DebugBlocks = function (blocks) {
var splitLength = split.length;
var dump = '\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tchars \ttype \tsect \tgroup \tfixed \tstring\n';
for ( var i = 0; i < blocks.lengthsplitLength; 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].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' + blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + wDiff.DebugShortenString(blocks[i].string) + '\n';
}
return dump;
};
 
// 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.DebugGroups: dump groups object
this.tokens[prev].next = current;
//
}
prev = current;
current ++;
}
 
// Connect last new item and existing next item
wDiff.DebugGroups = function (groups) {
if ( number > 0 && token !== undefined ) {
var dump = '\ni \tblSta \tblEnd \tmaxWo \twords \tchars \tfixed \toldNm \tmFrom \tcolor \tmoved \tdiff\n';
for if (var iprev !== 0; i < groups.length; inull ++) {
this.tokens[prev].next = next;
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].movedFrom + ' \t' + groups[i].color + ' \t' + groups[i].moved.toString() + ' \t' + wDiff.DebugShortenString(groups[i].diff) + '\n';
}
if ( next !== null ) {
return dump;
this.tokens[next].prev = prev;
};
}
}
 
// Set text first and last token index
if ( number > 0 ) {
 
// Initial text split
//
if ( token === undefined ) {
// wDiff.DebugGaps: dump gaps object
this.first = 0;
//
this.last = prev;
}
 
// First or last token has been split
wDiff.DebugGaps = function (gaps) {
else {
var dump = '\ni \tnFirs \tnLast \tnTok \toFirs \toLast \toTok \tcharSplit\n';
for if (var itoken === 0; i < gapsthis.length; ifirst ++) {
this.first = 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';
}
}
if ( token === this.last ) {
return dump;
this.last = prev;
};
}
}
}
return;
};
 
 
//**
* Split unique unmatched tokens into smaller tokens.
// wDiff.DebugShortenString: shorten string for debugging
*
//
* @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
wDiff.DebugShortenString = function (string) {
var i = this.first;
if (string === null) {
returnwhile ( i !== '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 + '"';
};
 
// Refine unique unmatched tokens into smaller tokens
if ( this.tokens[i].link === null ) {
this.splitText( regExp, i );
}
i = this.tokens[i].next;
}
return;
};
 
 
// initialize wDiff
/**
wDiff.Init();
* Enumerate text token list before detecting blocks.
*
* @param[out] array tokens Tokens list
*/
this.enumerateTokens = function () {
 
// Enumerate tokens list
var number = 0;
var i = this.first;
while ( i !== null ) {
this.tokens[i].number = number;
number ++;
i = this.tokens[i].next;
}
return;
};
 
 
/**
* Dump tokens object to browser console.
*
* @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;
var dump = 'first: ' + this.first + '\tlast: ' + this.last + '\n';
dump += '\ni \tlink \t(prev \tnext) \tuniq \t#num \t"token"\n';
var i = this.first;
while ( i !== null ) {
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();
};
 
// </syntaxhighlight>