User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.0.8 (September 02, 2014) fix sep class
another background that could use some darkmode friendly color
 
(35 intermediate revisions by 3 users not shown)
Line 2:
 
// ==UserScript==
// @name wDiffwikEd diff
// @version 1.02.84
// @date SeptemberOctober 0223, 2014
// @description improved word-based diff library with block move detection
// @homepage https://en.wikipedia.org/wiki/User:Cacycle/diff
Line 12:
// ==/UserScript==
 
/**
* wikEd diff: inline-style difference engine with block move support
*
* Improved JavaScript diff library that returns html/css-formatted new text version with
* highlighted deletions, insertions, and block moves. It is compatible with all browsers and is
* not dependent on external libraries.
*
* WikEdDiff.php and the JavaScript library wikEd diff are synced one-to-one ports. Changes and
* fixes are to be applied to both versions.
*
* JavaScript library (mirror): https://en.wikipedia.org/wiki/User:Cacycle/diff
* JavaScript online tool: http://cacycle.altervista.org/wikEd-diff-tool.html
* MediaWiki extension: https://www.mediawiki.org/wiki/Extension:wikEdDiff
*
* This difference engine applies a word-based algorithm that uses unique words as anchor points
* to identify matching text and moved blocks (Paul Heckel: A technique for isolating differences
* between files. Communications of the ACM 21(4):264 (1978)).
*
* Additional features:
*
* - Visual inline style, changes are shown in a single output text
* - Block move detection and highlighting
* - Resolution down to characters level
* - Unicode and multilingual support
* - Stepwise split (paragraphs, lines, sentences, words, characters)
* - Recursive diff
* - Optimized code for resolving unmatched sequences
* - Minimization of length of moved blocks
* - Alignment of ambiguous unmatched sequences to next line break or word border
* - Clipping of unchanged irrelevant parts from the output (optional)
* - Fully customizable
* - Text split optimized for MediaWiki source texts
* - Well commented and documented code
*
* Datastructures (abbreviations from publication):
*
* class WikEdDiffText: diff text object (new or old version)
* .text text of version
* .words[] word count table
* .first index of first token in tokens list
* .last index of last token in tokens list
*
* .tokens[]: token list for new or old string (doubly-linked list) (N and O)
* .prev previous list item
* .next next list item
* .token token string
* .link index of corresponding token in new or old text (OA and NA)
* .number list enumeration number
* .unique token is unique word in text
*
* class WikEdDiff: diff object
* .config[]: configuration settings, see top of code for customization options
* .regExp[]: all regular expressions
* .split regular expressions used for splitting text into tokens
* .htmlCode HTML code fragments used for creating the output
* .msg output messages
* .newText new text
* .oldText old text
* .maxWords word count of longest linked block
* .html diff html
* .error flag: result has not passed unit tests
* .bordersDown[] linked region borders downwards, [new index, old index]
* .bordersUp[] linked region borders upwards, [new index, old index]
* .symbols: symbols table for whole text at all refinement levels
* .token[] hash table of parsed tokens for passes 1 - 3, points to symbol[i]
* .symbol[]: array of objects that hold token counters and pointers:
* .newCount new text token counter (NC)
* .oldCount old text token counter (OC)
* .newToken token index in text.newText.tokens
* .oldToken token index in text.oldText.tokens
* .linked flag: at least one unique token pair has been linked
*
* .blocks[]: array, block data (consecutive text tokens) in new text order
* .oldBlock number of block in old text order
* .newBlock number of block in new text order
* .oldNumber old text token number of first token
* .newNumber new text token number of first token
* .oldStart old text token index of first token
* .count number of tokens
* .unique contains unique linked token
* .words word count
* .chars char length
* .type '=', '-', '+', '|' (same, deletion, insertion, mark)
* .section section number
* .group group number of block
* .fixed belongs to a fixed (not moved) group
* .moved moved block group number corresponding with mark block
* .text text of block tokens
*
* .sections[]: array, block sections with no block move crosses outside a section
* .blockStart first block in section
* .blockEnd last block in section
 
* .groups[]: array, section blocks that are consecutive in old text order
Improved JavaScript diff library that returns html/css-formatted new text version with highlighted deletions, inserts, and block moves.
* .oldNumber first block oldNumber
It is compatible with all browsers and is not dependent on external libraries.
* .blockStart first block index
An implementation of the word-based algorithm from:
* .blockEnd last block index
* .unique contains unique linked token
* .maxWords word count of longest linked block
* .words word count
* .chars char count
* .fixed not moved from original position
* .movedFrom group position this group has been moved from
* .color color number of moved group
*
* .fragments[]: diff fragment list ready for markup, abstraction layer for customization
* .text block or mark text
* .color moved block or mark color number
* .type '=', '-', '+' same, deletion, insertion
* '<', '>' mark left, mark right
* '(<', '(>', ')' block start and end
* '~', ' ~', '~ ' omission indicators
* '[', ']', ',' fragment start and end, fragment separator
* '{', '}' container start and end
*
*/
 
// JSHint options
Communications of the ACM 21(4):264 (1978)
/* jshint -W004, -W100, newcap: true, browser: true, jquery: true, sub: true, bitwise: true,
http://doi.acm.org/10.1145/359460.359467
curly: true, evil: true, forin: true, freeze: true, globalstrict: true, immed: true,
latedef: true, loopfunc: true, quotmark: single, strict: true, undef: true */
/* global console */
 
// Turn on ECMAScript 5 strict mode
Additional features:
'use strict';
* Word (token) types have been optimized for MediaWiki source texts
* Stepwise token size refinement, starting with paragraphs, then sentences, words, and finally characters
* Additional post-pass-5 code for resolving islands caused by common tokens at the border of sequences of common tokens
* Color coding of moved blocks and their marks at the original position
* Block detection minimizes length of moved vs. static blocks
* Optional omission of unchanged irrelevant parts from the output
* Fully customizable
* Well commented and documented code
 
/** Define global objects. */
This code is used by the MediaWiki in-browser text editors [[en:User:Cacycle/editor]] and [[en:User:Cacycle/wikEd]]
var wikEdDiffConfig;
and the enhanced diff view tool wikEdDiff [[en:User:Cacycle/wikEd]].
var WED;
 
Usage:
var diffHtml = wDiff.Diff(oldString, newString);
diffHtml = wDiff.ShortenOutput(diffHtml);
 
/**
Datastructures (abbreviations from publication):
* wikEd diff main class.
*
* @class WikEdDiff
*/
var WikEdDiff = function () {
 
/** @var array config Configuration and customization settings. */
text: objects for text related data
this.config = {
.newText, new text
.oldText: old text
.string: new or old text to be diffed
.tokens[]: token data list for new or old string (N and O)
.prev: previous list item
.next: next list item
.token: token string
.link: index of corresponding token in new or old text (OA and NA)
.number: list enumeration number
.parsed: token has been added to symbol table
.first: index of first token in tokens list
.last: index of last token in tokens list
.diff: diff html
 
/** Core diff settings (with default values). */
symbols[token]: associative array (hash) of parsed tokens for passes 1 - 3, points to symbol[i]
symbol[]: array of objects that hold token counters and pointers:
.newCount: new text token counter (NC)
.oldCount: old text token counter (OC)
.newToken: token index in text.newText.tokens
.oldToken: token index in text.oldText.tokens
 
/**
blocks[]: array of objects that holds block (consecutive text tokens) data in order of the new text
* @var bool config.fullDiff
.oldBlock: number of block in old text order
* Show complete un-clipped diff text (false)
.newBlock: number of block in new text order
*/
.oldNumber: old text token number of first token in block
'fullDiff': false,
.newNumber: new text token number of first token in block
.oldStart: old text token index of first token in block
.count number of token in block
.chars: char length of block
.type: 'same', 'del', 'ins'
.section: section number of block (for testing)
.group: group number of block
.fixed: block belongs to fixed (not moved) group (for testing)
.string: string of block tokens
 
/**
groups[]: section blocks that are consecutive in old text
* @var bool config.showBlockMoves
oldNumber: first block's oldNumber
* Enable block move layout with highlighted blocks and marks at the original positions (true)
blockStart: first block index of group
*/
blockEnd: last block index of group
'showBlockMoves': true,
maxWords: word count of longest block
words: word count of group
chars: char count of group
fixed: group is set to fixed (not moved)
moved[]: list of groups that have been moved from this position
movedFrom: position this group has been moved from
color: color number of moved group
diff: group diff
 
/**
*/
* @var bool config.charDiff
* Enable character-refined diff (true)
*/
'charDiff': true,
 
/**
// JSHint options: W004: is already defined, W097: Use the function form of "use strict", W100: This character may get silently deleted by one or more browsers
* @var bool config.repeatedDiff
/* jshint -W004, -W097, -W100, newcap: false, browser: true, jquery: true, sub: true, bitwise: true, curly: false, evil: true, forin: true, freeze: true, immed: true, latedef: true, loopfunc: true, quotmark: single, undef: true */
* Enable repeated diff to resolve problematic sequences (true)
/* global console */
*/
'repeatedDiff': true,
 
/**
// turn on ECMAScript 5 strict mode
* @var bool config.recursiveDiff
'use strict';
* Enable recursive diff to resolve problematic sequences (true)
*/
'recursiveDiff': true,
 
/**
// define global object
* @var int config.recursionMax
var wDiff; if (wDiff === undefined) { wDiff = {}; }
* Maximum recursion depth (10)
var WED;
*/
'recursionMax': 10,
 
/**
//
* @var bool config.unlinkBlocks
// core diff settings
* Reject blocks if they are too short and their words are not unique,
//
* prevents fragmentated diffs for very different versions (true)
*/
'unlinkBlocks': true,
 
/**
// enable block move layout with color coded blocks and marks at their original position
* @var int config.unlinkMax
if (wDiff.showBlockMoves === undefined) { wDiff.showBlockMoves = true; }
* Maximum number of rejection cycles (5)
*/
'unlinkMax': 5,
 
/**
// minimal number of real words for a moved block (0 for always showing color coded blocks)
if (wDiff.blockMinLength* ===@var undefined)int { wDiffconfig.blockMinLength = 3; }
* Reject blocks if shorter than this number of real words (3)
*/
'blockMinLength': 3,
 
/**
// further resolve replacements character-wise from start and end
* @var bool config.coloredBlocks
if (wDiff.charDiff === undefined) { wDiff.charDiff = true; }
* Display blocks in differing colors (rainbow color scheme) (false)
*/
'coloredBlocks': false,
 
/**
// enable recursive diff to resolve problematic sequences
* @var bool config.coloredBlocks
if (wDiff.recursiveDiff === undefined) { wDiff.recursiveDiff = true; }
* Do not use UniCode block move marks (legacy browsers) (false)
*/
'noUnicodeSymbols': false,
 
/**
// display blocks in different colors
* @var bool config.stripTrailingNewline
if (wDiff.coloredBlocks === undefined) { wDiff.coloredBlocks = false; }
* Strip trailing newline off of texts (true in .js, false in .php)
*/
'stripTrailingNewline': true,
 
/**
// UniCode letter support for regexps, from http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
* @var bool config.debug
if (wDiff.letters === undefined) { wDiff.letters = 'a-zA-Z0-9' + '00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCCA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC'.replace(/(\w{4})/g, '\\u$1'); }
* Show debug infos and stats (block, group, and fragment data) in debug console (false)
*/
'debug': false,
 
/**
// regExp for splitting into paragraphs after newline
* @var bool config.timer
if (wDiff.regExpParagraph === undefined) { wDiff.regExpParagraph = new RegExp('(.|\\n)+?(\\n|$)', 'g'); }
* Show timing results in debug console (false)
*/
'timer': false,
 
/**
// regExp for splitting into sentences after .spaces or before newline
* @var bool config.unitTesting
if (wDiff.regExpSentence === undefined) { wDiff.regExpSentence = new RegExp('\\n|.*?\\.( +|(?=\\n))|.+?(?=\\n)', 'g'); }
* Run unit tests to prove correct working, display results in debug console (false)
*/
'unitTesting': false,
 
/** RegExp character classes. */
// regExp for splitting into words, multi-char markup, and chars
if (wDiff.regExpWord === undefined) { wDiff.regExpWord = new RegExp('([' + wDiff.letters + '])+|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.', 'g'); }
 
// UniCode letter support for regexps
// regExp for splitting into chars
// From http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
if (wDiff.regExpChar === undefined) { wDiff.regExpChar = new RegExp('.', 'g'); }
'regExpLetters':
'a-zA-Z0-9' + (
'00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-' +
'037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA' +
'05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-' +
'07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D' +
'09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE' +
'09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A39' +
'0A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE0' +
'0AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B83' +
'0B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C' +
'0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-' +
'0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-' +
'0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E87' +
'0E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC4' +
'0EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066' +
'106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D' +
'1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-1315' +
'1318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-' +
'17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C' +
'1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA0' +
'1BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F15' +
'1F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC4' +
'1FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-2113' +
'21152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-' +
'2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE' +
'2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-' +
'3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCC' +
'A000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-' +
'A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3' +
'A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-' +
'AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-' +
'AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9' +
'FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3D' +
'FD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-' +
'FFCFFFD2-FFD7FFDA-FFDC'
).replace( /(\w{4})/g, '\\u$1' ),
 
// New line characters without and with \n and \r
// regExps for bubbling up gaps
'regExpNewLines': '\\u0085\\u2028',
if (wDiff.regExpBubbleStop === undefined) { wDiff.regExpBubbleStop = /\n$/; }
'regExpNewLinesAll': '\\n\\r\\u0085\\u2028',
if (wDiff.regExpBubbleClosing === undefined) { wDiff.regExpBubbleClosing = /^[\s)\]}>\-–—.,:;?!’\/\\=+]/; }
 
// Breaking white space characters without \n, \r, and \f
// regExp for counting words
'regExpBlanks': ' \\t\\x0b\\u2000-\\u200b\\u202f\\u205f\\u3000',
if (wDiff.regExpWordCount === undefined) { wDiff.regExpWordCount = new RegExp('(^|[^' + wDiff.letters + '])[' + wDiff.letters + '][' + wDiff.letters + '_\'’]*', 'g'); }
 
// Full stops without '.'
//
'regExpFullStops':
// shorten output settings
'\\u0589\\u06D4\\u0701\\u0702\\u0964\\u0DF4\\u1362\\u166E\\u1803\\u1809' +
//
'\\u2CF9\\u2CFE\\u2E3C\\u3002\\uA4FF\\uA60E\\uA6F3\\uFE52\\uFF0E\\uFF61',
 
// New paragraph characters without \n and \r
// characters before diff tag to search for previous heading, paragraph, line break, cut characters
'regExpNewParagraph': '\\f\\u2029',
if (wDiff.headingBefore === undefined) { wDiff.headingBefore = 1500; }
if (wDiff.paragraphBefore === undefined) { wDiff.paragraphBefore = 1500; }
if (wDiff.lineBeforeMax === undefined) { wDiff.lineBeforeMax = 1000; }
if (wDiff.lineBeforeMin === undefined) { wDiff.lineBeforeMin = 500; }
if (wDiff.blankBeforeMax === undefined) { wDiff.blankBeforeMax = 1000; }
if (wDiff.blankBeforeMin === undefined) { wDiff.blankBeforeMin = 500; }
if (wDiff.charsBefore === undefined) { wDiff.charsBefore = 500; }
 
// Exclamation marks without '!'
// characters after diff tag to search for next heading, paragraph, line break, or characters
'regExpExclamationMarks':
if (wDiff.headingAfter === undefined) { wDiff.headingAfter = 1500; }
'\\u01C3\\u01C3\\u01C3\\u055C\\u055C\\u07F9\\u1944\\u1944' +
if (wDiff.paragraphAfter === undefined) { wDiff.paragraphAfter = 1500; }
'\\u203C\\u203C\\u2048\\u2048\\uFE15\\uFE57\\uFF01',
if (wDiff.lineAfterMax === undefined) { wDiff.lineAfterMax = 1000; }
if (wDiff.lineAfterMin === undefined) { wDiff.lineAfterMin = 500; }
if (wDiff.blankAfterMax === undefined) { wDiff.blankAfterMax = 1000; }
if (wDiff.blankAfterMin === undefined) { wDiff.blankAfterMin = 500; }
if (wDiff.charsAfter === undefined) { wDiff.charsAfter = 500; }
 
// Question marks without '?'
// lines before and after diff tag to search for previous heading, paragraph, line break, cut characters
'regExpQuestionMarks':
if (wDiff.linesBeforeMax === undefined) { wDiff.linesBeforeMax = 10; }
'\\u037E\\u055E\\u061F\\u1367\\u1945\\u2047\\u2049' +
if (wDiff.linesAfterMax === undefined) { wDiff.linesAfterMax = 10; }
'\\u2CFA\\u2CFB\\u2E2E\\uA60F\\uA6F7\\uFE56\\uFF1F',
 
/** Clip settings. */
// maximal fragment distance to join close fragments
if (wDiff.fragmentJoinLines === undefined) { wDiff.fragmentJoinLines = 10; }
if (wDiff.fragmentJoinChars === undefined) { wDiff.fragmentJoinChars = 1000; }
 
// Find clip position: characters from right
//
'clipHeadingLeft': 1500,
// css classes
'clipParagraphLeftMax': 1500,
//
'clipParagraphLeftMin': 500,
'clipLineLeftMax': 1000,
'clipLineLeftMin': 500,
'clipBlankLeftMax': 1000,
'clipBlankLeftMin': 500,
'clipCharsLeft': 500,
 
// Find clip position: characters from right
if (wDiff.stylesheet === undefined) {
'clipHeadingRight': 1500,
wDiff.stylesheet =
'clipParagraphRightMax': 1500,
'.wDiffTab:before { content: "→"; color: #bbb; font-size: smaller; }' +
'clipParagraphRightMin': 500,
'.wDiffNewline:before { content: "¶"; color: #ccc; padding: 0 0.2em 0 1px; }' +
'clipLineRightMax': 1000,
'.wDiffMarkRight:before { content: "▶"; }' +
'clipLineRightMin': 500,
'.wDiffMarkLeft:before { content: "◀"; }' +
'clipBlankRightMax': 1000,
'.wDiffDelete { font-weight: normal; text-decoration: none; color: #fff; background-color: #c33; border-radius: 0.25em; padding: 0.2em 1px; }' +
'clipBlankRightMin': 500,
'.wDiffInsert { font-weight: normal; text-decoration: none; color: #fff; background-color: #07e; border-radius: 0.25em; padding: 0.2em 1px; }' +
'clipCharsRight': 500,
'.wDiffBlockLeft { background-color: #ddd; border-radius: 0.25em; padding: 0.25em 1px; margin: 0 1px; }' +
'.wDiffBlockRight { background-color: #ddd; border-radius: 0.25em; padding: 0.25em 1px; margin: 0 1px; }' +
'.wDiffMarkLeft { color: #ddd; background-color: #c33; border-radius: 0.25em; padding: 0.2em 0.2em; margin: 0 1px; }' +
'.wDiffMarkRight { color: #ddd; background-color: #c33; border-radius: 0.25em; padding: 0.2em 0.2em; margin: 0 1px; }' +
'.wDiffFragment { white-space: pre-wrap; background: #fcfcfc; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: inherit; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; }' +
'.wDiffNoChange { white-space: pre-wrap; background: #f0f0f0; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: inherit; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; }' +
'.wDiffSeparator { margin-bottom: 1em; }' +
'.wDiffBlock { background-color: #ddd; }' +
'.wDiffBlock0 { background-color: #ffff60; }' +
'.wDiffBlock1 { background-color: #c0ff60; }' +
'.wDiffBlock2 { background-color: #ffd8ff; }' +
'.wDiffBlock3 { background-color: #a0ffff; }' +
'.wDiffBlock4 { background-color: #ffe840; }' +
'.wDiffBlock5 { background-color: #bbccff; }' +
'.wDiffBlock6 { background-color: #ffaaff; }' +
'.wDiffBlock7 { background-color: #ffbbbb; }' +
'.wDiffBlock8 { background-color: #a0e8a0; }' +
'.wDiffMark { color: #ddd; }' +
'.wDiffMark0 { color: #ffff60; }' +
'.wDiffMark1 { color: #c0ff60; }' +
'.wDiffMark2 { color: #ffd8ff; }' +
'.wDiffMark3 { color: #a0ffff; }' +
'.wDiffMark4 { color: #ffd840; }' +
'.wDiffMark5 { color: #bbccff; }' +
'.wDiffMark6 { color: #ff99ff; }' +
'.wDiffMark7 { color: #ff9999; }' +
'.wDiffMark8 { color: #90d090; }' +
'.wDiffBlockHighlight { background-color: #333; color: #fff; }' +
'.wDiffMarkHighlight { background-color: #333; color: #fff; }';
}
 
// Maximum number of lines to search for clip position
//
'clipLinesRightMax': 10,
// css styles
'clipLinesLeftMax': 10,
//
 
// Skip clipping if ranges are too close
if (wDiff.styleContainer === undefined) { wDiff.styleContainer = ''; }
'clipSkipLines': 5,
if (wDiff.StyleDelete === undefined) { wDiff.styleDelete = ''; }
'clipSkipChars': 1000,
if (wDiff.styleInsert === undefined) { wDiff.styleInsert = ''; }
if (wDiff.styleBlockLeft === undefined) { wDiff.styleBlockLeft = ''; }
if (wDiff.styleBlockRight === undefined) { wDiff.styleBlockRight = ''; }
if (wDiff.styleBlockHighlight === undefined) { wDiff.styleBlockHighlight = ''; }
if (wDiff.styleBlockColor === undefined) { wDiff.styleBlockColor = []; }
if (wDiff.styleMarkLeft === undefined) { wDiff.styleMarkLeft = ''; }
if (wDiff.styleMarkRight === undefined) { wDiff.styleMarkRight = ''; }
if (wDiff.styleMarkColor === undefined) { wDiff.styleMarkColor = []; }
if (wDiff.styleNewline === undefined) { wDiff.styleNewline = ''; }
if (wDiff.styleTab === undefined) { wDiff.styleTab = ''; }
if (wDiff.styleFragment === undefined) { wDiff.styleFragment = ''; }
if (wDiff.styleNoChange === undefined) { wDiff.styleNoChange = ''; }
if (wDiff.styleSeparator === undefined) { wDiff.styleSeparator = ''; }
if (wDiff.styleOmittedChars === undefined) { wDiff.styleOmittedChars = ''; }
 
// Css stylesheet
//
'cssMarkLeft': '◀',
// html for core diff
'cssMarkRight': '▶',
//
'stylesheet':
 
// Insert
// dynamic replacements: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
'.wikEdDiffInsert {' +
// class plus html comment are required indicators for wDiff.ShortenOutput()
'font-weight: bold; background-color: #bbddff; ' +
if (wDiff.blockEvent === undefined) { wDiff.blockEvent = ' onmouseover="wDiff.BlockHandler(null, this);"'; }
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'} ' +
'.wikEdDiffInsertBlank { background-color: #66bbff; } ' +
'.wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; } ' +
 
// Delete
if (wDiff.htmlContainerStart === undefined) { wDiff.htmlContainerStart = '<div class="wDiffContainer" style="' + wDiff.styleContainer + '">'; }
'.wikEdDiffDelete {' +
if (wDiff.htmlContainerEnd === undefined) { wDiff.htmlContainerEnd = '</div>'; }
'font-weight: bold; background-color: #ffe49c; ' +
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'} ' +
'.wikEdDiffDeleteBlank { background-color: #ffd064; } ' +
'.wikEdDiffFragment:hover .wikEdDiffDeleteBlank { background-color: #ffe49c; } ' +
 
