User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.0.20 (September 14, 2014), paragraph split after double newline, sentence split after newline, bubbling to sliding
another background that could use some darkmode friendly color
 
(22 intermediate revisions by 3 users not shown)
Line 2:
 
// ==UserScript==
// @name wDiffwikEd diff
// @version 1.02.204
// @date SeptemberOctober 1423, 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';
 
/** Define global objects. */
* Word (token) types have been optimized for MediaWiki source texts
var wikEdDiffConfig;
* Resolution down to characters level
var WED;
* Highlighting of moved blocks and their original position marks
* Stepwise split (paragraphs, sentences, words, chars)
* Recursive diff
* Additional post-pass-5 code for resolving islands caused by common tokens at the border of sequences of common tokens
* Block move detection and visualization
* Minimizing length of moved vs. static blocks
* Sliding of ambiguous unresolved regions to next line break
* Optional omission of unchanged irrelevant parts from the output
* Fully customizable
* Well commented and documented code
 
This code is used by the MediaWiki in-browser text editors [[en:User:Cacycle/editor]] and [[en:User:Cacycle/wikEd]]
and the enhanced diff view tool wikEdDiff [[en:User:Cacycle/wikEd]].
 
/**
Usage:
* wikEd diff main class.
var diffHtml = wDiff.Diff(oldString, newString);
*
diffHtml = wDiff.ShortenOutput(diffHtml);
* @class WikEdDiff
*/
var WikEdDiff = function () {
 
/** @var array config Configuration and customization settings. */
Datastructures (abbreviations from publication):
this.config = {
 
/** Core diff settings (with default values). */
text: objects for text related data
.newText, new text
.oldText: old text
.string: new or old text to be diffed
.tokens[]: token data list for new or old string (N and O)
.prev: previous list item
.next: next list item
.token: token string
.link: index of corresponding token in new or old text (OA and NA)
.number: list enumeration number
.parsed: token has been added to symbol table
.unique: token is unique word in text
.first: index of first token in tokens list
.last: index of last token in tokens list
.words{}: word count
.diff: diff html
 
/**
symbols: object for symbols table data
* @var bool config.fullDiff
.token[]: associative array (hash) of parsed tokens for passes 1 - 3, points to symbol[i]
* Show complete un-clipped diff text (false)
.symbol[]: array of objects that hold token counters and pointers:
*/
.newCount: new text token counter (NC)
'fullDiff': false,
.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
 
/**
* @var bool config.showBlockMoves
* Enable block move layout with highlighted blocks and marks at the original positions (true)
*/
'showBlockMoves': true,
 
/**
blocks[]: array of objects that holds block (consecutive text tokens) data in order of the new text
* @var bool config.charDiff
.oldBlock: number of block in old text order
* Enable character-refined diff (true)
.newBlock: number of block in new text order
*/
.oldNumber: old text token number of first token
'charDiff': true,
.newNumber: new text token number of first token
.oldStart: old text token index of first token
.count number of tokens
.unique: contains unique matched token
.words: word count
.chars: char length
.type: 'same', 'del', 'ins'
.section: section number
.group: group number of block
.fixed: belongs to a fixed (not moved) group
.string: string of block tokens
 
/**
groups[]: section blocks that are consecutive in old text
* @var bool config.repeatedDiff
.oldNumber: first block oldNumber
* Enable repeated diff to resolve problematic sequences (true)
.blockStart: first block index
*/
.blockEnd: last block index
'repeatedDiff': true,
.unique: contains unique matched token
.maxWords: word count of longest block
.words: word count
.chars: char count
.fixed: not moved from original position
.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.recursiveDiff
* Enable recursive diff to resolve problematic sequences (true)
*/
'recursiveDiff': 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 int config.recursionMax
/* jshint -W004, -W097, -W100, newcap: false, browser: true, jquery: true, sub: true, bitwise: true, curly: true, evil: true, forin: true, freeze: true, immed: true, latedef: true, loopfunc: true, quotmark: single, undef: true */
* Maximum recursion depth (10)
/* global console */
*/
'recursionMax': 10,
 
/**
// turn on ECMAScript 5 strict mode
* @var bool config.unlinkBlocks
'use strict';
* Reject blocks if they are too short and their words are not unique,
* prevents fragmentated diffs for very different versions (true)
*/
'unlinkBlocks': true,
 
/**
// define global object
* @var int config.unlinkMax
var wDiff; if (wDiff === undefined) { wDiff = {}; }
* Maximum number of rejection cycles (5)
var WED;
*/
'unlinkMax': 5,
 
/**
//
* @var int config.blockMinLength
// core diff settings
* Reject blocks if shorter than this number of real words (3)
//
*/
'blockMinLength': 3,
 
/**
// enable block move layout with highlighted blocks and marks at their original positions
* @var bool config.coloredBlocks
if (wDiff.showBlockMoves === undefined) { wDiff.showBlockMoves = true; }
* Display blocks in differing colors (rainbow color scheme) (false)
*/
'coloredBlocks': false,
 
/**
// minimal number of real words for a moved block (0 for always showing highlighted blocks)
* @var bool config.coloredBlocks
if (wDiff.blockMinLength === undefined) { wDiff.blockMinLength = 3; }
* Do not use UniCode block move marks (legacy browsers) (false)
*/
'noUnicodeSymbols': false,
 
/**
// further resolve replacements character-wise from start and end
* @var bool config.stripTrailingNewline
if (wDiff.charDiff === undefined) { wDiff.charDiff = true; }
* Strip trailing newline off of texts (true in .js, false in .php)
*/
'stripTrailingNewline': true,
 
/**
// enable recursive diff to resolve problematic sequences
* @var bool config.debug
if (wDiff.recursiveDiff === undefined) { wDiff.recursiveDiff = true; }
* Show debug infos and stats (block, group, and fragment data) in debug console (false)
*/
'debug': false,
 
/**
// display blocks in different colors
* @var bool config.timer
if (wDiff.coloredBlocks === undefined) { wDiff.coloredBlocks = false; }
* Show timing results in debug console (false)
*/
'timer': false,
 
/**
// UniCode letter support for regexps, from http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
* @var bool config.unitTesting
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'); }
* Run unit tests to prove correct working, display results in debug console (false)
if (wDiff.smallSpaces === undefined) { wDiff.smallSpaces = ' \\u00a0\\u1680​\\u180e\\u2000​\\u2002\\u2004-\\u200a​\\u2028\\u2029​​\\u202f\\u205f​\\u3000'; }
*/
if (wDiff.wideSpaces === undefined) { wDiff.wideSpaces = '\\u2001\\u2003'; }
'unitTesting': false,
 
/** RegExp character classes. */
// regExps for splitting text
if (wDiff.regExpSplit === undefined) {
wDiff.regExpSplit = {
 
// UniCode letter support for regexps
// paragraphs: after double newlines
// From http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
paragraph: /(.|\n)*?\n{2,}/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
// sentences: after newlines and .spaces
'regExpNewLines': '\\u0085\\u2028',
sentence: /.*?(\.\s+|\n)/g,
'regExpNewLinesAll': '\\n\\r\\u0085\\u2028',
 
// Breaking white space characters without \n, \r, and \f
// inline chunks
'regExpBlanks': ' \\t\\x0b\\u2000-\\u200b\\u202f\\u205f\\u3000',
// [[wiki link]] | {{template}} | [ext. link] |<html> | [[wiki link| | {{template| | url
chunk: /\[\[[^\[\]\n]+\]\]|\{\{[^\{\}\n]+\}\}|\[[^\[\]\n]+\]|<\/?[^<>\[\]\{\}\n]+>|\[\[[^\[\]\|\n]+\]\]\||\{\{[^\{\}\|\n]+\||\b((https?:|)\/\/)[^\x00-\x20\s"\[\]\x7f]+/g,
 
// Full stops without '.'
// words, multi-char markup, and chars
'regExpFullStops':
word: new RegExp('[' + wDiff.letters + ']+([\'’_]?[' + wDiff.letters + ']+)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.', 'g'),
'\\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
// chars
'regExpNewParagraph': '\\f\\u2029',
character: /./g
};
}
 
// Exclamation marks without '!'
// regExps for sliding gaps
'regExpExclamationMarks':
if (wDiff.regExpSlideStop === undefined) { wDiff.regExpSlideStop = /\n$/; }
'\\u01C3\\u01C3\\u01C3\\u055C\\u055C\\u07F9\\u1944\\u1944' +
if (wDiff.regExpSlideClosing === undefined) { wDiff.regExpSlideClosing = /^[\s)\]}>\-–—.,:;?!’\/\\=+]/; }
'\\u203C\\u203C\\u2048\\u2048\\uFE15\\uFE57\\uFF01',
 
// Question marks without '?'
// regExp for counting words
'regExpQuestionMarks':
if (wDiff.regExpWordCount === undefined) { wDiff.regExpWordCount = new RegExp('[' + wDiff.letters + ']+([\'’_]?[' + wDiff.letters + ']+)*', 'g'); }
'\\u037E\\u055E\\u061F\\u1367\\u1945\\u2047\\u2049' +
'\\u2CFA\\u2CFB\\u2E2E\\uA60F\\uA6F7\\uFE56\\uFF1F',
 
/** Clip settings. */
// regExp for wiki code non-letter characters
if (wDiff.regExpWikiCodeChars === undefined) { wDiff.regExpWikiCodeChars = /^[ \t\n\[\]{}|+\-!*#:;=<>'\/_,.&?]+$/; }
 
// Find clip position: characters from right
// regExp detecting blank-only blocks
'clipHeadingLeft': 1500,
if (wDiff.regExpBlankBlock === undefined) { wDiff.regExpBlankBlock = new RegExp('^([' + wDiff.smallSpaces + ']{0,2}|[' + wDiff.wideSpaces + ']?|[' + wDiff.smallSpaces + ']?\\n([' + wDiff.smallSpaces + ']{0,2}|[' + wDiff.wideSpaces + ']?|[' + wDiff.smallSpaces + ']?\\n)?)$'); }
'clipParagraphLeftMax': 1500,
'clipParagraphLeftMin': 500,
'clipLineLeftMax': 1000,
'clipLineLeftMin': 500,
'clipBlankLeftMax': 1000,
'clipBlankLeftMin': 500,
'clipCharsLeft': 500,
 
// Find clip position: characters from right
//
'clipHeadingRight': 1500,
// shorten output settings
'clipParagraphRightMax': 1500,
//
'clipParagraphRightMin': 500,
'clipLineRightMax': 1000,
'clipLineRightMin': 500,
'clipBlankRightMax': 1000,
'clipBlankRightMin': 500,
'clipCharsRight': 500,
 
// charactersMaximum beforenumber diffof taglines to search for previousclip heading, paragraph, line break, cut charactersposition
'clipLinesRightMax': 10,
if (wDiff.headingBefore === undefined) { wDiff.headingBefore = 1500; }
'clipLinesLeftMax': 10,
if (wDiff.paragraphBeforeMax === undefined) { wDiff.paragraphBeforeMax = 1500; }
if (wDiff.paragraphBeforeMin === undefined) { wDiff.paragraphBeforeMin = 500; }
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; }
 
// Skip clipping if ranges are too close
// characters after diff tag to search for next heading, paragraph, line break, or characters
'clipSkipLines': 5,
if (wDiff.headingAfter === undefined) { wDiff.headingAfter = 1500; }
'clipSkipChars': 1000,
if (wDiff.paragraphAfterMax === undefined) { wDiff.paragraphAfterMax = 1500; }
if (wDiff.paragraphAfterMin === undefined) { wDiff.paragraphAfterMin = 500; }
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; }
 
// Css stylesheet
// lines before and after diff tag to search for previous heading, paragraph, line break, cut characters
'cssMarkLeft': '◀',
if (wDiff.linesBeforeMax === undefined) { wDiff.linesBeforeMax = 10; }
'cssMarkRight': '▶',
if (wDiff.linesAfterMax === undefined) { wDiff.linesAfterMax = 10; }
'stylesheet':
 
// Insert
// maximal fragment distance to join close fragments
'.wikEdDiffInsert {' +
if (wDiff.fragmentJoinLines === undefined) { wDiff.fragmentJoinLines = 5; }
'font-weight: bold; background-color: #bbddff; ' +
if (wDiff.fragmentJoinChars === undefined) { wDiff.fragmentJoinChars = 1000; }
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'} ' +
'.wikEdDiffInsertBlank { background-color: #66bbff; } ' +
'.wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; } ' +
 
// Delete
//
'.wikEdDiffDelete {' +
// css classes
'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.symbolMarkLeft === undefined) { wDiff.symbolMarkLeft = '◀'; }
'.wikEdDiffBlock {' +
if (wDiff.symbolMarkRight === undefined) { wDiff.symbolMarkRight = '▶'; }
'font-weight: bold; background-color: #e8e8e8; ' +
if (wDiff.stylesheet === undefined) {
'border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; ' +
wDiff.stylesheet =
'} ' +
'.wDiffTab:before { content: "→"; color: #bbb; font-size: smaller; }' +
'.wDiffNewline:beforewikEdDiffBlock { content: "¶"; color: transparent#000; } ' +
'.wDiffInsert:hover .wDiffNewline:beforewikEdDiffBlock0 { background-color: #999ffff80; } ' +
'.wDiffDelete:hover .wDiffNewline:beforewikEdDiffBlock1 { background-color: #aaad0ff80; } ' +
'.wDiffInsertBlank:hover .wDiffNewline:beforewikEdDiffBlock2 { background-color: #888ffd8f0; } ' +
'.wDiffDeleteBlank:hover .wDiffNewline:beforewikEdDiffBlock3 { background-color: #999c0ffff; } ' +
'.wikEdDiffBlock4 { background-color: #fff888; } ' +
'.wDiffBlockLeft:hover .wDiffNewline:before, .wDiffBlockRight:hover .wDiffNewline:before { color: #ccc; }' +
'.wikEdDiffBlock5 { background-color: #bbccff; } ' +
'.wDiffMarkRight:before { content: "' + wDiff.symbolMarkRight + '"; }' +
'.wikEdDiffBlock6 { background-color: #e8c8ff; } ' +
'.wDiffMarkLeft:before { content: "' + wDiff.symbolMarkLeft + '"; }' +
'.wDiffDeletewikEdDiffBlock7 { font-weight: bold; background-color: #ffe49cffbbbb; color:} #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
'.wDiffInsertwikEdDiffBlock8 { font-weight: bold; background-color: #bbddffa0e8a0; color:} #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
'.wikEdDiffBlockHighlight {' +
'.wDiffDeleteBlank { background-color: #ffc840; }' +
'.wDiffInsertBlank { background-color: #66b8ff777; color: #fff; }' +
'border: solid #777; border-width: 1px 0; ' +
'.wDiffBlockLeft { font-weight: bold; background-color: #e8e8e8; border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; }' +
'} ' +
'.wDiffBlockRight { font-weight: bold; background-color: #e8e8e8; border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; }' +
'.wDiffMarkLeft { font-weight: bold; background-color: #ffe49c; color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; }' +
'.wDiffMarkRight { font-weight: bold; background-color: #ffe49c; color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; }' +
'.wDiffBlock { }' +
'.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 { }' +
'.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: #777; color: #fff; border: solid #777; border-width: 1px 0; }' +
'.wDiffMarkHighlight { background-color: #777; color: #fff; }' +
'.wDiffContainer { }' +
'.wDiffFragment { white-space: pre-wrap; background: #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; }' +
'.wDiffNoChange { white-space: pre-wrap; background: #f0f0f0; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; }' +
'.wDiffSeparator { margin-bottom: 1em; }' +
'.wDiffOmittedChars { }';
}
 
// Mark
//
'.wikEdDiffMarkLeft, .wikEdDiffMarkRight {' +
// css styles
'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.styleDelete === undefined) { wDiff.styleDelete = ''; }
'.wikEdDiffContainer { } ' +
if (wDiff.styleInsert === undefined) { wDiff.styleInsert = ''; }
'.wikEdDiffFragment {' +
if (wDiff.styleDeleteBlank === undefined) { wDiff.styleDeleteBlank = ''; }
'white-space: pre-wrap; background-color: var(--background-color-base, #fff); border: #bbb solid; ' +
if (wDiff.styleInsertBlank === undefined) { wDiff.styleInsertBlank = ''; }
'border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; ' +
if (wDiff.styleBlockLeft === undefined) { wDiff.styleBlockLeft = ''; }
'font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; ' +
if (wDiff.styleBlockRight === undefined) { wDiff.styleBlockRight = ''; }
'} ' +
if (wDiff.styleBlockHighlight === undefined) { wDiff.styleBlockHighlight = ''; }
'.wikEdDiffNoChange { background: var(--background-color-interactive, #eaecf0); border: 1px #bbb solid; border-radius: 0.5em; ' +
if (wDiff.styleBlockColor === undefined) { wDiff.styleBlockColor = []; }
'line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; ' +
if (wDiff.styleMarkLeft === undefined) { wDiff.styleMarkLeft = ''; }
'text-align: center; ' +
if (wDiff.styleMarkRight === undefined) { wDiff.styleMarkRight = ''; }
'} ' +
if (wDiff.styleMarkColor === undefined) { wDiff.styleMarkColor = []; }
'.wikEdDiffSeparator { margin-bottom: 1em; } ' +
if (wDiff.styleContainer === undefined) { wDiff.styleContainer = ''; }
'.wikEdDiffOmittedChars { } ' +
if (wDiff.styleFragment === undefined) { wDiff.styleFragment = ''; }
if (wDiff.styleNoChange === undefined) { wDiff.styleNoChange = ''; }
if (wDiff.styleSeparator === undefined) { wDiff.styleSeparator = ''; }
if (wDiff.styleOmittedChars === undefined) { wDiff.styleOmittedChars = ''; }
if (wDiff.styleTab === undefined) { wDiff.styleTab = ''; }
if (wDiff.styleNewline === undefined) { wDiff.styleNewline = ''; }
 
// Newline
//
'.wikEdDiffNewline:before { content: "¶"; color: transparent; } ' +
// html for core diff
'.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
// dynamic replacements: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
'.wikEdDiffTab { position: relative; } ' +
// class plus html comment are required indicators for wDiff.ShortenOutput()
'.wikEdDiffTabSymbol { position: absolute; top: -0.2em; } ' +
if (wDiff.blockEvent === undefined) { wDiff.blockEvent = ' onmouseover="wDiff.BlockHandler(undefined, this, \'mouseover\');"'; }
'.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.htmlContainerStart === undefined) { wDiff.htmlContainerStart = '<div class="wDiffContainer" id="wDiffContainer" style="' + wDiff.styleContainer + '">'; }
'.wikEdDiffSpace { position: relative; } ' +
if (wDiff.htmlContainerEnd === undefined) { wDiff.htmlContainerEnd = '</div>'; }
'.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
if (wDiff.htmlDeleteStart === undefined) { wDiff.htmlDeleteStart = '<span class="wDiffDelete" style="' + wDiff.styleDelete + '" title="−">'; }
'.wikEdDiffError .wikEdDiffFragment,' +
if (wDiff.htmlDeleteStartBlank === undefined) { wDiff.htmlDeleteStartBlank = '<span class="wDiffDelete wDiffDeleteBlank" style="' + wDiff.styleDelete + ' ' + wDiff.styleDeleteBlank + '" title="−">'; }
'.wikEdDiffError .wikEdDiffNoChange' +
if (wDiff.htmlDeleteEnd === undefined) { wDiff.htmlDeleteEnd = '</span><!--wDiffDelete-->'; }
'{ background: #faa; }'
};
 
/** Add regular expressions to configuration settings. */
if (wDiff.htmlInsertStart === undefined) { wDiff.htmlInsertStart = '<span class="wDiffInsert" style="' + wDiff.styleInsert + '" title="+">'; }
if (wDiff.htmlInsertStartBlank === undefined) { wDiff.htmlInsertStartBlank = '<span class="wDiffInsert wDiffInsertBlank" style="' + wDiff.styleInsert + ' ' + wDiff.styleInsertBlank + '" title="+">'; }
if (wDiff.htmlInsertEnd === undefined) { wDiff.htmlInsertEnd = '</span><!--wDiffInsert-->'; }
 
this.config.regExp = {
if (wDiff.htmlBlockLeftStart === undefined) { wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft wDiffBlock{class}" style="' + wDiff.styleBlockLeft + '{block}" title="' + wDiff.symbolMarkLeft + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
if (wDiff.htmlBlockLeftEnd === undefined) { wDiff.htmlBlockLeftEnd = '</span><!--wDiffBlockLeft-->'; }
 
// RegExps for splitting text
if (wDiff.htmlBlockRightStart === undefined) { wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight wDiffBlock{class}" style="' + wDiff.styleBlockRight + '{block}" title="' + wDiff.symbolMarkRight + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
'split': {
if (wDiff.htmlBlockRightEnd === undefined) { wDiff.htmlBlockRightEnd = '</span><!--wDiffBlockRight-->'; }
 
// Split into paragraphs, after double newlines
if (wDiff.htmlMarkRight === undefined) { wDiff.htmlMarkRight = '<span class="wDiffMarkRight wDiffMark{class}" style="' + wDiff.styleMarkRight + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->'; }
'paragraph': new RegExp(
if (wDiff.htmlMarkLeft === undefined) { wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft wDiffMark{class}" style="' + wDiff.styleMarkLeft + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->'; }
'(\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
']',
'g'
),
 
// Split into lines
if (wDiff.htmlNewline === undefined) { wDiff.htmlNewline = '<span class="wDiffNewline" style="' + wDiff.styleNewline + '"></span>\n'; }
'line': new RegExp(
if (wDiff.htmlTab === undefined) { wDiff.htmlTab = '<span class="wDiffTab" style="' + wDiff.styleTab + '">\t</span>'; }
'\\r\\n|\\n|\\r|[' +
this.config.regExpNewLinesAll +
']',
'g'
),
 
// Split into sentences /[^ ].*?[.!?:;]+(?= |$)/
//
'sentence': new RegExp(
// html for shorten output
'[^' +
//
this.config.regExpBlanks +
'].*?[.!?:;' +
this.config.regExpFullStops +
this.config.regExpExclamationMarks +
this.config.regExpQuestionMarks +
']+(?=[' +
this.config.regExpBlanks +
']|$)',
'g'
),
 
// Split into inline chunks
if (wDiff.htmlFragmentStart === undefined) { wDiff.htmlFragmentStart = '<pre class="wDiffFragment" style="' + wDiff.styleFragment + '">'; }
'chunk': new RegExp(
if (wDiff.htmlFragmentEnd === undefined) { wDiff.htmlFragmentEnd = '</pre>'; }
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
 
// Split into words, multi-char markup, and chars
if (wDiff.htmlNoChange === undefined) { wDiff.htmlNoChange = '<pre class="wDiffNoChange" style="' + wDiff.styleNoChange + '" title="="></pre>'; }
// regExpLetters speed-up: \\w+
if (wDiff.htmlSeparator === undefined) { wDiff.htmlSeparator = '<div class="wDiffSeparator" style="' + wDiff.styleSeparator + '"></div>'; }
'word': new RegExp(
if (wDiff.htmlOmittedChars === undefined) { wDiff.htmlOmittedChars = '<span class="wDiffOmittedChars" style="' + wDiff.styleOmittedChars + '">…</span>'; }
'(\\w+|[_' +
this.config.regExpLetters +
'])+([\'’][_' +
this.config.regExpLetters +
']*)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.',
'g'
),
 
// Split into chars
'character': /./g
},
 
// RegExp to detect blank tokens
//
'blankOnlyToken': new RegExp(
// javascript handler for output code, compatible with IE 8
'[^' +
//
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']'
),
 
// RegExps for sliding gaps: newlines and space/word breaks
// wDiff.BlockHandler: event handler for block and mark elements
'slideStop': new RegExp(
if (wDiff.BlockHandler === undefined) { wDiff.BlockHandler = function (event, element, type) {
'[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']$'
),
'slideBorder': new RegExp(
'[' +
this.config.regExpBlanks +
']$'
),
 
// RegExps for counting words
// IE compatibility
'countWords': new RegExp(
if ( (event === undefined) && (window.event !== undefined) ) {
'(\\w+|[_' +
event = window.event;
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
// get mark/block elements
'blankBlock': /^([^\t\S]+|[^\t])$/,
var number = element.id.replace(/\D/g, '');
var block = document.getElementById('wDiffBlock' + number);
var mark = document.getElementById('wDiffMark' + number);
 
// RegExps for clipping
// highlight corresponding mark/block pairs
'clipLine': new RegExp(
if (type == 'mouseover') {
'[' + this.config.regExpNewLinesAll +
element.onmouseover = null;
this.config.regExpNewParagraph +
element.onmouseout = function (event) { wDiff.BlockHandler(event, element, 'mouseout'); };
']+',
element.onclick = function (event) { wDiff.BlockHandler(event, element, 'click'); };
'g'
block.className += ' wDiffBlockHighlight';
),
mark.className += ' wDiffMarkHighlight';
'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. */
// remove mark/block highlighting
if ( (type == 'mouseout') || (type == 'click') ) {
element.onmouseout = null;
element.onmouseover = function (event) { wDiff.BlockHandler(event, element, 'mouseover'); };
 
this.config.msg = {
// getElementsByClassName
'wiked-diff-empty': '(No difference)',
var container = document.getElementById('wDiffContainer');
'wiked-diff-same': '=',
var spans = container.getElementsByTagName('span');
'wiked-diff-ins': '+',
for (var i = 0; i < spans.length; i ++) {
'wiked-diff-del': '-',
if ( ( (spans[i] != block) && (spans[i] != mark) ) || (type != 'click') ) {
'wiked-diff-block-left': '◀',
if (spans[i].className.indexOf(' wDiffBlockHighlight') != -1) {
'wiked-diff-block-right': '▶',
spans[i].className = spans[i].className.replace(/ wDiffBlockHighlight/g, '');
'wiked-diff-block-left-nounicode': '<',
}
'wiked-diff-block-right-nounicode': '>',
else if (spans[i].className.indexOf(' wDiffMarkHighlight') != -1) {
'wiked-diff-error': 'Error: diff not consistent with versions!'
spans[i].className = spans[i].className.replace(/ wDiffMarkHighlight/g, '');
};
}
}
}
}
 
/**
// scroll to corresponding mark/block element
* Add output html fragments to configuration settings.
if (type == 'click') {
* 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">',
// get corresponding element
'containerEnd': '</div>',
var corrElement;
if (element == block) {
corrElement = mark;
}
else {
corrElement = block;
}
 
'fragmentStart': '<pre class="wikEdDiffFragment" style="white-space: pre-wrap;">',
// getOffsetTop
'fragmentEnd': '</pre>',
var corrElementPos = 0;
'separator': '<div class="wikEdDiffSeparator"></div>',
var node = corrElement;
do {
corrElementPos += node.offsetTop;
} while ( (node = node.offsetParent) !== null );
 
'insertStart':
// scroll element under mouse cursor
'<span class="wikEdDiffInsert" title="' +
var top;
this.config.msg['wiked-diff-ins'] +
if (window.pageYOffset !== undefined) {
'">',
top = window.pageYOffset;
'insertStartBlank':
}
'<span class="wikEdDiffInsert wikEdDiffInsertBlank" title="' +
else {
this.config.msg['wiked-diff-ins'] +
top = document.documentElement.scrollTop;
} '">',
'insertEnd': '</span>',
 
'deleteStart':
var cursor;
'<span class="wikEdDiffDelete" title="' +
if (event.pageY !== undefined) {
this.config.msg['wiked-diff-del'] +
cursor = event.pageY;
} '">',
'deleteStartBlank':
else if (event.clientY !== undefined) {
'<span class="wikEdDiffDelete wikEdDiffDeleteBlank" title="' +
cursor = event.clientY + top;
this.config.msg['wiked-diff-del'] +
}
'">',
'deleteEnd': '</span>',
 
'blockStart':
var line = 12;
'<span class="wikEdDiffBlock"' +
if (window.getComputedStyle !== undefined) {
'title="{title}" id="wikEdDiffBlock{number}"' +
line = parseInt(window.getComputedStyle(corrElement).getPropertyValue('line-height'));
'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':
window.scroll(0, corrElementPos + top - cursor + line / 2);
'<span class="wikEdDiffMarkLeft{nounicode}"' +
}
'title="{title}" id="wikEdDiffMark{number}"' +
return;
'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}"' +
// start of diff code
'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>',
// wDiff.Init: initialize wDiff
// called from: on code load
// calls: wDiff.AddStyleSheet()
 
'errorStart': '<div class="wikEdDiffError" title="Error: diff not consistent with versions!">',
wDiff.Init = function () {
'errorEnd': '</div>'
};
 
/*
// compatibility fixes for old names of functions
* Add JavaScript event handler function to configuration settings
window.StringDiff = wDiff.Diff;
* Highlights corresponding block and mark elements on hover and jumps between them on click
window.WDiffString = wDiff.Diff;
* Code for use in non-jQuery environments and legacy browsers (at least IE 8 compatible)
window.WDiffShortenOutput = wDiff.ShortenOutput;
*
* @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
// shortcut to wikEd.Debug()
if (WED event === undefined && window.event !== undefined ) {
event = window.event;
if (typeof console == 'object') {
WED = console.log;
}
 
else {
// Get mark/block elements
WED = window.alert;
var number = element.id.replace( /\D/g, '' );
var block = document.getElementById( 'wikEdDiffBlock' + number );
var mark = document.getElementById( 'wikEdDiffMark' + number );
if ( block === null || mark === null ) {
return;
}
}
 
// Highlight corresponding mark/block pairs
// add styles to head
if ( type === 'mouseover' ) {
wDiff.AddStyleSheet(wDiff.stylesheet);
element.onmouseover = null;
element.onmouseout = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'mouseout' );
};
element.onclick = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'click' );
};
block.className += ' wikEdDiffBlockHighlight';
mark.className += ' wikEdDiffMarkHighlight';
}
 
// Remove mark/block highlighting
// add block handler to head if running under Greasemonkey
if (typeof GM_infotype === 'objectmouseout' || type === 'click' ) {
element.onmouseout = null;
var script = 'var wDiff; if (wDiff === undefined) { wDiff = {}; } wDiff.BlockHandler = ' + wDiff.BlockHandler.toString();
element.onmouseover = function ( event ) {
wDiff.AddScript(script);
window.wikEdDiffBlockHandler( event, element, 'mouseover' );
}
};
return;
};
 
// Reset, allow outside container (e.g. legend)
if ( type !== 'click' ) {
block.className = block.className.replace( / wikEdDiffBlockHighlight/g, '' );
mark.className = mark.className.replace( / wikEdDiffMarkHighlight/g, '' );
 
// GetElementsByClassName
// wDiff.Diff: main method
var container = document.getElementById( 'wikEdDiffContainer' );
// input: oldString, newString, strings containing the texts to be diffed
if ( container !== null ) {
// called from: user code
var spans = container.getElementsByTagName( 'span' );
// calls: wDiff.Split(), wDiff.SplitRefine(), wDiff.CalculateDiff(), wDiff.DetectBlocks(), wDiff.AssembleDiff()
var spansLength = spans.length;
// returns: diff html code, call wDiff.ShortenOutput() for shortening this output
for ( var i = 0; i < spansLength; i ++ ) {
if ( spans[i] !== block && spans[i] !== mark ) {
if ( spans[i].className.indexOf( ' wikEdDiffBlockHighlight' ) !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffBlockHighlight/g, '' );
}
else if ( spans[i].className.indexOf( ' wikEdDiffMarkHighlight') !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffMarkHighlight/g, '' );
}
}
}
}
}
}
 
// Scroll to corresponding mark/block element
wDiff.Diff = function (oldString, newString) {
if ( type === 'click' ) {
 
// Get corresponding element
var diff = '';
var corrElement;
if ( element === block ) {
corrElement = mark;
}
else {
corrElement = block;
}
 
// Get element height (getOffsetTop)
// wikEd.debugTimer.push(['diff?', new Date]);
var corrElementPos = 0;
var node = corrElement;
do {
corrElementPos += node.offsetTop;
} while ( ( node = node.offsetParent ) !== null );
 
// IEGet / Macscroll fixheight
var top;
oldString = oldString.replace(/\r\n?/g, '\n');
if ( window.pageYOffset !== undefined ) {
newString = newString.replace(/\r\n?/g, '\n');
top = window.pageYOffset;
}
else {
top = document.documentElement.scrollTop;
}
 
// Get cursor pos
// prepare text data object
var text = {cursor;
if ( event.pageY !== undefined ) {
newText: {
cursor = event.pageY;
string: newString,
tokens: [],}
else if ( event.clientY !== undefined ) {
first: null,
cursor = event.clientY + top;
last: null,
words: {}
},
oldText: {
string: oldString,
tokens: [],
first: null,
last: null,
words: {}
},
diff: ''
};
 
// Get line height
// trap trivial changes: no change
var line = 12;
if (oldString == newString) {
if ( window.getComputedStyle !== undefined ) {
text.diff = wDiff.HtmlEscape(newString);
line = parseInt( window.getComputedStyle( corrElement ).getPropertyValue( 'line-height' ) );
wDiff.HtmlFormat(text);
}
return text.diff;
 
}
// Scroll element under mouse cursor
window.scroll( 0, corrElementPos + top - cursor + line / 2 );
}
return;
};
 
/** Internal data structures. */
// 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 WikEdDiffText newText New text version object with text and token list */
// trap trivial changes: new text deleted
this.newText = null;
if ( (newString === null) || (newString.length === 0) ) {
text.diff = wDiff.htmlDeleteStart + wDiff.HtmlEscape(oldString) + wDiff.htmlDeleteEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var WikEdDiffText oldText Old text version object with text and token list */
// parse and count count words in texts for later identification of unique words
this.oldText = null;
wDiff.CountTextWords(text.newText);
wDiff.CountTextWords(text.oldText);
 
/** @var object symbols Symbols table for whole text at all refinement levels */
// new symbols object
var this.symbols = {
token: [],
hashhashTable: {},
linked: false
};
 
/** @var array bordersDown Matched region borders downwards */
// split new and old text into paragraps
this.bordersDown = [];
wDiff.Split(text.newText, 'paragraph');
wDiff.Split(text.oldText, 'paragraph');
 
/** @var array bordersUp Matched region borders upwards */
// calculate diff
this.bordersUp = [];
wDiff.CalculateDiff(text, symbols, 'paragraph');
 
/** @var array blocks Block data (consecutive text tokens) in new text order */
// refine different paragraphs into sentences
this.blocks = [];
wDiff.SplitRefine(text.newText, 'sentence');
wDiff.SplitRefine(text.oldText, 'sentence');
 
/** @var int maxWords Maximal detected word count of all linked blocks */
// calculate refined diff
this.maxWords = 0;
wDiff.CalculateDiff(text, symbols, 'sentence');
 
/** @var array groups Section blocks that are consecutive in old text order */
// refine different paragraphs into chunks
this.groups = [];
wDiff.SplitRefine(text.newText, 'chunk');
wDiff.SplitRefine(text.oldText, 'chunk');
 
/** @var array sections Block sections with no block move crosses outside a section */
// calculate refined diff
this.sections = [];
wDiff.CalculateDiff(text, symbols, 'chunk');
 
/** @var object timer Debug timer array: string 'label' => float milliseconds. */
// refine different sentences into words
this.timer = {};
wDiff.SplitRefine(text.newText, 'word');
wDiff.SplitRefine(text.oldText, 'word');
 
/** @var array recursionTimer Count time spent in recursion level in milliseconds. */
// calculate refined diff information with recursion for unresolved gaps
this.recursionTimer = [];
wDiff.CalculateDiff(text, symbols, 'word', true);
 
//** slideOutput updata. gaps*/
wDiff.SlideGaps(text.newText, text.oldText);
wDiff.SlideGaps(text.oldText, text.newText);
 
/** @var bool error Unit tests have detected a diff error */
// split tokens into chars in selected unresolved gaps
this.error = false;
if (wDiff.charDiff === true) {
wDiff.SplitRefineChars(text);
 
/** @var array fragments Diff fragment list for markup, abstraction layer for customization */
// calculate refined diff information with recursion for unresolved gaps
this.fragments = [];
wDiff.CalculateDiff(text, symbols, 'character', true);
 
/** @var string html Html code of diff */
// slide up gaps
this.html = '';
wDiff.SlideGaps(text.newText, text.oldText);
wDiff.SlideGaps(text.oldText, text.newText);
}
 
// enumerate tokens lists
wDiff.EnumerateTokens(text.newText);
wDiff.EnumerateTokens(text.oldText);
 
/**
// detect moved blocks
* Constructor, initialize settings, load js and css.
var blocks = [];
*
var groups = [];
* @param[in] object wikEdDiffConfig Custom customization settings
wDiff.DetectBlocks(text, blocks, groups);
* @param[out] object config Settings
*/
 
this.init = function () {
// assemble diff blocks into formatted html text
diff = wDiff.AssembleDiff(text, blocks, groups);
 
// Import customizations from wikEdDiffConfig{}
// wikEd.debugTimer.push(['diff=', new Date]);
if ( typeof wikEdDiffConfig === 'object' ) {
// wikEd.DebugTimer();
this.deepCopy( wikEdDiffConfig, this.config );
}
 
// Add CSS stylescheet
return diff;
this.addStyleSheet( this.config.stylesheet );
};
 
// Load block handler script
if ( this.config.showBlockMoves === true ) {
 
// Add block handler to head if running under Greasemonkey
// wDiff.CountTextWords: parse and count words in text for later identification of unique words
if ( typeof GM_info === 'object' ) {
// changes: text (text.newText or text.oldText) .words
var script = 'var wikEdDiffBlockHandler = ' + this.config.blockHandler.toString() + ';';
// called from: wDiff.Diff()
this.addScript( script );
}
else {
window.wikEdDiffBlockHandler = this.config.blockHandler;
}
}
return;
};
 
wDiff.CountTextWords = function (text) {
 
/**
var regExpMatch;
* Main diff method.
while ( (regExpMatch = wDiff.regExpWordCount.exec(text.string)) !== null) {
*
var word = text.words[ regExpMatch[0] ];
* @param string oldString Old text version
if (word === undefined) {
* @param string newString New text version
word = 1;
* @param[out] array fragment
* Diff fragment list ready for markup, abstraction layer for customized diffs
* @param[out] string html Html code of diff
* @return string Html code of diff
*/
this.diff = function ( oldString, newString ) {
 
// Start total timer
if ( this.config.timer === true ) {
this.time( 'total' );
}
 
else {
// Start diff timer
word ++;
if ( this.config.timer === true ) {
this.time( 'diff' );
}
}
return;
};
 
// Reset error flag
this.error = false;
 
// Strip trailing newline (.js only)
// wDiff.Split: split text into paragraph, sentence, or word tokens
if ( this.config.stripTrailingNewline === true ) {
// 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
if ( newString.substr( -1 ) === '\n' && oldString.substr( -1 === '\n' ) ) {
// changes: text (text.newText or text.oldText): text.tokens list, text.first, text.last
newString = newString.substr( 0, newString.length - 1 );
// called from: wDiff.Diff()
oldString = oldString.substr( 0, oldString.length - 1 );
}
}
 
// Load version strings into WikEdDiffText objects
wDiff.Split = function (text, level, token) {
this.newText = new WikEdDiff.WikEdDiffText( newString, this );
this.oldText = new WikEdDiff.WikEdDiffText( oldString, this );
 
// Trap trivial changes: no change
var prev = null;
if ( this.newText.text === this.oldText.text ) {
var next = null;
this.html =
var current = text.tokens.length;
this.config.htmlCode.containerStart +
var first = current;
this.config.htmlCode.noChangeStart +
var string = '';
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
// split full text or specified token
if (
if (token === undefined) {
this.oldText.text === '' || (
string = text.string;
this.oldText.text === '\n' &&
}
( this.newText.text.charAt( this.newText.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.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 text into tokens, regExp match as separator
if (
var number = 0;
this.newText.text === '' || (
var split = [];
this.newText.text === '\n' &&
var regExpMatch;
( this.oldText.text.charAt( this.oldText.text.length - 1 ) === '\n' )
var lastIndex = 0;
)
while ( (regExpMatch = wDiff.regExpSplit[level].exec(string)) !== null) {
) {
if (regExpMatch.index > lastIndex) {
this.html =
split.push(string.substring(lastIndex, regExpMatch.index));
this.config.htmlCode.containerStart +
this.config.htmlCode.fragmentStart +
this.config.htmlCode.deleteStart +
this.htmlEscape( this.oldText.text ) +
this.config.htmlCode.deleteEnd +
this.config.htmlCode.fragmentEnd +
this.config.htmlCode.containerEnd;
return this.html;
}
split.push(regExpMatch[0]);
lastIndex = wDiff.regExpSplit[level].lastIndex;
}
if (lastIndex < string.length) {
split.push(string.substring(lastIndex));
}
 
// Split new and old text into paragraps
// cycle trough new tokens
if ( this.config.timer === true ) {
for (var i = 0; i < split.length; i ++) {
this.time( 'paragraph split' );
}
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: split[i],
prev: prev,
next: null,
link: null,
number: null,
parsed: false,
unique: 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;
}
}
}
}
 
// sameNext tokenlist lengthelements
else if (newToken !i === gaps[gap].newLast oldToken) {
break;
 
// tokens less than 50 % identical
var ident = 0;
for (var pos = 0; pos < shorterToken.length; pos ++) {
if (shorterToken.charAt(pos) == longerToken.charAt(pos)) {
ident ++;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
gaps[gap].charSplit = charSplit;
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 = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
gaps[gap].charSplit = charSplit;
}
 
/** Refine words into chars in selected gaps. */
//
// refine words into chars in selected gaps
//
 
for ( var gapgapsLength = 0; gap < gaps.length; gap ++) {
iffor (gaps[ var gap].charSplit === true0; gap < gapsLength; gap ++ ) {
if ( gaps[gap].charSplit === true ) {
 
// cycleCycle troughthrough new text tokens list, link spaces, and split into chars
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
var newGapLength = i - gaps[gap].newLast;
while (i !== null) {
var newTokenoldGapLength = text.newText.tokensj - gaps[igap].tokenoldLast;
while ( i !== null || j !== null ) {
var oldToken = text.oldText.tokens[j].token;
 
// linkLink identical tokens (spaces) to keep char refinement to words
if (newToken == oldToken) {
newGapLength === oldGapLength &&
text.newText.tokens[i].link = j;
text this.oldTextnewText.tokens[ji].linktoken === i;this.oldText.tokens[j].token
} ) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
 
// refine differentRefine words into chars
else {
if ( i !== null ) {
wDiff.Split(text.newText, 'character', i);
wDiff this.Split(textnewText.oldText,splitText( 'character', ji );
}
if ( j !== null ) {
this.oldText.splitText( 'character', j );
}
}
 
// nextNext list elements
if ( i === gaps[gap].newLast ) {
break 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;
}
}
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
}
return;
}
};
 
// WED('Gap', wDiff.DebugGaps(gaps));
 
/**
return;
* Move gaps with ambiguous identical fronts to last newline border or otherwise last word border.
};
*
* @param[in/out] wikEdDiffText text, textLinked These two are newText and oldText
*/
this.slideGaps = function ( text, textLinked ) {
 
var regExpSlideBorder = this.config.regExp.slideBorder;
var regExpSlideStop = this.config.regExp.slideStop;
 
// Cycle through tokens list
// wDiff.SlideGaps: move gaps with ambiguous identical fronts and backs up
var i = text.first;
// start ambiguous gap borders after line breaks and text section closing characters
var gapStart = null;
// changes: text (text.newText or text.oldText) .tokens list
while ( i !== null ) {
// called from: wDiff.Diff()
 
// Remember gap start
wDiff.SlideGaps = function (text, textLinked) {
if ( gapStart === null && text.tokens[i].link === null ) {
gapStart = i;
}
 
// Find gap end
// cycle through tokens list
else if ( gapStart !== null && text.tokens[i].link !== null ) {
var i = text.first;
var gapStartgapFront = nullgapStart;
while var (gapBack (i !== null) && (text.tokens[i] !== null) ) {.prev;
 
// Slide down as deep as possible
// remember gap start
var front = gapFront;
if ( (gapStart === null) && (text.tokens[i].link === null) ) {
var back = text.tokens[gapBack].next;
gapStart = i;
} 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;
// find gap end
else gapBack if ( (gapStart !== null) && (text.tokens[igapBack].link !== null) ) {next;
 
front = text.tokens[front].next;
// slide down as deep as possible
back = text.tokens[back].next;
var front = gapStart;
}
var back = i;
var frontPrev = null;
var backPrev = null;
while (
(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;
frontPrev = front;
backPrev = back;
front = text.tokens[front].next;
back = text.tokens[back].next;
}
 
// testTest baloonslide up, remember last line break or closingword textborder
var frontStopfront = nulltext.tokens[gapFront].prev;
var frontTestback = frontPrevgapBack;
var gapFrontBlankTest = regExpSlideBorder.test( text.tokens[gapFront].token );
var backTest = backPrev;
var frontStop = front;
while (
if (frontTest !text.tokens[back].link === null) && (backTest !== null) &&{
while (
(text.tokens[frontTest].link !== null) && (text.tokens[backTest].link === null) &&
front !== null &&
(text.tokens[frontTest].token == text.tokens[backTest].token)
back !== null &&
) {
if (wDiff.regExpSlideStop.test( text.tokens[frontTestfront].token)link =!== true)null {&&
text.tokens[front].token === text.tokens[back].token
frontStop = frontTest;
break;) {
if ( front !== null ) {
 
// Stop at line break
if ( regExpSlideStop.test( text.tokens[front].token ) === true ) {
frontStop = front;
break;
}
 
// Stop at first word border (blank/word or word/blank)
if (
regExpSlideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
frontStop = front;
}
}
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
}
else if ( (frontStop === null) && (wDiff.regExpSlideClosing.test(text.tokens[frontTest].token) === true) ) {
frontStop = frontTest;
}
frontTest = text.tokens[frontTest].prev;
backTest = text.tokens[backTest].prev;
}
 
// actuallyActually slide up to last line break or closing textstop
var front = text.tokens[gapFront].prev;
if (frontStop !== null) {
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;
back = text.tokens[back].prev;
}
gapStart = null;
}
gapStarti = nulltext.tokens[i].next;
}
i = text.tokens[i].next;
}
return;
};
 
 
// wDiff.EnumerateTokens: enumerate text token list
// changes: text (text.newText or text.oldText) .tokens list
// called from: wDiff.Diff()
 
wDiff.EnumerateTokens = function (text) {
 
// enumerate tokens list
var number = 0;
var i = text.first;
while ( (i !== null) && (text.tokens[i] !== null) ) {
text.tokens[i].number = number;
number ++;
i = text.tokens[i].next;
}
return;
};
 
 
// wDiff.CalculateDiff: calculate diff information, can be called repeatedly during refining
// input: text: object containing text data and tokens; level: 'paragraph', 'sentence', 'word', or 'character'
// optionally for recursive calls: newStart, newEnd, oldStart, oldEnd (tokens list indexes), recursionLevel
// changes: text.oldText/newText.tokens[].link, links corresponding tokens from old and new text
// steps:
// pass 1: parse new text into symbol table
// pass 2: parse old text into symbol table
// pass 3: connect unique matched tokens
// pass 4: connect adjacent identical tokens downwards
// pass 5: connect adjacent identical tokens upwards
// recursively diff still unresolved regions downwards
// recursively diff still unresolved regions upwards
 
wDiff.CalculateDiff = function (text, symbols, level, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
 
// if (recursionLevel === undefined) { wikEd.debugTimer.push([level + '?', new Date]); }
 
// set defaults
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; }
 
// limit recursion depth
if (recursionLevel > 10) {
return;
};
 
//
// pass 1: parse new text into symbol table
//
 
/**
// cycle trough new text tokens list
* Calculate diff information, can be called repeatedly during refining.
var i = newStart;
* Links corresponding tokens from old and new text.
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
* 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
// add new entry to symbol table
if ( repeating === undefined ) { repeating = false; }
var token = text.newText.tokens[i].token;
if ( recurse === undefined ) { recurse = false; }
if (Object.prototype.hasOwnProperty.call(symbols.hash, token) === false) {
if ( newStart === undefined ) { newStart = this.newText.first; }
var current = symbols.token.length;
if ( oldStart === undefined ) { oldStart = this.oldText.first; }
symbols.hash[token] = current;
if ( up === undefined ) { up = false; }
symbols.token[current] = {
if ( recursionLevel === undefined ) { recursionLevel = 0; }
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
};
}
 
// Start timers
// or update existing entry
if ( this.config.timer === true && repeating === false && recursionLevel === 0 ) {
else {
this.time( level );
 
// increment token counter for new text
var hashToArray = symbols.hash[token];
symbols.token[hashToArray].newCount ++;
}
if ( this.config.timer === true && repeating === false ) {
 
this.time( level + recursionLevel );
// next list element
if (i == newEnd) {
break;
}
i = text.newText.tokens[i].next;
}
 
// Get object symbols table and linked region borders
//
var symbols;
// pass 2: parse old text into symbol table
var bordersDown;
//
var bordersUp;
 
if ( recursionLevel === 0 && repeating === false ) {
// cycle trough old text tokens list
symbols = this.symbols;
var j = oldStart;
bordersDown = this.bordersDown;
while ( (j !== null) && (text.oldText.tokens[j] !== null) ) {
bordersUp = this.bordersUp;
 
// add new entry to symbol table
var token = text.oldText.tokens[j].token;
if (Object.prototype.hasOwnProperty.call(symbols.hash, token) === false) {
var current = symbols.token.length;
symbols.hash[token] = current;
symbols.token[current] = {
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
};
}
 
// Create empty local symbols table and linked region borders arrays
// or update existing entry
else {
symbols = {
 
// increment token: counter for old text[],
hashTable: {},
var hashToArray = symbols.hash[token];
linked: false
symbols.token[hashToArray].oldCount ++;
};
 
bordersDown = [];
// add token number for old text
bordersUp = [];
symbols.token[hashToArray].oldToken = j;
}
 
// next list element
if (j === oldEnd) {
break;
}
j = text.oldText.tokens[j].next;
}
 
// Updated versions of linked region borders
//
var bordersUpNext = [];
// pass 3: connect unique tokens
var bordersDownNext = [];
//
 
/**
// cycle trough symbol array
* Pass 1: parse new text into symbol table.
for (var i = 0; i < symbols.token.length; i ++) {
*/
 
// Cycle through new text tokens list
// find tokens in the symbol table that occur only once in both versions
var i = newStart;
if ( (symbols.token[i].newCount == 1) && (symbols.token[i].oldCount == 1) ) {
while ( i !== null ) {
var newToken = symbols.token[i].newToken;
if ( this.newText.tokens[i].link === null ) {
var oldToken = symbols.token[i].oldToken;
 
// doAdd notnew useentry spacesto assymbol unique markerstable
if var (/^\s+$/.test(texttoken = this.newText.tokens[newTokeni].token) === false) {;
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
// connect from new to old and from old to new
else {
if (text.newText.tokens[newToken].link === null) {
text.newText.tokens[newToken].link = oldToken;
text.oldText.tokens[oldToken].link = newToken;
symbols.linked = true;
 
// checkIncrement iftoken uniquecounter wordfor new text
var hashToArray = symbols.hashTable[token];
if ( (level == 'word') && (recursionLevel === 0) ) {
var symbols.token = text.newText.tokens[newTokenhashToArray].tokennewCount ++;
if ( (text.oldText.words[token] == 1) && (text.newText.words[token] == 1) ) {
text.newText.tokens[newToken].unique = true;
text.oldText.tokens[oldToken].unique = true;
}
}
}
}
}
}
 
// Stop after gap if recursing
// continue only if unique tokens have been linked previously
else if ( recursionLevel > 0 ) {
if (symbols.linked === true) {
break;
 
//
// pass 4: connect adjacent identical tokens downwards
//
 
// get surrounding connected tokens
var i = newStart;
if (text.newText.tokens[i].prev !== null) {
i = text.newText.tokens[i].prev;
}
var iStop = newEnd;
if (text.newText.tokens[iStop].next !== null) {
iStop = text.newText.tokens[iStop].next;
}
var j = null;
 
// cycle trough new text tokens list down
do {
 
// connected pair
var link = text.newText.tokens[i].link;
if (link !== null) {
j = text.oldText.tokens[link].next;
}
 
// connectGet ifnext tokens are the sametoken
if ( up === false ) {
else if ( (j !== null) && (text.oldText.tokens[j].link === null) && (text.newText.tokens[i].token == text.oldText.tokens[j].token) ) {
texti = this.newText.tokens[i].link = jnext;
text.oldText.tokens[j].link = i;
j = text.oldText.tokens[j].next;
}
 
// not same
else {
ji = nullthis.newText.tokens[i].prev;
}
}
i = text.newText.tokens[i].next;
} while (i !== iStop);
 
//**
* Pass 2: parse old text into symbol table.
// pass 5: connect adjacent identical tokens upwards
/ */
 
// getCycle surroundingthrough connectedold text tokens list
var ij = newEndoldStart;
ifwhile (text.newText.tokens[i].next j !== null ) {
iif =( textthis.newTextoldText.tokens[ij].next;link === null ) {
}
var iStop = newStart;
if (text.newText.tokens[iStop].prev !== null) {
iStop = text.newText.tokens[iStop].prev;
}
var j = null;
 
// cycle troughAdd new textentry tokensto listsymbol uptable
var token = this.oldText.tokens[j].token;
do {
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
symbols.hashTable[token] = symbols.token.length;
symbols.token.push( {
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
} );
}
 
// Or update existing entry
// connected pair
else {
var link = text.newText.tokens[i].link;
 
if (link !== null) {
// Increment token counter for old text
j = text.oldText.tokens[link].prev;
var hashToArray = symbols.hashTable[token];
symbols.token[hashToArray].oldCount ++;
 
// Add token number for old text
symbols.token[hashToArray].oldToken = j;
}
}
 
// connectStop ifafter tokensgap are theif samerecursing
else if ( recursionLevel > 0 ) {
else if ( (j !== null) && (text.oldText.tokens[j].link === null) && (text.newText.tokens[i].token == text.oldText.tokens[j].token) ) {
break;
text.newText.tokens[i].link = j;
text.oldText.tokens[j].link = i;
j = text.oldText.tokens[j].prev;
}
 
// notGet samenext token
if ( up === false ) {
j = this.oldText.tokens[j].next;
}
else {
j = nullthis.oldText.tokens[j].prev;
}
}
i = text.newText.tokens[i].prev;
} while (i !== iStop);
 
//**
* Pass 3: connect unique tokens.
// connect adjacent identical tokens downwards from text start, treat boundary as connected, stop after first connected token
/ */
 
// onlyCycle forthrough fullsymbol text diffarray
var symbolsLength = symbols.token.length;
if ( (newStart == text.newText.first) && (newEnd == text.newText.last) ) {
for ( var i = 0; i < symbolsLength; i ++ ) {
 
// Find tokens in the symbol table that occur only once in both versions
// from start
if ( symbols.token[i].newCount === 1 && symbols.token[i].oldCount === 1 ) {
var i = text.newText.first;
var jnewToken = textsymbols.oldTexttoken[i].firstnewToken;
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
// cycle trough new text tokens list down, connect identical tokens, stop after first connected token
if ( newTokenObj.link === null ) {
while ( (i !== null) && (j !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) && (text.newText.tokens[i].token == text.oldText.tokens[j].token) ) {
text.newText.tokens[i].link = j;
text.oldText.tokens[j].link = i;
j = text.oldText.tokens[j].next;
i = text.newText.tokens[i].next;
}
 
// Do not use spaces as unique markers
// from end
if (
var i = text.newText.last;
this.config.regExp.blankOnlyToken.test( newTokenObj.token ) === true
var j = text.oldText.last;
) {
 
// Link new and old tokens
// cycle trough old text tokens list up, connect identical tokens, stop after first connected token
newTokenObj.link = oldToken;
while ( (i !== null) && (j !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) && (text.newText.tokens[i].token == text.oldText.tokens[j].token) ) {
text.newText.tokens[i] oldTokenObj.link = jnewToken;
text symbols.oldText.tokens[j].linklinked = itrue;
j = text.oldText.tokens[j].prev;
i = text.newText.tokens[i].prev;
}
}
 
// Save linked region borders
//
bordersDown.push( [newToken, oldToken] );
// refine by recursively diffing unresolved regions caused by addition of common tokens around sequences of common tokens, only at word level split
bordersUp.push( [newToken, oldToken] );
//
 
// Check if token contains unique word
if ( (recurse === true) && (wDiff.recursiveDiff === true) ) {
if ( recursionLevel === 0 ) {
var unique = false;
if ( level === 'character' ) {
unique = true;
}
else {
var token = newTokenObj.token;
var words =
( token.match( this.config.regExp.countWords ) || [] ).concat(
( token.match( this.config.regExp.countChunks ) || [] )
);
 
// Unique if longer than min block length
//
var wordsLength = words.length;
// recursively diff still unresolved regions downwards
if ( wordsLength >= this.config.blockMinLength ) {
//
unique = true;
}
 
// Unique if it contains at least one unique word
// cycle trough new text tokens list
else {
var i = newStart;
for ( var i = 0;i < wordsLength; i ++ ) {
var j = oldStart;
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
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
if ( unique === true ) {
 
newTokenObj.unique = true;
// get j from previous tokens match
oldTokenObj.unique = true;
var iPrev = text.newText.tokens[i].prev;
}
if (iPrev !== null) {
}
var jPrev = text.newText.tokens[iPrev].link;
if (jPrev !== null) {
j = text.oldText.tokens[jPrev].next;
}
}
}
}
 
// Continue passes only if unique tokens have been linked previously
// check for the start of an unresolved sequence
if ( symbols.linked === true ) {
if ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) ) {
 
/**
// determine the limits of of the unresolved new sequence
* Pass 4: connect adjacent identical tokens downwards.
var iStart = i;
*/
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;
}
 
// determineCycle thethrough limitslist of of thelinked unresolvednew oldtext sequencetokens
var jStartbordersLength = jbordersDown.length;
for ( var match = 0; match < bordersLength; match ++ ) {
var jEnd = null;
var jLengthi = bordersDown[match][0];
var jNextj = jbordersDown[match][1];
while ( (jNext !== null) && (text.oldText.tokens[jNext].link === null) ) {
jEnd = jNext;
jLength ++;
if (jEnd == oldEnd) {
break;
}
jNext = text.oldText.tokens[jNext].next;
}
 
// Next down
// recursively diff the unresolved sequence
var iMatch = i;
if ( (iLength > 1) || (jLength > 1) ) {
var jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
 
// Cycle through new symbolstext objectlist forgap sub-region downwards
while (
var symbolsRecurse = {
token:i !== [],null &&
hash:j !== null {},&&
this.newText.tokens[i].link === null &&
linked: false
this.oldText.tokens[j].link === null
};
) {
wDiff.CalculateDiff(text, symbolsRecurse, level, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
 
// 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;
}
i = iEnd;
}
 
// Not a match yet, maybe in next listrefinement elementlevel
if (i == newEnd) else {
bordersDownNext.push( [iMatch, jMatch] );
break;
break;
}
 
// Next token down
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
i = text.newText.tokens[i].next;
}
 
//**
// recursively* diffPass still5: unresolvedconnect regionsadjacent identical tokens upwards.
/ */
 
// cycleCycle troughthrough list of connected new text tokens list
var ibordersLength = newEndbordersUp.length;
for ( var match = 0; match < bordersLength; match ++ ) {
var j = oldEnd;
var i = bordersUp[match][0];
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
var j = bordersUp[match][1];
 
// getNext j from next matched tokensup
var iPreviMatch = text.newText.tokens[i].next;
ifvar (iPrevjMatch !== null) {j;
var jPrevi = textthis.newText.tokens[iPrevi].linkprev;
j = this.oldText.tokens[j].prev;
if (jPrev !== null) {
j = text.oldText.tokens[jPrev].prev;
}
}
 
// checkCycle forthrough thenew starttext ofgap anregion unresolved sequenceupwards
while (
if ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) ) {
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
 
// Connect if same token
// determine the limits of of the unresolved new sequence
if ( this.newText.tokens[i].token === this.oldText.tokens[j].token ) {
var iStart = null;
var iEnd this.newText.tokens[i].link = ij;
var iLength this.oldText.tokens[j].link = 0i;
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;
}
 
// determineNot thea limitsmatch ofyet, ofmaybe thein unresolvednext oldrefinement sequencelevel
varelse jStart = null;{
bordersUpNext.push( [iMatch, jMatch] );
var jEnd = j;
var jLength = 0 break;
var jNext = j;
while ( (jNext !== null) && (text.oldText.tokens[jNext].link === null) ) {
jStart = jNext;
jLength ++;
if (jStart == oldStart) {
break;
}
jNext = text.oldText.tokens[jNext].prev;
}
 
// recursivelyNext difftoken the unresolved sequenceup
iMatch = i;
if ( (iLength > 1) || (jLength > 1) ) {
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
}
 
/**
// new symbols object for sub-region
* Connect adjacent identical tokens downwards from text start.
var symbolsRecurse = {
* Treat boundary as connected, stop after first connected token.
token: [],
hash: {},*/
 
linked: false
// Only for full text diff
};
if ( recursionLevel === 0 && repeating === false ) {
wDiff.CalculateDiff(text, symbolsRecurse, level, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
 
}
i// =From iStart;start
var i = this.newText.first;
var j = this.oldText.first;
var iMatch = null;
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] );
}
 
// nextFrom list elementend
i = this.newText.last;
if (i == newStart) {
j = this.oldText.last;
break;
iMatch = null;
jMatch = null;
 
// 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 = text.newText.tokens[i].prev;
}
}
}
 
// Save updated linked region borders to object
// if (recursionLevel === 0) { wikEd.debugTimer.push([level + '=', new Date]); }
if ( recursionLevel === 0 && repeating === false ) {
this.bordersDown = bordersDownNext;
this.bordersUp = bordersUpNext;
}
 
// Merge local updated linked region borders into object
return;
else {
};
this.bordersDown = this.bordersDown.concat( bordersDownNext );
this.bordersUp = this.bordersUp.concat( bordersUpNext );
}
 
 
/**
// wDiff.DetectBlocks: extract block data for inserted, deleted, or moved blocks from diff data in text object
* Repeat once with empty symbol table to link hidden unresolved common tokens in cross-overs.
// input:
* ("and" in "and this a and b that" -> "and this a and b that")
// text: object containing text tokens list
*/
// blocks: empty array for block data
// groups: empty array for group data
// changes: text, blocks, groups
// called from: wDiff.Diff()
// scheme of blocks, sections, and groups (old block numbers):
// old: 1 2 3D4 5E6 7 8 9 10 11
// | ‾/-/_ X | >|< |
// new: 1 I 3D4 2 E6 5 N 7 10 9 8 11
// section: 0 0 0 1 1 2 2 2
// group: 0 10 111 2 33 4 11 5 6 7 8 9
// fixed: + +++ - ++ - + + - - +
// type: = + =-= = -= = + = = = = =
 
if ( repeating === false && this.config.repeatedDiff === true ) {
wDiff.DetectBlocks = function (text, blocks, groups) {
var repeat = true;
this.calculateDiff( level, recurse, repeat, newStart, oldStart, up, recursionLevel );
}
 
/**
// WED('text.oldText', wDiff.DebugText(text.oldText));
* Refine by recursively diffing not linked regions with new symbol table.
// WED('text.newText', wDiff.DebugText(text.newText));
* At word and character level only.
* Helps against gaps caused by addition of common tokens around sequences of common tokens.
*/
 
if (
// collect identical corresponding ('same') blocks from old text and sort by new text
recurse === true &&
wDiff.GetSameBlocks(text, blocks);
this.config['recursiveDiff'] === true &&
recursionLevel < this.config.recursionMax
) {
 
/**
// collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
* Recursively diff gap downwards.
var sections = [];
*/
wDiff.GetSections(blocks, sections);
 
// Cycle through list of linked region borders
// find groups of continuous old text blocks
var bordersLength = bordersDownNext.length;
wDiff.GetGroups(blocks, groups);
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersDownNext[match][0];
var j = bordersDownNext[match][1];
 
// Next token down
// set longest sequence of increasing groups in sections as fixed (not moved)
i = this.newText.tokens[i].next;
wDiff.SetFixed(blocks, groups, sections);
j = this.oldText.tokens[j].next;
 
// Start recursion at first gap token pair
// collect deletion ('del') blocks from old text
if (
wDiff.GetDelBlocks(text, blocks);
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 );
}
}
 
/**
// position 'del' blocks into new text order
* Recursively diff gap upwards.
wDiff.PositionDelBlocks(blocks);
*/
 
// Cycle through list of linked region borders
// sort blocks by new text token number and update groups
var bordersLength = bordersUpNext.length;
wDiff.SortBlocks(blocks, groups);
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersUpNext[match][0];
var j = bordersUpNext[match][1];
 
// Next token up
// convert groups to insertions/deletions if maximal block length is too short
i = this.newText.tokens[i].prev;
if (wDiff.blockMinLength > 0) {
j = this.oldText.tokens[j].prev;
var unlinked = wDiff.UnlinkBlocks(text, blocks, groups);
 
// Start recursion at first gap token pair
// repeat from start after conversion
if (unlinked === true) {
i !== null &&
wDiff.SlideGaps(text.newText, text.oldText);
j !== null &&
wDiff.SlideGaps(text.oldText, text.newText);
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 );
}
}
}
}
 
// Stop timers
// repeat block detection from start
if ( this.config.timer === true && repeating === false ) {
wDiff.GetSameBlocks(text, blocks);
if ( this.recursionTimer[recursionLevel] === undefined ) {
wDiff.GetSections(blocks, sections);
this.recursionTimer[recursionLevel] = 0;
wDiff.GetGroups(blocks, groups);
}
wDiff.SetFixed(blocks, groups, sections);
this.recursionTimer[recursionLevel] += this.timeEnd( level + recursionLevel, true );
wDiff.GetDelBlocks(text, blocks);
}
wDiff.PositionDelBlocks(blocks);
if ( this.config.timer === true && repeating === false && recursionLevel === 0 ) {
this.timeRecursionEnd( level );
this.timeEnd( level );
}
}
 
return;
// collect insertion ('ins') blocks from new text
};
wDiff.GetInsBlocks(text, blocks);
 
// sort blocks by new text token number and update groups
wDiff.SortBlocks(blocks, groups);
 
/**
// set group numbers of 'ins' and 'del' blocks
* Main method for processing raw diff data, extracting deleted, inserted, and moved blocks.
wDiff.SetInsDelGroups(blocks, groups);
*
* Scheme of blocks, sections, and groups (old block numbers):
* Old: 1 2 3D4 5E6 7 8 9 10 11
* | ‾/-/_ X | >|< |
* New: 1 I 3D4 2 E6 5 N 7 10 9 8 11
* Section: 0 0 0 1 1 2 2 2
* Group: 0 10 111 2 33 4 11 5 6 7 8 9
* Fixed: . +++ - ++ - . . - - +
* Type: = . =-= = -= = . = = = = =
*
* @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
// mark original positions of moved groups
if ( this.config.debug === true ) {
wDiff.MarkMoved(groups);
this.oldText.debugText( 'Old text' );
this.newText.debugText( 'New text' );
}
 
// Collect identical corresponding ('=') blocks from old text and sort by new text
// set moved block colors
this.getSameBlocks();
wDiff.ColorMoved(groups);
 
// Collect independent block sections with no block move crosses outside a section
// WED('Groups', wDiff.DebugGroups(groups));
this.getSections();
// WED('Blocks', wDiff.DebugBlocks(blocks));
 
// Find groups of continuous old text blocks
return;
this.getGroups();
};
 
// Set longest sequence of increasing groups in sections as fixed (not moved)
this.setFixed();
 
// Convert groups to insertions/deletions if maximum block length is too short
// wDiff.GetSameBlocks: collect identical corresponding ('same') blocks from old text and sort by new text
// Only for more complex texts that actually have blocks of minimum block length
// called from: DetectBlocks()
var unlinkCount = 0;
// changes: creates blocks
if (
this.config.unlinkBlocks === true &&
this.config.blockMinLength > 0 &&
this.maxWords >= this.config.blockMinLength
) {
if ( this.config.timer === true ) {
this.time( 'total unlinking' );
}
 
// Repeat as long as unlinking is possible
wDiff.GetSameBlocks = function (text, blocks) {
var unlinked = true;
while ( unlinked === true && unlinkCount < this.config.unlinkMax ) {
 
// Convert '=' to '+'/'-' pairs
// clear blocks array
unlinked = this.unlinkBlocks();
blocks.splice(0);
 
// Start over after conversion
// cycle through old text to find matched (linked) blocks
if ( unlinked === true ) {
var j = text.oldText.first;
unlinkCount ++;
var i = null;
this.slideGaps( this.newText, this.oldText );
while (j !== null) {
this.slideGaps( this.oldText, this.newText );
 
// Repeat block detection from start
// skip 'del' blocks
this.maxWords = 0;
while ( (j !== null) && (text.oldText.tokens[j].link === null) ) {
this.getSameBlocks();
j = text.oldText.tokens[j].next;
this.getSections();
}
this.getGroups();
 
this.setFixed();
// get 'same' block
if (j !== null) {
i = text.oldText.tokens[j].link;
var iStart = i;
var jStart = j;
 
// detect matching blocks ('same')
var count = 0;
var unique = false;
var chars = 0;
var string = '';
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
var token = text.oldText.tokens[j].token;
count ++;
if (text.newText.tokens[i].unique === true) {
unique = true;
}
chars += token.length;
string += token;
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
if ( this.config.timer === true ) {
 
this.timeEnd( 'total unlinking' );
// save old text 'same' block
}
blocks.push({
oldBlock: blocks.length,
newBlock: null,
oldNumber: text.oldText.tokens[jStart].number,
newNumber: text.newText.tokens[iStart].number,
oldStart: jStart,
count: count,
unique: unique,
words: wDiff.WordCount(string),
chars: chars,
type: 'same',
section: null,
group: null,
fixed: null,
string: string
});
}
}
 
// Collect deletion ('-') blocks from old text
// sort blocks by new text token number
this.getDelBlocks();
blocks.sort(function(a, b) {
return a.newNumber - b.newNumber;
});
 
// numberPosition '-' blocks ininto new text order
this.positionDelBlocks();
for (var block = 0; block < blocks.length; block ++) {
blocks[block].newBlock = block;
}
return;
};
 
// Collect insertion ('+') blocks from new text
this.getInsBlocks();
 
// Set group numbers of '+' blocks
// wDiff.GetSections: collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
this.setInsGroups();
// called from: DetectBlocks()
// changes: creates sections, blocks[].section
 
// Mark original positions of moved groups
wDiff.GetSections = function (blocks, sections) {
this.insertMarks();
 
// Debug log
// clear sections array
if ( this.config.timer === true || this.config.debug === true ) {
sections.splice(0);
console.log( 'Unlink count: ', unlinkCount );
}
if ( this.config.debug === true ) {
this.debugGroups( 'Groups' );
this.debugBlocks( 'Blocks' );
}
return;
};
 
// cycle through blocks
for (var block = 0; block < blocks.length; block ++) {
 
/**
var sectionStart = block;
* Collect identical corresponding matching ('=') blocks from old text and sort by new text.
var sectionEnd = block;
*
* @param[in] WikEdDiffText newText, oldText Text objects
* @param[in/out] array blocks Blocks table object
*/
this.getSameBlocks = function () {
 
if ( this.config.timer === true ) {
var oldMax = blocks[sectionStart].oldNumber;
this.time( 'getSameBlocks' );
var sectionOldMax = oldMax;
}
 
var blocks = this.blocks;
// check right
for (var j = sectionStart + 1; j < blocks.length; j ++) {
 
// Clear blocks array
// check for crossing over to the left
if (blocks[j].oldNumbersplice( >0 oldMax) {;
oldMax = blocks[j].oldNumber;
}
else if (blocks[j].oldNumber < sectionOldMax) {
sectionEnd = j;
sectionOldMax = oldMax;
}
}
 
// Cycle through old text to find connected (linked, matched) blocks
// save crossing sections
var j = this.oldText.first;
if (sectionEnd > sectionStart) {
var i = null;
while ( j !== null ) {
 
// saveSkip section'-' to blockblocks
while ( j !== null && this.oldText.tokens[j].link === null ) {
for (var i = sectionStart; i <= sectionEnd; i ++) {
j = this.oldText.tokens[j].next;
blocks[i].section = sections.length;
}
 
// saveGet section'=' block
if ( j !== null ) {
sections.push({
i = this.oldText.tokens[j].link;
blockStart: sectionStart,
var iStart = i;
blockEnd: sectionEnd,
deleted:var jStart = falsej;
});
block = sectionEnd;
}
}
return;
};
 
// Detect matching blocks ('=')
var count = 0;
var unique = false;
var text = '';
while ( i !== null && j !== null && this.oldText.tokens[j].link === i ) {
text += this.oldText.tokens[j].token;
count ++;
if ( this.newText.tokens[i].unique === true ) {
unique = true;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
 
// Save old text '=' block
// wDiff.GetGroups: find groups of continuous old text blocks
blocks.push( {
// called from: DetectBlocks()
// changes oldBlock: creates groups, blocks[].grouplength,
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
} );
}
}
 
// Sort blocks by new text token number
wDiff.GetGroups = function (blocks, groups) {
blocks.sort( function( a, b ) {
return a.newNumber - b.newNumber;
} );
 
// Number blocks in new text order
// clear groups array
var blocksLength = blocks.length;
groups.splice(0);
for ( var block = 0; block < blocksLength; block ++ ) {
blocks[block].newBlock = block;
}
 
if ( this.config.timer === true ) {
// cycle through blocks
this.timeEnd( 'getSameBlocks' );
for (var block = 0; block < blocks.length; block ++) {
if (blocks[block].deleted === true) {
continue;
}
return;
var groupStart = block;
};
var groupEnd = block;
var oldBlock = blocks[groupStart].oldBlock;
 
// get word and char count of block
var words = wDiff.WordCount(blocks[block].string);
var maxWords = words;
var unique = false;
var chars = blocks[block].chars;
 
/**
// check right
* Collect independent block sections with no block move crosses
for (var i = groupEnd + 1; i < blocks.length; i ++) {
* 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 ) {
// check for crossing over to the left
this.time( 'getSections' );
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;
}
 
var blocks = this.blocks;
// save crossing group
var sections = this.sections;
if (groupEnd >= groupStart) {
 
// set groups outsideClear sections as fixedarray
sections.splice( 0 );
var fixed = false;
if (blocks[groupStart].section === null) {
fixed = true;
}
 
// saveCycle groupthrough to blockblocks
var blocksLength = blocks.length;
for (var i = groupStart; i <= groupEnd; i ++) {
for ( var block = 0; block < blocksLength; block ++ ) {
blocks[i].group = groups.length;
blocks[i].fixed = fixed;
}
 
var sectionStart = block;
// save group
var sectionEnd = block;
groups.push({
oldNumber: blocks[groupStart].oldNumber,
blockStart: groupStart,
blockEnd: groupEnd,
unique: unique,
maxWords: maxWords,
words: words,
chars: chars,
fixed: fixed,
moved: [],
movedFrom: null,
color: null,
diff: ''
});
block = groupEnd;
}
}
return;
};
 
var oldMax = blocks[sectionStart].oldNumber;
var sectionOldMax = oldMax;
 
// Check right
// wDiff.SetFixed: set longest sequence of increasing groups in sections as fixed (not moved)
for ( var j = sectionStart + 1; j < blocksLength; j ++ ) {
// called from: DetectBlocks()
// calls: wDiff.FindMaxPath()
// changes: groups[].fixed, blocks[].fixed
 
// Check for crossing over to the left
wDiff.SetFixed = function (blocks, groups, sections) {
if ( blocks[j].oldNumber > oldMax ) {
oldMax = blocks[j].oldNumber;
}
else if ( blocks[j].oldNumber < sectionOldMax ) {
sectionEnd = j;
sectionOldMax = oldMax;
}
}
 
// cycleSave throughcrossing sections
if ( sectionEnd > sectionStart ) {
for (var section = 0; section < sections.length; section ++) {
var blockStart = sections[section].blockStart;
var blockEnd = sections[section].blockEnd;
 
// Save section to block
var groupStart = blocks[blockStart].group;
for ( var i = sectionStart; i <= sectionEnd; i ++ ) {
var groupEnd = blocks[blockEnd].group;
blocks[i].section = sections.length;
}
 
// Save section
// recusively find path of groups in increasing old group order with longest char length
sections.push( {
 
blockStart: sectionStart,
// start at each group of section
blockEnd: sectionEnd
var cache = [];
var } maxChars = 0);
var maxPath block = nullsectionEnd;
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;
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getSections' );
}
return;
};
 
// mark fixed groups
for (var i = 0; i < maxPath.length; i ++) {
var group = maxPath[i];
groups[group].fixed = true;
 
/**
// mark fixed blocks
* Find groups of continuous old text blocks.
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
*
blocks[block].fixed = true;
* @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 ) {
this.time( 'getGroups' );
}
}
return;
};
 
var blocks = this.blocks;
var groups = this.groups;
 
// Clear groups array
// wDiff.FindMaxPath: recusively find path of groups in increasing old group order with longest char length
groups.splice( 0 );
// 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
// returns: returnObj, contains path and length
// called from: wDiff.SetFixed()
// calls: itself recursively
 
// Cycle through blocks
wDiff.FindMaxPath = function (start, path, chars, cache, groups, groupEnd) {
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
// add current path point
var words = this.wordCount( blocks[block].text );
var pathLocal = path.slice();
var maxWords = words;
pathLocal.push(start);
var unique = blocks[block].unique;
chars = chars + groups[start].chars;
var chars = blocks[block].chars;
 
// Check right
// last group, terminate recursion
for ( var i = groupEnd + 1; i < blocksLength; i ++ ) {
var returnObj = { path: pathLocal, chars: chars };
if (start == groupEnd) {
return returnObj;
}
 
// Check for crossing over to the left
// find longest sub-path
if ( blocks[i].oldBlock !== oldBlock + 1 ) {
var maxChars = 0;
break;
var oldNumber = groups[start].oldNumber;
}
for (var i = start + 1; i <= groupEnd; i ++) {
oldBlock = blocks[i].oldBlock;
 
// onlyGet inword increasingand oldchar groupcount orderof block
if (groups blocks[i].oldNumberwords > <maxWords oldNumber) {
maxWords = blocks[i].words;
continue;
}
if ( blocks[i].unique === true ) {
unique = true;
}
words += blocks[i].words;
chars += blocks[i].chars;
groupEnd = i;
}
 
// Save crossing group
// get longest sub-path from cache
if (cache[start] !=groupEnd >= undefinedgroupStart ) {
returnObj = cache[start];
}
 
// Set groups outside sections as fixed
// get longest sub-path by recursion
var fixed = false;
else {
if ( blocks[groupStart].section === null ) {
var pathObj = wDiff.FindMaxPath(i, pathLocal, chars, cache, groups, groupEnd);
fixed = true;
}
 
// selectSave longestgroup sub-pathto block
for ( var i = groupStart; i <= groupEnd; i ++ ) {
if (pathObj.chars > maxChars) {
returnObj blocks[i].group = pathObjgroups.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 ) {
}
this.timeEnd( 'getGroups' );
}
return;
};
 
// save longest path to cache
if (cache[i] === undefined) {
cache[start] = returnObj;
}
return returnObj;
};
 
/**
* Set longest sequence of increasing groups in sections as fixed (not moved).
*
* @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 ) {
// wDiff.GetDelBlocks: collect deletion ('del') blocks from old text
this.time( 'setFixed' );
// called from: DetectBlocks()
}
// changes: blocks
 
var blocks = this.blocks;
wDiff.GetDelBlocks = function (text, blocks) {
var groups = this.groups;
var sections = this.sections;
 
// cycleCycle through old text to find matched (linked) blockssections
var sectionsLength = sections.length;
var j = text.oldText.first;
for ( var section = 0; section < sectionsLength; section ++ ) {
var i = null;
var blockStart = sections[section].blockStart;
while (j !== null) {
var blockEnd = sections[section].blockEnd;
 
// var collectgroupStart 'del'= blocks[blockStart].group;
var oldStartgroupEnd = jblocks[blockEnd].group;
var count = 0;
var string = '';
while ( (j !== null) && (text.oldText.tokens[j].link === null) ) {
count ++;
string += text.oldText.tokens[j].token;
j = text.oldText.tokens[j].next;
}
 
// Recusively find path of groups in increasing old group order with longest char length
// save old text 'del' block
if var (countcache !== 0) {[];
var maxChars = 0;
blocks.push({
oldBlock:var maxPath = null,;
newBlock: null,
oldNumber: text.oldText.tokens[oldStart].number,
newNumber: null,
oldStart: oldStart,
count: count,
unique: false,
words: null,
chars: null,
type: 'del',
section: null,
group: null,
fixed: null,
string: string
});
}
 
// Start at each group of section
// skip 'same' block
if for (j !var i = groupStart; i <= nullgroupEnd; i ++ ) {
var pathObj = this.findMaxPath( i, groupEnd, cache );
i = text.oldText.tokens[j].link;
if ( pathObj.chars > maxChars ) {
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
maxPath = pathObj.path;
i = text.newText.tokens[i].next;
maxChars = pathObj.chars;
j = text.oldText.tokens[j].next;
}
}
}
}
return;
};
 
// Mark fixed groups
var maxPathLength = maxPath.length;
for ( var i = 0; i < maxPathLength; i ++ ) {
var group = maxPath[i];
groups[group].fixed = true;
 
// Mark fixed blocks
// wDiff.PositionDelBlocks: position 'del' blocks into new text order
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
// called from: DetectBlocks()
// changes: blocks[block].section/group/fixed/newNumber = true;
}
//
}
// deletion blocks move with fixed neighbor (new number +/- 0.1):
}
// old: 1 D 2 1 D 2
if ( this.config.timer === true ) {
// / / \ / \ \
this.timeEnd( 'setFixed' );
// new: 1 D 2 1 D 2
}
// fixed: * *
return;
// new number: 1 1.1 1.9 2
};
 
wDiff.PositionDelBlocks = function (blocks) {
 
/**
// sort shallow copy of blocks by oldNumber
* Recusively find path of groups in increasing old group order with longest char length.
var blocksOld = blocks.slice();
*
blocksOld.sort(function(a, b) {
* @param int start Path start group
return a.oldNumber - b.oldNumber;
* @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;
// cycle through 'del' blocks in old text order
for (var blockOld = 0; blockOld < blocksOld.length; blockOld ++) {
var delBlock = blocksOld[blockOld];
if (delBlock.type != 'del') {
continue;
}
 
// getFind oldlongest text prev blocksub-path
var prevBlockmaxChars = 0;
var oldNumber = groups[start].oldNumber;
if (blockOld > 0) {
var returnObj = { path: [], chars: 0};
prevBlock = blocks[ blocksOld[blockOld - 1].newBlock ];
for ( var i = start + 1; i <= groupEnd; i ++ ) {
}
 
// getOnly oldin textincreasing old nextgroup blockorder
if ( groups[i].oldNumber < oldNumber ) {
var nextBlock;
continue;
if (blockOld < blocksOld.length - 1) {
}
nextBlock = blocks[ blocksOld[blockOld + 1].newBlock ];
}
 
// Get longest sub-path from cache (deep copy)
// move after prev block if fixed
var neighborpathObj;
if ( (prevBlockcache[i] !== undefined) && (prevBlock.fixed === true) ) {
pathObj = { path: cache[i].path.slice(), chars: cache[i].chars };
neighbor = prevBlock;
}
delBlock.newNumber = neighbor.newNumber + 0.1;
}
 
// moveGet beforelongest nextsub-path blockby if fixedrecursion
else {
else if ( (nextBlock !== undefined) && (nextBlock.fixed === true) ) {
pathObj = this.findMaxPath( i, groupEnd, cache );
neighbor = nextBlock;
}
delBlock.newNumber = neighbor.newNumber - 0.1;
}
 
// Select longest sub-path
// move after prev block if existent
if ( pathObj.chars > maxChars ) {
else if (prevBlock !== undefined) {
neighbor maxChars = prevBlockpathObj.chars;
returnObj = pathObj;
delBlock.newNumber = neighbor.newNumber + 0.1;
}
}
 
// moveAdd beforecurrent nextstart blockto path
returnObj.path.unshift( start );
else if (nextBlock !== undefined) {
returnObj.chars += groups[start].chars;
neighbor = nextBlock;
delBlock.newNumber = neighbor.newNumber - 0.1;
}
 
// Save path to cache (deep copy)
// move before first block
if ( cache[start] === undefined ) {
else {
cache[start] = { path: returnObj.path.slice(), chars: returnObj.chars };
delBlock.newNumber = -0.1;
}
 
return returnObj;
// update 'del' block with neighbor data
};
if (neighbor !== undefined) {
delBlock.section = neighbor.section;
delBlock.group = neighbor.group;
delBlock.fixed = neighbor.fixed;
}
}
return;
};
 
 
// wDiff.UnlinkBlocks: convert 'same' blocks in groups into 'ins'/'del' pairs if too short
// called from: DetectBlocks()
// changes: text.newText/oldText[].link
// returns: true if text tokens were unlinked
 
/**
wDiff.UnlinkBlocks = function (text, blocks, groups) {
* 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 blockStartgroupsLength = groups[group].blockStartlength;
for ( var blockEndgroup = groups[0; group].blockEnd < groupsLength; group ++ ) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
 
// noUnlink block inwhole group if no block is at least blockMinLength words long and unique
if ( groups[group].maxWords < wDiffthis.config.blockMinLength && groups[group].unique === false ) {
for ( var block = blockStart; block <= blockEnd; block ++ ) {
 
if ( blocks[block].type === '=' ) {
// unlink whole moved group if it contains no unique matched token
this.unlinkSingleBlock( blocks[block] );
if ( (groups[group].fixed === false) && (groups[group].unique === false) ) {
 
for (var block = blockStart; block <= blockEnd; block ++) {
if (blocks[block].type == 'same') {
wDiff.UnlinkSingleBlock(blocks[block], text);
unlinked = true;
}
Line 2,015 ⟶ 2,562:
}
 
// Otherwise unlink block flanks
else {
 
// unlinkUnlink blocks from start if preceded by 'del'
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( (block > 0) && (blocks[block - 1].type === 'del') && (blocks[block].type == 'same') ) {
 
// stopStop unlinking if more than one word or a unique word
if ( (blocks[block].words > 1) || ( (blocks[block].words == 1) && (blocks[block].unique === true) ) ) {
break;
}
wDiffthis.UnlinkSingleBlockunlinkSingleBlock( blocks[block], text);
unlinked = true;
blockStart = block;
Line 2,032 ⟶ 2,579:
}
 
// unlinkUnlink blocks from end if followed by 'del'
for ( var block = blockEnd; block > blockStart; block -- ) {
if ( (blockEnd < blocks.length - 1) && (blocks[block + 1].type === 'del') && (blocks[block].type == 'same') ) {
 
// stopStop unlinking if more than one word or a unique word
if (
if ( (blocks[block].words > 1) || ( (blocks[block].words == 1) && (blocks[block].unique === true) ) ) {
blocks[block].words > 1 ||
( blocks[block].words === 1 && blocks[block].unique === true )
) {
break;
}
wDiffthis.UnlinkSingleBlockunlinkSingleBlock( blocks[block], text);
unlinked = true;
}
Line 2,046 ⟶ 2,596:
}
}
return unlinked;
}
};
return unlinked;
};
 
 
/**
// wDiff.UnlinkBlock: un-link text tokens of single block, converting them into 'ins'/'del' pairs
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs.
// called from: wDiff.UnlinkBlocks()
*
// changes: text.newText/oldText[].link
* @param[in] array blocks Blocks table object
* @param[out] WikEdDiffText newText, oldText Text objects, link property
*/
this.unlinkSingleBlock = function ( block ) {
 
// Cycle through old text
wDiff.UnlinkSingleBlock = function (block, text) {
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
 
// Unlink tokens
// cycle through old text
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
var j = block.oldStart;
this.oldText.tokens[j].link = null;
for (var count = 0; count < block.count; count ++) {
j = this.oldText.tokens[j].next;
}
return;
};
 
// unlink tokens
text.newText.tokens[ text.oldText.tokens[j].link ].link = null;
text.oldText.tokens[j].link = null;
j = text.oldText.tokens[j].next;
}
return;
};
 
/**
* 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 ) {
// wDiff.GetInsBlocks: collect insertion ('ins') blocks from new text
this.time( 'getDelBlocks' );
// called from: DetectBlocks()
}
// changes: blocks
 
var blocks = this.blocks;
wDiff.GetInsBlocks = function (text, blocks) {
 
// cycleCycle through newold text to find insertionconnected (linked, matched) blocks
var ij = textthis.newTextoldText.first;
while var (i !== null) {;
while ( j !== null ) {
 
// Collect '-' blocks
// jump over linked (matched) block
var oldStart = j;
while ( (i !== null) && (text.newText.tokens[i].link !== null) ) {
i = text.newText.tokens[i].next;
}
 
// detect insertion blocks ('ins')
if (i !== null) {
var iStart = i;
var count = 0;
var stringtext = '';
while ( (ij !== null) && (textthis.newTextoldText.tokens[ij].link === null) ) {
count ++;
stringtext += textthis.newTextoldText.tokens[ij].token;
ij = textthis.newTextoldText.tokens[ij].next;
}
 
// saveSave newold text 'ins-' block
if ( count !== 0 ) {
blocks.push({
oldBlock:blocks.push( null,{
newBlock oldBlock: null,
oldNumber newBlock: null,
newNumber oldNumber: textthis.newTextoldText.tokens[iStartoldStart].number,
oldStart newNumber: null,
count oldStart: countoldStart,
unique count: false count,
words unique: nullfalse,
chars words: null,
type chars: 'ins'text.length,
section type: null '-',
group section: null,
fixed group: null,
string fixed: string 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.SortBlocks: sort blocks by new text token number and update groups
* Position deletion '-' blocks into new text order.
// called from: DetectBlocks()
* Deletion blocks move with fixed reference:
// changes: blocks
* Old: 1 D 2 1 D 2
* / \ / \ \
* New: 1 D 2 1 D 2
* Fixed: * *
* newNumber: 1 1 2 2
*
* Marks '|' and deletions '-' get newNumber of reference block
* and are sorted around it by old text number.
*
* @param[in/out] array blocks Blocks table, newNumber, section, group, and fixed properties
*
*/
this.positionDelBlocks = function () {
 
if ( this.config.timer === true ) {
wDiff.SortBlocks = function (blocks, groups) {
this.time( 'positionDelBlocks' );
}
 
var blocks = this.blocks;
// sort by newNumber
var groups = this.groups;
blocks.sort(function(a, b) {
return a.newNumber - b.newNumber;
});
 
// Sort shallow copy of blocks by oldNumber
// cycle through blocks and update groups with new block numbers
var groupblocksOld = nullblocks.slice();
blocksOld.sort( function( a, b ) {
for (var block = 0; block < blocks.length; block ++) {
return a.oldNumber - b.oldNumber;
var blockGroup = blocks[block].group;
} );
if (blockGroup !== null) {
 
if (blockGroup != group) {
group// =Cycle through blocks[block].group; in old text order
var blocksOldLength = blocksOld.length;
groups[group].blockStart = block;
for ( var block = 0; block < blocksOldLength; block ++ ) {
groups[group].oldNumber = blocks[block].oldNumber;
var delBlock = blocksOld[block];
 
// '-' block only
if ( delBlock.type !== '-' ) {
continue;
}
groups[blockGroup].blockEnd = block;
}
}
return;
};
 
// Find fixed '=' reference block from original block position to position '-' block
// Similar to position marks '|' code
 
// Get old text prev block
// wDiff.SetInsDelGroups: set group numbers of 'ins' and 'del' blocks
var prevBlockNumber = null;
// called from: DetectBlocks()
var prevBlock = null;
// changes: groups, blocks[].fixed/group
if ( block > 0 ) {
prevBlockNumber = blocksOld[block - 1].newBlock;
prevBlock = blocks[prevBlockNumber];
}
 
// Get old text next block
wDiff.SetInsDelGroups = function (blocks, groups) {
var nextBlockNumber = null;
var nextBlock = null;
if ( block < blocksOld.length - 1 ) {
nextBlockNumber = blocksOld[block + 1].newBlock;
nextBlock = blocks[nextBlockNumber];
}
 
// Move after prev block if fixed
// set group numbers of 'ins' and 'del' blocks inside existing groups
var refBlock = null;
for (var group = 0; group < groups.length; group ++) {
if ( prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
var fixed = groups[group].fixed;
refBlock = prevBlock;
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
if (blocks[block].group === null) {
blocks[block].group = group;
blocks[block].fixed = fixed;
}
}
}
 
// Move before next block if fixed
// add remaining 'ins' and 'del' blocks to groups
else if ( nextBlock !== null && nextBlock.type === '=' && nextBlock.fixed === true ) {
refBlock = nextBlock;
}
 
// Move after prev block if not start of group
// cycle through blocks
else if (
for (var block = 0; block < blocks.length; block ++) {
prevBlock !== null &&
prevBlock.type === '=' &&
prevBlockNumber !== groups[ prevBlock.group ].blockEnd
) {
refBlock = prevBlock;
}
 
// Move before next block if not start of group
// skip existing groups
else if (
if (blocks[block].group === null) {
nextBlock !== null &&
blocks[block].group = groups.length;
nextBlock.type === '=' &&
var fixed = blocks[block].fixed;
nextBlockNumber !== groups[ nextBlock.group ].blockStart
) {
refBlock = nextBlock;
}
 
// Move after closest previous fixed block
// save group
groups.push(else {
for ( var fixed = block; fixed >= 0; fixed -- ) {
oldNumber: blocks[block].oldNumber,
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
blockStart: block,
refBlock = blocksOld[fixed];
blockEnd: block,
break;
unique: false,
}
maxWords: null,
}
words: null,
}
chars: null,
 
fixed: fixed,
moved:// Move before first [],block
movedFrom:if ( refBlock === null, ) {
color:delBlock.newNumber = null,-1;
}
diff: ''
 
});
// Update '-' block data
else {
delBlock.newNumber = refBlock.newNumber;
delBlock.section = refBlock.section;
delBlock.group = refBlock.group;
delBlock.fixed = refBlock.fixed;
}
}
}
return;
};
 
// Sort '-' blocks in and update groups
this.sortBlocks();
 
if ( this.config.timer === true ) {
// wDiff.MarkMoved: mark original positions of moved groups
this.timeEnd( 'positionDelBlocks' );
// called from: DetectBlocks()
}
// changes: groups[].moved/movedFrom
return;
// 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)
* Collect insertion ('+') blocks from new text.
for (var movedGroup = 0; movedGroup < groups.length; movedGroup ++) {
*
if (groups[movedGroup].fixed !== false) {
* @param[in] WikEdDiffText newText New Text object
continue;
* @param[out] array blocks Blocks table object
*/
this.getInsBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'getInsBlocks' );
}
var movedOldNumber = groups[movedGroup].oldNumber;
 
var blocks = this.blocks;
// find closest fixed groups
var nextSmallerNumber = null;
var nextSmallerGroup = null;
var nextLargerNumber = null;
var nextLargerGroup = null;
 
// cycleCycle through groupsnew (originaltext positions)to find insertion blocks
var i = this.newText.first;
for (var group = 0; group < groups.length; group ++) {
ifwhile ( (groups[group].fixedi !== true) || (group == movedGroup)null ) {
continue;
}
 
// Jump over linked (matched) block
// find fixed group with closest smaller oldNumber
while ( i !== null && this.newText.tokens[i].link !== null ) {
var oldNumber = groups[group].oldNumber;
i = this.newText.tokens[i].next;
if ( (oldNumber < movedOldNumber) && ( (nextSmallerNumber === null) || (oldNumber > nextSmallerNumber) ) ) {
nextSmallerNumber = oldNumber;
nextSmallerGroup = group;
}
 
// Detect insertion blocks ('+')
// find fixed group with closest larger oldNumber
if ( i !== null ) {
if ( (oldNumber > movedOldNumber) && ( (nextLargerNumber === null) || (oldNumber < nextLargerNumber) ) ) {
nextLargerNumbervar iStart = oldNumberi;
nextLargerGroupvar count = group0;
var text = '';
while ( i !== null && this.newText.tokens[i].link === null ) {
count ++;
text += this.newText.tokens[i].token;
i = this.newText.tokens[i].next;
}
 
// Save new text '+' block
blocks.push( {
oldBlock: null,
newBlock: null,
oldNumber: null,
newNumber: this.newText.tokens[iStart].number,
oldStart: null,
count: count,
unique: false,
words: null,
chars: text.length,
type: '+',
section: null,
group: null,
fixed: null,
moved: null,
text: text
} );
}
}
 
// Sort '+' blocks in and update groups
// no larger fixed group, moved right
this.sortBlocks();
var movedFrom = '';
if (nextLargerGroup === null) {
movedFrom = 'left';
}
 
if ( this.config.timer === true ) {
// no smaller fixed group, moved right
this.timeEnd( 'getInsBlocks' );
else if (nextSmallerGroup === null) {
movedFrom = 'right';
}
return;
};
 
// group moved from between two closest fixed neighbors, moved left or right depending on char distance
else {
var rightChars = 0;
for (var group = nextSmallerGroup + 1; group < movedGroup; group ++) {
rightChars += groups[group].chars;
}
var leftChars = 0;
for (var group = movedGroup + 1; group < nextLargerGroup; group ++) {
leftChars += groups[group].chars;
}
 
/**
// moved right
* Sort blocks by new text token number and update groups.
if (rightChars <= leftChars) {
*
movedFrom = 'left';
* @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
// moved left
var group = null;
else {
var blocksLength = blocks.length;
movedFrom = 'right';
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;
};
 
 
// check for null-moves
/**
if (movedFrom == 'left') {
* Set group numbers of insertion '+' blocks.
if (groups[nextSmallerGroup].blockEnd + 1 != groups[movedGroup].blockStart) {
*
groups[nextSmallerGroup].moved.push(movedGroup);
* @param[in/out] array groups Groups table object
groups[movedGroup].movedFrom = nextSmallerGroup;
* @param[in/out] array blocks Blocks table object, fixed and group properties
}
*/
this.setInsGroups = function () {
 
if ( this.config.timer === true ) {
this.time( 'setInsGroups' );
}
 
else if (movedFrom == 'right') {
var blocks = this.blocks;
if (groups[movedGroup].blockEnd + 1 != groups[nextLargerGroup].blockStart) {
var groups = this.groups;
groups[nextLargerGroup].moved.push(movedGroup);
 
groups[movedGroup].movedFrom = nextLargerGroup;
// Set group numbers of '+' blocks inside existing groups
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;
}
}
}
}
 
// cycleAdd throughremaining groups, sort'+' blocks movedto fromnew here by old numbergroups
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;
};
 
// Cycle through blocks
var blocksLength = blocks.length;
for ( var block = 0; block < blocksLength; block ++ ) {
 
// Skip existing groups
// wDiff.ColorMoved: set moved block color numbers
if ( blocks[block].group === null ) {
// called from: DetectBlocks()
blocks[block].group = groups.length;
// changes: groups[].color
 
// Save new single-block group
wDiff.ColorMoved = function (groups) {
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;
};
 
// cycle through groups
var moved = [];
for (var group = 0; group < groups.length; group ++) {
moved = moved.concat(groups[group].moved);
}
 
/**
// sort moved array by old number
* Mark original positions of moved groups.
moved.sort(function(a, b) {
* Scheme: moved block marks at original positions relative to fixed groups:
return groups[a].oldNumber - groups[b].oldNumber;
* 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 ) {
// set color
this.time( 'insertMarks' );
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;
};
 
var blocks = this.blocks;
var groups = this.groups;
var moved = [];
var color = 1;
 
// Make shallow copy of blocks
// wDiff.AssembleDiff: process diff data into formatted html text
var blocksOld = blocks.slice();
// input: text, object containing text tokens list; blocks, array containing block type; groups, array containing fixed (not moved), color, and moved mark data
// returns: diff html string
// called from: wDiff.Diff()
// calls: wDiff.HtmlCustomize(), wDiff.HtmlFormat()
 
// Enumerate copy
wDiff.AssembleDiff = function (text, blocks, groups) {
var blocksOldLength = blocksOld.length;
for ( var i = 0; i < blocksOldLength; i ++ ) {
blocksOld[i].number = i;
}
 
// Sort copy by oldNumber
//
blocksOld.sort( function( a, b ) {
// create group diffs
var comp = a.oldNumber - b.oldNumber;
//
if ( comp === 0 ) {
comp = a.newNumber - b.newNumber;
}
return comp;
} );
 
// Create lookup table: original to sorted
// cycle through groups
var lookupSorted = [];
for (var group = 0; group < groups.length; group ++) {
for ( var i = 0; i < blocksOldLength; i ++ ) {
var color = groups[group].color;
lookupSorted[ blocksOld[i].number ] = i;
var blockStart = groups[group].blockStart;
}
var blockEnd = groups[group].blockEnd;
var diff = '';
 
// Cycle through groups (moved group)
// check for colored block and move direction
var blockFromgroupsLength = nullgroups.length;
for ( var moved = 0; moved < groupsLength; moved ++ ) {
if (color !== null) {
var movedGroup = groups[moved];
if (groups[ groups[group].movedFrom ].blockStart < blockStart) {
if ( movedGroup.fixed !== false ) {
blockFrom = 'left';
continue;
}
var movedOldNumber = movedGroup.oldNumber;
else {
blockFrom = 'right';
}
}
 
// Find fixed '=' reference block from original block position to position '|' block
// add colored block start markup
// Similar to position deletions '-' code
if (blockFrom == 'left') {
diff += wDiff.HtmlCustomize(wDiff.htmlBlockLeftStart, color);
}
else if (blockFrom == 'right') {
diff += wDiff.HtmlCustomize(wDiff.htmlBlockRightStart, color);
}
 
// Get old text prev block
// cycle through blocks
var prevBlock = null;
for (var block = blockStart; block <= blockEnd; block ++) {
var typeblock = blockslookupSorted[block] movedGroup.typeblockStart ];
varif string( = blocks[block].string; > 0 ) {
prevBlock = blocksOld[block - 1];
}
 
// htmlGet escapeold text stringnext block
var nextBlock = null;
string = wDiff.HtmlEscape(string);
var block = lookupSorted[ movedGroup.blockEnd ];
if ( block < blocksOld.length - 1 ) {
nextBlock = blocksOld[block + 1];
}
 
// addMove 'same'after (unchanged)prev textblock if fixed
ifvar (typerefBlock == 'same') {null;
if (color prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
refBlock = prevBlock;
string = string.replace(/\n/g, wDiff.htmlNewline);
}
diff += string;
}
 
// addMove 'del'before textnext block if fixed
else if ( nextBlock !== null && nextBlock.type === 'del=' && nextBlock.fixed === true ) {
refBlock = nextBlock;
if (wDiff.regExpBlankBlock.test(string) === true) {
diff += wDiff.htmlDeleteStartBlank;
}
else {
diff += wDiff.htmlDeleteStart;
}
diff += string.replace(/\n/g, wDiff.htmlNewline) + wDiff.htmlDeleteEnd;
}
 
// Find closest fixed block to the left
// add 'ins' text
else if (type == 'ins') {
for ( var fixed = lookupSorted[ movedGroup.blockStart ] - 1; fixed >= 0; fixed -- ) {
if (wDiff.regExpBlankBlock.test(string) === true) {
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
diff += wDiff.htmlInsertStartBlank;
refBlock = blocksOld[fixed];
break;
}
}
else {
diff += wDiff.htmlInsertStart;
}
diff += string.replace(/\n/g, wDiff.htmlNewline) + wDiff.htmlInsertEnd;
}
}
 
// addGet coloredposition blockof endnew markupmark block
var newNumber;
if (blockFrom == 'left') {
var markGroup;
diff += wDiff.htmlBlockLeftEnd;
 
// 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: movedOldNumber,
newNumber: newNumber,
oldStart: null,
count: null,
unique: null,
words: null,
chars: 0,
type: '|',
section: null,
group: markGroup,
fixed: true,
moved: moved,
text: ''
} );
 
// Set group color
movedGroup.color = color;
movedGroup.movedFrom = markGroup;
color ++;
}
 
else if (blockFrom == 'right') {
// Sort '|' blocks in and update groups
diff += wDiff.htmlBlockRightEnd;
this.sortBlocks();
 
if ( this.config.timer === true ) {
this.timeEnd( 'insertMarks' );
}
return;
};
 
groups[group].diff = diff;
}
 
//**
* Collect diff fragment list for markup, create abstraction layer for customized diffs.
// mark original block positions
* Adds the following fagment types:
//
* '=', '-', '+' same, deletion, insertion
* '<', '>' mark left, mark right
* '(<', '(>', ')' block start and end
* '[', ']' fragment start and end
* '{', '}' container start and end
*
* @param[in] array groups Groups table object
* @param[in] array blocks Blocks table object
* @param[out] array fragments Fragments array, abstraction layer for diff code
*/
this.getDiffFragments = function () {
 
var blocks = this.blocks;
// cycle through groups
for ( var groupgroups = 0; group < this.groups.length; group ++) {
var movedfragments = groups[group]this.movedfragments;
 
// cycleMake throughshallow listcopy of groups movedand fromsort hereby blockStart
var leftMarksgroupsSort = ''groups.slice();
groupsSort.sort( function( a, b ) {
var rightMarks = '';
return a.blockStart - b.blockStart;
for (var i = 0; i < moved.length; i ++) {
} );
var movedGroup = moved[i];
var markColor = groups[movedGroup].color;
var mark;
 
// getCycle movedthrough block textgroups
var movedTextgroupsSortLength = ''groupsSort.length;
for ( var blockgroup = groups[movedGroup].blockStart0; blockgroup <= groups[movedGroup].blockEndgroupsSortLength; blockgroup ++ ) {
var blockStart = groupsSort[group].blockStart;
if (blocks[block].type != 'ins') {
movedTextvar blockEnd += blocksgroupsSort[blockgroup].stringblockEnd;
}
}
 
// Add moved block start
// display as deletion at original position
var color = groupsSort[group].color;
if (wDiff.showBlockMoves === false) {
if (wDiff.regExpBlankBlock.test(movedText) color !=== truenull ) {
var type;
mark = wDiff.htmlDeleteStartBlank;
if ( groupsSort[group].movedFrom < blocks[ blockStart ].group ) {
type = '(<';
}
else {
marktype = wDiff.htmlDeleteStart'(>';
}
fragments.push( {
mark += wDiff.HtmlEscape(movedText) + wDiff.htmlDeleteEnd;
text: '',
type: type,
color: color
} );
}
 
// getCycle markthrough directionblocks
for ( var block = blockStart; block <= blockEnd; block ++ ) {
else {
var type = blocks[block].type;
if (groups[movedGroup].blockStart < groups[group].blockStart) {
 
mark = wDiff.htmlMarkLeft;
// Add '=' unchanged text and moved block
if ( type === '=' || type === '-' || type === '+' ) {
fragments.push( {
text: blocks[block].text,
type: type,
color: color
} );
}
 
else {
// Add '<' and '>' marks
mark = wDiff.htmlMarkRight;
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
} );
}
mark = wDiff.HtmlCustomize(mark, markColor, movedText);
}
 
// getAdd sidemoved ofblock group to markend
if ( color !== null ) {
if (groups[movedGroup].oldNumber < groups[group].oldNumber) {
fragments.push( {
leftMarks += mark;
text: '',
}
else type: { ' )',
rightMarks color: += mark;color
} );
}
}
groups[group].diff = leftMarks + groups[group].diff + rightMarks;
}
 
// Cycle through fragments, join consecutive fragments of same type (i.e. '-' blocks)
//
var fragmentsLength = fragments.length;
// join diffs
for ( var fragment = 1; fragment < fragmentsLength; fragment ++ ) {
//
 
// Check if joinable
// make shallow copy of groups and sort by blockStart
if (
var groupsSort = groups.slice();
fragments[fragment].type === fragments[fragment - 1].type &&
groupsSort.sort(function(a, b) {
fragments[fragment].color === fragments[fragment - 1].color &&
return a.blockStart - b.blockStart;
fragments[fragment].text !== '' && fragments[fragment - 1].text !== ''
});
) {
 
// Join and splice
// cycle through sorted groups and assemble diffs
fragments[fragment - 1].text += fragments[fragment].text;
for (var group = 0; group < groupsSort.length; group ++) {
fragments.splice( fragment, 1 );
text.diff += groupsSort[group].diff;
fragment --;
}
}
}
 
// Enclose in containers
// WED('Groups', wDiff.DebugGroups(groups));
fragments.unshift( { text: '', type: '{', color: null }, { text: '', type: '[', color: null } );
fragments.push( { text: '', type: ']', color: null }, { text: '', type: '}', color: null } );
 
return;
// keep newlines and multiple spaces
};
wDiff.HtmlFormat(text);
 
// WED('text.diff', text.diff);
 
/**
return text.diff;
* Clip unchanged sections from unmoved block text.
};
* Adds the following fagment types:
* '~', ' ~', '~ ' omission indicators
* '[', ']', ',' fragment start and end, fragment separator
*
* @param[in/out] array fragments Fragments array, abstraction layer for diff code
*/
this.clipDiffFragments = function () {
 
var fragments = this.fragments;
 
// Skip if only one fragment in containers, no change
//
if ( fragments.length === 5 ) {
// wDiff.HtmlCustomize: customize move indicator html: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
return;
// input: text (html or css code)
}
// returns: customized text
// called from: wDiff.AssembleDiff()
 
// Min length for clipping right
wDiff.HtmlCustomize = function (text, number, title) {
var minRight = this.config.clipHeadingRight;
 
if ( this.config.clipParagraphRightMin < minRight ) {
if (wDiff.coloredBlocks === true) {
minRight = this.config.clipParagraphRightMin;
var blockStyle = wDiff.styleBlockColor[number];
if (blockStyle === undefined) {
blockStyle = '';
}
if ( this.config.clipLineRightMin < minRight ) {
var markStyle = wDiff.styleMarkColor[number];
minRight = this.config.clipLineRightMin;
if (markStyle === undefined) {
}
markStyle = '';
if ( this.config.clipBlankRightMin < minRight ) {
minRight = this.config.clipBlankRightMin;
}
if ( this.config.clipCharsRight < minRight ) {
minRight = this.config.clipCharsRight;
}
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);
 
// Min length for clipping left
// shorten title text, replace {title}
var minLeft = this.config.clipHeadingLeft;
if ( (title !== undefined) && (title !== '') ) {
if ( this.config.clipParagraphLeftMin < minLeft ) {
var max = 512;
minLeft = this.config.clipParagraphLeftMin;
var end = 128;
}
var gapMark = ' [...] ';
if (title this.lengthconfig.clipLineLeftMin < >minLeft max) {
minLeft = this.config.clipLineLeftMin;
title = title.substr(0, max - gapMark.length - end) + gapMark + title.substr(title.length - end);
}
if ( this.config.clipBlankLeftMin < minLeft ) {
minLeft = this.config.clipBlankLeftMin;
}
if ( this.config.clipCharsLeft < minLeft ) {
minLeft = this.config.clipCharsLeft;
}
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;
};
 
// Cycle through fragments
var fragmentsLength = fragments.length;
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
 
// Skip if not an unmoved and unchanged block
//
var type = fragments[fragment].type;
// wDiff.HtmlEscape: replace html-sensitive characters in output text with character entities
var color = fragments[fragment].color;
// input: text
if ( type !== '=' || color !== null ) {
// returns: escaped text
continue;
// called from: wDiff.Diff(), wDiff.AssembleDiff()
}
 
// Skip if too short for clipping
wDiff.HtmlEscape = function (text) {
var text = fragments[fragment].text;
var textLength = text.length;
if ( textLength < minRight && textLength < minLeft ) {
continue;
}
 
// Get line positions including start and end
text = text.replace(/&/g, '&amp;');
var lines = [];
text = text.replace(/</g, '&lt;');
var lastIndex = null;
text = text.replace(/>/g, '&gt;');
var regExpMatch;
text = text.replace(/"/g, '&quot;');
while ( ( regExpMatch = this.config.regExp.clipLine.exec( text ) ) !== null ) {
return (text);
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
var headings = [];
var headingsEnd = [];
while ( ( regExpMatch = this.config.regExp.clipHeading.exec( text ) ) !== null ) {
headings.push( regExpMatch.index );
headingsEnd.push( regExpMatch.index + regExpMatch[0].length );
}
 
// Get paragraph positions including start and end
//
var paragraphs = [];
// wDiff.HtmlFormat: tidy html, keep newlines and multiple spaces, add container
var lastIndex = null;
// changes: text.diff
while ( ( regExpMatch = this.config.regExp.clipParagraph.exec( text ) ) !== null ) {
// called from: wDiff.Diff(), wDiff.AssembleDiff()
paragraphs.push( regExpMatch.index );
lastIndex = this.config.regExp.clipParagraph.lastIndex;
}
if ( paragraphs[0] !== 0 ) {
paragraphs.unshift( 0 );
}
if ( lastIndex !== textLength ) {
paragraphs.push( textLength );
}
 
// Determine ranges to keep on left and right side
wDiff.HtmlFormat = function (text) {
var rangeRight = null;
var rangeLeft = null;
var rangeRightType = '';
var rangeLeftType = '';
 
// Find clip pos from left, skip for first non-container block
text.diff = text.diff.replace(/<\/(\w+)><!--wDiff(Delete|Insert)--><\1\b[^>]*\bclass="wDiff\2"[^>]*>/g, '');
if ( fragment !== 2 ) {
text.diff = text.diff.replace(/\t/g, wDiff.htmlTab);
text.diff = wDiff.htmlContainerStart + wDiff.htmlFragmentStart + text.diff + wDiff.htmlFragmentEnd + wDiff.htmlContainerEnd;
return;
};
 
// 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
// wDiff.ShortenOutput: shorten diff html by removing unchanged parts
if ( rangeLeft === null ) {
// input: diff html string from wDiff.Diff()
var headingsLength = headingsEnd.length;
// returns: shortened html with removed unchanged passages indicated by (...) or separator
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
wDiff.ShortenOutput = function (html) {
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
var diff = '';
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
// wikEd.debugTimer.push(['shorten?', new Date]);
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
// empty text
if ( (html === undefined) || (htmlrangeLeft === '')null ) {
if ( this.config.clipCharsLeft < rangeLeftMax ) {
return '';
rangeLeft = this.config.clipCharsLeft;
}
rangeLeftType = 'chars';
}
}
 
// Fixed number of lines from left
// remove container by non-regExp replace
if ( rangeLeft === null ) {
html = html.replace(wDiff.htmlContainerStart, '');
rangeLeft = rangeLeftMax;
html = html.replace(wDiff.htmlFragmentStart, '');
rangeLeftType = 'fixed';
html = html.replace(wDiff.htmlFragmentEnd, '');
}
html = html.replace(wDiff.htmlContainerEnd, '');
}
 
// Find clip pos from right, skip for last non-container block
// scan for diff html tags
if ( fragment !== fragments.length - 3 ) {
var regExpDiff = /<\w+\b[^>]*\bclass="[^">]*?\bwDiff(MarkLeft|MarkRight|BlockLeft|BlockRight|Delete|Insert)\b[^">]*"[^>]*>(.|\n)*?<!--wDiff\1-->/g;
var tagsStart = [];
var tagsEnd = [];
var i = 0;
var regExpMatch;
 
// Maximum lines to search from right
// save tag positions
var rangeRightMin = 0;
while ( (regExpMatch = regExpDiff.exec(html)) !== null ) {
if ( lines.length >= this.config.clipLinesRightMax ) {
rangeRightMin = lines[lines.length - this.config.clipLinesRightMax];
}
 
// Find last heading from right
// combine consecutive diff tags
if ( (i > 0) && (tagsEnd[i - 1]rangeRight === regExpMatch.index)null ) {
for ( var j = headings.length - 1; j >= 0; j -- ) {
tagsEnd[i - 1] = regExpMatch.index + regExpMatch[0].length;
if (
}
headings[j] < textLength - this.config.clipHeadingRight ||
else {
headings[j] < rangeRightMin
tagsStart[i] = regExpMatch.index;
) {
tagsEnd[i] = regExpMatch.index + regExpMatch[0].length;
i ++ break;
}
rangeRight = headings[j];
}
rangeRightType = 'heading';
break;
}
}
 
// Find last paragraph from right
// no diff tags detected
if (tagsStart.length rangeRight === 0null ) {
for ( var j = paragraphs.length - 1; j >= 0 ; j -- ) {
return wDiff.htmlNoChange;
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
// define regexps
if ( rangeRight === null ) {
var regExpLine = /^(\n+|.)|(\n+|.)$|\n+/g;
for ( var j = lines.length - 1; j >= 0; j -- ) {
var regExpHeading = /(^|\n)(<[^>]+>)*(==+.+?==+|\{\||\|\}).*?\n?/g;
if (
var regExpParagraph = /^(\n\n+|.)|(\n\n+|.)$|\n\n+/g;
lines[j] < textLength - this.config.clipLineRightMax ||
var regExpBlank = /(<[^>]+>)*\s+/g;
lines[j] < rangeRightMin
) {
break;
}
if ( lines[j] < textLength - this.config.clipLineRightMin ) {
rangeRight = lines[j];
rangeRightType = 'line';
break;
}
}
}
 
// Find last blank from right
// get line positions
if ( rangeRight === null ) {
var regExpMatch;
var startPos = textLength - this.config.clipBlankRightMax;
var lines = [];
if ( startPos < rangeRightMin ) {
while ( (regExpMatch = regExpLine.exec(html)) !== null) {
startPos = rangeRightMin;
lines.push(regExpMatch.index);
}
}
this.config.regExp.clipBlank.lastIndex = startPos;
var lastPos = null;
while ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if ( regExpMatch.index > textLength - this.config.clipBlankRightMin ) {
if ( lastPos !== null ) {
rangeRight = lastPos;
rangeRightType = 'blank';
}
break;
}
lastPos = regExpMatch.index;
}
}
 
// Fixed number of chars from right
// get heading positions
if ( rangeRight === null ) {
var headings = [];
if ( textLength - this.config.clipCharsRight > rangeRightMin ) {
var headingsEnd = [];
rangeRight = textLength - this.config.clipCharsRight;
while ( (regExpMatch = regExpHeading.exec(html)) !== null ) {
rangeRightType = 'chars';
headings.push(regExpMatch.index);
}
headingsEnd.push(regExpMatch.index + regExpMatch[0].length);
}
}
 
// Fixed number of lines from right
// get paragraph positions
if ( rangeRight === null ) {
var paragraphs = [];
rangeRight = rangeRightMin;
while ( (regExpMatch = regExpParagraph.exec(html)) !== null ) {
rangeRightType = 'fixed';
paragraphs.push(regExpMatch.index);
}
}
}
 
// Check if we skip clipping if ranges are close together
// determine fragment border positions around diff tags
if ( rangeLeft !== null && rangeRight !== null ) {
var lineMaxBefore = 0;
var headingBefore = 0;
var paragraphBefore = 0;
var lineBefore = 0;
 
// Skip if overlapping ranges
var lineMaxAfter = 0;
if ( rangeLeft > rangeRight ) {
var headingAfter = 0;
continue;
var paragraphAfter = 0;
}
var lineAfter = 0;
 
// Skip if chars too close
var rangeStart = [];
var skipChars = rangeRight - rangeLeft;
var rangeEnd = [];
if ( skipChars < this.config.clipSkipChars ) {
var rangeStartType = [];
continue;
var rangeEndType = [];
}
 
// cycle through diff tag start positions
for (var i = 0; i < tagsStart.length; i ++) {
var tagStart = tagsStart[i];
var tagEnd = tagsEnd[i];
 
// maximalSkip if lines to search before difftoo tagclose
var rangeStartMinskipLines = 0;
for ( var jlinesLength = lineMaxBefore; j < lines.length - 1; j ++) {
if for (tagStart var j = 0; j < lines[linesLength; j ++ 1]) {
if ( lines[j] -> wDiff.linesBeforeMaxrangeRight || skipLines >= 0this.config.clipSkipLines ) {
break;
rangeStartMin = lines[j - wDiff.linesBeforeMax];
}
if ( lines[j] > rangeLeft ) {
skipLines ++;
}
}
if ( skipLines < this.config.clipSkipLines ) {
continue;
}
lineMaxBefore = j;
break;
}
}
 
// findSkip lastif headingnothing beforeto diff tagclip
if (rangeStart[i] rangeLeft === undefinednull && rangeRight === null ) {
continue;
for (var j = headingBefore; j < headings.length - 1; j ++) {
}
if (headings[j] > tagStart) {
 
break;
// Split left text
var textLeft = null;
var omittedLeft = null;
if ( rangeLeft !== null ) {
textLeft = text.slice( 0, rangeLeft );
 
// Remove trailing empty lines
textLeft = textLeft.replace( this.config.regExp.clipTrimNewLinesLeft, '' );
 
// Get omission indicators, remove trailing blanks
if ( rangeLeftType === 'chars' ) {
omittedLeft = '~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
else if (headings[j +rangeLeftType 1]=== >'blank' tagStart) {
omittedLeft = ' ~';
if ( (headings[j] > tagStart - wDiff.headingBefore) && (headings[j] > rangeStartMin) ) {
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
rangeStart[i] = headings[j];
rangeStartType[i] = 'heading';
headingBefore = j;
}
break;
}
}
}
 
// Split right text
// find last paragraph before diff tag
var textRight = null;
if (rangeStart[i] === undefined) {
var omittedRight = null;
for (var j = paragraphBefore; j < paragraphs.length - 1; j ++) {
if (paragraphs[j] >rangeRight !== null tagStart) {
textRight = text.slice( rangeRight );
break;
 
// Remove leading empty lines
textRight = textRight.replace( this.config.regExp.clipTrimNewLinesRight, '' );
 
// Get omission indicators, remove leading blanks
if ( rangeRightType === 'chars' ) {
omittedRight = '~';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
else if ( rangeRightType === 'blank' ) {
if (paragraphs[j + 1] > tagStart - wDiff.paragraphBeforeMin) {
omittedRight = '~ ';
if ( (paragraphs[j] > tagStart - wDiff.paragraphBeforeMax) && (paragraphs[j] > rangeStartMin) ) {
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
rangeStart[i] = paragraphs[j];
rangeStartType[i] = 'paragraph';
paragraphBefore = j;
}
break;
}
}
}
 
// Remove split element
// find last line break before diff tag
fragments.splice( fragment, 1 );
if (rangeStart[i] === undefined) {
fragmentsLength --;
for (var j = lineBefore; j < lines.length - 1; j ++) {
 
if (lines[j + 1] > tagStart - wDiff.lineBeforeMin) {
// Add left text to fragments list
if ( (lines[j] > tagStart - wDiff.lineBeforeMax) && (lines[j] > rangeStartMin) ) {
if ( rangeLeft !== null ) {
rangeStart[i] = lines[j];
fragments.splice( fragment ++, 0, { text: textLeft, type: '=', color: null } );
rangeStartType[i] = 'line';
lineBeforefragmentsLength = j++;
if ( omittedLeft !== null ) {
}
fragments.splice( fragment ++, 0, { text: '', type: omittedLeft, color: null } );
break;
fragmentsLength ++;
}
}
}
 
// Add fragment container and separator to list
// find last blank before diff tag
if ( rangeLeft !== null && rangeRight !== null ) {
if (rangeStart[i] === undefined) {
fragments.splice( fragment ++, 0, { text: '', type: ']', color: null } );
var lastPos = tagStart - wDiff.blankBeforeMax;
fragments.splice( fragment ++, 0, { text: '', type: ',', color: null } );
if (lastPos < rangeStartMin) {
fragments.splice( fragment ++, 0, { text: '', type: '[', color: null } );
lastPos = rangeStartMin;
fragmentsLength += 3;
}
 
regExpBlank.lastIndex = lastPos;
// Add right text to fragments list
while ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
if ( rangeRight !== null ) {
if (regExpMatch.index > tagStart - wDiff.blankBeforeMin) {
if ( omittedRight !== null ) {
rangeStart[i] = lastPos;
fragments.splice( fragment ++, 0, { text: '', type: omittedRight, color: null } );
rangeStartType[i] = 'blank';
breakfragmentsLength ++;
}
fragments.splice( fragment ++, 0, { text: textRight, type: '=', color: null } );
lastPos = regExpMatch.index;
fragmentsLength ++;
}
}
 
// Debug log
// fixed number of chars before diff tag
if (rangeStart[i] this.config.debug === undefinedtrue ) {
this.debugFragments( 'Fragments' );
if (tagStart - wDiff.charsBefore > rangeStartMin) {
rangeStart[i] = tagStart - wDiff.charsBefore;
rangeStartType[i] = 'chars';
}
}
 
return;
// fixed number of lines before diff tag
};
if (rangeStart[i] === undefined) {
 
rangeStart[i] = rangeStartMin;
 
rangeStartType[i] = 'lines';
/**
* 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;
}
 
// Cycle through fragments
// maximal lines to search after diff tag
var rangeEndMaxhtmlFragments = html.length[];
for (var jfragmentsLength = lineMaxAfter; j < linesfragments.length; j ++) {
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
if (lines[j] > tagEnd) {
var text = fragments[fragment].text;
if (j + wDiff.linesAfterMax < lines.length) {
var type = fragments[fragment].type;
rangeEndMax = lines[j + wDiff.linesAfterMax];
var color = fragments[fragment].color;
var html = '';
 
// Test if text is blanks-only or a single character
var blank = false;
if ( text !== '' ) {
blank = this.config.regExp.blankBlock.test( text );
}
 
// Add container start markup
if ( type === '{' ) {
html = this.config.htmlCode.containerStart;
}
 
// Add container end markup
else if ( type === '}' ) {
html = this.config.htmlCode.containerEnd;
}
 
// Add fragment start markup
if ( type === '[' ) {
html = this.config.htmlCode.fragmentStart;
}
 
// Add fragment end markup
else if ( type === ']' ) {
html = this.config.htmlCode.fragmentEnd;
}
 
// Add fragment separator markup
else if ( type === ',' ) {
html = this.config.htmlCode.separator;
}
 
// Add omission markup
if ( type === '~' ) {
html = this.config.htmlCode.omittedChars;
}
 
// Add omission markup
if ( type === ' ~' ) {
html = ' ' + this.config.htmlCode.omittedChars;
}
 
// Add omission markup
if ( type === '~ ' ) {
html = this.config.htmlCode.omittedChars + ' ';
}
 
// Add colored left-pointing block start markup
else if ( type === '(<' ) {
if ( version !== 'old' ) {
 
// Get title
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 );
}
lineMaxAfter = j;
break;
}
}
 
// Add colored right-pointing block start markup
// find first heading after diff tag
else if (rangeEnd[i] type === undefined'(>' ) {
if ( version !== 'old' ) {
for (var j = headingAfter; j < headingsEnd.length; j ++) {
 
if (headingsEnd[j] > tagEnd) {
// Get title
if ( (headingsEnd[j] < tagEnd + wDiff.headingAfter) && (headingsEnd[j] < rangeEndMax) ) {
var title;
rangeEnd[i] = headingsEnd[j];
if ( this.config.noUnicodeSymbols === true ) {
rangeEndType[i] = 'heading';
title = this.config.msg['wiked-diff-block-right-nounicode'];
paragraphAfter = j;
}
break;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 );
}
}
}
 
// findAdd firstcolored paragraphblock afterend diff tagmarkup
else if (rangeEnd[i] type === undefined' )' ) {
if ( version !== 'old' ) {
for (var j = paragraphAfter; j < paragraphs.length; j ++) {
html = this.config.htmlCode.blockEnd;
if (paragraphs[j] > tagEnd + wDiff.paragraphAfterMin) {
}
if ( (paragraphs[j] < tagEnd + wDiff.paragraphAfterMax) && (paragraphs[j] < rangeEndMax) ) {
}
rangeEnd[i] = paragraphs[j];
 
rangeEndType[i] = 'paragraph';
// Add '=' (unchanged) text and moved block
paragraphAfter = j;
if ( type === '=' ) {
text = this.htmlEscape( text );
if ( color !== null ) {
if ( version !== 'old' ) {
html = this.markupBlanks( text, true );
}
break;}
else {
html = this.markupBlanks( text );
}
}
}
 
// Add '-' text
// find first line break after diff tag
else if (rangeEnd[i] type === undefined'-' ) {
for if (var jversion !== lineAfter; j < lines.length; j'new' ++) {
 
if (lines[j] > tagEnd + wDiff.lineAfterMin) {
// For old version skip '-' inside moved group
if ( (lines[j] < tagEnd + wDiff.lineAfterMax) && (lines[j] < rangeEndMax) ) {
if ( version !== 'old' || color === null ) {
rangeEnd[i] = lines[j];
rangeEndType[i]text = 'line'this.htmlEscape( text );
text = this.markupBlanks( text, true );
lineAfter = j;
if ( blank === true ) {
html = this.config.htmlCode.deleteStartBlank;
}
else {
html = this.config.htmlCode.deleteStart;
}
html += text + this.config.htmlCode.deleteEnd;
}
break;
}
}
}
 
// findAdd blank'+' after diff tagtext
else if (rangeEnd[i] type === undefined'+' ) {
if ( version !== 'old' ) {
regExpBlank.lastIndex = tagEnd + wDiff.blankAfterMin;
text = this.htmlEscape( text );
if ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
text = this.markupBlanks( text, true );
if ( (regExpMatch.index < tagEnd + wDiff.blankAfterMax) && (regExpMatch.index < rangeEndMax) ) {
if ( blank === true ) {
rangeEnd[i] = regExpMatch.index;
html = this.config.htmlCode.insertStartBlank;
rangeEndType[i] = 'blank';
}
else {
html = this.config.htmlCode.insertStart;
}
html += text + this.config.htmlCode.insertEnd;
}
}
 
// Add '<' and '>' code
else if ( type === '<' || type === '>' ) {
if ( version !== 'new' ) {
 
// Display as deletion at original position
if ( this.config.showBlockMoves === false || version === 'old' ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
if ( version === 'old' ) {
if ( this.config.coloredBlocks === true ) {
html =
this.htmlCustomize( this.config.htmlCode.blockColoredStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
else {
html =
this.htmlCustomize( this.config.htmlCode.blockStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
}
else {
if ( blank === true ) {
html =
this.config.htmlCode.deleteStartBlank +
text +
this.config.htmlCode.deleteEnd;
}
else {
html = this.config.htmlCode.deleteStart + text + this.config.htmlCode.deleteEnd;
}
}
}
 
// Display as mark
else {
if ( type === '<' ) {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markLeftColored, color, text );
}
else {
html = this.htmlCustomize( this.config.htmlCode.markLeft, color, text );
}
}
else {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markRightColored, color, text );
}
else {
html = this.htmlCustomize( this.config.htmlCode.markRight, color, text );
}
}
}
}
}
htmlFragments.push( html );
}
 
// Join fragments
// fixed number of chars after diff tag
this.html = htmlFragments.join( '' );
if (rangeEnd[i] === undefined) {
 
if (tagEnd + wDiff.charsAfter < rangeEndMax) {
return;
rangeEnd[i] = tagEnd + wDiff.charsAfter;
};
rangeEndType[i] = 'chars';
 
 
/**
* 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 ) {
html = html.replace( /\{nounicode\}/g, ' wikEdDiffNoUnicode');
}
else {
html = html.replace( /\{nounicode\}/g, '');
}
 
// Shorten title text, replace {title}
if ( title !== undefined ) {
var max = 512;
var end = 128;
var gapMark = ' [...] ';
if ( title.length > max ) {
title =
title.substr( 0, max - gapMark.length - end ) +
gapMark +
title.substr( title.length - end );
}
title = this.htmlEscape( title );
title = title.replace( /\t/g, '&nbsp;&nbsp;');
title = title.replace( / /g, '&nbsp;&nbsp;');
html = html.replace( /\{title\}/, title);
}
return html;
};
 
 
// fixed number of lines after diff tag
/**
if (rangeEnd[i] === undefined) {
* Replace html-sensitive characters in output text with character entities.
rangeEnd[i] = rangeEndMax;
*
rangeEndType[i] = 'lines';
* @param string html Html code to be escaped
* @return string Escaped html code
*/
this.htmlEscape = function ( html ) {
 
html = html.replace( /&/g, '&amp;');
html = html.replace( /</g, '&lt;');
html = html.replace( />/g, '&gt;');
html = html.replace( /"/g, '&quot;');
return html;
};
 
 
/**
* 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);
html = html.replace( /\n/g, this.config.htmlCode.newline);
}
html = html.replace( /\t/g, this.config.htmlCode.tab);
}
return html;
};
 
// remove overlaps, join close fragments
var fragmentStart = [];
var fragmentEnd = [];
var fragmentStartType = [];
var fragmentEndType = [];
fragmentStart[0] = rangeStart[0];
fragmentEnd[0] = rangeEnd[0];
fragmentStartType[0] = rangeStartType[0];
fragmentEndType[0] = rangeEndType[0];
var j = 1;
for (var i = 1; i < rangeStart.length; i ++) {
 
/**
// get lines between fragments
* Count real words in text.
var lines = 0;
*
if (fragmentEnd[j - 1] < rangeStart[i]) {
* @param string text Text for word counting
var join = html.substring(fragmentEnd[j - 1], rangeStart[i]);
* @return int Number of words in text
lines = (join.match(/\n/g) || []).length;
*/
this.wordCount = function ( text ) {
 
return ( text.match( this.config.regExp.countWords ) || [] ).length;
};
 
 
/**
* Test diff code for consistency with input versions.
* Prints results to debug console.
*
* @param[in] WikEdDiffText newText, oldText Text objects
*/
this.unitTests = function () {
 
// Check if output is consistent with new text
this.getDiffHtml( 'new' );
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
if ( (rangeStart[i] > fragmentEnd[j - 1] + wDiff.fragmentJoinChars) || (lines > wDiff.fragmentJoinLines) ) {
this.getDiffHtml( 'old' );
fragmentStart[j] = rangeStart[i];
var diff = this.html.replace( /<[^>]*>/g, '');
fragmentEnd[j] = rangeEnd[i];
var text = this.htmlEscape( this.oldText.text );
fragmentStartType[j] = rangeStartType[i];
if ( diff !== text ) {
fragmentEndType[j] = rangeEndType[i];
console.log(
j ++;
'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.' );
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
var fragment = html.substring(fragmentStart[i], fragmentEnd[i]);
fragment = fragment.replace(/^\n+|\n+$/g, '');
 
/**
// add inline marks for omitted chars and words
* Dump blocks object to browser console.
if (fragmentStart[i] > 0) {
*
if (fragmentStartType[i] == 'chars') {
* @param string name Block name
fragment = wDiff.htmlOmittedChars + fragment;
* @param[in] array blocks Blocks table object
}
*/
else if (fragmentStartType[i] == 'blank') {
this.debugBlocks = function ( name, blocks ) {
fragment = wDiff.htmlOmittedChars + ' ' + fragment;
 
if ( blocks === undefined ) {
blocks = this.blocks;
}
var dump =
'\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq' +
'\twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \ttext\n';
var blocksLength = blocks.length;
for ( var i = 0; i < blocksLength; i ++ ) {
dump +=
i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' +
blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \t' + blocks[i].oldStart + ' \t' +
blocks[i].count + ' \t' + blocks[i].unique + ' \t' + blocks[i].words + ' \t' +
blocks[i].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' +
blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + blocks[i].moved + ' \t' +
this.debugShortenText( blocks[i].text ) + '\n';
}
console.log( name + ':\n' + dump );
};
 
 
/**
* Dump groups object to browser console.
*
* @param string name Group name
* @param[in] array groups Groups table object
*/
this.debugGroups = function ( name, groups ) {
 
if ( groups === undefined ) {
groups = this.groups;
}
var dump =
'\ni \toldNm \tblSta \tblEnd \tuniq \tmaxWo' +
'\twords \tchars \tfixed \toldNm \tmFrom \tcolor\n';
var groupsLength = groupsLength;
for ( var i = 0; i < groups.length; i ++ ) {
dump +=
i + ' \t' + groups[i].oldNumber + ' \t' + groups[i].blockStart + ' \t' +
groups[i].blockEnd + ' \t' + groups[i].unique + ' \t' + groups[i].maxWords + ' \t' +
groups[i].words + ' \t' + groups[i].chars + ' \t' + groups[i].fixed + ' \t' +
groups[i].oldNumber + ' \t' + groups[i].movedFrom + ' \t' + groups[i].color + '\n';
}
console.log( name + ':\n' + dump );
};
 
 
/**
* Dump fragments array to browser console.
*
* @param string name Fragments name
* @param[in] array fragments Fragments array
*/
this.debugFragments = function ( name ) {
 
var fragments = this.fragments;
var dump = '\ni \ttype \tcolor \ttext\n';
var fragmentsLength = fragments.length;
for ( var i = 0; i < fragmentsLength; i ++ ) {
dump +=
i + ' \t"' + fragments[i].type + '" \t' + fragments[i].color + ' \t' +
this.debugShortenText( fragments[i].text, 120, 40 ) + '\n';
}
console.log( name + ':\n' + dump );
};
 
 
/**
* Dump borders array to browser console.
*
* @param string name Arrays name
* @param[in] array border Match border array
*/
this.debugBorders = function ( name, borders ) {
 
var dump = '\ni \t[ new \told ]\n';
var bordersLength = borders.length;
for ( var i = 0; i < bordersLength; i ++ ) {
dump += i + ' \t[ ' + borders[i][0] + ' \t' + borders[i][1] + ' ]\n';
}
console.log( name, dump );
};
 
 
/**
* Shorten text for dumping.
*
* @param string text Text to be shortened
* @param int max Max length of (shortened) text
* @param int end Length of trailing fragment of shortened text
* @return string Shortened text
*/
this.debugShortenText = function ( text, max, end ) {
 
if ( typeof text !== 'string' ) {
text = text.toString();
}
text = text.replace( /\n/g, '\\n');
text = text.replace( /\t/g, ' ');
if ( max === undefined ) {
max = 50;
}
if ( end === undefined ) {
end = 15;
}
if ( text.length > max ) {
text = text.substr( 0, max - 1 - end ) + '…' + text.substr( text.length - end );
}
return '"' + text + '"';
};
 
 
/**
* Start timer 'label', analogous to JavaScript console timer.
* Usage: this.time( 'label' );
*
* @param string label Timer label
* @param[out] array timer Current time in milliseconds (float)
*/
this.time = function ( label ) {
 
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;
if (fragmentEnd[i] < html.length) {
};
if (fragmentStartType[i] == 'chars') {
 
fragment = fragment + wDiff.htmlOmittedChars;
 
/**
* Log recursion timer results to browser console.
* Usage: this.timeRecursionEnd();
*
* @param string text Text label for output
* @param[in] array recursionTimer Accumulated recursion times
*/
this.timeRecursionEnd = function ( text ) {
 
if ( this.recursionTimer.length > 1 ) {
 
// Subtract times spent in deeper recursions
var timerEnd = this.recursionTimer.length - 1;
for ( var i = 0; i < timerEnd; i ++ ) {
this.recursionTimer[i] -= this.recursionTimer[i + 1];
}
 
else if (fragmentStartType[i] == 'blank') {
// Log recursion times
fragment = fragment + ' ' + wDiff.htmlOmittedChars;
var timerLength = this.recursionTimer.length;
for ( var i = 0; i < timerLength; i ++ ) {
console.log( text + ' recursion ' + i + ': ' + this.recursionTimer[i].toFixed( 2 ) + ' ms' );
}
}
this.recursionTimer = [];
return;
};
 
 
/**
// remove leading and trailing empty lines
* Log variable values to debug console.
fragment = fragment.replace(/^\n+|\n+$/g, '');
* Usage: this.debug( 'var', var );
*
* @param string name Object identifier
* @param mixed|undefined name Object to be logged
*/
this.debug = function ( name, object ) {
 
if ( object === undefined ) {
// add fragment separator
if console.log(i >name 0) {;
diff += wDiff.htmlSeparator;
}
else {
console.log( name + ': ' + object );
}
return;
};
 
// encapsulate span errors
diff += wDiff.htmlFragmentStart + fragment + wDiff.htmlFragmentEnd;
}
 
/**
// add to container
* Add script to document head.
diff = wDiff.htmlContainerStart + diff + wDiff.htmlContainerEnd;
*
* @param string code JavaScript code
*/
this.addScript = function ( code ) {
 
if ( document.getElementById( 'wikEdDiffBlockHandler' ) === null ) {
// WED('diff', diff);
var script = document.createElement( 'script' );
script.id = 'wikEdDiffBlockHandler';
if ( script.innerText !== undefined ) {
script.innerText = code;
}
else {
script.textContent = code;
}
document.getElementsByTagName( 'head' )[0].appendChild( script );
}
return;
};
 
// wikEd.debugTimer.push(['shorten=', new Date]);
// wikEd.DebugTimer();
 
/**
return diff;
* 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);
// wDiff.AddScript: add script to head
css = css.replace( /\{cssMarkRight\}/g, this.config.cssMarkRight);
//
 
var style = document.createElement( 'style' );
wDiff.AddScript = function (code) {
style.id = 'wikEdDiffStyles';
style.type = 'text/css';
if ( style.styleSheet !== undefined ) {
style.styleSheet.cssText = css;
}
else {
style.appendChild( document.createTextNode( css ) );
}
document.getElementsByTagName( 'head' )[0].appendChild( style );
}
return;
};
 
 
/**
* Recursive deep copy from target over source for customization import.
*
* @param object source Source object
* @param object target Target object
*/
this.deepCopy = function ( source, target ) {
 
for ( var key in source ) {
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
var script = document.createElement('script');
this.init();
script.id = 'wDiffBlockHandler';
if (script.innerText !== undefined) {
script.innerText = code;
}
else {
script.textContent = code;
}
document.getElementsByTagName('head')[0].appendChild(script);
return;
};
 
 
//**
* Data and methods for single text version (old or new one).
// wDiff.AddStyleSheet: add CSS rules to new style sheet, cross-browser >= IE6
*
//
* @class WikEdDiffText
*/
WikEdDiff.WikEdDiffText = function ( text, parent ) {
 
/** @var WikEdDiff parent Parent object for configuration settings and debugging methods */
wDiff.AddStyleSheet = function (css) {
this.parent = parent;
 
/** @var string text Text of this version */
var style = document.createElement('style');
stylethis.typetext = 'text/css'null;
if (style.styleSheet !== undefined) {
style.styleSheet.cssText = css;
}
else {
style.appendChild( document.createTextNode(css) );
}
document.getElementsByTagName('head')[0].appendChild(style);
return;
};
 
/** @var array tokens Tokens list */
this.tokens = [];
 
/** @var int first, last First and last index of tokens list */
//
this.first = null;
// wDiff.WordCount: count words in string
this.last = null;
//
 
/** @var array words Word counts for version text */
wDiff.WordCount = function (string) {
this.words = {};
 
return (string.match(wDiff.regExpWordCount) || []).length;
};
 
/**
* Constructor, initialize text object.
*
* @param string text Text of version
* @param WikEdDiff parent Parent, for configuration settings and debugging methods
*/
this.init = function () {
 
if ( typeof text !== 'string' ) {
//
text = text.toString();
// wDiff.DebugText: dump text (text.oldText or text.newText) object
}
//
 
// IE / Mac fix
wDiff.DebugText = function (text) {
var dump this.text = 'first: ' + text.firstreplace( + '/\tlast: ' + text.last +r\n?/g, '\n');
dump += '\ni \tlink \t(prev \tnext) \t#num \t"token"\n';
var i = text.first;
while ( (i !== null) && (text.tokens[i] !== null) ) {
dump += i + ' \t' + text.tokens[i].link + ' \t(' + text.tokens[i].prev + ' \t' + text.tokens[i].next + ') \t#' + text.tokens[i].number + ' \t' + wDiff.DebugShortenString(text.tokens[i].token) + '\n';
i = text.tokens[i].next;
}
return dump;
};
 
// 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.DebugBlocks: dump blocks object
//
 
/**
wDiff.DebugBlocks = function (blocks) {
* Parse and count words and chunks for identification of unique words.
var dump = '\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq \twords \tchars \ttype \tsect \tgroup \tfixed \tstring\n';
*
for (var i = 0; i < blocks.length; i ++) {
* @param string regExp Regular expression for counting words
dump += i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' + blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \t' + blocks[i].oldStart + ' \t' + blocks[i].count + ' \t' + blocks[i].unique + ' \t' + blocks[i].words + ' \t' + blocks[i].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' + blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + wDiff.DebugShortenString(blocks[i].string) + '\n';
* @param[in] string text Text of version
}
* @param[out] array words Number of word occurrences
return dump;
*/
};
this.wordParse = function ( regExp ) {
 
var regExpMatch = this.text.match( regExp );
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;
};
 
//
// wDiff.DebugGroups: dump groups object
//
 
/**
wDiff.DebugGroups = function (groups) {
* Split text into paragraph, line, sentence, chunk, word, or character tokens.
var dump = '\ni \tblSta \tblEnd \tuniq \tmaxWo \twords \tchars \tfixed \toldNm \tmFrom \tcolor \tmoved \tdiff\n';
*
for (var i = 0; i < groups.length; i ++) {
* @param string level Level of splitting: paragraph, line, sentence, chunk, word, or character
dump += i + ' \t' + groups[i].blockStart + ' \t' + groups[i].blockEnd + ' \t' + groups[i].unique + ' \t' + groups[i].maxWords + ' \t' + groups[i].words + ' \t' + groups[i].chars + ' \t' + groups[i].fixed + ' \t' + groups[i].oldNumber + ' \t' + groups[i].movedFrom + ' \t' + groups[i].color + ' \t' + groups[i].moved.toString() + ' \t' + wDiff.DebugShortenString(groups[i].diff) + '\n';
* @param int|null token Index of token to be split, otherwise uses full text
}
* @param[in] string text Full text to be split
return dump;
* @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;
var next = null;
var current = this.tokens.length;
var first = current;
var text = '';
 
// Split full text or specified token
//
if ( token === undefined ) {
// wDiff.DebugGaps: dump gaps object
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
wDiff.DebugGaps = function (gaps) {
var number = 0;
var dump = '\ni \tnFirs \tnLast \tnTok \toFirs \toLast \toTok \tcharSplit\n';
var split = [];
for (var i = 0; i < gaps.length; i ++) {
var regExpMatch;
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';
var lastIndex = 0;
}
var regExp = this.parent.config.regExp.split[level];
return dump;
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
var splitLength = split.length;
for ( var i = 0; i < splitLength; i ++ ) {
 
// Insert current item, link to previous
//
this.tokens.push( {
// wDiff.DebugShortenString: shorten string for debugging
token: split[i],
//
prev: prev,
next: null,
link: null,
number: null,
unique: false
} );
number ++;
 
// Link previous item to current
wDiff.DebugShortenString = function (string) {
if (string =prev !== null ) {
this.tokens[prev].next = current;
return 'null';
}
prev = current;
string = string.replace(/\n/g, '\\n');
current ++;
string = string.replace(/\t/g, ' ');
}
var max = 100;
if (string.length > max) {
string = string.substr(0, max - 1 - 30) + '…' + string.substr(string.length - 30);
}
return '"' + string + '"';
};
 
// Connect last new item and existing next item
if ( number > 0 && token !== undefined ) {
if ( prev !== null ) {
this.tokens[prev].next = next;
}
if ( next !== null ) {
this.tokens[next].prev = prev;
}
}
 
// Set text first and last token index
// initialize wDiff
if ( number > 0 ) {
wDiff.Init();
 
// Initial text split
if ( token === undefined ) {
this.first = 0;
this.last = prev;
}
 
// First or last token has been split
else {
if ( token === this.first ) {
this.first = first;
}
if ( token === this.last ) {
this.last = prev;
}
}
}
return;
};
 
 
/**
* Split unique unmatched tokens into smaller tokens.
*
* @param string level Level of splitting: line, sentence, chunk, or word
* @param[in] array tokens Tokens list
*/
this.splitRefine = function ( regExp ) {
 
// Cycle through tokens list
var i = this.first;
while ( i !== null ) {
 
// Refine unique unmatched tokens into smaller tokens
if ( this.tokens[i].link === null ) {
this.splitText( regExp, i );
}
i = this.tokens[i].next;
}
return;
};
 
 
/**
* 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>