// Block
if (wDiff.htmlDeleteStart === undefined) { wDiff.htmlDeleteStart = '<span class="wDiffDelete" style="' + wDiff.styleDelete + '" title="−">'; }
'.wikEdDiffBlock {' +
if (wDiff.htmlDeleteEnd === undefined) { wDiff.htmlDeleteEnd = '</span><!--wDiffDelete-->'; }
'font-weight: bold; background-color: #e8e8e8; ' +
'border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; ' +
'} ' +
'.wikEdDiffBlock { color: #000; } ' +
'.wikEdDiffBlock0 { background-color: #ffff80; } ' +
'.wikEdDiffBlock1 { background-color: #d0ff80; } ' +
'.wikEdDiffBlock2 { background-color: #ffd8f0; } ' +
'.wikEdDiffBlock3 { background-color: #c0ffff; } ' +
'.wikEdDiffBlock4 { background-color: #fff888; } ' +
'.wikEdDiffBlock5 { background-color: #bbccff; } ' +
'.wikEdDiffBlock6 { background-color: #e8c8ff; } ' +
'.wikEdDiffBlock7 { background-color: #ffbbbb; } ' +
'.wikEdDiffBlock8 { background-color: #a0e8a0; } ' +
'.wikEdDiffBlockHighlight {' +
'background-color: #777; color: #fff; ' +
'border: solid #777; border-width: 1px 0; ' +
'} ' +
 
// Mark
if (wDiff.htmlInsertStart === undefined) { wDiff.htmlInsertStart = '<span class="wDiffInsert" style="' + wDiff.styleInsert + '" title="+">'; }
'.wikEdDiffMarkLeft, .wikEdDiffMarkRight {' +
if (wDiff.htmlInsertEnd === undefined) { wDiff.htmlInsertEnd = '</span><!--wDiffInsert-->'; }
'font-weight: bold; background-color: #ffe49c; ' +
'color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; ' +
'} ' +
'.wikEdDiffMarkLeft:before { content: "{cssMarkLeft}"; } ' +
'.wikEdDiffMarkRight:before { content: "{cssMarkRight}"; } ' +
'.wikEdDiffMarkLeft.wikEdDiffNoUnicode:before { content: "<"; } ' +
'.wikEdDiffMarkRight.wikEdDiffNoUnicode:before { content: ">"; } ' +
'.wikEdDiffMark { background-color: #e8e8e8; color: #666; } ' +
'.wikEdDiffMark0 { background-color: #ffff60; } ' +
'.wikEdDiffMark1 { background-color: #c8f880; } ' +
'.wikEdDiffMark2 { background-color: #ffd0f0; } ' +
'.wikEdDiffMark3 { background-color: #a0ffff; } ' +
'.wikEdDiffMark4 { background-color: #fff860; } ' +
'.wikEdDiffMark5 { background-color: #b0c0ff; } ' +
'.wikEdDiffMark6 { background-color: #e0c0ff; } ' +
'.wikEdDiffMark7 { background-color: #ffa8a8; } ' +
'.wikEdDiffMark8 { background-color: #98e898; } ' +
'.wikEdDiffMarkHighlight { background-color: #777; color: #fff; } ' +
 
// Wrappers
if (wDiff.htmlBlockLeftStart === undefined) { wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft wDiffBlock{class}" style="' + wDiff.styleBlockLeft + '{block}" title="▶ ▢" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
'.wikEdDiffContainer { } ' +
if (wDiff.htmlBlockLeftEnd === undefined) { wDiff.htmlBlockLeftEnd = '</span><!--wDiffBlockLeft-->'; }
'.wikEdDiffFragment {' +
'white-space: pre-wrap; background-color: var(--background-color-base, #fff); border: #bbb solid; ' +
'border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; ' +
'font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; ' +
'} ' +
'.wikEdDiffNoChange { background: var(--background-color-interactive, #eaecf0); border: 1px #bbb solid; border-radius: 0.5em; ' +
'line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; ' +
'text-align: center; ' +
'} ' +
'.wikEdDiffSeparator { margin-bottom: 1em; } ' +
'.wikEdDiffOmittedChars { } ' +
 
// Newline
if (wDiff.htmlBlockRightStart === undefined) { wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight wDiffBlock{class}" style="' + wDiff.styleBlockRight + '{block}" title="▭ ◀" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
'.wikEdDiffNewline:before { content: "¶"; color: transparent; } ' +
if (wDiff.htmlBlockRightEnd === undefined) { wDiff.htmlBlockRightEnd = '</span><!--wDiffBlockRight-->'; }
'.wikEdDiffBlock:hover .wikEdDiffNewline:before { color: #aaa; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffNewline:before { color: transparent; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffNewline:before { color: #ccc; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffNewline:before, ' +
'.wikEdDiffInsert:hover .wikEdDiffNewline:before' +
'{ color: #999; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffNewline:before, ' +
'.wikEdDiffDelete:hover .wikEdDiffNewline:before' +
'{ color: #aaa; } ' +
 
// Tab
if (wDiff.htmlMarkRight === undefined) { wDiff.htmlMarkRight = '<span class="wDiffMarkRight wDiffMark{class}" style="' + wDiff.styleMarkRight + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->'; }
'.wikEdDiffTab { position: relative; } ' +
if (wDiff.htmlMarkLeft === undefined) { wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft wDiffMark{class}" style="' + wDiff.styleMarkLeft + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->'; }
'.wikEdDiffTabSymbol { position: absolute; top: -0.2em; } ' +
'.wikEdDiffTabSymbol:before { content: "→"; font-size: smaller; color: #ccc; } ' +
'.wikEdDiffBlock .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffInsert .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffDelete .wikEdDiffTabSymbol:before { color: #bbb; } ' +
 
// Space
if (wDiff.htmlNewline === undefined) { wDiff.htmlNewline = '<span class="wDiffNewline" style="' + wDiff.styleNewline + '"></span>\n'; }
'.wikEdDiffSpace { position: relative; } ' +
if (wDiff.htmlTab === undefined) { wDiff.htmlTab = '<span class="wDiffTab" style="' + wDiff.styleTab + '">\t</span>'; }
'.wikEdDiffSpaceSymbol { position: absolute; top: -0.2em; left: -0.05em; } ' +
'.wikEdDiffSpaceSymbol:before { content: "·"; color: transparent; } ' +
'.wikEdDiffBlock:hover .wikEdDiffSpaceSymbol:before { color: #999; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffSpaceSymbol:before { color: transparent; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffSpaceSymbol:before { color: #ddd; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffInsert:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #888; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffDelete:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #999; } ' +
 
// Error
//
'.wikEdDiffError .wikEdDiffFragment,' +
// html for shorten output
'.wikEdDiffError .wikEdDiffNoChange' +
//
'{ background: #faa; }'
};
 
/** Add regular expressions to configuration settings. */
if (wDiff.htmlFragmentStart === undefined) { wDiff.htmlFragmentStart = '<pre class="wDiffFragment" style="' + wDiff.styleFragment + '">'; }
if (wDiff.htmlFragmentEnd === undefined) { wDiff.htmlFragmentEnd = '</pre>'; }
 
this.config.regExp = {
if (wDiff.htmlNoChange === undefined) { wDiff.htmlNoChange = '<pre class="wDiffFragment" style="' + wDiff.styleNoChange + '" title="="></pre>'; }
if (wDiff.htmlSeparator === undefined) { wDiff.htmlSeparator = '<div class="wDiffStyleSeparator" style="' + wDiff.styleSeparator + '"></div>'; }
if (wDiff.htmlOmittedChars === undefined) { wDiff.htmlOmittedChars = '<span class="wDiffOmittedChars" style="' + wDiff.styleOmittedChars + '">…</span>'; }
 
// RegExps for splitting text
'split': {
 
// Split into paragraphs, after double newlines
//
'paragraph': new RegExp(
// javascript handler for output code
'(\\r\\n|\\n|\\r){2,}|[' +
//
this.config.regExpNewParagraph +
']',
'g'
),
 
// Split into lines
// wDiff.BlockHandler: event handler for block and mark elements
'line': new RegExp(
if (wDiff.BlockHandler === undefined) { wDiff.BlockHandler = function (event, element) {
'\\r\\n|\\n|\\r|[' +
this.config.regExpNewLinesAll +
']',
'g'
),
 
// Split into sentences /[^ ].*?[.!?:;]+(?= |$)/
// get event data
'sentence': new RegExp(
var type;
'[^' +
if (event !== null) {
this.config.regExpBlanks +
element = event.currentTarget;
'].*?[.!?:;' +
type = event.type;
this.config.regExpFullStops +
event.stopPropagation();
this.config.regExpExclamationMarks +
}
this.config.regExpQuestionMarks +
']+(?=[' +
this.config.regExpBlanks +
']|$)',
'g'
),
 
// Split into inline chunks
// get mark/block elements
'chunk': new RegExp(
var number = element.id.replace(/\D/g, '');
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
var block = document.getElementById('wDiffBlock' + number);
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
var mark = document.getElementById('wDiffMark' + number);
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
switch (type) {
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
 
// Split into words, multi-char markup, and chars
// highlight corresponding mark/block pairs
// regExpLetters speed-up: \\w+
case undefined:
'word': new RegExp(
case 'mouseover':
'(\\w+|[_' +
if (element.addEventListener !== undefined) {
this.config.regExpLetters +
element.addEventListener('mouseout', wDiff.BlockHandler, false);
'])+([\'’][_' +
element.addEventListener('click', wDiff.BlockHandler, false);
this.config.regExpLetters +
}
']*)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.',
else if (element.attachEvent !== undefined) {
'g'
element.attachEvent('onmouseout', wDiff.BlockHandler);
),
element.attachEvent('onclick', wDiff.BlockHandler);
}
else {
return;
}
block.className += ' wDiffBlockHighlight';
mark.className += ' wDiffMarkHighlight';
break;
 
// Split into chars
// remove mark/block highlighting
case 'mouseoutcharacter': /./g
},
var highlighted = document.getElementsByClassName('wDiffBlockHighlight');
for (var i = 0; i < highlighted.length; i ++) {
highlighted[i].className = highlighted[i].className.replace(/ wDiffBlockHighlight/g, '');
}
var highlighted = document.getElementsByClassName('wDiffMarkHighlight');
for (var i = 0; i < highlighted.length; i ++) {
highlighted[i].className = highlighted[i].className.replace(/ wDiffMarkHighlight/g, '');
}
break;
 
// scrollRegExp to correspondingdetect mark/blockblank elementtokens
'blankOnlyToken': new RegExp(
case 'click':
'[^' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']'
),
 
// RegExps for sliding gaps: newlines and space/word breaks
// remove element click handler
'slideStop': new RegExp(
if (element.removeEventListener !== undefined) {
'[' +
element.removeEventListener('mouseout', wDiff.BlockHandler, false);
this.config.regExpNewLinesAll +
}
this.config.regExpNewParagraph +
else {
']$'
element.detachEvent('onmouseout', wDiff.BlockHandler);
),
'slideBorder': new RegExp(
'[' +
this.config.regExpBlanks +
']$'
),
 
// RegExps for counting words
'countWords': new RegExp(
'(\\w+|[_' +
this.config.regExpLetters +
'])+([\'’][_' +
this.config.regExpLetters +
']*)*',
'g'
),
'countChunks': new RegExp(
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
 
// RegExp detecting blank-only and single-char blocks
'blankBlock': /^([^\t\S]+|[^\t])$/,
 
// RegExps for clipping
'clipLine': new RegExp(
'[' + this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g'
),
'clipHeading': new RegExp(
'( ^|\\n)(==+.+?==+|\\{\\||\\|\\}).*?(?=\\n|$)', 'g' ),
'clipParagraph': new RegExp(
'( (\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
'])+',
'g'
),
'clipBlank': new RegExp(
'[' +
this.config.regExpBlanks + ']+',
'g'
),
'clipTrimNewLinesLeft': new RegExp(
'[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimNewLinesRight': new RegExp(
'^[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g'
),
'clipTrimBlanksLeft': new RegExp(
'[' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimBlanksRight': new RegExp(
'^[' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g'
)
};
 
/** Add messages to configuration settings. */
 
this.config.msg = {
'wiked-diff-empty': '(No difference)',
'wiked-diff-same': '=',
'wiked-diff-ins': '+',
'wiked-diff-del': '-',
'wiked-diff-block-left': '◀',
'wiked-diff-block-right': '▶',
'wiked-diff-block-left-nounicode': '<',
'wiked-diff-block-right-nounicode': '>',
'wiked-diff-error': 'Error: diff not consistent with versions!'
};
 
/**
* Add output html fragments to configuration settings.
* Dynamic replacements:
* {number}: class/color/block/mark/id number
* {title}: title attribute (popup)
* {nounicode}: noUnicodeSymbols fallback
*/
this.config.htmlCode = {
'noChangeStart':
'<div class="wikEdDiffNoChange" title="' +
this.config.msg['wiked-diff-same'] +
'">',
'noChangeEnd': '</div>',
 
'containerStart': '<div class="wikEdDiffContainer" id="wikEdDiffContainer">',
'containerEnd': '</div>',
 
'fragmentStart': '<pre class="wikEdDiffFragment" style="white-space: pre-wrap;">',
'fragmentEnd': '</pre>',
'separator': '<div class="wikEdDiffSeparator"></div>',
 
'insertStart':
'<span class="wikEdDiffInsert" title="' +
this.config.msg['wiked-diff-ins'] +
'">',
'insertStartBlank':
'<span class="wikEdDiffInsert wikEdDiffInsertBlank" title="' +
this.config.msg['wiked-diff-ins'] +
'">',
'insertEnd': '</span>',
 
'deleteStart':
'<span class="wikEdDiffDelete" title="' +
this.config.msg['wiked-diff-del'] +
'">',
'deleteStartBlank':
'<span class="wikEdDiffDelete wikEdDiffDeleteBlank" title="' +
this.config.msg['wiked-diff-del'] +
'">',
'deleteEnd': '</span>',
 
'blockStart':
'<span class="wikEdDiffBlock"' +
'title="{title}" id="wikEdDiffBlock{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
'blockColoredStart':
'<span class="wikEdDiffBlock wikEdDiffBlock wikEdDiffBlock{number}"' +
'title="{title}" id="wikEdDiffBlock{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
'blockEnd': '</span>',
 
'markLeft':
'<span class="wikEdDiffMarkLeft{nounicode}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
'markLeftColored':
'<span class="wikEdDiffMarkLeft{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
 
'markRight':
'<span class="wikEdDiffMarkRight{nounicode}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
'markRightColored':
'<span class="wikEdDiffMarkRight{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
 
'newline': '<span class="wikEdDiffNewline">\n</span>',
'tab': '<span class="wikEdDiffTab"><span class="wikEdDiffTabSymbol"></span>\t</span>',
'space': '<span class="wikEdDiffSpace"><span class="wikEdDiffSpaceSymbol"></span> </span>',
 
'omittedChars': '<span class="wikEdDiffOmittedChars">…</span>',
 
'errorStart': '<div class="wikEdDiffError" title="Error: diff not consistent with versions!">',
'errorEnd': '</div>'
};
 
/*
* Add JavaScript event handler function to configuration settings
* Highlights corresponding block and mark elements on hover and jumps between them on click
* Code for use in non-jQuery environments and legacy browsers (at least IE 8 compatible)
*
* @option Event|undefined event Browser event if available
* @option element Node DOM node
* @option type string Event type
*/
this.config.blockHandler = function ( event, element, type ) {
 
// IE compatibility
if ( event === undefined && window.event !== undefined ) {
event = window.event;
}
 
// Get mark/block elements
var number = element.id.replace( /\D/g, '' );
var block = document.getElementById( 'wikEdDiffBlock' + number );
var mark = document.getElementById( 'wikEdDiffMark' + number );
if ( block === null || mark === null ) {
return;
}
 
// Highlight corresponding mark/block pairs
if ( type === 'mouseover' ) {
element.onmouseover = null;
element.onmouseout = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'mouseout' );
};
element.onclick = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'click' );
};
block.className += ' wikEdDiffBlockHighlight';
mark.className += ' wikEdDiffMarkHighlight';
}
 
// Remove mark/block highlighting
if ( type === 'mouseout' || type === 'click' ) {
element.onmouseout = null;
element.onmouseover = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'mouseover' );
};
 
// Reset, allow outside container (e.g. legend)
if ( type !== 'click' ) {
block.className = block.className.replace( / wikEdDiffBlockHighlight/g, '' );
mark.className = mark.className.replace( / wikEdDiffMarkHighlight/g, '' );
 
// GetElementsByClassName
var container = document.getElementById( 'wikEdDiffContainer' );
if ( container !== null ) {
var spans = container.getElementsByTagName( 'span' );
var spansLength = spans.length;
for ( var i = 0; i < spansLength; i ++ ) {
if ( spans[i] !== block && spans[i] !== mark ) {
if ( spans[i].className.indexOf( ' wikEdDiffBlockHighlight' ) !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffBlockHighlight/g, '' );
}
else if ( spans[i].className.indexOf( ' wikEdDiffMarkHighlight') !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffMarkHighlight/g, '' );
}
}
}
}
}
}
 
// Scroll to corresponding mark/block element
if ( type === 'click' ) {
 
// getGet corresponding element
var corrElement;
if ( element === block ) {
corrElement = mark;
}
Line 349 ⟶ 794:
}
 
// Get element height (getOffsetTop)
// get offsetTop
var corrElementPos = 0;
var node = corrElement;
do {
corrElementPos += node.offsetTop;
} while ( ( node = node.offsetParent ) !== null );
 
// Get scroll element under mouse cursorheight
var top = window.pageYOffset;
if ( window.pageYOffset !== undefined ) {
var cursor = event.pageY;
top = window.pageYOffset;
var line = parseInt(window.getComputedStyle(corrElement).getPropertyValue('line-height'));
}
window.scroll(0, corrElementPos + top - cursor + line / 2);
break;else {
top = document.documentElement.scrollTop;
}
}
return;
}; }
 
// Get cursor pos
//
var cursor;
// start of diff code
if ( event.pageY !== undefined ) {
//
cursor = event.pageY;
}
else if ( event.clientY !== undefined ) {
cursor = event.clientY + top;
}
 
// Get line height
var line = 12;
if ( window.getComputedStyle !== undefined ) {
line = parseInt( window.getComputedStyle( corrElement ).getPropertyValue( 'line-height' ) );
}
 
// Scroll element under mouse cursor
// wDiff.Init: initialize wDiff
window.scroll( 0, corrElementPos + top - cursor + line / 2 );
// called from: on code load
}
// calls: wDiff.AddStyleSheet()
return;
};
 
/** Internal data structures. */
wDiff.Init = function () {
 
/** @var WikEdDiffText newText New text version object with text and token list */
// compatibility fixes for old names of functions
this.newText = null;
window.StringDiff = wDiff.Diff;
window.WDiffString = wDiff.Diff;
window.WDiffShortenOutput = wDiff.ShortenOutput;
 
/** @var WikEdDiffText oldText Old text version object with text and token list */
// shortcut to wikEd.Debug()
this.oldText = null;
if (WED === undefined) {
if (typeof console == 'object') {
WED = console.log;
}
else {
WED = window.alert;
}
}
 
/** @var object symbols Symbols table for whole text at all refinement levels */
// add styles to head
this.symbols = {
wDiff.AddStyleSheet(wDiff.stylesheet);
token: [],
hashTable: {},
linked: false
};
 
/** @var array bordersDown Matched region borders downwards */
// add block handler to head if running under Greasemonkey
this.bordersDown = [];
if (typeof GM_info == 'object') {
var script = 'var wDiff; if (wDiff === undefined) { wDiff = {}; } wDiff.BlockHandler = ' + wDiff.BlockHandler.toString();
wDiff.AddScript(script);
}
return;
};
 
/** @var array bordersUp Matched region borders upwards */
this.bordersUp = [];
 
/** @var array blocks Block data (consecutive text tokens) in new text order */
// wDiff.Diff: main method
this.blocks = [];
// input: oldString, newString, strings containing the texts to be diffed
// called from: user code
// calls: wDiff.Split(), wDiff.SplitRefine(), wDiff.CalculateDiff(), wDiff.DetectBlocks(), wDiff.AssembleDiff()
// returns: diff html code, call wDiff.ShortenOutput() for shortening this output
 
/** @var int maxWords Maximal detected word count of all linked blocks */
wDiff.Diff = function (oldString, newString) {
this.maxWords = 0;
 
/** @var array groups Section blocks that are consecutive in old text order */
var diff = '';
this.groups = [];
 
/** @var array sections Block sections with no block move crosses outside a section */
// IE / Mac fix
this.sections = [];
oldString = oldString.replace(/\r\n?/g, '\n');
newString = newString.replace(/\r\n?/g, '\n');
 
/** @var object timer Debug timer array: string 'label' => float milliseconds. */
// prepare text data object
var textthis.timer = {};
newText: {
string: newString,
tokens: [],
first: null,
last: null
},
oldText: {
string: oldString,
tokens: [],
first: null,
last: null
},
diff: ''
};
 
/** @var array recursionTimer Count time spent in recursion level in milliseconds. */
// trap trivial changes: no change
this.recursionTimer = [];
if (oldString == newString) {
text.diff = wDiff.HtmlEscape(newString);
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** Output data. */
// trap trivial changes: old text deleted
if ( (oldString === null) || (oldString.length === 0) ) {
text.diff = wDiff.htmlInsertStart + wDiff.HtmlEscape(newString) + wDiff.htmlInsertEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var bool error Unit tests have detected a diff error */
// trap trivial changes: new text deleted
this.error = false;
if ( (newString === null) || (newString.length === 0) ) {
text.diff = wDiff.htmlDeleteStart + wDiff.HtmlEscape(oldString) + wDiff.htmlDeleteEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var array fragments Diff fragment list for markup, abstraction layer for customization */
// split new and old text into paragraps
this.fragments = [];
wDiff.Split(text.newText, wDiff.regExpParagraph);
wDiff.Split(text.oldText, wDiff.regExpParagraph);
 
/** @var string html Html code of diff */
// calculate diff
this.html = '';
wDiff.CalculateDiff(text);
 
// refine different paragraphs into sentences
wDiff.SplitRefine(text.newText, wDiff.regExpSentence);
wDiff.SplitRefine(text.oldText, wDiff.regExpSentence);
 
/**
// calculate refined diff
* Constructor, initialize settings, load js and css.
wDiff.CalculateDiff(text);
*
* @param[in] object wikEdDiffConfig Custom customization settings
* @param[out] object config Settings
*/
 
this.init = function () {
// refine different sentences into words
wDiff.SplitRefine(text.newText, wDiff.regExpWord);
wDiff.SplitRefine(text.oldText, wDiff.regExpWord);
 
// Import customizations from wikEdDiffConfig{}
// calculate refined diff information with recursion for unresolved gaps
if ( typeof wikEdDiffConfig === 'object' ) {
wDiff.CalculateDiff(text, true);
this.deepCopy( wikEdDiffConfig, this.config );
}
 
// Add CSS stylescheet
// bubble up gaps
this.addStyleSheet( this.config.stylesheet );
wDiff.BubbleUpGaps(text.newText, text.oldText);
wDiff.BubbleUpGaps(text.oldText, text.newText);
 
// Load block handler script
// split tokens into chars in selected unresolved gaps
if (wDiff this.charDiffconfig.showBlockMoves === true ) {
wDiff.SplitRefineChars(text);
 
// Add block handler to head if running under Greasemonkey
// calculate refined diff information with recursion for unresolved gaps
if ( typeof GM_info === 'object' ) {
wDiff.CalculateDiff(text, true);
var script = 'var wikEdDiffBlockHandler = ' + this.config.blockHandler.toString() + ';';
}
this.addScript( script );
}
else {
window.wikEdDiffBlockHandler = this.config.blockHandler;
}
}
return;
};
 
// bubble up gaps
wDiff.BubbleUpGaps(text.newText, text.oldText);
wDiff.BubbleUpGaps(text.oldText, text.newText);
 
/**
// enumerate tokens lists
* Main diff method.
wDiff.EnumerateTokens(text.newText);
*
wDiff.EnumerateTokens(text.oldText);
* @param string oldString Old text version
* @param string newString New text version
* @param[out] array fragment
* Diff fragment list ready for markup, abstraction layer for customized diffs
* @param[out] string html Html code of diff
* @return string Html code of diff
*/
this.diff = function ( oldString, newString ) {
 
// detectStart movedtotal blockstimer
if ( this.config.timer === true ) {
var blocks = [];
this.time( 'total' );
var groups = [];
}
wDiff.DetectBlocks(text, blocks, groups);
 
// Start diff timer
// assemble diff blocks into formatted html text
if ( this.config.timer === true ) {
diff = wDiff.AssembleDiff(text, blocks, groups);
this.time( 'diff' );
}
 
// Reset error flag
return diff;
this.error = false;
};
 
// Strip trailing newline (.js only)
if ( this.config.stripTrailingNewline === true ) {
if ( newString.substr( -1 ) === '\n' && oldString.substr( -1 === '\n' ) ) {
newString = newString.substr( 0, newString.length - 1 );
oldString = oldString.substr( 0, oldString.length - 1 );
}
}
 
// Load version strings into WikEdDiffText objects
// wDiff.Split: split text into paragraph, sentence, or word tokens
this.newText = new WikEdDiff.WikEdDiffText( newString, this );
// input: text (text.newText or text.oldText), object containing text data and strings; regExp, regular expression for splitting text into tokens; token, tokens index of token to be split
this.oldText = new WikEdDiff.WikEdDiffText( oldString, this );
// changes: text (text.newText or text.oldText): text.tokens list, text.first, text.last
// called from: wDiff.Diff()
 
// Trap trivial changes: no change
wDiff.Split = function (text, regExp, token) {
if ( this.newText.text === this.oldText.text ) {
this.html =
this.config.htmlCode.containerStart +
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd +
this.config.htmlCode.containerEnd;
return this.html;
}
 
// Trap trivial changes: old text deleted
var prev = null;
if (
var next = null;
this.oldText.text === '' || (
var current = text.tokens.length;
this.oldText.text === '\n' &&
var first = current;
( this.newText.text.charAt( this.newText.text.length - 1 ) === '\n' )
var string = '';
)
) {
this.html =
this.config.htmlCode.containerStart +
this.config.htmlCode.fragmentStart +
this.config.htmlCode.insertStart +
this.htmlEscape( this.newText.text ) +
this.config.htmlCode.insertEnd +
this.config.htmlCode.fragmentEnd +
this.config.htmlCode.containerEnd;
return this.html;
}
 
// Trap trivial changes: new text deleted
// split full text or specified token
if (
if (token === undefined) {
this.newText.text === '' || (
string = text.string;
this.newText.text === '\n' &&
}
( this.oldText.text.charAt( this.oldText.text.length - 1 ) === '\n' )
else {
)
prev = text.tokens[token].prev;
) {
next = text.tokens[token].next;
this.html =
string = text.tokens[token].token;
this.config.htmlCode.containerStart +
}
this.config.htmlCode.fragmentStart +
this.config.htmlCode.deleteStart +
this.htmlEscape( this.oldText.text ) +
this.config.htmlCode.deleteEnd +
this.config.htmlCode.fragmentEnd +
this.config.htmlCode.containerEnd;
return this.html;
}
 
// splitSplit new and old text into tokensparagraps
if ( this.config.timer === true ) {
var number = 0;
this.time( 'paragraph split' );
var regExpMatch;
}
while ( (regExpMatch = regExp.exec(string)) !== null) {
this.newText.splitText( 'paragraph' );
this.oldText.splitText( 'paragraph' );
if ( this.config.timer === true ) {
this.timeEnd( 'paragraph split' );
}
 
// Calculate diff
// insert current item, link to previous
this.calculateDiff( 'line' );
text.tokens[current] = {
token: regExpMatch[0],
prev: prev,
next: null,
link: null,
number: null,
parsed: false
};
number ++;
 
// Refine different paragraphs into lines
// link previous item to current
if (prev !this.config.timer === true null) {
this.time( 'line split' );
text.tokens[prev].next = current;
}
this.newText.splitRefine( 'line' );
prev = current;
this.oldText.splitRefine( 'line' );
current ++;
if ( this.config.timer === true ) {
}
this.timeEnd( 'line split' );
}
 
// Calculate refined diff
this.calculateDiff( 'line' );
 
// Refine different lines into sentences
// connect last new item and existing next item
if ( (numberthis.config.timer > 0) && (token !=== undefined)true ) {
this.time( 'sentence split' );
if (prev !== null) {
text.tokens[prev].next = next;
}
this.newText.splitRefine( 'sentence' );
if (next !== null) {
this.oldText.splitRefine( 'sentence' );
text.tokens[next].prev = prev;
if ( this.config.timer === true ) {
this.timeEnd( 'sentence split' );
}
}
 
// Calculate refined diff
// set text first and last token index
this.calculateDiff( 'sentence' );
if (number > 0) {
 
// Refine different sentences into chunks
// initial text split
if (token this.config.timer === undefinedtrue ) {
textthis.firsttime( ='chunk 0split' );
}
text.last = prev;
this.newText.splitRefine( 'chunk' );
this.oldText.splitRefine( 'chunk' );
if ( this.config.timer === true ) {
this.timeEnd( 'chunk split' );
}
 
// Calculate refined diff
// first or last token has been split
this.calculateDiff( 'chunk' );
else {
 
if (token == text.first) {
// Refine different chunks into words
text.first = first;
if ( this.config.timer === true ) {
this.time( 'word split' );
}
this.newText.splitRefine( 'word' );
this.oldText.splitRefine( 'word' );
if ( this.config.timer === true ) {
this.timeEnd( 'word split' );
}
 
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( 'word', true );
 
// Slide gaps
if ( this.config.timer === true ) {
this.time( 'word slide' );
}
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
if ( this.config.timer === true ) {
this.timeEnd( 'word slide' );
}
 
// Split tokens into chars
if ( this.config.charDiff === true ) {
 
// Split tokens into chars in selected unresolved gaps
if ( this.config.timer === true ) {
this.time( 'character split' );
}
this.splitRefineChars();
if (token == text.last) {
if ( this.config.timer === true ) {
text.last = prev;
this.timeEnd( 'character split' );
}
}
}
return;
};
 
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( 'character', true );
 
// Slide gaps
// wDiff.SplitRefine: split unique unmatched tokens into smaller tokens
if ( this.config.timer === true ) {
// changes: text (text.newText or text.oldText) .tokens list
this.time( 'character slide' );
// called from: wDiff.Diff()
}
// calls: wDiff.Split()
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
if ( this.config.timer === true ) {
this.timeEnd( 'character slide' );
}
}
 
// Free memory
wDiff.SplitRefine = function (text, regExp) {
this.symbols = undefined;
this.bordersDown = undefined;
this.bordersUp = undefined;
this.newText.words = undefined;
this.oldText.words = undefined;
 
// Enumerate token lists
// cycle through tokens list
this.newText.enumerateTokens();
var i = text.first;
this.oldText.enumerateTokens();
while ( (i !== null) && (text.tokens[i] !== null) ) {
 
// Detect moved blocks
// refine unique unmatched tokens into smaller tokens
if (text this.tokens[i]config.linktimer === nulltrue ) {
wDiffthis.Splittime(text, regExp,'blocks' i);
}
this.detectBlocks();
if ( this.config.timer === true ) {
this.timeEnd( 'blocks' );
}
i = text.tokens[i].next;
}
return;
};
 
// Free memory
this.newText.tokens = undefined;
this.oldText.tokens = undefined;
 
// Assemble blocks into fragment table
// wDiff.SplitRefineChars: split tokens into chars in the following unresolved regions (gaps):
this.getDiffFragments();
// - one token became separated by space, dash, or any string
// - same number of tokens in gap and strong similarity of all tokens:
// - addition or deletion of flanking strings in tokens
// - addition or deletion of internal string in tokens
// - same length and at least 50 % identity
// - same start or end, same text longer than different text
// - same length and at least 50 % identity
// identical tokens including space separators will be linked, resulting in word-wise char-level diffs
// changes: text (text.newText or text.oldText) .tokens list
// called from: wDiff.Diff()
// calls: wDiff.Split()
// steps:
// find corresponding gaps
// select gaps of identical token number and strong similarity in all tokens
// refine words into chars in selected gaps
 
// Free memory
wDiff.SplitRefineChars = function (text) {
this.blocks = undefined;
this.groups = undefined;
this.sections = undefined;
 
// Stop diff timer
//
if ( this.config.timer === true ) {
// find corresponding gaps
this.timeEnd( 'diff' );
//
}
 
// Unit tests
// cycle trough new text tokens list
if ( this.config.unitTesting === true ) {
var gaps = [];
var gap = null;
var i = text.newText.first;
var j = text.oldText.first;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
 
// Test diff to test consistency between input and output
// get token links
if ( this.config.timer === true ) {
var newLink = text.newText.tokens[i].link;
this.time( 'unit tests' );
var oldLink = null;
}
if (j !== null) {
this.unitTests();
oldLink = text.oldText.tokens[j].link;
if ( this.config.timer === true ) {
this.timeEnd( 'unit tests' );
}
}
 
// Clipping
// start of gap in new and old
if ( this.config.fullDiff === false ) {
if ( (gap === null) && (newLink === null) && (oldLink === null) ) {
 
gap = gaps.length;
// Clipping unchanged sections from unmoved block text
gaps.push({
if ( this.config.timer === true ) {
newFirst: i,
newLast:this.time( 'clip' i,);
}
newTokens: 1,
this.clipDiffFragments();
oldFirst: j,
if ( this.config.timer === true ) {
oldLast: j,
this.timeEnd( 'clip' );
oldTokens: null,
}
charSplit: null
});
}
 
// Create html formatted diff code from diff fragments
// count chars and tokens in gap
else if ( (gap !== null) && (newLinkthis.config.timer === null)true ) {
gaps[gap]this.newLasttime( ='html' i);
}
gaps[gap].newTokens ++;
this.getDiffHtml();
if ( this.config.timer === true ) {
this.timeEnd( 'html' );
}
 
// gapNo endedchange
else if ( (gapthis.html !== null) && (newLink !== null)'' ) {
gapthis.html = null;
this.config.htmlCode.containerStart +
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd +
this.config.htmlCode.containerEnd;
}
 
// nextAdd listerror elementsindicator
if (newLink !this.error === true null) {
this.html = this.config.htmlCode.errorStart + this.html + this.config.htmlCode.errorEnd;
j = text.oldText.tokens[newLink].next;
}
i = text.newText.tokens[i].next;
}
 
// Stop total timer
// cycle trough gaps and add old text gap data
if ( this.config.timer === true ) {
for (var gap = 0; gap < gaps.length; gap ++) {
this.timeEnd( 'total' );
}
 
return this.html;
// cycle trough old text tokens list
};
var j = gaps[gap].oldFirst;
while ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.oldText.tokens[j].link === null) ) {
 
// count old chars and tokens in gap
gaps[gap].oldLast = j;
gaps[gap].oldTokens ++;
 
/**
j = text.oldText.tokens[j].next;
* Split tokens into chars in the following unresolved regions (gaps):
}
* - One token became connected or separated by space or dash (or any token)
}
* - Same number of tokens in gap and strong similarity of all tokens:
* - Addition or deletion of flanking strings in tokens
* - Addition or deletion of internal string in tokens
* - Same length and at least 50 % identity
* - Same start or end, same text longer than different text
* Identical tokens including space separators will be linked,
* resulting in word-wise char-level diffs
*
* @param[in/out] WikEdDiffText newText, oldText Text object tokens list
*/
this.splitRefineChars = function () {
 
/** Find corresponding gaps. */
//
// select gaps of identical token number and strong similarity of all tokens
//
 
// Cycle through new text tokens list
for (var gap = 0; gap < gaps.length; gap ++) {
var charSplitgaps = true[];
var gap = null;
var i = this.newText.first;
var j = this.oldText.first;
while ( i !== null ) {
 
// notGet sametoken gap lengthlinks
var newLink = this.newText.tokens[i].link;
if (gaps[gap].newTokens != gaps[gap].oldTokens) {
var oldLink = null;
if ( j !== null ) {
oldLink = this.oldText.tokens[j].link;
}
 
// Start of gap in new and old
// one word became separated by space, dash, or any string
if ( (gaps[gap].newTokens === 1)null && (gaps[gap].oldTokensnewLink === null && oldLink === 3)null ) {
gap = gaps.length;
if (text.newText.tokens[ gaps[gap].newFirst ].token != text.oldText.tokens[ gaps[gap].oldFirst ].token + text.oldText.tokens[ gaps[gap].oldLast ].token ) {
continue;gaps.push( {
} newFirst: i,
newLast: i,
newTokens: 1,
oldFirst: j,
oldLast: j,
oldTokens: null,
charSplit: null
} );
}
 
else if ( (gaps[gap].oldTokens == 1) && (gaps[gap].newTokens == 3) ) {
// Count chars and tokens in gap
if (text.oldText.tokens[ gaps[gap].oldFirst ].token != text.newText.tokens[ gaps[gap].newFirst ].token + text.newText.tokens[ gaps[gap].newLast ].token ) {
else if ( gap !== null && newLink === null ) {
continue;
gaps[gap].newLast = i;
}
gaps[gap].newTokens ++;
}
 
else {
// Gap ended
continue;
else if ( gap !== null && newLink !== null ) {
gap = null;
}
 
// Next list elements
if ( newLink !== null ) {
j = this.oldText.tokens[newLink].next;
}
i = this.newText.tokens[i].next;
}
 
// cycleCycle troughthrough newgaps textand tokensadd listold andtext setgap charSplitdata
var igapsLength = gaps[gap].newFirstlength;
for ( var jgap = gaps[0; gap].oldFirst < gapsLength; gap ++ ) {
while (i !== null) {
var newToken = text.newText.tokens[i].token;
var oldToken = text.oldText.tokens[j].token;
 
// getCycle shorterthrough andold longertext tokentokens list
var shorterTokenj = gaps[gap].oldFirst;
while (
var longerToken;
j !== null &&
if (newToken.length < oldToken.length) {
this.oldText.tokens[j] !== null &&
shorterToken = newToken;
this.oldText.tokens[j].link === null
longerToken = oldToken;
}) {
 
else {
// Count old chars and tokens in gap
shorterToken = oldToken;
longerTokengaps[gap].oldLast = newTokenj;
gaps[gap].oldTokens ++;
 
j = this.oldText.tokens[j].next;
}
}
 
/** Select gaps of identical token number and strong similarity of all tokens. */
// not same token length
if (newToken.length != oldToken.length) {
 
var gapsLength = gaps.length;
// test for addition or deletion of internal string in tokens
for ( var gap = 0; gap < gapsLength; gap ++ ) {
var charSplit = true;
 
// Not same gap length
// find number of identical chars from left
if ( gaps[gap].newTokens !== gaps[gap].oldTokens ) {
var left = 0;
 
while (left < shorterToken.length) {
// One word became separated by space, dash, or any string
if (newToken.charAt(left) != oldToken.charAt(left)) {
if ( gaps[gap].newTokens === 1 && gaps[gap].oldTokens === 3 ) {
break;
var token = this.newText.tokens[ gaps[gap].newFirst ].token;
var tokenFirst = this.oldText.tokens[ gaps[gap].oldFirst ].token;
var tokenLast = this.oldText.tokens[ gaps[gap].oldLast ].token;
if (
token.indexOf( tokenFirst ) !== 0 ||
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
left ++;
}
else if ( gaps[gap].oldTokens === 1 && gaps[gap].newTokens === 3 ) {
 
var token = this.oldText.tokens[ gaps[gap].oldFirst ].token;
// find number of identical chars from right
var tokenFirst = this.newText.tokens[ gaps[gap].newFirst ].token;
var right = 0;
var tokenLast = this.newText.tokens[ gaps[gap].newLast ].token;
while (right < shorterToken.length) {
if (
if (newToken.charAt(newToken.length - 1 - right) != oldToken.charAt(oldToken.length - 1 - right)) {
token.indexOf( tokenFirst ) !== 0 ||
break;
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
right ++;
}
else {
continue;
}
gaps[gap].charSplit = true;
}
 
// Cycle through new text tokens list and set charSplit
// no simple insertion or deletion of internal string
else {
if (left + right != shorterToken.length) {
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
while ( i !== null ) {
var newToken = this.newText.tokens[i].token;
var oldToken = this.oldText.tokens[j].token;
 
// Get shorter and longer token
// not addition or deletion of flanking strings in tokens (smaller token not part of larger token)
ifvar (longerToken.indexOf(shorterToken) == -1) {;
var longerToken;
if ( newToken.length < oldToken.length ) {
shorterToken = newToken;
longerToken = oldToken;
}
else {
shorterToken = oldToken;
longerToken = newToken;
}
 
// Not same text at start or end shorter than differenttoken textlength
if ( (left < shorterTokennewToken.length /!== 2) && (right < shorterTokenoldToken.length / 2) ) {
 
// doTest notfor splitaddition intoor charsdeletion thisof gapinternal string in tokens
 
// Find number of identical chars from left
var left = 0;
while ( left < shorterToken.length ) {
if ( newToken.charAt( left ) !== oldToken.charAt( left ) ) {
break;
}
left ++;
}
 
// Find number of identical chars from right
var right = 0;
while ( right < shorterToken.length ) {
if (
newToken.charAt( newToken.length - 1 - right ) !==
oldToken.charAt( oldToken.length - 1 - right )
) {
break;
}
right ++;
}
 
// No simple insertion or deletion of internal string
if ( left + right !== shorterToken.length ) {
 
// Not addition or deletion of flanking strings in tokens
// Smaller token not part of larger token
if ( longerToken.indexOf( shorterToken ) === -1 ) {
 
// Same text at start or end shorter than different text
if ( left < shorterToken.length / 2 && (right < shorterToken.length / 2) ) {
 
// Do not split into chars in this gap
charSplit = false;
break;
}
}
}
}
 
// Same token length
else if ( newToken !== oldToken ) {
 
// Tokens less than 50 % identical
var ident = 0;
var tokenLength = shorterToken.length;
for ( var pos = 0; pos < tokenLength; pos ++ ) {
if ( shorterToken.charAt( pos ) === longerToken.charAt( pos ) ) {
ident ++;
}
}
if ( ident / shorterToken.length < 0.49 ) {
 
// Do not split into chars this gap
charSplit = false;
break;
}
}
 
// Next list elements
if ( i === gaps[gap].newLast ) {
break;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
gaps[gap].charSplit = charSplit;
}
}
 
/** Refine words into chars in selected gaps. */
// same token length
else if (newToken != oldToken) {
 
var gapsLength = gaps.length;
// tokens less than 50 % identical
for ( var identgap = 0; gap < gapsLength; gap ++ ) {
if ( gaps[gap].charSplit === true ) {
for (var pos = 0; pos < shorterToken.length; pos ++) {
 
if (shorterToken.charAt(pos) == longerToken.charAt(pos)) {
// Cycle through new text tokens list, link spaces, and split into chars
ident ++;
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
var newGapLength = i - gaps[gap].newLast;
var oldGapLength = j - gaps[gap].oldLast;
while ( i !== null || j !== null ) {
 
// Link identical tokens (spaces) to keep char refinement to words
if (
newGapLength === oldGapLength &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
}
if (ident/shorterToken.length < 0.49) {
 
// doRefine not splitwords into chars this gap
charSplitelse = false;{
if ( i !== null ) {
break;
this.newText.splitText( 'character', i );
}
if ( j !== null ) {
this.oldText.splitText( 'character', j );
}
}
 
// Next list elements
if ( i === gaps[gap].newLast ) {
i = null;
}
if ( j === gaps[gap].oldLast ) {
j = null;
}
if ( i !== null ) {
i = this.newText.tokens[i].next;
}
if ( j !== null ) {
j = this.oldText.tokens[j].next;
}
}
}
 
// next list elements
if (i == gaps[gap].newLast) {
break;
}
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
return;
gaps[gap].charSplit = charSplit;
};
 
//
// refine words into chars in selected gaps
//
 
/**
for (var gap = 0; gap < gaps.length; gap ++) {
* Move gaps with ambiguous identical fronts to last newline border or otherwise last word border.
if (gaps[gap].charSplit === true) {
*
* @param[in/out] wikEdDiffText text, textLinked These two are newText and oldText
*/
this.slideGaps = function ( text, textLinked ) {
 
var regExpSlideBorder = this.config.regExp.slideBorder;
// cycle trough new text tokens list
var regExpSlideStop = this.config.regExp.slideStop;
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
while (i !== null) {
var newToken = text.newText.tokens[i].token;
var oldToken = text.oldText.tokens[j].token;
 
// linkCycle identicalthrough tokens (spaces)list
var i = text.first;
if (newToken == oldToken) {
var gapStart = null;
text.newText.tokens[i].link = j;
while ( i !== null ) {
text.oldText.tokens[j].link = i;
}
 
// Remember gap start
// refine different words into chars
if ( gapStart === null && text.tokens[i].link === null ) {
else {
gapStart = i;
wDiff.Split(text.newText, wDiff.regExpChar, i);
wDiff.Split(text.oldText, wDiff.regExpChar, j);
}
 
// next list elements
if (i == gaps[gap].newLast) {
break;
}
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
}
}
 
// Find gap end
// WED('Gap', wDiff.DebugGaps(gaps));
else if ( gapStart !== null && text.tokens[i].link !== null ) {
var gapFront = gapStart;
var gapBack = text.tokens[i].prev;
 
// Slide down as deep as possible
return;
var front = gapFront;
};
var back = text.tokens[gapBack].next;
if (
front !== null &&
back !== null &&
text.tokens[front].link === null &&
text.tokens[back].link !== null &&
text.tokens[front].token === text.tokens[back].token
) {
text.tokens[front].link = text.tokens[back].link;
textLinked.tokens[ text.tokens[front].link ].link = front;
text.tokens[back].link = null;
 
gapFront = text.tokens[gapFront].next;
gapBack = text.tokens[gapBack].next;
 
front = text.tokens[front].next;
// wDiff.BubbleUpGaps: move gaps with ambiguous identical fronts and backs up
back = text.tokens[back].next;
// start ambiguous gap borders after line breaks and text section closing characters
}
// changes: text (text.newText or text.oldText) .tokens list
// called from: wDiff.Diff()
 
// Test slide up, remember last line break or word border
wDiff.BubbleUpGaps = function (text, textLinked) {
var front = text.tokens[gapFront].prev;
var back = gapBack;
var gapFrontBlankTest = regExpSlideBorder.test( text.tokens[gapFront].token );
var frontStop = front;
if ( text.tokens[back].link === null ) {
while (
front !== null &&
back !== null &&
text.tokens[front].link !== null &&
text.tokens[front].token === text.tokens[back].token
) {
if ( front !== null ) {
 
// Stop at line break
// cycle through tokens list
if ( regExpSlideStop.test( text.tokens[front].token ) === true ) {
var i = text.first;
frontStop = front;
var gapStart = null;
break;
while ( (i !== null) && (text.tokens[i] !== null) ) {
}
 
// Stop at first word border (blank/word or word/blank)
// remember gap start
if (
if ( (gapStart === null) && (text.tokens[i].link === null) ) {
regExpSlideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
gapStart = i;
frontStop = front;
}
}
}
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
}
 
// findActually gapslide endup to stop
else var iffront ( (gapStart !== null) && (text.tokens[igapFront].link !== null) ) {prev;
var back = gapBack;
while (
front !== null &&
back !== null &&
front !== frontStop &&
text.tokens[front].link !== null &&
text.tokens[back].link === null &&
text.tokens[front].token === text.tokens[back].token
) {
text.tokens[back].link = text.tokens[front].link;
textLinked.tokens[ text.tokens[back].link ].link = back;
text.tokens[front].link = null;
 
front = text.tokens[front].prev;
// bubble up, stop at line breaks
var front back = text.tokens[gapStartback].prev;
}
var back = text.tokens[i].prev;
gapStart = null;
while (
(front !== null) && (back !== null) && (wDiff.regExpBubbleStop.test(text.tokens[front].token) === false) &&
(text.tokens[front].link !== null) && (text.tokens[back].link === null) &&
(text.tokens[front].token == text.tokens[back].token)
) {
text.tokens[back].link = text.tokens[front].link;
textLinked.tokens[ text.tokens[back].link ].link = back;
text.tokens[front].link = null;
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
i = text.tokens[i].next;
 
// do not start gap with spaces or other closing characters, roll back (bubble down)
if ( (back !== null) && (front !== null) ) {
front = text.tokens[front].next;
back = text.tokens[back].next;
}
while (
(back !== null) && (front !== null) && (wDiff.regExpBubbleClosing.test(text.tokens[front].token) === true) &&
(text.tokens[front].link === null) && (text.tokens[back].link !== null) &&
(text.tokens[front].token === text.tokens[back].token)
) {
text.tokens[front].link = text.tokens[back].link;
textLinked.tokens[ text.tokens[front].link ].link = front;
text.tokens[back].link = null;
front = text.tokens[front].next;
back = text.tokens[back].next;
}
gapStart = null;
}
return;
i = text.tokens[i].next;
};
return;
};
 
 
/**
// wDiff.EnumerateTokens: enumerate text token list
* Calculate diff information, can be called repeatedly during refining.
// changes: text (text.newText or text.oldText) .tokens list
* Links corresponding tokens from old and new text.
// called from: wDiff.Diff()
* Steps:
* Pass 1: parse new text into symbol table
* Pass 2: parse old text into symbol table
* Pass 3: connect unique matching tokens
* Pass 4: connect adjacent identical tokens downwards
* Pass 5: connect adjacent identical tokens upwards
* Repeat with empty symbol table (against crossed-over gaps)
* Recursively diff still unresolved regions downwards with empty symbol table
* Recursively diff still unresolved regions upwards with empty symbol table
*
* @param array symbols Symbol table object
* @param string level Split level: 'paragraph', 'line', 'sentence', 'chunk', 'word', 'character'
*
* Optionally for recursive or repeated calls:
* @param bool repeating Currently repeating with empty symbol table
* @param bool recurse Enable recursion
* @param int newStart, newEnd, oldStart, oldEnd Text object tokens indices
* @param int recursionLevel Recursion level
* @param[in/out] WikEdDiffText newText, oldText Text object, tokens list link property
*/
this.calculateDiff = function (
level,
recurse,
repeating,
newStart,
oldStart,
up,
recursionLevel
) {
 
// Set defaults
wDiff.EnumerateTokens = function (text) {
if ( repeating === undefined ) { repeating = false; }
if ( recurse === undefined ) { recurse = false; }
if ( newStart === undefined ) { newStart = this.newText.first; }
if ( oldStart === undefined ) { oldStart = this.oldText.first; }
if ( up === undefined ) { up = false; }
if ( recursionLevel === undefined ) { recursionLevel = 0; }
 
// Start timers
// enumerate tokens list
if ( this.config.timer === true && repeating === false && recursionLevel === 0 ) {
var number = 0;
this.time( level );
var i = text.first;
}
while ( (i !== null) && (text.tokens[i] !== null) ) {
if ( this.config.timer === true && repeating === false ) {
text.tokens[i].number = number;
this.time( level + recursionLevel );
number ++;
}
i = text.tokens[i].next;
}
return;
};
 
// Get object symbols table and linked region borders
var symbols;
var bordersDown;
var bordersUp;
if ( recursionLevel === 0 && repeating === false ) {
symbols = this.symbols;
bordersDown = this.bordersDown;
bordersUp = this.bordersUp;
}
 
// Create empty local symbols table and linked region borders arrays
// wDiff.CalculateDiff: calculate diff information, can be called repeatedly during refining
else {
// input: text, object containing text data and tokens
symbols = {
// optionally for recursive calls: newStart, newEnd, oldStart, oldEnd (tokens list indexes), recursionLevel
token: [],
// changes: text.oldText/newText.tokens[].link, links corresponding tokens from old and new text
hashTable: {},
// steps:
linked: false
// pass 1: parse new text into symbol table
};
// pass 2: parse old text into symbol table
bordersDown = [];
// pass 3: connect unique tokens
bordersUp = [];
// pass 4: connect adjacent identical tokens downwards
}
// pass 5: connect adjacent identical tokens upwards
// recursively diff still unresolved regions downwards
// recursively diff still unresolved regions upwards
 
wDiff.CalculateDiff = function (text, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
 
// Updated versions of linked region borders
// symbol (token) data
var symbolbordersUpNext = [];
var symbolsbordersDownNext = {}[];
 
/**
// set defaults
* Pass 1: parse new text into symbol table.
if (newStart === undefined) { newStart = text.newText.first; }
*/
if (newEnd === undefined) { newEnd = text.newText.last; }
if (oldStart === undefined) { oldStart = text.oldText.first; }
if (oldEnd === undefined) { oldEnd = text.oldText.last; }
if (recursionLevel === undefined) { recursionLevel = 0; }
 
// Cycle through new text tokens list
// limit recursion depth
var i = newStart;
if (recursionLevel > 10) {
while ( i !== null ) {
return;
if ( this.newText.tokens[i].link === null ) {
}
 
// Add new entry to symbol table
//
var token = this.newText.tokens[i].token;
// pass 1: parse new text into symbol table
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
//
symbols.hashTable[token] = symbols.token.length;
symbols.token.push( {
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
} );
}
 
// Or update existing entry
// cycle trough new text tokens list
else {
var i = newStart;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
 
// parseIncrement token onlycounter oncefor duringnew split refinementtext
var hashToArray = symbols.hashTable[token];
if ( (text.newText.tokens[i].parsed === false) || (recursionLevel > 0) ) {
symbols.token[hashToArray].newCount ++;
text.newText.tokens[i].parsed = true;
}
}
 
// addStop newafter entrygap toif symbol tablerecursing
else if ( recursionLevel > 0 ) {
var token = text.newText.tokens[i].token;
break;
if (Object.prototype.hasOwnProperty.call(symbols, token) === false) {
var current = symbol.length;
symbols[token] = current;
symbol[current] = {
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
};
}
 
// orGet updatenext existing entrytoken
if ( up === false ) {
i = this.newText.tokens[i].next;
}
else {
i = this.newText.tokens[i].prev;
 
// increment token counter for new text
var hashToArray = symbols[token];
symbol[hashToArray].newCount ++;
}
}
 
/**
// next list element
* Pass 2: parse old text into symbol table.
if (i == newEnd) {
break; */
}
i = text.newText.tokens[i].next;
}
 
// Cycle through old text tokens list
//
var j = oldStart;
// pass 2: parse old text into symbol table
while ( j !== null ) {
//
if ( this.oldText.tokens[j].link === null ) {
 
// Add new entry to symbol table
// cycle trough old text tokens list
var token = this.oldText.tokens[j].token;
var j = oldStart;
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
while ( (j !== null) && (text.oldText.tokens[j] !== null) ) {
symbols.hashTable[token] = symbols.token.length;
symbols.token.push( {
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
} );
}
 
// Or update existing entry
// parse token only once during split refinement
else {
if ( (text.oldText.tokens[j].parsed === false) || (recursionLevel > 0) ) {
text.oldText.tokens[j].parsed = true;
 
// addIncrement newtoken entrycounter tofor symbolold tabletext
var tokenhashToArray = textsymbols.oldText.tokenshashTable[jtoken].token;
symbols.token[hashToArray].oldCount ++;
if (Object.prototype.hasOwnProperty.call(symbols, token) === false) {
 
var current = symbol.length;
symbols[ // Add token] =number for old current;text
symbols.token[hashToArray].oldToken = j;
symbol[current] = {
newCount: 0,}
oldCount: 1,
newToken: null,
oldToken: j
};
}
 
// orStop updateafter existinggap entryif recursing
else if ( recursionLevel > 0 ) {
break;
}
 
// incrementGet next token counter for old text
if ( up === false ) {
var hashToArray = symbols[token];
j = this.oldText.tokens[j].next;
symbol[hashToArray].oldCount ++;
}
 
else {
// add token number for old text
j = this.oldText.tokens[j].prev;
symbol[hashToArray].oldToken = j;
}
}
 
/**
// next list element
* Pass 3: connect unique tokens.
if (j === oldEnd) {
break; */
}
j = text.oldText.tokens[j].next;
}
 
// Cycle through symbol array
//
var symbolsLength = symbols.token.length;
// pass 3: connect unique tokens
for ( var i = 0; i < symbolsLength; i ++ ) {
//
 
// Find tokens in the symbol table that occur only once in both versions
// cycle trough symbol array
if ( symbols.token[i].newCount === 1 && symbols.token[i].oldCount === 1 ) {
for (var i = 0; i < symbol.length; i ++) {
var newToken = symbols.token[i].newToken;
var oldToken = symbols.token[i].oldToken;
var newTokenObj = this.newText.tokens[newToken];
var oldTokenObj = this.oldText.tokens[oldToken];
 
// Connect from new to old and from old to new
// find tokens in the symbol table that occur only once in both versions
if ( (symbol[i]newTokenObj.newCountlink == 1) && (symbol[i].oldCount == 1)null ) {
var newToken = symbol[i].newToken;
var oldToken = symbol[i].oldToken;
 
// doDo not use spaces as unique markers
if (
if (/^\s+$/.test(text.newText.tokens[newToken].token) === false) {
this.config.regExp.blankOnlyToken.test( newTokenObj.token ) === true
) {
 
// connect fromLink new to old and from old to newtokens
newTokenObj.link = oldToken;
if (text.newText.tokens[newToken].link === null) {
text.newText.tokens[newToken] oldTokenObj.link = oldTokennewToken;
symbols.linked = true;
text.oldText.tokens[oldToken].link = newToken;
}
}
}
}
 
// Save linked region borders
//
bordersDown.push( [newToken, oldToken] );
// pass 4: connect adjacent identical tokens downwards
bordersUp.push( [newToken, oldToken] );
//
 
// Check if token contains unique word
// cycle trough new text tokens list
if ( recursionLevel === 0 ) {
var i = text.newText.first;
var unique = false;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
if ( level === 'character' ) {
var iNext = text.newText.tokens[i].next;
unique = true;
}
else {
var token = newTokenObj.token;
var words =
( token.match( this.config.regExp.countWords ) || [] ).concat(
( token.match( this.config.regExp.countChunks ) || [] )
);
 
// Unique if longer than min block length
// find already connected pairs
var wordsLength = words.length;
var j = text.newText.tokens[i].link;
if (j !=wordsLength >= nullthis.config.blockMinLength ) {
unique = true;
var jNext = text.oldText.tokens[j].next;
}
 
// checkUnique if theit followingcontains tokensat areleast notone yetunique connectedword
else {
if ( (iNext !== null) && (jNext !== null) ) {
for ( var i = 0;i < wordsLength; i ++ ) {
if ( (text.newText.tokens[iNext].link === null) && (text.oldText.tokens[jNext].link === null) ) {
var word = words[i];
if (
this.oldText.words[word] === 1 &&
this.newText.words[word] === 1 &&
Object.prototype.hasOwnProperty.call( this.oldText.words, word ) === true &&
Object.prototype.hasOwnProperty.call( this.newText.words, word ) === true
) {
unique = true;
break;
}
}
}
}
 
// Set unique
// connect if the following tokens are the same
if ( unique === true ) {
if (text.newText.tokens[iNext].token == text.oldText.tokens[jNext].token) {
newTokenObj.unique = true;
text.newText.tokens[iNext].link = jNext;
oldTokenObj.unique = true;
text.oldText.tokens[jNext].link = iNext;
}
}
}
}
}
}
i = iNext;
}
 
// Continue passes only if unique tokens have been linked previously
//
if ( symbols.linked === true ) {
// pass 5: connect adjacent identical tokens upwards
//
 
/**
// cycle trough new text tokens list
* Pass 4: connect adjacent identical tokens downwards.
var i = text.newText.last;
*/
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
var iNext = text.newText.tokens[i].prev;
 
// Cycle through list of linked new text tokens
// find already connected pairs
var bordersLength = bordersDown.length;
var j = text.newText.tokens[i].link;
for ( var match = 0; match < bordersLength; match ++ ) {
if (j !== null) {
var jNexti = text.oldText.tokensbordersDown[jmatch][0].prev;
var j = bordersDown[match][1];
 
// Next down
// check if the preceeding tokens are not yet connected
var iMatch = i;
if ( (iNext !== null) && (jNext !== null) ) {
var jMatch = j;
if ( (text.newText.tokens[iNext].link === null) && (text.oldText.tokens[jNext].link === null) ) {
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
 
// connectCycle ifthrough thenew preceedingtext tokenslist aregap theregion samedownwards
while (
if (text.newText.tokens[iNext].token == text.oldText.tokens[jNext].token) {
i !== null &&
text.newText.tokens[iNext].link = jNext;
j !== null &&
text.oldText.tokens[jNext].link = iNext;
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
 
// Connect if same token
if ( this.newText.tokens[i].token === this.oldText.tokens[j].token ) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
 
// Not a match yet, maybe in next refinement level
else {
bordersDownNext.push( [iMatch, jMatch] );
break;
}
 
// Next token down
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
}
i = iNext;
}
 
/**
// refine by recursively diffing unresolved regions caused by addition of common tokens around sequences of common tokens, only at word level split
* Pass 5: connect adjacent identical tokens upwards.
if ( (recurse === true) && (wDiff.recursiveDiff === true) ) {
*/
 
// Cycle through list of connected new text tokens
//
var bordersLength = bordersUp.length;
// recursively diff still unresolved regions downwards
for ( var match = 0; match < bordersLength; match ++ ) {
//
var i = bordersUp[match][0];
var j = bordersUp[match][1];
 
// Next up
// cycle trough new text tokens list
var iiMatch = newStarti;
var jjMatch = oldStartj;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
 
// Cycle through new text gap region upwards
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
 
// Connect if same token
// get j from previous tokens match
var if iPrev( = textthis.newText.tokens[i].prev;token === this.oldText.tokens[j].token ) {
this.newText.tokens[i].link = j;
if (iPrev !== null) {
var jPrev = text this.newTextoldText.tokens[iPrevj].link = i;
}
if (jPrev !== null) {
j = text.oldText.tokens[jPrev].next;
}
}
 
// check for the start of an unresolved sequence
if ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) ) {
 
// determineNot thea limitsmatch ofyet, ofmaybe thein unresolvednext newrefinement sequencelevel
var else iStart = i;{
bordersUpNext.push( [iMatch, jMatch] );
var iEnd = null;
var iLength = 0;
var iNext = i;
while ( (iNext !== null) && (text.newText.tokens[iNext].link === null) ) {
iEnd = iNext;
iLength ++;
if (iEnd == newEnd) {
break;
}
 
iNext = text.newText.tokens[iNext].next;
// Next token up
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
}
 
/**
// determine the limits of of the unresolved old sequence
* Connect adjacent identical tokens downwards from text start.
var jStart = j;
* Treat boundary as connected, stop after first connected token.
var jEnd = null;
*/
var jLength = 0;
 
var jNext = j;
// Only for full text diff
while ( (jNext !== null) && (text.oldText.tokens[jNext].link === null) ) {
if ( recursionLevel === 0 && repeating === false ) {
jEnd = jNext;
 
jLength ++;
if// (jEndFrom == oldEnd) {start
var i = this.newText.first;
break;
var j = this.oldText.first;
}
var iMatch = null;
jNext = text.oldText.tokens[jNext].next;
var jMatch = null;
 
// Cycle through old text tokens down
// Connect identical tokens, stop after first connected token
while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
if ( iMatch !== null ) {
bordersDownNext.push( [iMatch, jMatch] );
}
 
// From end
// recursively diff the unresolved sequence
i = this.newText.last;
if ( (iLength > 0) && (jLength > 0) ) {
j = this.oldText.last;
if ( (iLength > 1) || (jLength > 1) ) {
iMatch = null;
if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) {
jMatch = null;
wDiff.CalculateDiff(text, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
 
}
// Cycle through old text tokens up
}
// Connect identical tokens, stop after first connected token
while (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
if ( iMatch !== null ) {
bordersUpNext.push( [iMatch, jMatch] );
}
i = iEnd;
}
 
// Save updated linked region borders to object
// next list element
if (i recursionLevel === 0 && repeating === false newEnd) {
this.bordersDown = bordersDownNext;
break;
this.bordersUp = bordersUpNext;
}
i = text.newText.tokens[i].next;
}
 
// Merge local updated linked region borders into object
//
else {
// recursively diff still unresolved regions upwards
this.bordersDown = this.bordersDown.concat( bordersDownNext );
//
this.bordersUp = this.bordersUp.concat( bordersUpNext );
}
 
// cycle trough new text tokens list
var i = newEnd;
var j = oldEnd;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
 
/**
// get j from next matched tokens
* Repeat once with empty symbol table to link hidden unresolved common tokens in cross-overs.
var iPrev = text.newText.tokens[i].next;
* ("and" in "and this a and b that" -> "and this a and b that")
if (iPrev !== null) {
*/
var jPrev = text.newText.tokens[iPrev].link;
 
if (jPrev !== null) {
if ( repeating === false && this.config.repeatedDiff === true ) {
j = text.oldText.tokens[jPrev].prev;
var repeat = true;
}
this.calculateDiff( level, recurse, repeat, newStart, oldStart, up, recursionLevel );
}
 
/**
// check for the start of an unresolved sequence
* Refine by recursively diffing not linked regions with new symbol table.
if ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) ) {
* At word and character level only.
* Helps against gaps caused by addition of common tokens around sequences of common tokens.
*/
 
if (
// determine the limits of of the unresolved new sequence
var iStartrecurse === true null;&&
this.config['recursiveDiff'] === true &&
var iEnd = i;
recursionLevel < this.config.recursionMax
var iLength = 0;
var) iNext = i;{
while ( (iNext !== null) && (text.newText.tokens[iNext].link === null) ) {
iStart = iNext;
iLength ++;
if (iStart == newStart) {
break;
}
iNext = text.newText.tokens[iNext].prev;
}
 
/**
// determine the limits of of the unresolved old sequence
* Recursively diff gap downwards.
var jStart = null;
var jEnd = j;*/
 
var jLength = 0;
// Cycle through list of linked region borders
var jNext = j;
var bordersLength = bordersDownNext.length;
while ( (jNext !== null) && (text.oldText.tokens[jNext].link === null) ) {
for ( match = 0; match < bordersLength; match ++ ) {
jStart = jNext;
var i = bordersDownNext[match][0];
jLength ++;
var j = bordersDownNext[match][1];
if (jStart == oldStart) {
 
break;
// Next token down
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
 
// Start recursion at first gap token pair
if (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
var repeat = false;
var dirUp = false;
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
}
jNext = text.oldText.tokens[jNext].prev;
}
 
/**
// recursively diff the unresolved sequence
* Recursively diff gap upwards.
if ( (iLength > 0) && (jLength > 0) ) {
*/
if ( (iLength > 1) || (jLength > 1) ) {
 
if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) {
// Cycle through list of linked region borders
wDiff.CalculateDiff(text, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
var bordersLength = bordersUpNext.length;
}
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersUpNext[match][0];
var j = bordersUpNext[match][1];
 
// Next token up
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
 
// Start recursion at first gap token pair
if (
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
var repeat = false;
var dirUp = true;
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
}
}
i = iStart;
}
}
 
// nextStop list elementtimers
if ( this.config.timer === true && repeating === false ) {
if (i == newStart) {
if ( this.recursionTimer[recursionLevel] === undefined ) {
break;
this.recursionTimer[recursionLevel] = 0;
}
this.recursionTimer[recursionLevel] += this.timeEnd( level + recursionLevel, true );
i = text.newText.tokens[i].prev;
}
if ( this.config.timer === true && repeating === false && recursionLevel === 0 ) {
this.timeRecursionEnd( level );
this.timeEnd( level );
}
}
return;
};
 
return;
};
 
// wDiff.DetectBlocks: extract block data for inserted, deleted, or moved blocks from diff data in text object
// input:
// text: object containing text tokens list
// blocks: empty array for block data
// groups: empty array for group data
// changes: text, blocks, groups
// called from: wDiff.Diff()
// scheme of blocks, sections, and groups (old block numbers):
// old: 1 2 3D4 5E6 7 8 9 10 11
// | ‾/-/_ X | >|< |
// new: 1 I 3D4 2 E6 5 N 7 10 9 8 11
// section: 0 0 0 1 1 2 2 2
// group: 0 10 111 2 33 4 11 5 6 7 8 9
// fixed: + +++ - ++ - + + - - +
// type: = + =-= = -= = + = = = = =
 
/**
wDiff.DetectBlocks = function (text, blocks, groups) {
* Main method for processing raw diff data, extracting deleted, inserted, and moved blocks.
*
* Scheme of blocks, sections, and groups (old block numbers):
* Old: 1 2 3D4 5E6 7 8 9 10 11
* | ‾/-/_ X | >|< |
* New: 1 I 3D4 2 E6 5 N 7 10 9 8 11
* Section: 0 0 0 1 1 2 2 2
* Group: 0 10 111 2 33 4 11 5 6 7 8 9
* Fixed: . +++ - ++ - . . - - +
* Type: = . =-= = -= = . = = = = =
*
* @param[out] array groups Groups table object
* @param[out] array blocks Blocks table object
* @param[in/out] WikEdDiffText newText, oldText Text object tokens list
*/
this.detectBlocks = function () {
 
// Debug log
// WED('text.oldText', wDiff.DebugText(text.oldText));
if ( this.config.debug === true ) {
// WED('text.newText', wDiff.DebugText(text.newText));
this.oldText.debugText( 'Old text' );
this.newText.debugText( 'New text' );
}
 
// collectCollect identical corresponding ('same=') blocks from old text and sort by new text
this.getSameBlocks();
wDiff.GetSameBlocks(text, blocks);
 
// collectCollect independent block sections (with no old/newblock move crosses outside section)a for per-section determination of non-moving (fixed) groups
this.getSections();
var sections = [];
wDiff.GetSections(blocks, sections);
 
// findFind groups of continuous old text blocks
this.getGroups();
wDiff.GetGroups(blocks, groups);
 
// Set longest sequence of increasing groups in sections as fixed (not moved)
// convert groups to insertions/deletions if maximal block length is too short
this.setFixed();
if ( (wDiff.blockMinLength > 0) && (wDiff.UnlinkBlocks(text, blocks, groups) === true) ) {
 
// repeatConvert from start after conversiongroups to insertions/deletions if maximum block length is too short
// Only for more complex texts that actually have blocks of minimum block length
wDiff.GetSameBlocks(text, blocks);
var unlinkCount = 0;
wDiff.GetSections(blocks, sections);
if (
wDiff.GetGroups(blocks, groups);
this.config.unlinkBlocks === true &&
}
this.config.blockMinLength > 0 &&
this.maxWords >= this.config.blockMinLength
) {
if ( this.config.timer === true ) {
this.time( 'total unlinking' );
}
 
// Repeat as long as unlinking is possible
// set longest sequence of increasing groups in sections as fixed (not moved)
var unlinked = true;
wDiff.SetFixed(blocks, groups, sections);
while ( unlinked === true && unlinkCount < this.config.unlinkMax ) {
 
// Convert '=' to '+'/'-' pairs
// collect deletion ('del') blocks from old text
unlinked = this.unlinkBlocks();
wDiff.GetDelBlocks(text, blocks);
 
// Start over after conversion
// position 'del' blocks into new text order
if ( unlinked === true ) {
wDiff.PositionDelBlocks(blocks);
unlinkCount ++;
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
 
// collectRepeat insertionblock ('ins') blocksdetection from new textstart
this.maxWords = 0;
wDiff.GetInsBlocks(text, blocks);
this.getSameBlocks();
this.getSections();
this.getGroups();
this.setFixed();
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'total unlinking' );
}
}
 
// sortCollect deletion ('-') blocks byfrom newold text token number and update groups
this.getDelBlocks();
wDiff.SortBlocks(blocks, groups);
 
// Position '-' blocks into new text order
// set group numbers of 'ins' and 'del' blocks
this.positionDelBlocks();
wDiff.SetInsDelGroups(blocks, groups);
 
// Collect insertion ('+') blocks from new text
// mark original positions of moved groups
this.getInsBlocks();
wDiff.MarkMoved(groups);
 
// Set group numbers of '+' blocks
// set moved block colors
this.setInsGroups();
wDiff.ColorMoved(groups);
 
// Mark original positions of moved groups
// WED('Groups', wDiff.DebugGroups(groups));
this.insertMarks();
// WED('Blocks', wDiff.DebugBlocks(blocks));
 
// Debug log
return;
if ( this.config.timer === true || this.config.debug === true ) {
};
console.log( 'Unlink count: ', unlinkCount );
}
if ( this.config.debug === true ) {
this.debugGroups( 'Groups' );
this.debugBlocks( 'Blocks' );
}
return;
};
 
 
/**
// wDiff.GetSameBlocks: collect identical corresponding ('same') blocks from old text and sort by new text
* Collect identical corresponding matching ('=') blocks from old text and sort by new text.
// called from: DetectBlocks()
*
// changes: creates blocks
* @param[in] WikEdDiffText newText, oldText Text objects
* @param[in/out] array blocks Blocks table object
*/
this.getSameBlocks = function () {
 
if ( this.config.timer === true ) {
wDiff.GetSameBlocks = function (text, blocks) {
this.time( 'getSameBlocks' );
}
 
// clear var blocks array= this.blocks;
blocks.splice(0);
 
// Clear blocks array
// cycle through old text to find matched (linked) blocks
blocks.splice( 0 );
var j = text.oldText.first;
var i = null;
while (j !== null) {
 
// Cycle through old text to find connected (linked, matched) blocks
// skip 'del' blocks
whilevar ( (j !== null) && (textthis.oldText.tokens[j].link === null) ) {first;
var i = null;
j = text.oldText.tokens[j].next;
while ( j !== null ) {
}
 
// getSkip 'same-' blockblocks
if while ( j !== null && this.oldText.tokens[j].link === null ) {
i j = textthis.oldText.tokens[j].linknext;
}
var iStart = i;
var jStart = j;
 
// detectGet matching blocks ('same=') block
varif count( j !== null ) 0;{
i = this.oldText.tokens[j].link;
var chars = 0;
var stringiStart = ''i;
var jStart = j;
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
 
var token = text.oldText.tokens[j].token;
// Detect matching blocks ('=')
count ++;
charsvar count += token.length0;
stringvar unique += tokenfalse;
var text = '';
i = text.newText.tokens[i].next;
while ( i !== null && j !== null && textthis.oldText.tokens[j].next;link === i ) {
text += this.oldText.tokens[j].token;
count ++;
if ( this.newText.tokens[i].unique === true ) {
unique = true;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
 
// Save old text '=' block
blocks.push( {
oldBlock: blocks.length,
newBlock: null,
oldNumber: this.oldText.tokens[jStart].number,
newNumber: this.newText.tokens[iStart].number,
oldStart: jStart,
count: count,
unique: unique,
words: this.wordCount( text ),
chars: text.length,
type: '=',
section: null,
group: null,
fixed: null,
moved: null,
text: text
} );
}
}
 
// saveSort oldblocks by new text 'same'token blocknumber
blocks.pushsort( function( a, b ) {
return a.newNumber - b.newNumber;
oldBlock: blocks.length,
} );
newBlock: null,
 
oldNumber: text.oldText.tokens[jStart].number,
// Number blocks in new text order
newNumber: text.newText.tokens[iStart].number,
var blocksLength = blocks.length;
oldStart: jStart,
for ( var block = 0; block < blocksLength; block ++ ) {
count: count,
blocks[block].newBlock = block;
chars: chars,
type: 'same',
section: null,
group: null,
fixed: null,
string: string
});
}
}
 
if ( this.config.timer === true ) {
// sort blocks by new text token number
this.timeEnd( 'getSameBlocks' );
blocks.sort(function(a, b) {
}
return a.newNumber - b.newNumber;
return;
});
};
 
// number blocks in new text order
for (var block = 0; block < blocks.length; block ++) {
blocks[block].newBlock = block;
}
return;
};
 
/**
* Collect independent block sections with no block move crosses
* outside a section for per-section determination of non-moving fixed groups.
*
* @param[out] array sections Sections table object
* @param[in/out] array blocks Blocks table object, section property
*/
this.getSections = function () {
 
if ( this.config.timer === true ) {
// wDiff.GetSections: collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
this.time( 'getSections' );
// called from: DetectBlocks()
}
// changes: creates sections, blocks[].section
 
var blocks = this.blocks;
wDiff.GetSections = function (blocks, sections) {
var sections = this.sections;
 
// clearClear sections array
sections.splice( 0 );
 
// cycleCycle through blocks
for ( var blockblocksLength = 0; block < blocks.length; block ++) {
for ( var block = 0; block < blocksLength; block ++ ) {
 
var sectionStart = block;
var sectionEnd = block;
 
var oldMax = blocks[sectionStart].oldNumber;
var sectionOldMax = oldMax;
 
// checkCheck right
for ( var j = sectionStart + 1; j < blocks.lengthblocksLength; j ++ ) {
 
// checkCheck for crossing over to the left
if ( blocks[j].oldNumber > oldMax ) {
oldMax = blocks[j].oldNumber;
}
else if ( blocks[j].oldNumber < sectionOldMax ) {
sectionEnd = j;
sectionOldMax = oldMax;
}
}
 
else if (blocks[j].oldNumber < sectionOldMax) {
// Save crossing sections
sectionEnd = j;
if ( sectionEnd > sectionStart ) {
sectionOldMax = oldMax;
 
// Save section to block
for ( var i = sectionStart; i <= sectionEnd; i ++ ) {
blocks[i].section = sections.length;
}
 
// Save section
sections.push( {
blockStart: sectionStart,
blockEnd: sectionEnd
} );
block = sectionEnd;
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getSections' );
}
return;
};
 
// save crossing sections
if (sectionEnd > sectionStart) {
 
/**
// save section to block
* Find groups of continuous old text blocks.
for (var i = sectionStart; i <= sectionEnd; i ++) {
*
blocks[i].section = sections.length;
* @param[out] array groups Groups table object
}
* @param[in/out] array blocks Blocks table object, group property
*/
this.getGroups = function () {
 
if ( this.config.timer === true ) {
// save section
this.time( 'getGroups' );
sections.push({
blockStart: sectionStart,
blockEnd: sectionEnd,
deleted: false
});
block = sectionEnd;
}
}
return;
};
 
var blocks = this.blocks;
var groups = this.groups;
 
// Clear groups array
// wDiff.GetGroups: find groups of continuous old text blocks
groups.splice( 0 );
// called from: DetectBlocks()
// changes: creates groups, blocks[].group
 
// Cycle through blocks
wDiff.GetGroups = function (blocks, groups) {
var blocksLength = blocks.length;
for ( var block = 0; block < blocksLength; block ++ ) {
var groupStart = block;
var groupEnd = block;
var oldBlock = blocks[groupStart].oldBlock;
 
// Get word and char count of block
// clear groups array
var words = this.wordCount( blocks[block].text );
groups.splice(0);
var maxWords = words;
var unique = blocks[block].unique;
var chars = blocks[block].chars;
 
// Check right
// cycle through blocks
for ( var blocki = 0groupEnd + 1; blocki < blocks.lengthblocksLength; blocki ++ ) {
 
if (blocks[block].deleted === true) {
// Check for crossing over to the left
continue;
if ( blocks[i].oldBlock !== oldBlock + 1 ) {
break;
}
oldBlock = blocks[i].oldBlock;
 
// Get word and char count of block
if ( blocks[i].words > maxWords ) {
maxWords = blocks[i].words;
}
if ( blocks[i].unique === true ) {
unique = true;
}
words += blocks[i].words;
chars += blocks[i].chars;
groupEnd = i;
}
 
// Save crossing group
if ( groupEnd >= groupStart ) {
 
// Set groups outside sections as fixed
var fixed = false;
if ( blocks[groupStart].section === null ) {
fixed = true;
}
 
// Save group to block
for ( var i = groupStart; i <= groupEnd; i ++ ) {
blocks[i].group = groups.length;
blocks[i].fixed = fixed;
}
 
// Save group
groups.push( {
oldNumber: blocks[groupStart].oldNumber,
blockStart: groupStart,
blockEnd: groupEnd,
unique: unique,
maxWords: maxWords,
words: words,
chars: chars,
fixed: fixed,
movedFrom: null,
color: null
} );
block = groupEnd;
 
// Set global word count of longest linked block
if ( maxWords > this.maxWords ) {
this.maxWords = maxWords;
}
}
}
if ( this.config.timer === true ) {
var groupStart = block;
this.timeEnd( 'getGroups' );
var groupEnd = block;
}
var oldBlock = blocks[groupStart].oldBlock;
return;
};
 
// get word and char count of block
var words = wDiff.WordCount(blocks[block].string);
var maxWords = words;
var chars = blocks[block].chars;
 
/**
// check right
* Set longest sequence of increasing groups in sections as fixed (not moved).
for (var i = groupEnd + 1; i < blocks.length; i ++) {
*
* @param[in] array sections Sections table object
* @param[in/out] array groups Groups table object, fixed property
* @param[in/out] array blocks Blocks table object, fixed property
*/
this.setFixed = function () {
 
if ( this.config.timer === true ) {
// check for crossing over to the left
this.time( 'setFixed' );
if (blocks[i].oldBlock != oldBlock + 1) {
}
break;
 
var blocks = this.blocks;
var groups = this.groups;
var sections = this.sections;
 
// Cycle through sections
var sectionsLength = sections.length;
for ( var section = 0; section < sectionsLength; section ++ ) {
var blockStart = sections[section].blockStart;
var blockEnd = sections[section].blockEnd;
 
var groupStart = blocks[blockStart].group;
var groupEnd = blocks[blockEnd].group;
 
// Recusively find path of groups in increasing old group order with longest char length
var cache = [];
var maxChars = 0;
var maxPath = null;
 
// Start at each group of section
for ( var i = groupStart; i <= groupEnd; i ++ ) {
var pathObj = this.findMaxPath( i, groupEnd, cache );
if ( pathObj.chars > maxChars ) {
maxPath = pathObj.path;
maxChars = pathObj.chars;
}
}
oldBlock = blocks[i].oldBlock;
 
// Mark fixed groups
// get word and char count of block
var blockWordsmaxPathLength = wDiffmaxPath.WordCount(blocks[i].string)length;
for ( var i = 0; i < maxPathLength; i ++ ) {
if (blockWords > maxWords) {
maxWordsvar group = blockWordsmaxPath[i];
groups[group].fixed = true;
 
// Mark fixed blocks
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
blocks[block].fixed = true;
}
}
words += blockWords;
chars += blocks[i].chars;
groupEnd = i;
}
if ( this.config.timer === true ) {
this.timeEnd( 'setFixed' );
}
return;
};
 
// save crossing group
if (groupEnd >= groupStart) {
 
/**
// set groups outside sections as fixed
* Recusively find path of groups in increasing old group order with longest char length.
var fixed = false;
*
if (blocks[groupStart].section === null) {
* @param int start Path start group
fixed = true;
* @param int groupEnd Path last group
* @param array cache Cache object, contains returnObj for start
* @return array returnObj Contains path and char length
*/
this.findMaxPath = function ( start, groupEnd, cache ) {
 
var groups = this.groups;
 
// Find longest sub-path
var maxChars = 0;
var oldNumber = groups[start].oldNumber;
var returnObj = { path: [], chars: 0};
for ( var i = start + 1; i <= groupEnd; i ++ ) {
 
// Only in increasing old group order
if ( groups[i].oldNumber < oldNumber ) {
continue;
}
 
// Get longest sub-path from cache (deep copy)
// save group to block
var pathObj;
for (var i = groupStart; i <= groupEnd; i ++) {
if ( cache[i] !== undefined ) {
blocks[i].group = groups.length;
pathObj = { path: cache[i].path.slice(), chars: cache[i].chars };
blocks[i].fixed = fixed;
}
 
// Get longest sub-path by recursion
// save group
groups.push(else {
pathObj = this.findMaxPath( i, groupEnd, cache );
oldNumber: blocks[groupStart].oldNumber,
}
blockStart: groupStart,
 
blockEnd: groupEnd,
// Select longest sub-path
maxWords: maxWords,
if ( pathObj.chars > maxChars ) {
words: words,
chars: maxChars = pathObj.chars,;
fixed:returnObj = fixed,pathObj;
}
moved: [],
movedFrom: null,
color: null,
diff: ''
});
block = groupEnd;
}
}
return;
};
 
// Add current start to path
returnObj.path.unshift( start );
returnObj.chars += groups[start].chars;
 
// Save path to cache (deep copy)
if ( cache[start] === undefined ) {
cache[start] = { path: returnObj.path.slice(), chars: returnObj.chars };
}
 
return returnObj;
};
 
// wDiff.UnlinkBlocks: remove 'same' blocks in groups of continuous old text blocks if too short
// called from: DetectBlocks()
// changes: text.newText/oldText[].link
// returns: true if text tokens were unlinked
 
/**
wDiff.UnlinkBlocks = function (text, blocks, groups) {
* Convert matching '=' blocks in groups into insertion/deletion ('+'/'-') pairs
* if too short and too common.
* Prevents fragmentated diffs for very different versions.
*
* @param[in] array blocks Blocks table object
* @param[in/out] WikEdDiffText newText, oldText Text object, linked property
* @param[in/out] array groups Groups table object
* @return bool True if text tokens were unlinked
*/
this.unlinkBlocks = function () {
 
var unlinkedblocks = falsethis.blocks;
var groups = this.groups;
 
// cycleCycle through groups
var unlinked = false;
for (var group = 0; group < groups.length; group ++) {
var groupsLength = groups.length;
if ( (groups[group].maxWords < wDiff.blockMinLength) && (groups[group].fixed === false) ) {
for ( var group = 0; group < groupsLength; group ++ ) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
 
// Unlink whole group if no block is at least blockMinLength words long and unique
// cycle through blocks
if ( groups[group].maxWords < this.config.blockMinLength && groups[group].unique === false ) {
for (var block = blockStart; block <= blockEnd; block ++) {
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === '=' ) {
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
 
// cycleOtherwise throughunlink oldblock textflanks
else {
var j = blocks[block].oldStart;
for (var count = 0; count < blocks[block].count; count ++) {
 
// unlinkUnlink tokensblocks from start
for ( var block = blockStart; block <= blockEnd; block ++ ) {
text.newText.tokens[ text.oldText.tokens[j].link ].link = null;
text.oldText.tokensif ( blocks[jblock].linktype === '=' ) null;{
 
j = text.oldText.tokens[j].next;
// Stop unlinking if more than one word or a unique word
if ( blocks[block].words > 1 || blocks[block].unique === true ) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
blockStart = block;
}
}
 
// Unlink blocks from end
for ( var block = blockEnd; block > blockStart; block -- ) {
if ( blocks[block].type === '=' ) {
 
// Stop unlinking if more than one word or a unique word
if (
blocks[block].words > 1 ||
( blocks[block].words === 1 && blocks[block].unique === true )
) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
unlinked = true;
}
}
return unlinked;
}
};
return unlinked;
};
 
 
/**
// wDiff.SetFixed: set longest sequence of increasing groups in sections as fixed (not moved)
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs.
// called from: DetectBlocks()
*
// calls: wDiff.FindMaxPath()
* @param[in] array blocks Blocks table object
// changes: groups[].fixed, blocks[].fixed
* @param[out] WikEdDiffText newText, oldText Text objects, link property
*/
this.unlinkSingleBlock = function ( block ) {
 
// Cycle through old text
wDiff.SetFixed = function (blocks, groups, sections) {
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
 
// Unlink tokens
// cycle through sections
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
for (var section = 0; section < sections.length; section ++) {
this.oldText.tokens[j].link = null;
var blockStart = sections[section].blockStart;
j = this.oldText.tokens[j].next;
var blockEnd = sections[section].blockEnd;
}
return;
};
 
var groupStart = blocks[blockStart].group;
var groupEnd = blocks[blockEnd].group;
 
/**
// recusively find path of groups in increasing old group order with longest char length
* Collect deletion ('-') blocks from old text.
*
* @param[in] WikEdDiffText oldText Old Text object
* @param[out] array blocks Blocks table object
*/
this.getDelBlocks = function () {
 
if ( this.config.timer === true ) {
// start at each group of section
this.time( 'getDelBlocks' );
var cache = [];
var maxChars = 0;
var maxPath = null;
for (var i = groupStart; i <= groupEnd; i ++) {
var pathObj = wDiff.FindMaxPath(i, [], 0, cache, groups, groupEnd);
if (pathObj.chars > maxChars) {
maxPath = pathObj.path;
maxChars = pathObj.chars;
}
}
 
var blocks = this.blocks;
// mark fixed groups
for (var i = 0; i < maxPath.length; i ++) {
var group = maxPath[i];
groups[group].fixed = true;
 
// Cycle through old text to find connected (linked, matched) blocks
// mark fixed blocks
var j = this.oldText.first;
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
var i = null;
blocks[block].fixed = true;
while ( j !== null ) {
 
// Collect '-' blocks
var oldStart = j;
var count = 0;
var text = '';
while ( j !== null && this.oldText.tokens[j].link === null ) {
count ++;
text += this.oldText.tokens[j].token;
j = this.oldText.tokens[j].next;
}
 
// Save old text '-' block
if ( count !== 0 ) {
blocks.push( {
oldBlock: null,
newBlock: null,
oldNumber: this.oldText.tokens[oldStart].number,
newNumber: null,
oldStart: oldStart,
count: count,
unique: false,
words: null,
chars: text.length,
type: '-',
section: null,
group: null,
fixed: null,
moved: null,
text: text
} );
}
 
// Skip '=' blocks
if ( j !== null ) {
i = this.oldText.tokens[j].link;
while ( i !== null && j !== null && this.oldText.tokens[j].link === i ) {
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
}
if ( this.config.timer === true ) {
}
this.timeEnd( 'getDelBlocks' );
return;
};
return;
};
 
 
/**
// wDiff.FindMaxPath: recusively find path of groups in increasing old group order with longest char length
* Position deletion '-' blocks into new text order.
// input: start, path start group; path, array of path groups; chars, char count of path; cache, cached sub-path lengths; groups, groups, group object; groupEnd, last group
* Deletion blocks move with fixed reference:
// returns: returnObj, contains path and length
* Old: 1 D 2 1 D 2
// called from: wDiff.SetFixed()
* / \ / \ \
// calls: itself recursively
* New: 1 D 2 1 D 2
* Fixed: * *
* newNumber: 1 1 2 2
*
* Marks '|' and deletions '-' get newNumber of reference block
* and are sorted around it by old text number.
*
* @param[in/out] array blocks Blocks table, newNumber, section, group, and fixed properties
*
*/
this.positionDelBlocks = function () {
 
if ( this.config.timer === true ) {
wDiff.FindMaxPath = function (start, path, chars, cache, groups, groupEnd) {
this.time( 'positionDelBlocks' );
}
 
var blocks = this.blocks;
// add current path point
var pathLocalgroups = paththis.slice()groups;
pathLocal.push(start);
chars = chars + groups[start].chars;
 
// Sort shallow copy of blocks by oldNumber
// last group, terminate recursion
var blocksOld = blocks.slice();
var returnObj = { path: pathLocal, chars: chars };
blocksOld.sort( function( a, b ) {
if (start == groupEnd) {
return returnObja.oldNumber - b.oldNumber;
} );
}
 
// Cycle through blocks in old text order
// find longest sub-path
var blocksOldLength = blocksOld.length;
var maxChars = 0;
for ( var block = 0; block < blocksOldLength; block ++ ) {
var oldNumber = groups[start].oldNumber;
var delBlock = blocksOld[block];
for (var i = start + 1; i <= groupEnd; i ++) {
 
// '-' block only
// only in increasing old group order
if (groups[i] delBlock.oldNumbertype !== <'-' oldNumber) {
continue;
}
 
// Find fixed '=' reference block from original block position to position '-' block
// get longest sub-path from cache
// Similar to position marks '|' code
if (cache[start] !== undefined) {
returnObj = cache[start];
}
 
// getGet longestold sub-pathtext byprev recursionblock
var prevBlockNumber = null;
else {
var prevBlock = null;
var pathObj = wDiff.FindMaxPath(i, pathLocal, chars, cache, groups, groupEnd);
if ( block > 0 ) {
prevBlockNumber = blocksOld[block - 1].newBlock;
prevBlock = blocks[prevBlockNumber];
}
 
// selectGet longestold sub-pathtext next block
var nextBlockNumber = null;
if (pathObj.chars > maxChars) {
returnObjvar nextBlock = pathObjnull;
if ( block < blocksOld.length - 1 ) {
nextBlockNumber = blocksOld[block + 1].newBlock;
nextBlock = blocks[nextBlockNumber];
}
}
}
 
// Move after prev block if fixed
// save longest path to cache
var refBlock = null;
if (cache[i] === undefined) {
if ( prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
cache[start] = returnObj;
refBlock = prevBlock;
}
}
return returnObj;
};
 
// Move before next block if fixed
else if ( nextBlock !== null && nextBlock.type === '=' && nextBlock.fixed === true ) {
refBlock = nextBlock;
}
 
// Move after prev block if not start of group
// wDiff.GetDelBlocks: collect deletion ('del') blocks from old text
else if (
// called from: DetectBlocks()
prevBlock !== null &&
// changes: blocks
prevBlock.type === '=' &&
prevBlockNumber !== groups[ prevBlock.group ].blockEnd
) {
refBlock = prevBlock;
}
 
// Move before next block if not start of group
wDiff.GetDelBlocks = function (text, blocks) {
else if (
nextBlock !== null &&
nextBlock.type === '=' &&
nextBlockNumber !== groups[ nextBlock.group ].blockStart
) {
refBlock = nextBlock;
}
 
// Move after closest previous fixed block
// cycle through old text to find matched (linked) blocks
else {
var j = text.oldText.first;
for ( var fixed = block; fixed >= 0; fixed -- ) {
var i = null;
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
while (j !== null) {
refBlock = blocksOld[fixed];
break;
}
}
}
 
// collectMove 'del'before blocksfirst block
if ( refBlock === null ) {
var oldStart = j;
var count delBlock.newNumber = 0 -1;
}
var string = '';
 
while ( (j !== null) && (text.oldText.tokens[j].link === null) ) {
// Update '-' block data
count ++;
else {
string += text.oldText.tokens[j].token;
delBlock.newNumber = refBlock.newNumber;
j = text.oldText.tokens[j].next;
delBlock.section = refBlock.section;
delBlock.group = refBlock.group;
delBlock.fixed = refBlock.fixed;
}
}
 
// Sort '-' blocks in and update groups
// save old text 'del' block
this.sortBlocks();
if (count !== 0) {
 
blocks.push({
if ( this.config.timer === true ) {
oldBlock: null,
this.timeEnd( 'positionDelBlocks' );
newBlock: null,
oldNumber: text.oldText.tokens[oldStart].number,
newNumber: null,
oldStart: oldStart,
count: count,
chars: null,
type: 'del',
section: null,
group: null,
fixed: null,
string: string
});
}
return;
};
 
 
// skip 'same' block
/**
if (j !== null) {
* Collect insertion ('+') blocks from new text.
i = text.oldText.tokens[j].link;
*
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
* @param[in] WikEdDiffText newText New Text object
i = text.newText.tokens[i].next;
* @param[out] array blocks Blocks table object
j = text.oldText.tokens[j].next;
} */
this.getInsBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'getInsBlocks' );
}
}
return;
};
 
var blocks = this.blocks;
 
// Cycle through new text to find insertion blocks
// wDiff.PositionDelBlocks: position 'del' blocks into new text order
var i = this.newText.first;
// called from: DetectBlocks()
while ( i !== null ) {
// changes: blocks[].section/group/fixed/newNumber
//
// deletion blocks move with fixed neighbor (new number +/- 0.1):
// old: 1 D 2 1 D 2
// / / \ / \ \
// new: 1 D 2 1 D 2
// fixed: * *
// new number: 1 1.1 1.9 2
 
// Jump over linked (matched) block
wDiff.PositionDelBlocks = function (blocks) {
while ( i !== null && this.newText.tokens[i].link !== null ) {
i = this.newText.tokens[i].next;
}
 
// sortDetect shallow copy ofinsertion blocks by oldNumber('+')
if ( i !== null ) {
var blocksOld = blocks.slice();
var iStart = i;
blocksOld.sort(function(a, b) {
var count = 0;
return a.oldNumber - b.oldNumber;
var text = '';
});
while ( i !== null && this.newText.tokens[i].link === null ) {
count ++;
text += this.newText.tokens[i].token;
i = this.newText.tokens[i].next;
}
 
// Save new text '+' block
// cycle through 'del' blocks in old text order
blocks.push( {
for (var blockOld = 0; blockOld < blocksOld.length; blockOld ++) {
oldBlock: null,
var delBlock = blocksOld[blockOld];
newBlock: null,
if (delBlock.type != 'del') {
oldNumber: null,
continue;
newNumber: this.newText.tokens[iStart].number,
oldStart: null,
count: count,
unique: false,
words: null,
chars: text.length,
type: '+',
section: null,
group: null,
fixed: null,
moved: null,
text: text
} );
}
}
 
// getSort old'+' textblocks previn blockand update groups
this.sortBlocks();
var prevBlock;
 
if (blockOld > 0) {
if ( this.config.timer === true ) {
prevBlock = blocks[ blocksOld[blockOld - 1].newBlock ];
this.timeEnd( 'getInsBlocks' );
}
return;
};
 
 
// get old text next block
/**
var nextBlock;
* Sort blocks by new text token number and update groups.
if (blockOld < blocksOld.length - 1) {
*
nextBlock = blocks[ blocksOld[blockOld + 1].newBlock ];
* @param[in/out] array groups Groups table object
* @param[in/out] array blocks Blocks table object
*/
this.sortBlocks = function () {
 
var blocks = this.blocks;
var groups = this.groups;
 
// Sort by newNumber, then by old number
blocks.sort( function( a, b ) {
var comp = a.newNumber - b.newNumber;
if ( comp === 0 ) {
comp = a.oldNumber - b.oldNumber;
}
return comp;
} );
 
// Cycle through blocks and update groups with new block numbers
var group = null;
var blocksLength = blocks.length;
for ( var block = 0; block < blocksLength; block ++ ) {
var blockGroup = blocks[block].group;
if ( blockGroup !== null ) {
if ( blockGroup !== group ) {
group = blocks[block].group;
groups[group].blockStart = block;
groups[group].oldNumber = blocks[block].oldNumber;
}
groups[blockGroup].blockEnd = block;
}
}
return;
};
 
 
// move after prev block if fixed
/**
var neighbor;
* Set group numbers of insertion '+' blocks.
if ( (prevBlock !== undefined) && (prevBlock.fixed === true) ) {
*
neighbor = prevBlock;
* @param[in/out] array groups Groups table object
delBlock.newNumber = neighbor.newNumber + 0.1;
* @param[in/out] array blocks Blocks table object, fixed and group properties
*/
this.setInsGroups = function () {
 
if ( this.config.timer === true ) {
this.time( 'setInsGroups' );
}
 
var blocks = this.blocks;
// move before next block if fixed
var groups = this.groups;
else if ( (nextBlock !== undefined) && (nextBlock.fixed === true) ) {
 
neighbor = nextBlock;
// Set group numbers of '+' blocks inside existing groups
delBlock.newNumber = neighbor.newNumber - 0.1;
var groupsLength = groups.length;
for ( var group = 0; group < groupsLength; group ++ ) {
var fixed = groups[group].fixed;
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
if ( blocks[block].group === null ) {
blocks[block].group = group;
blocks[block].fixed = fixed;
}
}
}
 
// moveAdd afterremaining prev'+' blockblocks ifto existentnew groups
 
else if (prevBlock !== undefined) {
// Cycle through blocks
neighbor = prevBlock;
var blocksLength = blocks.length;
delBlock.newNumber = neighbor.newNumber + 0.1;
for ( var block = 0; block < blocksLength; block ++ ) {
 
// Skip existing groups
if ( blocks[block].group === null ) {
blocks[block].group = groups.length;
 
// Save new single-block group
groups.push( {
oldNumber: blocks[block].oldNumber,
blockStart: block,
blockEnd: block,
unique: blocks[block].unique,
maxWords: blocks[block].words,
words: blocks[block].words,
chars: blocks[block].chars,
fixed: blocks[block].fixed,
movedFrom: null,
color: null
} );
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'setInsGroups' );
}
return;
};
 
 
// move before next block
/**
else if (nextBlock !== undefined) {
* Mark original positions of moved groups.
neighbor = nextBlock;
* Scheme: moved block marks at original positions relative to fixed groups:
delBlock.newNumber = neighbor.newNumber - 0.1;
* Groups: 3 7
* 1 <| | (no next smaller fixed)
* 5 |< |
* |> 5 |
* | 5 <|
* | >| 5
* | |> 9 (no next larger fixed)
* Fixed: * *
*
* Mark direction: groups.movedGroup.blockStart < groups.group.blockStart
* Group side: groups.movedGroup.oldNumber < groups.group.oldNumber
*
* Marks '|' and deletions '-' get newNumber of reference block
* and are sorted around it by old text number.
*
* @param[in/out] array groups Groups table object, movedFrom property
* @param[in/out] array blocks Blocks table object
*/
this.insertMarks = function () {
 
if ( this.config.timer === true ) {
this.time( 'insertMarks' );
}
 
var blocks = this.blocks;
// move before first block
var groups = this.groups;
else {
delBlock.newNumbervar moved = -0.1[];
var color = 1;
 
// Make shallow copy of blocks
var blocksOld = blocks.slice();
 
// Enumerate copy
var blocksOldLength = blocksOld.length;
for ( var i = 0; i < blocksOldLength; i ++ ) {
blocksOld[i].number = i;
}
 
// Sort copy by oldNumber
// update 'del' block with neighbor data
blocksOld.sort( function( a, b ) {
if (neighbor !== undefined) {
var comp = a.oldNumber - b.oldNumber;
delBlock.section = neighbor.section;
if ( comp === 0 ) {
delBlock.group = neighbor.group;
comp = a.newNumber - b.newNumber;
delBlock.fixed = neighbor.fixed;
}
return comp;
} );
 
// Create lookup table: original to sorted
var lookupSorted = [];
for ( var i = 0; i < blocksOldLength; i ++ ) {
lookupSorted[ blocksOld[i].number ] = i;
}
}
return;
};
 
// Cycle through groups (moved group)
var groupsLength = groups.length;
for ( var moved = 0; moved < groupsLength; moved ++ ) {
var movedGroup = groups[moved];
if ( movedGroup.fixed !== false ) {
continue;
}
var movedOldNumber = movedGroup.oldNumber;
 
// Find fixed '=' reference block from original block position to position '|' block
// wDiff.GetInsBlocks: collect insertion ('ins') blocks from new text
// Similar to position deletions '-' code
// called from: DetectBlocks()
// changes: blocks
 
// Get old text prev block
wDiff.GetInsBlocks = function (text, blocks) {
var prevBlock = null;
var block = lookupSorted[ movedGroup.blockStart ];
if ( block > 0 ) {
prevBlock = blocksOld[block - 1];
}
 
// cycleGet through newold text to find insertionnext blocksblock
var inextBlock = text.newText.firstnull;
var block = lookupSorted[ movedGroup.blockEnd ];
while (i !== null) {
if ( block < blocksOld.length - 1 ) {
nextBlock = blocksOld[block + 1];
}
 
// jumpMove overafter linked (matched)prev block if fixed
var refBlock = null;
while ( (i !== null) && (text.newText.tokens[i].link !== null) ) {
if ( prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
i = text.newText.tokens[i].next;
refBlock = prevBlock;
}
}
 
// Move before next block if fixed
// detect insertion blocks ('ins')
else if (i nextBlock !== null && nextBlock.type === '=' && nextBlock.fixed === true ) {
var iStart refBlock = inextBlock;
var count = 0;
var string = '';
while ( (i !== null) && (text.newText.tokens[i].link === null) ) {
count ++;
string += text.newText.tokens[i].token;
i = text.newText.tokens[i].next;
}
 
// saveFind newclosest text 'ins'fixed block to the left
blocks.push(else {
for ( var fixed = lookupSorted[ movedGroup.blockStart ] - 1; fixed >= 0; fixed -- ) {
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
refBlock = blocksOld[fixed];
break;
}
}
}
 
// Get position of new mark block
var newNumber;
var markGroup;
 
// No smaller fixed block, moved right from before first block
if ( refBlock === null ) {
newNumber = -1;
markGroup = groups.length;
 
// Save new single-mark-block group
groups.push( {
oldNumber: 0,
blockStart: blocks.length,
blockEnd: blocks.length,
unique: false,
maxWords: null,
words: null,
chars: 0,
fixed: null,
movedFrom: null,
color: null
} );
}
else {
newNumber = refBlock.newNumber;
markGroup = refBlock.group;
}
 
// Insert '|' block
blocks.push( {
oldBlock: null,
newBlock: null,
oldNumber: nullmovedOldNumber,
newNumber: text.newText.tokens[iStart].numbernewNumber,
oldStart: null,
count: countnull,
charsunique: null,
typewords: 'ins'null,
chars: 0,
type: '|',
section: null,
group: nullmarkGroup,
fixed: nulltrue,
stringmoved: string moved,
text: ''
});
} );
 
// Set group color
movedGroup.color = color;
movedGroup.movedFrom = markGroup;
color ++;
}
}
return;
};
 
// Sort '|' blocks in and update groups
this.sortBlocks();
 
if ( this.config.timer === true ) {
// wDiff.SortBlocks: sort blocks by new text token number and update groups
this.timeEnd( 'insertMarks' );
// called from: DetectBlocks()
}
// changes: blocks
return;
};
 
wDiff.SortBlocks = function (blocks, groups) {
 
/**
// sort by newNumber
* Collect diff fragment list for markup, create abstraction layer for customized diffs.
blocks.sort(function(a, b) {
* Adds the following fagment types:
return a.newNumber - b.newNumber;
* '=', '-', '+' same, deletion, insertion
});
* '<', '>' mark left, mark right
* '(<', '(>', ')' block start and end
* '[', ']' fragment start and end
* '{', '}' container start and end
*
* @param[in] array groups Groups table object
* @param[in] array blocks Blocks table object
* @param[out] array fragments Fragments array, abstraction layer for diff code
*/
this.getDiffFragments = function () {
 
var blocks = this.blocks;
// cycle through blocks and update groups with new block numbers
var groupgroups = nullthis.groups;
var fragments = this.fragments;
for (var block = 0; block < blocks.length; block ++) {
 
var blockGroup = blocks[block].group;
// Make shallow copy of groups and sort by blockStart
if (blockGroup !== null) {
var groupsSort = groups.slice();
if (blockGroup != group) {
groupsSort.sort( function( a, b ) {
group = blocks[block].group;
groups[group]return a.blockStart =- blockb.blockStart;
} );
groups[group].oldNumber = blocks[block].oldNumber;
 
// Cycle through groups
var groupsSortLength = groupsSort.length;
for ( var group = 0; group < groupsSortLength; group ++ ) {
var blockStart = groupsSort[group].blockStart;
var blockEnd = groupsSort[group].blockEnd;
 
// Add moved block start
var color = groupsSort[group].color;
if ( color !== null ) {
var type;
if ( groupsSort[group].movedFrom < blocks[ blockStart ].group ) {
type = '(<';
}
else {
type = '(>';
}
fragments.push( {
text: '',
type: type,
color: color
} );
}
groups[blockGroup].blockEnd = block;
}
}
return;
};
 
// Cycle through blocks
for ( var block = blockStart; block <= blockEnd; block ++ ) {
var type = blocks[block].type;
 
// Add '=' unchanged text and moved block
// wDiff.SetInsDelGroups: set group numbers of 'ins' and 'del' blocks
if ( type === '=' || type === '-' || type === '+' ) {
// called from: DetectBlocks()
fragments.push( {
// changes: groups, blocks[].fixed/group
text: blocks[block].text,
type: type,
color: color
} );
}
 
// Add '<' and '>' marks
wDiff.SetInsDelGroups = function (blocks, groups) {
else if ( type === '|' ) {
var movedGroup = groups[ blocks[block].moved ];
 
// Get mark text
var markText = '';
for (
var movedBlock = movedGroup.blockStart;
movedBlock <= movedGroup.blockEnd;
movedBlock ++
) {
if ( blocks[movedBlock].type === '=' || blocks[movedBlock].type === '-' ) {
markText += blocks[movedBlock].text;
}
}
 
// Get mark direction
var markType;
if ( movedGroup.blockStart < blockStart ) {
markType = '<';
}
else {
markType = '>';
}
 
// Add mark
fragments.push( {
text: markText,
type: markType,
color: movedGroup.color
} );
}
}
 
// Add moved block end
// set group numbers of 'ins' and 'del' blocks inside existing groups
if ( color !== null ) {
for (var group = 0; group < groups.length; group ++) {
fragments.push( {
var fixed = groups[group].fixed;
text: '',
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
type: ' )',
if (blocks[block].group === null) {
color: color
blocks[block].group = group;
} );
blocks[block].fixed = fixed;
}
}
}
 
// Cycle through fragments, join consecutive fragments of same type (i.e. '-' blocks)
// add remaining 'ins' and 'del' blocks to groups
var fragmentsLength = fragments.length;
for ( var fragment = 1; fragment < fragmentsLength; fragment ++ ) {
 
// Check if joinable
// cycle through blocks
if (
for (var block = 0; block < blocks.length; block ++) {
fragments[fragment].type === fragments[fragment - 1].type &&
fragments[fragment].color === fragments[fragment - 1].color &&
fragments[fragment].text !== '' && fragments[fragment - 1].text !== ''
) {
 
// skipJoin existingand groupssplice
fragments[fragment - 1].text += fragments[fragment].text;
if (blocks[block].group === null) {
fragments.splice( fragment, 1 );
blocks[block].group = groups.length;
fragment --;
var fixed = blocks[block].fixed;
}
 
// save group
groups.push({
oldNumber: blocks[block].oldNumber,
blockStart: block,
blockEnd: block,
maxWords: null,
words: null,
chars: null,
fixed: fixed,
moved: [],
movedFrom: null,
color: null,
diff: ''
});
}
}
return;
};
 
// Enclose in containers
fragments.unshift( { text: '', type: '{', color: null }, { text: '', type: '[', color: null } );
fragments.push( { text: '', type: ']', color: null }, { text: '', type: '}', color: null } );
 
return;
// wDiff.MarkMoved: mark original positions of moved groups
};
// called from: DetectBlocks()
// changes: groups[].moved/movedFrom
// moved block marks at original positions relative to fixed groups:
// groups: 3 7
// 1 <| | (no next smaller fixed)
// 5 |< |
// |> 5 |
// | 5 <|
// | >| 5
// | |> 9 (no next larger fixed)
// fixed: * *
// mark direction: groups[movedGroup].blockStart < groups[group].blockStart
// group side: groups[movedGroup].oldNumber < groups[group].oldNumber
 
wDiff.MarkMoved = function (groups) {
 
/**
// cycle through groups (moved group)
* Clip unchanged sections from unmoved block text.
for (var movedGroup = 0; movedGroup < groups.length; movedGroup ++) {
* Adds the following fagment types:
if (groups[movedGroup].fixed !== false) {
* '~', ' ~', '~ ' omission indicators
continue;
* '[', ']', ',' fragment start and end, fragment separator
*
* @param[in/out] array fragments Fragments array, abstraction layer for diff code
*/
this.clipDiffFragments = function () {
 
var fragments = this.fragments;
 
// Skip if only one fragment in containers, no change
if ( fragments.length === 5 ) {
return;
}
 
// Min length for clipping right
var minRight = this.config.clipHeadingRight;
if ( this.config.clipParagraphRightMin < minRight ) {
minRight = this.config.clipParagraphRightMin;
}
if ( this.config.clipLineRightMin < minRight ) {
minRight = this.config.clipLineRightMin;
}
if ( this.config.clipBlankRightMin < minRight ) {
minRight = this.config.clipBlankRightMin;
}
if ( this.config.clipCharsRight < minRight ) {
minRight = this.config.clipCharsRight;
}
 
// Min length for clipping left
var minLeft = this.config.clipHeadingLeft;
if ( this.config.clipParagraphLeftMin < minLeft ) {
minLeft = this.config.clipParagraphLeftMin;
}
if ( this.config.clipLineLeftMin < minLeft ) {
minLeft = this.config.clipLineLeftMin;
}
if ( this.config.clipBlankLeftMin < minLeft ) {
minLeft = this.config.clipBlankLeftMin;
}
if ( this.config.clipCharsLeft < minLeft ) {
minLeft = this.config.clipCharsLeft;
}
var movedOldNumber = groups[movedGroup].oldNumber;
 
// findCycle closestthrough fixed groupsfragments
var nextSmallerNumberfragmentsLength = nullfragments.length;
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
var nextSmallerGroup = null;
var nextLargerNumber = null;
var nextLargerGroup = null;
 
// Skip if not an unmoved and unchanged block
// cycle through groups (original positions)
var type = fragments[fragment].type;
for (var group = 0; group < groups.length; group ++) {
var color = fragments[fragment].color;
if ( (groups[group].fixed !== true) || (group == movedGroup) ) {
if ( type !== '=' || color !== null ) {
continue;
}
 
// findSkip fixedif grouptoo withshort closestfor smaller oldNumberclipping
var oldNumbertext = groupsfragments[groupfragment].oldNumbertext;
var textLength = text.length;
if ( (oldNumber < movedOldNumber) && ( (nextSmallerNumber === null) || (oldNumber > nextSmallerNumber) ) ) {
if ( textLength < minRight && textLength < minLeft ) {
nextSmallerNumber = oldNumber;
continue;
nextSmallerGroup = group;
}
 
// Get line positions including start and end
// find fixed group with closest larger oldNumber
var lines = [];
if ( (oldNumber > movedOldNumber) && ( (nextLargerNumber === null) || (oldNumber < nextLargerNumber) ) ) {
nextLargerNumbervar lastIndex = oldNumbernull;
var regExpMatch;
nextLargerGroup = group;
while ( ( regExpMatch = this.config.regExp.clipLine.exec( text ) ) !== null ) {
lines.push( regExpMatch.index );
lastIndex = this.config.regExp.clipLine.lastIndex;
}
if ( lines[0] !== 0 ) {
lines.unshift( 0 );
}
if ( lastIndex !== textLength ) {
lines.push( textLength );
}
}
 
// Get heading positions
// no larger fixed group, moved right
var movedFromheadings = ''[];
var headingsEnd = [];
if (nextLargerGroup === null) {
while ( ( regExpMatch = this.config.regExp.clipHeading.exec( text ) ) !== null ) {
movedFrom = 'left';
headings.push( regExpMatch.index );
}
headingsEnd.push( regExpMatch.index + regExpMatch[0].length );
}
 
// Get paragraph positions including start and end
// no smaller fixed group, moved right
var paragraphs = [];
else if (nextSmallerGroup === null) {
movedFromvar lastIndex = 'right'null;
while ( ( regExpMatch = this.config.regExp.clipParagraph.exec( text ) ) !== null ) {
}
paragraphs.push( regExpMatch.index );
 
lastIndex = this.config.regExp.clipParagraph.lastIndex;
// group moved from between two closest fixed neighbors, moved left or right depending on char distance
else {
var rightChars = 0;
for (var group = nextSmallerGroup + 1; group < movedGroup; group ++) {
rightChars += groups[group].chars;
}
if ( paragraphs[0] !== 0 ) {
var leftChars = 0;
paragraphs.unshift( 0 );
for (var group = movedGroup + 1; group < nextLargerGroup; group ++) {
leftChars += groups[group].chars;
}
if ( lastIndex !== textLength ) {
 
paragraphs.push( textLength );
// moved right
if (rightChars <= leftChars) {
movedFrom = 'left';
}
 
// movedDetermine ranges to keep on left and right side
var rangeRight = null;
else {
movedFromvar rangeLeft = 'right'null;
var rangeRightType = '';
var rangeLeftType = '';
 
// Find clip pos from left, skip for first non-container block
if ( fragment !== 2 ) {
 
// Maximum lines to search from left
var rangeLeftMax = textLength;
if ( this.config.clipLinesLeftMax < lines.length ) {
rangeLeftMax = lines[this.config.clipLinesLeftMax];
}
 
// Find first heading from left
if ( rangeLeft === null ) {
var headingsLength = headingsEnd.length;
for ( var j = 0; j < headingsLength; j ++ ) {
if ( headingsEnd[j] > this.config.clipHeadingLeft || headingsEnd[j] > rangeLeftMax ) {
break;
}
rangeLeft = headingsEnd[j];
rangeLeftType = 'heading';
break;
}
}
 
// Find first paragraph from left
if ( rangeLeft === null ) {
var paragraphsLength = paragraphs.length;
for ( var j = 0; j < paragraphsLength; j ++ ) {
if (
paragraphs[j] > this.config.clipParagraphLeftMax ||
paragraphs[j] > rangeLeftMax
) {
break;
}
if ( paragraphs[j] > this.config.clipParagraphLeftMin ) {
rangeLeft = paragraphs[j];
rangeLeftType = 'paragraph';
break;
}
}
}
 
// Find first line break from left
if ( rangeLeft === null ) {
var linesLength = lines.length;
for ( var j = 0; j < linesLength; j ++ ) {
if ( lines[j] > this.config.clipLineLeftMax || lines[j] > rangeLeftMax ) {
break;
}
if ( lines[j] > this.config.clipLineLeftMin ) {
rangeLeft = lines[j];
rangeLeftType = 'line';
break;
}
}
}
 
// Find first blank from left
if ( rangeLeft === null ) {
this.config.regExp.clipBlank.lastIndex = this.config.clipBlankLeftMin;
if ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if (
regExpMatch.index < this.config.clipBlankLeftMax &&
regExpMatch.index < rangeLeftMax
) {
rangeLeft = regExpMatch.index;
rangeLeftType = 'blank';
}
}
}
 
// Fixed number of chars from left
if ( rangeLeft === null ) {
if ( this.config.clipCharsLeft < rangeLeftMax ) {
rangeLeft = this.config.clipCharsLeft;
rangeLeftType = 'chars';
}
}
 
// Fixed number of lines from left
if ( rangeLeft === null ) {
rangeLeft = rangeLeftMax;
rangeLeftType = 'fixed';
}
}
}
 
// Find clip pos from right, skip for last non-container block
// check for null-moves
if (movedFrom fragment !== 'left'fragments.length - 3 ) {
 
if (groups[nextSmallerGroup].blockEnd + 1 != groups[movedGroup].blockStart) {
// Maximum lines to search from right
groups[nextSmallerGroup].moved.push(movedGroup);
var rangeRightMin = 0;
groups[movedGroup].movedFrom = nextSmallerGroup;
if ( lines.length >= this.config.clipLinesRightMax ) {
rangeRightMin = lines[lines.length - this.config.clipLinesRightMax];
}
 
// Find last heading from right
if ( rangeRight === null ) {
for ( var j = headings.length - 1; j >= 0; j -- ) {
if (
headings[j] < textLength - this.config.clipHeadingRight ||
headings[j] < rangeRightMin
) {
break;
}
rangeRight = headings[j];
rangeRightType = 'heading';
break;
}
}
 
// Find last paragraph from right
if ( rangeRight === null ) {
for ( var j = paragraphs.length - 1; j >= 0 ; j -- ) {
if (
paragraphs[j] < textLength - this.config.clipParagraphRightMax ||
paragraphs[j] < rangeRightMin
) {
break;
}
if ( paragraphs[j] < textLength - this.config.clipParagraphRightMin ) {
rangeRight = paragraphs[j];
rangeRightType = 'paragraph';
break;
}
}
}
 
// Find last line break from right
if ( rangeRight === null ) {
for ( var j = lines.length - 1; j >= 0; j -- ) {
if (
lines[j] < textLength - this.config.clipLineRightMax ||
lines[j] < rangeRightMin
) {
break;
}
if ( lines[j] < textLength - this.config.clipLineRightMin ) {
rangeRight = lines[j];
rangeRightType = 'line';
break;
}
}
}
 
// Find last blank from right
if ( rangeRight === null ) {
var startPos = textLength - this.config.clipBlankRightMax;
if ( startPos < rangeRightMin ) {
startPos = rangeRightMin;
}
this.config.regExp.clipBlank.lastIndex = startPos;
var lastPos = null;
while ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if ( regExpMatch.index > textLength - this.config.clipBlankRightMin ) {
if ( lastPos !== null ) {
rangeRight = lastPos;
rangeRightType = 'blank';
}
break;
}
lastPos = regExpMatch.index;
}
}
 
// Fixed number of chars from right
if ( rangeRight === null ) {
if ( textLength - this.config.clipCharsRight > rangeRightMin ) {
rangeRight = textLength - this.config.clipCharsRight;
rangeRightType = 'chars';
}
}
 
// Fixed number of lines from right
if ( rangeRight === null ) {
rangeRight = rangeRightMin;
rangeRightType = 'fixed';
}
}
}
else if (movedFrom == 'right') {
if (groups[movedGroup].blockEnd + 1 != groups[nextLargerGroup].blockStart) {
groups[nextLargerGroup].moved.push(movedGroup);
groups[movedGroup].movedFrom = nextLargerGroup;
}
}
}
 
// Check if we skip clipping if ranges are close together
// cycle through groups, sort blocks moved from here by old number
if ( rangeLeft !== null && rangeRight !== null ) {
for (var group = 0; group < groups.length; group ++) {
var moved = groups[group].moved;
if (moved !== null) {
moved.sort(function(a, b) {
return groups[a].oldNumber - groups[b].oldNumber;
});
}
}
return;
};
 
// Skip if overlapping ranges
if ( rangeLeft > rangeRight ) {
continue;
}
 
// Skip if chars too close
// wDiff.ColorMoved: set moved block color numbers
var skipChars = rangeRight - rangeLeft;
// called from: DetectBlocks()
if ( skipChars < this.config.clipSkipChars ) {
// changes: groups[].color
continue;
}
 
// Skip if lines too close
wDiff.ColorMoved = function (groups) {
var skipLines = 0;
var linesLength = lines.length;
for ( var j = 0; j < linesLength; j ++ ) {
if ( lines[j] > rangeRight || skipLines > this.config.clipSkipLines ) {
break;
}
if ( lines[j] > rangeLeft ) {
skipLines ++;
}
}
if ( skipLines < this.config.clipSkipLines ) {
continue;
}
}
 
// Skip if nothing to clip
// cycle through groups
if ( rangeLeft === null && rangeRight === null ) {
var moved = [];
continue;
for (var group = 0; group < groups.length; group ++) {
}
moved = moved.concat(groups[group].moved);
}
 
// Split left text
// sort moved array by old number
var textLeft = null;
moved.sort(function(a, b) {
var omittedLeft = null;
return groups[a].oldNumber - groups[b].oldNumber;
if ( rangeLeft !== null ) {
});
textLeft = text.slice( 0, rangeLeft );
 
// Remove trailing empty lines
// set color
textLeft = textLeft.replace( this.config.regExp.clipTrimNewLinesLeft, '' );
var color = 0;
for (var i = 0; i < moved.length; i ++) {
var movedGroup = moved[i];
if (wDiff.showBlockMoves === true) {
groups[movedGroup].color = color;
color ++;
}
}
return;
};
 
// Get omission indicators, remove trailing blanks
if ( rangeLeftType === 'chars' ) {
omittedLeft = '~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
else if ( rangeLeftType === 'blank' ) {
omittedLeft = ' ~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
}
 
// Split right text
// wDiff.AssembleDiff: process diff data into formatted html text
var textRight = null;
// input: text, object containing text tokens list; blocks, array containing block type; groups, array containing fixed (not moved), color, and moved mark data
var omittedRight = null;
// returns: diff html string
if ( rangeRight !== null ) {
// called from: wDiff.Diff()
textRight = text.slice( rangeRight );
// calls: wDiff.HtmlCustomize(), wDiff.HtmlFormat()
 
// Remove leading empty lines
wDiff.AssembleDiff = function (text, blocks, groups) {
textRight = textRight.replace( this.config.regExp.clipTrimNewLinesRight, '' );
 
// Get omission indicators, remove leading blanks
//
if ( rangeRightType === 'chars' ) {
// create group diffs
omittedRight = '~';
//
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
else if ( rangeRightType === 'blank' ) {
omittedRight = '~ ';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
}
 
// Remove split element
// cycle through groups
fragments.splice( fragment, 1 );
for (var group = 0; group < groups.length; group ++) {
fragmentsLength --;
var fixed = groups[group].fixed;
var color = groups[group].color;
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
var diff = '';
 
// Add left text to fragments list
// check for colored block and move direction
var if blockFrom( rangeLeft !== null; ) {
fragments.splice( fragment ++, 0, { text: textLeft, type: '=', color: null } );
if ( (fixed === false) && (color !== null) ) {
fragmentsLength ++;
if (groups[ groups[group].movedFrom ].blockStart < blockStart) {
if ( omittedLeft !== null ) {
blockFrom = 'left';
fragments.splice( fragment ++, 0, { text: '', type: omittedLeft, color: null } );
fragmentsLength ++;
}
}
 
else {
// Add fragment container and separator to list
blockFrom = 'right';
if ( rangeLeft !== null && rangeRight !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: ']', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: ',', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: '[', color: null } );
fragmentsLength += 3;
}
 
// Add right text to fragments list
if ( rangeRight !== null ) {
if ( omittedRight !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedRight, color: null } );
fragmentsLength ++;
}
fragments.splice( fragment ++, 0, { text: textRight, type: '=', color: null } );
fragmentsLength ++;
}
}
 
// Debug log
// add colored block start markup
if (blockFrom this.config.debug === true 'left') {
this.debugFragments( 'Fragments' );
diff += wDiff.HtmlCustomize(wDiff.htmlBlockLeftStart, color);
}
 
else if (blockFrom == 'right') {
return;
diff += wDiff.HtmlCustomize(wDiff.htmlBlockRightStart, color);
};
 
 
/**
* Create html formatted diff code from diff fragments.
*
* @param[in] array fragments Fragments array, abstraction layer for diff code
* @param string|undefined version
* Output version: 'new' or 'old': only text from new or old version, used for unit tests
* @param[out] string html Html code of diff
*/
this.getDiffHtml = function ( version ) {
 
var fragments = this.fragments;
 
// No change, only one unchanged block in containers
if ( fragments.length === 5 && fragments[2].type === '=' ) {
this.html = '';
return;
}
 
// cycleCycle through blocksfragments
var htmlFragments = [];
for (var block = blockStart; block <= blockEnd; block ++) {
var typefragmentsLength = blocks[block]fragments.typelength;
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
var string = blocks[block].string;
var text = fragments[fragment].text;
var type = fragments[fragment].type;
var color = fragments[fragment].color;
var html = '';
 
// htmlTest escapeif text stringis blanks-only or a single character
var blank = false;
string = wDiff.HtmlEscape(string);
if ( text !== '' ) {
blank = this.config.regExp.blankBlock.test( text );
}
 
// addAdd 'same'container (unchanged)start textmarkup
if ( type === 'same{' ) {
html = this.config.htmlCode.containerStart;
diff += string;
}
 
// addAdd 'del'container textend markup
else if ( type === 'del}' ) {
html = this.config.htmlCode.containerEnd;
string = string.replace(/\n/g, wDiff.htmlNewline);
diff += wDiff.htmlDeleteStart + string + wDiff.htmlDeleteEnd;
}
 
// addAdd 'ins'fragment textstart markup
else if ( type === 'ins[' ) {
html = this.config.htmlCode.fragmentStart;
string = string.replace(/\n/g, wDiff.htmlNewline);
diff += wDiff.htmlInsertStart + string + wDiff.htmlInsertEnd;
}
}
 
// addAdd colored blockfragment end markup
else if (blockFrom type === 'left]' ) {
html = this.config.htmlCode.fragmentEnd;
diff += wDiff.htmlBlockLeftEnd;
}
else if (blockFrom == 'right') {
diff += wDiff.htmlBlockRightEnd;
}
 
// Add fragment separator markup
groups[group].diff = diff;
else if ( type === ',' ) {
}
html = this.config.htmlCode.separator;
}
 
// Add omission markup
//
if ( type === '~' ) {
// mark original block positions
html = this.config.htmlCode.omittedChars;
//
}
 
// Add omission markup
// cycle through groups
if ( type === ' ~' ) {
for (var group = 0; group < groups.length; group ++) {
html = ' ' + this.config.htmlCode.omittedChars;
var moved = groups[group].moved;
}
 
// Add omission markup
// cycle through list of groups moved from here
var if leftMarks( type === '~ '; ) {
html = this.config.htmlCode.omittedChars + ' ';
var rightMarks = '';
}
for (var i = 0; i < moved.length; i ++) {
var movedGroup = moved[i];
var markColor = groups[movedGroup].color;
var mark = '';
 
// getAdd movedcolored left-pointing block textstart markup
varelse movedTextif ( type === '(<'; ) {
if ( version !== 'old' ) {
for (var block = groups[movedGroup].blockStart; block <= groups[movedGroup].blockEnd; block ++) {
 
if (blocks[block].type != 'ins') {
// Get title
movedText += blocks[block].string;
var title;
if ( this.config.noUnicodeSymbols === true ) {
title = this.config.msg['wiked-diff-block-left-nounicode'];
}
else {
title = this.config.msg['wiked-diff-block-left'];
}
 
// Get html
if ( this.config.coloredBlocks === true ) {
html = this.config.htmlCode.blockColoredStart;
}
else {
html = this.config.htmlCode.blockStart;
}
html = this.htmlCustomize( html, color, title );
}
}
 
// Add colored right-pointing block start markup
// display as deletion at original position
else if (wDiff.showBlockMoves type === false'(>' ) {
if ( version !== 'old' ) {
mark = wDiff.htmlDeleteStart + wDiff.HtmlEscape(movedText) + wDiff.htmlDeleteEnd;
 
// Get title
var title;
if ( this.config.noUnicodeSymbols === true ) {
title = this.config.msg['wiked-diff-block-right-nounicode'];
}
else {
title = this.config.msg['wiked-diff-block-right'];
}
 
// Get html
if ( this.config.coloredBlocks === true ) {
html = this.config.htmlCode.blockColoredStart;
}
else {
html = this.config.htmlCode.blockStart;
}
html = this.htmlCustomize( html, color, title );
}
}
 
// getAdd markcolored directionblock end markup
else if ( type === ' )' ) {
if ( version !== 'old' ) {
if (groups[movedGroup].blockStart < groups[group].blockStart) {
markhtml = wDiffthis.htmlMarkLeftconfig.htmlCode.blockEnd;
}
}
 
// Add '=' (unchanged) text and moved block
if ( type === '=' ) {
text = this.htmlEscape( text );
if ( color !== null ) {
if ( version !== 'old' ) {
html = this.markupBlanks( text, true );
}
}
else {
markhtml = wDiffthis.htmlMarkRightmarkupBlanks( text );
}
mark = wDiff.HtmlCustomize(mark, markColor, movedText);
}
 
// Add '-' text
else if ( type === '-' ) {
if ( version !== 'new' ) {
 
// getFor sideold ofversion groupskip to'-' markinside moved group
if ( version !== 'old' || color === null ) {
if (groups[movedGroup].oldNumber < groups[group].oldNumber) {
text = this.htmlEscape( text );
leftMarks += mark;
text = this.markupBlanks( text, true );
if ( blank === true ) {
html = this.config.htmlCode.deleteStartBlank;
}
else {
html = this.config.htmlCode.deleteStart;
}
html += text + this.config.htmlCode.deleteEnd;
}
}
}
 
else {
rightMarks// Add '+=' mark;text
else if ( type === '+' ) {
if ( version !== 'old' ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
if ( blank === true ) {
html = this.config.htmlCode.insertStartBlank;
}
else {
html = this.config.htmlCode.insertStart;
}
html += text + this.config.htmlCode.insertEnd;
}
}
}
groups[group].diff = leftMarks + groups[group].diff + rightMarks;
}
 
// Add '<' and '>' code
//
else if ( type === '<' || type === '>' ) {
// join diffs
if ( version !== 'new' ) {
//
 
// Display as deletion at original position
// make shallow copy of groups and sort by blockStart
if ( this.config.showBlockMoves === false || version === 'old' ) {
var groupsSort = groups.slice();
text = this.htmlEscape( text );
groupsSort.sort(function(a, b) {
text = this.markupBlanks( text, true );
return a.blockStart - b.blockStart;
if ( version === 'old' ) {
});
if ( this.config.coloredBlocks === true ) {
html =
this.htmlCustomize( this.config.htmlCode.blockColoredStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
else {
html =
this.htmlCustomize( this.config.htmlCode.blockStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
}
else {
if ( blank === true ) {
html =
this.config.htmlCode.deleteStartBlank +
text +
this.config.htmlCode.deleteEnd;
}
else {
html = this.config.htmlCode.deleteStart + text + this.config.htmlCode.deleteEnd;
}
}
}
 
// Display as mark
// cycle through sorted groups and assemble diffs
else {
for (var group = 0; group < groupsSort.length; group ++) {
if ( type === '<' ) {
text.diff += groupsSort[group].diff;
if ( this.config.coloredBlocks === true ) {
}
html = this.htmlCustomize( this.config.htmlCode.markLeftColored, color, text );
}
else {
html = this.htmlCustomize( this.config.htmlCode.markLeft, color, text );
}
}
else {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markRightColored, color, text );
}
else {
html = this.htmlCustomize( this.config.htmlCode.markRight, color, text );
}
}
}
}
}
htmlFragments.push( html );
}
 
// Join fragments
// WED('Groups', wDiff.DebugGroups(groups));
this.html = htmlFragments.join( '' );
 
return;
// keep newlines and multiple spaces
};
wDiff.HtmlFormat(text);
 
// WED('text.diff', text.diff);
 
/**
return text.diff;
* Customize html code fragments.
};
* Replaces:
* {number}: class/color/block/mark/id number
* {title}: title attribute (popup)
* {nounicode}: noUnicodeSymbols fallback
* input: html, number: block number, title: title attribute (popup) text
*
* @param string html Html code to be customized
* @return string Customized html code
*/
this.htmlCustomize = function ( html, number, title ) {
 
// Replace {number} with class/color/block/mark/id number
html = html.replace( /\{number\}/g, number);
 
// Replace {nounicode} with wikEdDiffNoUnicode class name
//
if ( this.config.noUnicodeSymbols === true ) {
// wDiff.HtmlCustomize: customize move indicator html: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
html = html.replace( /\{nounicode\}/g, ' wikEdDiffNoUnicode');
// input: text (html or css code)
// returns: customized text
// called from: wDiff.AssembleDiff()
 
wDiff.HtmlCustomize = function (text, number, title) {
 
if (wDiff.coloredBlocks === true) {
var blockStyle = wDiff.styleBlockColor[number];
if (blockStyle === undefined) {
blockStyle = '';
}
else {
var markStyle = wDiff.styleMarkColor[number];
html = html.replace( /\{nounicode\}/g, '');
if (markStyle === undefined) {
markStyle = '';
}
text = text.replace(/\{block\}/g, ' ' + blockStyle);
text = text.replace(/\{mark\}/g, ' ' + markStyle);
text = text.replace(/\{class\}/g, number);
}
else {
text = text.replace(/\{block\}|\{mark\}|\{class\}/g, '');
}
text = text.replace(/\{number\}/g, number);
 
// shortenShorten title text, replace {title}
if ( (title !== undefined) && (title !== '') ) {
var max = 512;
var end = 128;
var gapMark = ' [...] ';
if ( title.length > max ) {
title =
title = title.substr(0, max - gapMark.length - end) + gapMark + title.substr(title.length - end);
title.substr( 0, max - gapMark.length - end ) +
gapMark +
title.substr( title.length - end );
}
title = this.htmlEscape( title );
title = title.replace( /\t/g, '&nbsp;&nbsp;');
title = title.replace( / /g, '&nbsp;&nbsp;');
html = html.replace( /\{title\}/, title);
}
return html;
title = wDiff.HtmlEscape(title);
};
title = title.replace(/\t/g, '&nbsp;&nbsp;');
title = title.replace(/ /g, '&nbsp;&nbsp;');
text = text.replace(/\{title\}/, ' title="' + title + '"');
}
else {
text = text.replace(/\{title\}/, '');
}
return text;
};
 
 
//**
// wDiff.HtmlEscape:* replaceReplace html-sensitive characters in output text with character entities.
*
// input: text
* @param string html Html code to be escaped
// returns: escaped text
* @return string Escaped html code
// called from: wDiff.Diff(), wDiff.AssembleDiff()
*/
this.htmlEscape = function ( html ) {
 
html = html.replace( /&/g, '&amp;');
wDiff.HtmlEscape = function (text) {
html = html.replace( /</g, '&lt;');
html = html.replace( />/g, '&gt;');
html = html.replace( /"/g, '&quot;');
return html;
};
 
text = text.replace(/&/g, '&amp;');
text = text.replace(/</g, '&lt;');
text = text.replace(/>/g, '&gt;');
text = text.replace(/"/g, '&quot;');
return (text);
};
 
/**
* Markup tabs, newlines, and spaces in diff fragment text.
*
* @param bool highlight Highlight newlines and spaces in addition to tabs
* @param string html Text code to be marked-up
* @return string Marked-up text
*/
this.markupBlanks = function ( html, highlight ) {
 
if ( highlight === true ) {
//
html = html.replace( / /g, this.config.htmlCode.space);
// wDiff.HtmlFormat: tidy html, keep newlines and multiple spaces, add container
html = html.replace( /\n/g, this.config.htmlCode.newline);
// changes: text.diff
}
// called from: wDiff.Diff(), wDiff.AssembleDiff()
html = html.replace( /\t/g, this.config.htmlCode.tab);
return html;
};
 
wDiff.HtmlFormat = function (text) {
 
/**
text.diff = text.diff.replace(/<\/(\w+)><!--wDiff(Delete|Insert)--><\1\b[^>]*\bclass="wDiff\2"[^>]*>/g, '');
* Count real words in text.
text.diff = text.diff.replace(/\t/g, wDiff.htmlTab);
*
text.diff = wDiff.htmlContainerStart + wDiff.htmlFragmentStart + text.diff + wDiff.htmlFragmentEnd + wDiff.htmlContainerEnd;
* @param string text Text for word counting
return;
* @return int Number of words in text
};
*/
this.wordCount = function ( text ) {
 
return ( text.match( this.config.regExp.countWords ) || [] ).length;
};
 
// wDiff.ShortenOutput: shorten diff html by removing unchanged parts
// input: diff html string from wDiff.Diff()
// returns: shortened html with removed unchanged passages indicated by (...) or separator
 
/**
wDiff.ShortenOutput = function (html) {
* Test diff code for consistency with input versions.
* Prints results to debug console.
*
* @param[in] WikEdDiffText newText, oldText Text objects
*/
this.unitTests = function () {
 
// Check if output is consistent with new text
var diff = '';
this.getDiffHtml( 'new' );
var diff = this.html.replace( /<[^>]*>/g, '');
var text = this.htmlEscape( this.newText.text );
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with new text version!'
);
this.error = true;
console.log( 'new text:\n', text );
console.log( 'new diff:\n', diff );
}
else {
console.log( 'OK: wikEdDiff unit test passed: diff consistent with new text.' );
}
 
// Check if output is consistent with old text
// empty text
this.getDiffHtml( 'old' );
if ( (html === undefined) || (html === '') ) {
var diff = this.html.replace( /<[^>]*>/g, '');
return '';
var text = this.htmlEscape( this.oldText.text );
}
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with old text version!'
);
this.error = true;
console.log( 'old text:\n', text );
console.log( 'old diff:\n', diff );
}
else {
console.log( 'OK: wikEdDiff unit test passed: diff consistent with old text.' );
}
 
return;
// remove container by non-regExp replace
};
html = html.replace(wDiff.htmlContainerStart, '');
html = html.replace(wDiff.htmlFragmentStart, '');
html = html.replace(wDiff.htmlFragmentEnd, '');
html = html.replace(wDiff.htmlContainerEnd, '');
 
// scan for diff html tags
var regExpDiff = /<\w+\b[^>]*\bclass="[^">]*?\bwDiff(MarkLeft|MarkRight|BlockLeft|BlockRight|Delete|Insert)\b[^">]*"[^>]*>(.|\n)*?<!--wDiff\1-->/g;
var tagStart = [];
var tagEnd = [];
var i = 0;
var regExpMatch;
 
/**
// save tag positions
* Dump blocks object to browser console.
while ( (regExpMatch = regExpDiff.exec(html)) !== null ) {
*
* @param string name Block name
* @param[in] array blocks Blocks table object
*/
this.debugBlocks = function ( name, blocks ) {
 
if ( blocks === undefined ) {
// combine consecutive diff tags
blocks = this.blocks;
if ( (i > 0) && (tagEnd[i - 1] == regExpMatch.index) ) {
tagEnd[i - 1] = regExpMatch.index + regExpMatch[0].length;
}
elsevar {dump =
'\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq' +
tagStart[i] = regExpMatch.index;
'\twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \ttext\n';
tagEnd[i] = regExpMatch.index + regExpMatch[0].length;
var blocksLength = blocks.length;
i ++;
for ( var i = 0; i < blocksLength; i ++ ) {
dump +=
i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' +
blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \t' + blocks[i].oldStart + ' \t' +
blocks[i].count + ' \t' + blocks[i].unique + ' \t' + blocks[i].words + ' \t' +
blocks[i].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' +
blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + blocks[i].moved + ' \t' +
this.debugShortenText( blocks[i].text ) + '\n';
}
console.log( name + ':\n' + dump );
}
};
 
// no diff tags detected
if (tagStart.length === 0) {
return wDiff.htmlNoChange;
}
 
/**
// define regexps
* Dump groups object to browser console.
var regExpHeading = /\n=+.+?=+ *\n|\n\{\||\n\|\}/g;
*
var regExpParagraph = /\n\n+/g;
* @param string name Group name
var regExpLine = /\n+/g;
* @param[in] array groups Groups table object
var regExpBlank = /(<[^>]+>)*\s+/g;
*/
this.debugGroups = function ( name, groups ) {
 
if ( groups === undefined ) {
// determine fragment border positions around diff tags
groups = this.groups;
var rangeStart = [];
}
var rangeEnd = [];
var rangeStartTypedump = [];
'\ni \toldNm \tblSta \tblEnd \tuniq \tmaxWo' +
var rangeEndType = [];
'\twords \tchars \tfixed \toldNm \tmFrom \tcolor\n';
var groupsLength = groupsLength;
for ( var i = 0; i < groups.length; i ++ ) {
dump +=
i + ' \t' + groups[i].oldNumber + ' \t' + groups[i].blockStart + ' \t' +
groups[i].blockEnd + ' \t' + groups[i].unique + ' \t' + groups[i].maxWords + ' \t' +
groups[i].words + ' \t' + groups[i].chars + ' \t' + groups[i].fixed + ' \t' +
groups[i].oldNumber + ' \t' + groups[i].movedFrom + ' \t' + groups[i].color + '\n';
}
console.log( name + ':\n' + dump );
};
 
// get line break positions
var lineBreaks = [];
var pos = 0;
do {
lineBreaks.push(pos);
pos = html.indexOf('\n', pos + 1);
} while (pos != -1);
lineBreaks.push(html.length);
 
/**
// cycle through diff tag start positions
* Dump fragments array to browser console.
for (var i = 0; i < tagStart.length; i ++) {
*
var regExpMatch;
* @param string name Fragments name
* @param[in] array fragments Fragments array
*/
this.debugFragments = function ( name ) {
 
var fragments = this.fragments;
// maximal lines to search before diff tag
var dump = '\ni \ttype \tcolor \ttext\n';
var rangeStartMin = 0;
for (var jfragmentsLength = 0; j < lineBreaksfragments.length - 1; j ++) {
iffor (tagStart[ var i = 0; i] < lineBreaks[jfragmentsLength; i ++ 1]) {
dump +=
if (j >= wDiff.linesBeforeMax) {
i + ' \t"' + fragments[i].type + '" \t' + fragments[i].color + ' \t' +
rangeStartMin = lineBreaks[j - wDiff.linesBeforeMax];
this.debugShortenText( fragments[i].text, 120, 40 ) + '\n';
}
break;
}
}
console.log( name + ':\n' + dump );
};
 
// find last heading before diff tag
var lastPos = tagStart[i] - wDiff.headingBefore;
if (lastPos < rangeStartMin) {
lastPos = rangeStartMin;
}
regExpHeading.lastIndex = lastPos;
while ( (regExpMatch = regExpHeading.exec(html)) !== null ) {
if (regExpMatch.index > tagStart[i]) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'heading';
}
 
/**
// find last paragraph before diff tag
* Dump borders array to browser console.
if (rangeStart[i] === undefined) {
*
lastPos = tagStart[i] - wDiff.paragraphBefore;
* @param string name Arrays name
if (lastPos < rangeStartMin) {
* @param[in] array border Match border array
lastPos = rangeStartMin;
} */
this.debugBorders = function ( name, borders ) {
regExpParagraph.lastIndex = lastPos;
while ( (regExpMatch = regExpParagraph.exec(html)) !== null) {
if (regExpMatch.index > tagStart[i]) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'paragraph';
}
}
 
var dump = '\ni \t[ new \told ]\n';
// find last line break before diff tag
var bordersLength = borders.length;
if (rangeStart[i] === undefined) {
for ( var i = 0; i < bordersLength; i ++ ) {
lastPos = tagStart[i] - wDiff.lineBeforeMax;
dump += i + ' \t[ ' + borders[i][0] + ' \t' + borders[i][1] + ' ]\n';
if (lastPos < rangeStartMin) {
lastPos = rangeStartMin;
}
regExpLine.lastIndex = lastPos;
while ( (regExpMatch = regExpLine.exec(html)) !== null ) {
if (regExpMatch.index > tagStart[i] - wDiff.lineBeforeMin) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'line';
}
}
console.log( name, dump );
};
 
// find last blank before diff tag
if (rangeStart[i] === undefined) {
lastPos = tagStart[i] - wDiff.blankBeforeMax;
if (lastPos < rangeStartMin) {
lastPos = rangeStartMin;
}
regExpBlank.lastIndex = lastPos;
while ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
if (regExpMatch.index > tagStart[i] - wDiff.blankBeforeMin) {
break;
}
rangeStart[i] = regExpMatch.index;
rangeStartType[i] = 'blank';
}
}
 
/**
// fixed number of chars before diff tag
* Shorten text for dumping.
if (rangeStart[i] === undefined) {
*
if (rangeStart[i] > rangeStartMin) {
* @param string text Text to be shortened
rangeStart[i] = tagStart[i] - wDiff.charsBefore;
* @param int max Max length of (shortened) text
rangeStartType[i] = 'chars';
* @param int end Length of trailing fragment of shortened text
}
* @return string Shortened text
}
*/
this.debugShortenText = function ( text, max, end ) {
 
if ( typeof text !== 'string' ) {
// fixed number of lines before diff tag
text = text.toString();
if (rangeStart[i] === undefined) {
rangeStart[i] = rangeStartMin;
rangeStartType[i] = 'lines';
}
text = text.replace( /\n/g, '\\n');
 
text = text.replace( /\t/g, ' ');
// maximal lines to search after diff tag
if ( max === undefined ) {
var rangeEndMax = html.length;
var pos max = tagEnd[i]50;
for (var j = 0; j < wDiff.linesAfterMax; j ++) {
pos = html.indexOf('\n', pos + 1);
if (pos == -1) {
rangeEndMax = html.length;
break;
}
rangeEndMax = pos;
}
if ( end === undefined ) {
 
end = 15;
// find first heading after diff tag
regExpHeading.lastIndex = tagEnd[i];
if ( (regExpMatch = regExpHeading.exec(html)) !== null ) {
if ( (regExpMatch.index < tagEnd[i] + wDiff.headingAfter) && (regExpMatch.index < rangeEndMax) ) {
rangeEnd[i] = regExpMatch.index + regExpMatch[0].length;
rangeEndType[i] = 'heading';
}
}
if ( text.length > max ) {
 
text = text.substr( 0, max - 1 - end ) + '…' + text.substr( text.length - end );
// find first paragraph after diff tag
if (rangeEnd[i] === undefined) {
regExpParagraph.lastIndex = tagEnd[i];
if ( (regExpMatch = regExpParagraph.exec(html)) !== null ) {
if ( (regExpMatch.index < tagEnd[i] + wDiff.paragraphAfter) && (regExpMatch.index < rangeEndMax) ) {
rangeEnd[i] = regExpMatch.index;
rangeEndType[i] = 'paragraph';
}
}
}
return '"' + text + '"';
};
 
 
// find first line break after diff tag
/**
if (rangeEnd[i] === undefined) {
* Start timer 'label', analogous to JavaScript console timer.
regExpLine.lastIndex = tagEnd[i] + wDiff.lineAfterMin;
* Usage: this.time( 'label' );
if ( (regExpMatch = regExpLine.exec(html)) !== null ) {
*
if ( (regExpMatch.index < tagEnd[i] + wDiff.lineAfterMax) && (regExpMatch.index < rangeEndMax) ) {
* @param string label Timer label
rangeEnd[i] = regExpMatch.index;
* @param[out] array timer Current time in milliseconds (float)
rangeEndType[i] = 'break';
*/
}
this.time = function ( label ) {
 
this.timer[label] = new Date().getTime();
return;
};
 
 
/**
* Stop timer 'label', analogous to JavaScript console timer.
* Logs time in milliseconds since start to browser console.
* Usage: this.timeEnd( 'label' );
*
* @param string label Timer label
* @param bool noLog Do not log result
* @return float Time in milliseconds
*/
this.timeEnd = function ( label, noLog ) {
 
var diff = 0;
if ( this.timer[label] !== undefined ) {
var start = this.timer[label];
var stop = new Date().getTime();
diff = stop - start;
this.timer[label] = undefined;
if ( noLog !== true ) {
console.log( label + ': ' + diff.toFixed( 2 ) + ' ms' );
}
}
return diff;
};
 
 
/**
// find blank after diff tag
* Log recursion timer results to browser console.
if (rangeEnd[i] === undefined) {
* Usage: this.timeRecursionEnd();
regExpBlank.lastIndex = tagEnd[i] + wDiff.blankAfterMin;
*
if ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
* @param string text Text label for output
if ( (regExpMatch.index < tagEnd[i] + wDiff.blankAfterMax) && (regExpMatch.index < rangeEndMax) ) {
* @param[in] array recursionTimer Accumulated recursion times
rangeEnd[i] = regExpMatch.index;
*/
rangeEndType[i] = 'blank';
this.timeRecursionEnd = function ( text ) {
}
 
if ( this.recursionTimer.length > 1 ) {
 
// Subtract times spent in deeper recursions
var timerEnd = this.recursionTimer.length - 1;
for ( var i = 0; i < timerEnd; i ++ ) {
this.recursionTimer[i] -= this.recursionTimer[i + 1];
}
}
 
// Log recursion times
// fixed number of chars after diff tag
var timerLength = this.recursionTimer.length;
if (rangeEnd[i] === undefined) {
iffor (rangeEnd[ var i = 0; i] < rangeEndMaxtimerLength; i ++ ) {
console.log( text + ' recursion ' + i + ': ' + this.recursionTimer[i].toFixed( 2 ) + ' ms' );
rangeEnd[i] = tagEnd[i] + wDiff.charsAfter;
rangeEndType[i] = 'chars';
}
}
this.recursionTimer = [];
return;
};
 
// fixed number of lines after diff tag
if (rangeEnd[i] === undefined) {
rangeEnd[i] = rangeEndMax;
rangeEndType[i] = 'lines';
}
}
 
/**
// remove overlaps, join close fragments
* Log variable values to debug console.
var fragmentStart = [];
* Usage: this.debug( 'var', var );
var fragmentEnd = [];
*
var fragmentStartType = [];
* @param string name Object identifier
var fragmentEndType = [];
* @param mixed|undefined name Object to be logged
fragmentStart[0] = rangeStart[0];
*/
fragmentEnd[0] = rangeEnd[0];
this.debug = function ( name, object ) {
fragmentStartType[0] = rangeStartType[0];
fragmentEndType[0] = rangeEndType[0];
var j = 1;
for (var i = 1; i < rangeStart.length; i ++) {
 
if ( object === undefined ) {
// get lines between fragments
console.log( name );
var lines = 0;
if (fragmentEnd[j - 1] < rangeStart[i]) {
var join = html.substring(fragmentEnd[j - 1], rangeStart[i]);
lines = (join.match(/\n/g) || []).length;
}
 
if ( (rangeStart[i] > fragmentEnd[j - 1] + wDiff.fragmentJoinChars) || (lines > wDiff.fragmentJoinLines) ) {
fragmentStart[j] = rangeStart[i];
fragmentEnd[j] = rangeEnd[i];
fragmentStartType[j] = rangeStartType[i];
fragmentEndType[j] = rangeEndType[i];
j ++;
}
else {
console.log( name + ': ' + object );
fragmentEnd[j - 1] = rangeEnd[i];
fragmentEndType[j - 1] = rangeEndType[i];
}
return;
}
};
 
// assemble the fragments
for (var i = 0; i < fragmentStart.length; i ++) {
 
/**
// get text fragment
* Add script to document head.
var fragment = html.substring(fragmentStart[i], fragmentEnd[i]);
*
fragment = fragment.replace(/^\n+|\n+$/g, '');
* @param string code JavaScript code
*/
this.addScript = function ( code ) {
 
if ( document.getElementById( 'wikEdDiffBlockHandler' ) === null ) {
// add inline marks for omitted chars and words
var script = document.createElement( 'script' );
if (fragmentStart[i] > 0) {
script.id = 'wikEdDiffBlockHandler';
if (fragmentStartType[i] == 'chars') {
if ( script.innerText !== undefined ) {
fragment = wDiff.htmlOmittedChars + fragment;
script.innerText = code;
}
else {
else if (fragmentStartType[i] == 'blank') {
script.textContent = code;
fragment = wDiff.htmlOmittedChars + ' ' + fragment;
}
document.getElementsByTagName( 'head' )[0].appendChild( script );
}
return;
if (fragmentEnd[i] < html.length) {
};
if (fragmentStartType[i] == 'chars') {
 
fragment = fragment + wDiff.htmlOmittedChars;
 
/**
* Add stylesheet to document head, cross-browser >= IE6.
*
* @param string css CSS code
*/
this.addStyleSheet = function ( css ) {
 
if ( document.getElementById( 'wikEdDiffStyles' ) === null ) {
 
// Replace mark symbols
css = css.replace( /\{cssMarkLeft\}/g, this.config.cssMarkLeft);
css = css.replace( /\{cssMarkRight\}/g, this.config.cssMarkRight);
 
var style = document.createElement( 'style' );
style.id = 'wikEdDiffStyles';
style.type = 'text/css';
if ( style.styleSheet !== undefined ) {
style.styleSheet.cssText = css;
}
else {
else if (fragmentStartType[i] == 'blank') {
style.appendChild( document.createTextNode( css ) );
fragment = fragment + ' ' + wDiff.htmlOmittedChars;
}
document.getElementsByTagName( 'head' )[0].appendChild( style );
}
return;
};
 
// remove leading and trailing empty lines
fragment = fragment.replace(/^\n+|\n+$/g, '');
 
/**
// add fragment separator
* Recursive deep copy from target over source for customization import.
if (i > 0) {
*
diff += wDiff.htmlSeparator;
* @param object source Source object
* @param object target Target object
*/
this.deepCopy = function ( source, target ) {
 
for ( var key in source ) {
if ( Object.prototype.hasOwnProperty.call( source, key ) === true ) {
if ( typeof source[key] === 'object' ) {
this.deepCopy( source[key], target[key] );
}
else {
target[key] = source[key];
}
}
}
return;
};
 
// Initialze WikEdDiff object
// encapsulate span errors
this.init();
diff += wDiff.htmlFragmentStart + fragment + wDiff.htmlFragmentEnd;
};
 
// add to container
diff = wDiff.htmlContainerStart + diff + wDiff.htmlContainerEnd;
 
/**
// WED('diff', diff);
* Data and methods for single text version (old or new one).
*
* @class WikEdDiffText
*/
WikEdDiff.WikEdDiffText = function ( text, parent ) {
 
/** @var WikEdDiff parent Parent object for configuration settings and debugging methods */
return diff;
this.parent = parent;
};
 
/** @var string text Text of this version */
this.text = null;
 
/** @var array tokens Tokens list */
//
this.tokens = [];
// wDiff.AddScript: add script to head
//
 
/** @var int first, last First and last index of tokens list */
wDiff.AddScript = function (code) {
this.first = null;
this.last = null;
 
/** @var array words Word counts for version text */
var script = document.createElement('script');
this.words = {};
script.id = 'wDiffBlockHandler';
if (script.innerText !== undefined) {
script.innerText = code;
}
else {
script.textContent = code;
}
wikEd.head.appendChild(script);
return;
};
 
 
//**
* Constructor, initialize text object.
// wDiff.AddStyleSheet: add CSS rules to new style sheet, cross-browser >= IE6
*
//
* @param string text Text of version
* @param WikEdDiff parent Parent, for configuration settings and debugging methods
*/
this.init = function () {
 
if ( typeof text !== 'string' ) {
wDiff.AddStyleSheet = function (css) {
text = text.toString();
}
 
// IE / Mac fix
var style = document.createElement('style');
style this.typetext = 'text.replace( /css\r\n?/g, '\n');
if (style.styleSheet !== undefined) {
style.styleSheet.cssText = css;
}
else {
style.appendChild( document.createTextNode(css) );
}
document.getElementsByTagName('head')[0].appendChild(style);
return;
};
 
// Parse and count words and chunks for identification of unique real words
if ( this.parent.config.timer === true ) {
this.parent.time( 'wordParse' );
}
this.wordParse( this.parent.config.regExp.countWords );
this.wordParse( this.parent.config.regExp.countChunks );
if ( this.parent.config.timer === true ) {
this.parent.timeEnd( 'wordParse' );
}
return;
};
 
//
// wDiff.WordCount: count words in string
//
 
/**
wDiff.WordCount = function (string) {
* Parse and count words and chunks for identification of unique words.
*
* @param string regExp Regular expression for counting words
* @param[in] string text Text of version
* @param[out] array words Number of word occurrences
*/
this.wordParse = function ( regExp ) {
 
var regExpMatch = this.text.match( regExp );
return (string.match(wDiff.regExpWordCount) || []).length;
if ( regExpMatch !== null ) {
};
var matchLength = regExpMatch.length;
for (var i = 0; i < matchLength; i ++) {
var word = regExpMatch[i];
if ( Object.prototype.hasOwnProperty.call( this.words, word ) === false ) {
this.words[word] = 1;
}
else {
this.words[word] ++;
}
}
}
return;
};
 
 
//**
* Split text into paragraph, line, sentence, chunk, word, or character tokens.
// wDiff.DebugText: dump text (text.oldText or text.newText) object
*
//
* @param string level Level of splitting: paragraph, line, sentence, chunk, word, or character
* @param int|null token Index of token to be split, otherwise uses full text
* @param[in] string text Full text to be split
* @param[out] array tokens Tokens list
* @param[out] int first, last First and last index of tokens list
*/
this.splitText = function ( level, token ) {
 
var prev = null;
wDiff.DebugText = function (text) {
var next = null;
var dump = 'first: ' + text.first + '\tlast: ' + text.last + '\n';
var current = this.tokens.length;
dump += '\ni \tlink \t(prev \tnext) \t#num \t"token"\n';
var ifirst = text.firstcurrent;
var text = '';
while ( (i !== null) && (text.tokens[i] !== null) ) {
dump += i + ' \t' + text.tokens[i].link + ' \t(' + text.tokens[i].prev + ' \t' + text.tokens[i].next + ') \t#' + text.tokens[i].number + ' \t' + wDiff.DebugShortenString(text.tokens[i].token) + '\n';
i = text.tokens[i].next;
}
return dump;
};
 
// Split full text or specified token
if ( token === undefined ) {
text = this.text;
}
else {
prev = this.tokens[token].prev;
next = this.tokens[token].next;
text = this.tokens[token].token;
}
 
// Split text into tokens, regExp match as separator
//
var number = 0;
// wDiff.DebugBlocks: dump blocks object
var split = [];
//
var regExpMatch;
var lastIndex = 0;
var regExp = this.parent.config.regExp.split[level];
while ( ( regExpMatch = regExp.exec( text ) ) !== null ) {
if ( regExpMatch.index > lastIndex ) {
split.push( text.substring( lastIndex, regExpMatch.index ) );
}
split.push( regExpMatch[0] );
lastIndex = regExp.lastIndex;
}
if ( lastIndex < text.length ) {
split.push( text.substring( lastIndex ) );
}
 
// Cycle through new tokens
wDiff.DebugBlocks = function (blocks) {
var splitLength = split.length;
var dump = '\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tchars \ttype \tsect \tgroup \tfixed \tstring\n';
for ( var i = 0; i < blocks.lengthsplitLength; i ++ ) {
dump += i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' + blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \t' + blocks[i].oldStart + ' \t' + blocks[i].count + ' \t' + blocks[i].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' + blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + wDiff.DebugShortenString(blocks[i].string) + '\n';
}
return dump;
};
 
// Insert current item, link to previous
this.tokens.push( {
token: split[i],
prev: prev,
next: null,
link: null,
number: null,
unique: false
} );
number ++;
 
// Link previous item to current
//
if ( prev !== null ) {
// wDiff.DebugGroups: dump groups object
this.tokens[prev].next = current;
//
}
prev = current;
current ++;
}
 
// Connect last new item and existing next item
wDiff.DebugGroups = function (groups) {
if ( number > 0 && token !== undefined ) {
var dump = '\ni \tblSta \tblEnd \tmaxWo \twords \tchars \tfixed \toldNm \tmFrom \tcolor \tmoved \tdiff\n';
for if (var iprev !== 0; i < groups.length; inull ++) {
this.tokens[prev].next = next;
dump += i + ' \t' + groups[i].blockStart + ' \t' + groups[i].blockEnd + ' \t' + groups[i].maxWords + ' \t' + groups[i].words + ' \t' + groups[i].chars + ' \t' + groups[i].fixed + ' \t' + groups[i].oldNumber + ' \t' + groups[i].movedFrom + ' \t' + groups[i].color + ' \t' + groups[i].moved.toString() + ' \t' + wDiff.DebugShortenString(groups[i].diff) + '\n';
}
if ( next !== null ) {
return dump;
this.tokens[next].prev = prev;
};
}
}
 
// Set text first and last token index
if ( number > 0 ) {
 
// Initial text split
//
if ( token === undefined ) {
// wDiff.DebugGaps: dump gaps object
this.first = 0;
//
this.last = prev;
}
 
// First or last token has been split
wDiff.DebugGaps = function (gaps) {
else {
var dump = '\ni \tnFirs \tnLast \tnTok \toFirs \toLast \toTok \tcharSplit\n';
for if (var itoken === 0; i < gapsthis.length; ifirst ++) {
this.first = first;
dump += i + ' \t' + gaps[i].newFirst + ' \t' + gaps[i].newLast + ' \t' + gaps[i].newTokens + ' \t' + gaps[i].oldFirst + ' \t' + gaps[i].oldLast + ' \t' + gaps[i].oldTokens + ' \t' + gaps[i].charSplit + '\n';
}
}
if ( token === this.last ) {
return dump;
this.last = prev;
};
}
}
}
return;
};
 
 
//**
* Split unique unmatched tokens into smaller tokens.
// wDiff.DebugShortenString: shorten string for debugging
*
//
* @param string level Level of splitting: line, sentence, chunk, or word
* @param[in] array tokens Tokens list
*/
this.splitRefine = function ( regExp ) {
 
// Cycle through tokens list
wDiff.DebugShortenString = function (string) {
var i = this.first;
if (string === null) {
returnwhile ( i !== 'null'; ) {
}
string = string.replace(/\n/g, '\\n');
var max = 100;
if (string.length > max) {
string = string.substr(0, max - 1 - 30) + '…' + string.substr(string.length - 30);
}
return '"' + string + '"';
};
 
// Refine unique unmatched tokens into smaller tokens
if ( this.tokens[i].link === null ) {
this.splitText( regExp, i );
}
i = this.tokens[i].next;
}
return;
};
 
 
// initialize wDiff
/**
wDiff.Init();
* Enumerate text token list before detecting blocks.
*
* @param[out] array tokens Tokens list
*/
this.enumerateTokens = function () {
 
// Enumerate tokens list
var number = 0;
var i = this.first;
while ( i !== null ) {
this.tokens[i].number = number;
number ++;
i = this.tokens[i].next;
}
return;
};
 
 
/**
* Dump tokens object to browser console.
*
* @param string name Text name
* @param[in] int first, last First and last index of tokens list
* @param[in] array tokens Tokens list
*/
this.debugText = function ( name ) {
 
var tokens = this.tokens;
var dump = 'first: ' + this.first + '\tlast: ' + this.last + '\n';
dump += '\ni \tlink \t(prev \tnext) \tuniq \t#num \t"token"\n';
var i = this.first;
while ( i !== null ) {
dump +=
i + ' \t' + tokens[i].link + ' \t(' + tokens[i].prev + ' \t' + tokens[i].next + ') \t' +
tokens[i].unique + ' \t#' + tokens[i].number + ' \t' +
parent.debugShortenText( tokens[i].token ) + '\n';
i = tokens[i].next;
}
console.log( name + ':\n' + dump );
return;
};
 
 
// Initialize WikEdDiffText object
this.init();
};
 
// </syntaxhighlight>