User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.0.14 (September 11, 2014 arrow only moved block title, no change indicator fix
another background that could use some darkmode friendly color
 
(28 intermediate revisions by 3 users not shown)
Line 2:
 
// ==UserScript==
// @name wDiffwikEd diff
// @version 1.02.144
// @date SeptemberOctober 1123, 2014
// @description improved word-based diff library with block move detection
// @homepage https://en.wikipedia.org/wiki/User:Cacycle/diff
Line 12:
// ==/UserScript==
 
/**
* wikEd diff: inline-style difference engine with block move support
*
* Improved JavaScript diff library that returns html/css-formatted new text version with
* highlighted deletions, insertions, and block moves. It is compatible with all browsers and is
* not dependent on external libraries.
*
* WikEdDiff.php and the JavaScript library wikEd diff are synced one-to-one ports. Changes and
* fixes are to be applied to both versions.
*
* JavaScript library (mirror): https://en.wikipedia.org/wiki/User:Cacycle/diff
* JavaScript online tool: http://cacycle.altervista.org/wikEd-diff-tool.html
* MediaWiki extension: https://www.mediawiki.org/wiki/Extension:wikEdDiff
*
* This difference engine applies a word-based algorithm that uses unique words as anchor points
* to identify matching text and moved blocks (Paul Heckel: A technique for isolating differences
* between files. Communications of the ACM 21(4):264 (1978)).
*
* Additional features:
*
* - Visual inline style, changes are shown in a single output text
* - Block move detection and highlighting
* - Resolution down to characters level
* - Unicode and multilingual support
* - Stepwise split (paragraphs, lines, sentences, words, characters)
* - Recursive diff
* - Optimized code for resolving unmatched sequences
* - Minimization of length of moved blocks
* - Alignment of ambiguous unmatched sequences to next line break or word border
* - Clipping of unchanged irrelevant parts from the output (optional)
* - Fully customizable
* - Text split optimized for MediaWiki source texts
* - Well commented and documented code
*
* Datastructures (abbreviations from publication):
*
* class WikEdDiffText: diff text object (new or old version)
* .text text of version
* .words[] word count table
* .first index of first token in tokens list
* .last index of last token in tokens list
*
* .tokens[]: token list for new or old string (doubly-linked list) (N and O)
* .prev previous list item
* .next next list item
* .token token string
* .link index of corresponding token in new or old text (OA and NA)
* .number list enumeration number
* .unique token is unique word in text
*
* class WikEdDiff: diff object
* .config[]: configuration settings, see top of code for customization options
* .regExp[]: all regular expressions
* .split regular expressions used for splitting text into tokens
* .htmlCode HTML code fragments used for creating the output
* .msg output messages
* .newText new text
* .oldText old text
* .maxWords word count of longest linked block
* .html diff html
* .error flag: result has not passed unit tests
* .bordersDown[] linked region borders downwards, [new index, old index]
* .bordersUp[] linked region borders upwards, [new index, old index]
* .symbols: symbols table for whole text at all refinement levels
* .token[] hash table of parsed tokens for passes 1 - 3, points to symbol[i]
* .symbol[]: array of objects that hold token counters and pointers:
* .newCount new text token counter (NC)
* .oldCount old text token counter (OC)
* .newToken token index in text.newText.tokens
* .oldToken token index in text.oldText.tokens
* .linked flag: at least one unique token pair has been linked
*
* .blocks[]: array, block data (consecutive text tokens) in new text order
* .oldBlock number of block in old text order
* .newBlock number of block in new text order
* .oldNumber old text token number of first token
* .newNumber new text token number of first token
* .oldStart old text token index of first token
* .count number of tokens
* .unique contains unique linked token
* .words word count
* .chars char length
* .type '=', '-', '+', '|' (same, deletion, insertion, mark)
* .section section number
* .group group number of block
* .fixed belongs to a fixed (not moved) group
* .moved moved block group number corresponding with mark block
* .text text of block tokens
*
* .sections[]: array, block sections with no block move crosses outside a section
* .blockStart first block in section
* .blockEnd last block in section
 
* .groups[]: array, section blocks that are consecutive in old text order
Improved JavaScript diff library that returns html/css-formatted new text version with highlighted deletions, inserts, and block moves.
* .oldNumber first block oldNumber
It is compatible with all browsers and is not dependent on external libraries.
* .blockStart first block index
An implementation of the word-based algorithm from:
* .blockEnd last block index
* .unique contains unique linked token
* .maxWords word count of longest linked block
* .words word count
* .chars char count
* .fixed not moved from original position
* .movedFrom group position this group has been moved from
* .color color number of moved group
*
* .fragments[]: diff fragment list ready for markup, abstraction layer for customization
* .text block or mark text
* .color moved block or mark color number
* .type '=', '-', '+' same, deletion, insertion
* '<', '>' mark left, mark right
* '(<', '(>', ')' block start and end
* '~', ' ~', '~ ' omission indicators
* '[', ']', ',' fragment start and end, fragment separator
* '{', '}' container start and end
*
*/
 
// JSHint options
Communications of the ACM 21(4):264 (1978)
/* jshint -W004, -W100, newcap: true, browser: true, jquery: true, sub: true, bitwise: true,
http://doi.acm.org/10.1145/359460.359467
curly: true, evil: true, forin: true, freeze: true, globalstrict: true, immed: true,
latedef: true, loopfunc: true, quotmark: single, strict: true, undef: true */
/* global console */
 
// Turn on ECMAScript 5 strict mode
Additional features:
'use strict';
 
/** Define global objects. */
* Word (token) types have been optimized for MediaWiki source texts
var wikEdDiffConfig;
* Resolution down to characters level
var WED;
* Highlighting of moved blocks and their original position marks
* Stepwise split (paragraphs, sentences, words, chars)
* Recursive diff
* Additional post-pass-5 code for resolving islands caused by common tokens at the border of sequences of common tokens
* Block move detection and visualization
* Minimizing length of moved vs. static blocks
* Bubbling up of ambiguous unresolved regions to next line break
* Optional omission of unchanged irrelevant parts from the output
* Fully customizable
* Well commented and documented code
 
This code is used by the MediaWiki in-browser text editors [[en:User:Cacycle/editor]] and [[en:User:Cacycle/wikEd]]
and the enhanced diff view tool wikEdDiff [[en:User:Cacycle/wikEd]].
 
/**
Usage:
* wikEd diff main class.
var diffHtml = wDiff.Diff(oldString, newString);
*
diffHtml = wDiff.ShortenOutput(diffHtml);
* @class WikEdDiff
*/
var WikEdDiff = function () {
 
/** @var array config Configuration and customization settings. */
Datastructures (abbreviations from publication):
this.config = {
 
/** Core diff settings (with default values). */
text: objects for text related data
.newText, new text
.oldText: old text
.string: new or old text to be diffed
.tokens[]: token data list for new or old string (N and O)
.prev: previous list item
.next: next list item
.token: token string
.link: index of corresponding token in new or old text (OA and NA)
.number: list enumeration number
.parsed: token has been added to symbol table
.unique: unique token in whole text
.first: index of first token in tokens list
.last: index of last token in tokens list
.diff: diff html
 
/**
symbols: object for symbols table data
* @var bool config.fullDiff
.token[]: associative array (hash) of parsed tokens for passes 1 - 3, points to symbol[i]
* Show complete un-clipped diff text (false)
.symbol[]: array of objects that hold token counters and pointers:
*/
.newCount: new text token counter (NC)
'fullDiff': false,
.oldCount: old text token counter (OC)
.newToken: token index in text.newText.tokens
.oldToken: token index in text.oldText.tokens
.linked: flag: at least one unique token pair has been linked
 
/**
* @var bool config.showBlockMoves
* Enable block move layout with highlighted blocks and marks at the original positions (true)
*/
'showBlockMoves': true,
 
/**
blocks[]: array of objects that holds block (consecutive text tokens) data in order of the new text
* @var bool config.charDiff
.oldBlock: number of block in old text order
* Enable character-refined diff (true)
.newBlock: number of block in new text order
*/
.oldNumber: old text token number of first token
'charDiff': true,
.newNumber: new text token number of first token
.oldStart: old text token index of first token
.count number of tokens
.unique: contains unique matched token
.words: word count
.chars: char length
.type: 'same', 'del', 'ins'
.section: section number
.group: group number of block
.fixed: belongs to a fixed (not moved) group
.string: string of block tokens
 
/**
groups[]: section blocks that are consecutive in old text
* @var bool config.repeatedDiff
.oldNumber: first block oldNumber
* Enable repeated diff to resolve problematic sequences (true)
.blockStart: first block index
*/
.blockEnd: last block index
'repeatedDiff': true,
.unique: contains unique matched token
.maxWords: word count of longest block
.words: word count
.chars: char count
.fixed: not moved from original position
.moved[]: list of groups that have been moved from this position
.movedFrom: position this group has been moved from
.color: color number of moved group
.diff: group diff
 
/**
*/
* @var bool config.recursiveDiff
* Enable recursive diff to resolve problematic sequences (true)
*/
'recursiveDiff': true,
 
/**
// JSHint options: W004: is already defined, W097: Use the function form of "use strict", W100: This character may get silently deleted by one or more browsers
* @var int config.recursionMax
/* jshint -W004, -W097, -W100, newcap: false, browser: true, jquery: true, sub: true, bitwise: true, curly: true, evil: true, forin: true, freeze: true, immed: true, latedef: true, loopfunc: true, quotmark: single, undef: true */
* Maximum recursion depth (10)
/* global console */
*/
'recursionMax': 10,
 
/**
// turn on ECMAScript 5 strict mode
* @var bool config.unlinkBlocks
'use strict';
* Reject blocks if they are too short and their words are not unique,
* prevents fragmentated diffs for very different versions (true)
*/
'unlinkBlocks': true,
 
/**
// define global object
* @var int config.unlinkMax
var wDiff; if (wDiff === undefined) { wDiff = {}; }
* Maximum number of rejection cycles (5)
var WED;
*/
'unlinkMax': 5,
 
/**
//
* @var int config.blockMinLength
// core diff settings
* Reject blocks if shorter than this number of real words (3)
//
*/
'blockMinLength': 3,
 
/**
// enable block move layout with color coded blocks and marks at their original position
* @var bool config.coloredBlocks
if (wDiff.showBlockMoves === undefined) { wDiff.showBlockMoves = true; }
* Display blocks in differing colors (rainbow color scheme) (false)
*/
'coloredBlocks': false,
 
/**
// minimal number of real words for a moved block (0 for always showing color coded blocks)
* @var bool config.coloredBlocks
if (wDiff.blockMinLength === undefined) { wDiff.blockMinLength = 3; }
* Do not use UniCode block move marks (legacy browsers) (false)
*/
'noUnicodeSymbols': false,
 
/**
// further resolve replacements character-wise from start and end
* @var bool config.stripTrailingNewline
if (wDiff.charDiff === undefined) { wDiff.charDiff = true; }
* Strip trailing newline off of texts (true in .js, false in .php)
*/
'stripTrailingNewline': true,
 
/**
// enable recursive diff to resolve problematic sequences
* @var bool config.debug
if (wDiff.recursiveDiff === undefined) { wDiff.recursiveDiff = true; }
* Show debug infos and stats (block, group, and fragment data) in debug console (false)
*/
'debug': false,
 
/**
// display blocks in different colors
* @var bool config.timer
if (wDiff.coloredBlocks === undefined) { wDiff.coloredBlocks = false; }
* Show timing results in debug console (false)
*/
'timer': false,
 
/**
// UniCode letter support for regexps, from http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
* @var bool config.unitTesting
if (wDiff.letters === undefined) { wDiff.letters = 'a-zA-Z0-9' + '00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCCA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC'.replace(/(\w{4})/g, '\\u$1'); }
* Run unit tests to prove correct working, display results in debug console (false)
*/
'unitTesting': false,
 
/** RegExp character classes. */
// regExps for splitting text
if (wDiff.regExpSplit === undefined) {
wDiff.regExpSplit = {
 
// UniCode letter support for regexps
// paragraphs: after newline
// From http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
paragraph: /(.|\n)+?(?=(\n|$))/g,
'regExpLetters':
'a-zA-Z0-9' + (
'00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-' +
'037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA' +
'05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-' +
'07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D' +
'09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE' +
'09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A39' +
'0A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE0' +
'0AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B83' +
'0B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C' +
'0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-' +
'0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-' +
'0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E87' +
'0E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC4' +
'0EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066' +
'106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D' +
'1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-1315' +
'1318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-' +
'17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C' +
'1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA0' +
'1BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F15' +
'1F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC4' +
'1FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-2113' +
'21152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-' +
'2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE' +
'2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-' +
'3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCC' +
'A000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-' +
'A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3' +
'A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-' +
'AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-' +
'AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9' +
'FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3D' +
'FD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-' +
'FFCFFFD2-FFD7FFDA-FFDC'
).replace( /(\w{4})/g, '\\u$1' ),
 
// New line characters without and with \n and \r
// sentences: after .spaces or before newline
'regExpNewLines': '\\u0085\\u2028',
sentence: /\n|.*?\.( +|(?=\n))|.+?(?=\n)/g,
'regExpNewLinesAll': '\\n\\r\\u0085\\u2028',
 
// Breaking white space characters without \n, \r, and \f
// inline chunks
'regExpBlanks': ' \\t\\x0b\\u2000-\\u200b\\u202f\\u205f\\u3000',
// [[wiki link]] | {{template}} | [ext. link] |<html> | [[wiki link| | {{template| | url
chunk: /\[\[[^\[\]\n]+\]\]|\{\{[^\{\}\n]+\}\}|\[[^\[\]\n]+\]|<[^<>\[\]\{\}\n]+>|\[\[[^\[\]\|\n]+\]\]\||\{\{[^\{\}\|\n]+\||\b((https?:|)\/\/)[^\x00-\x20\s"\[\]\x7f]+/g,
 
// Full stops without '.'
// words, multi-char markup, and chars
'regExpFullStops':
word: new RegExp('([' + wDiff.letters + '])+|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.', 'g'),
'\\u0589\\u06D4\\u0701\\u0702\\u0964\\u0DF4\\u1362\\u166E\\u1803\\u1809' +
'\\u2CF9\\u2CFE\\u2E3C\\u3002\\uA4FF\\uA60E\\uA6F3\\uFE52\\uFF0E\\uFF61',
 
// New paragraph characters without \n and \r
// chars
'regExpNewParagraph': '\\f\\u2029',
character: /./g
};
}
 
// Exclamation marks without '!'
// regExps for bubbling up gaps
'regExpExclamationMarks':
if (wDiff.regExpBubbleStop === undefined) { wDiff.regExpBubbleStop = /\n$/; }
'\\u01C3\\u01C3\\u01C3\\u055C\\u055C\\u07F9\\u1944\\u1944' +
if (wDiff.regExpBubbleClosing === undefined) { wDiff.regExpBubbleClosing = /^[\s)\]}>\-–—.,:;?!’\/\\=+]/; }
'\\u203C\\u203C\\u2048\\u2048\\uFE15\\uFE57\\uFF01',
 
// Question marks without '?'
// regExp for counting words
'regExpQuestionMarks':
if (wDiff.regExpWordCount === undefined) { wDiff.regExpWordCount = new RegExp('(^|[^' + wDiff.letters + '])[' + wDiff.letters + '][' + wDiff.letters + '_\'’]*', 'g'); }
'\\u037E\\u055E\\u061F\\u1367\\u1945\\u2047\\u2049' +
'\\u2CFA\\u2CFB\\u2E2E\\uA60F\\uA6F7\\uFE56\\uFF1F',
 
/** Clip settings. */
// regExp for wiki code non-letter characters
if (wDiff.regExpWikiCodeChars === undefined) { wDiff.regExpWikiCodeChars = /^[ \t\n\[\]{}|+\-!*#:;=<>'\/_,.&?]+$/; }
 
// Find clip position: characters from right
//
'clipHeadingLeft': 1500,
// shorten output settings
'clipParagraphLeftMax': 1500,
//
'clipParagraphLeftMin': 500,
'clipLineLeftMax': 1000,
'clipLineLeftMin': 500,
'clipBlankLeftMax': 1000,
'clipBlankLeftMin': 500,
'clipCharsLeft': 500,
 
// Find clip position: characters from right
// characters before diff tag to search for previous heading, paragraph, line break, cut characters
'clipHeadingRight': 1500,
if (wDiff.headingBefore === undefined) { wDiff.headingBefore = 1500; }
'clipParagraphRightMax': 1500,
if (wDiff.paragraphBeforeMax === undefined) { wDiff.paragraphBeforeMax = 1500; }
'clipParagraphRightMin': 500,
if (wDiff.paragraphBeforeMin === undefined) { wDiff.paragraphBeforeMin = 500; }
'clipLineRightMax': 1000,
if (wDiff.lineBeforeMax === undefined) { wDiff.lineBeforeMax = 1000; }
'clipLineRightMin': 500,
if (wDiff.lineBeforeMin === undefined) { wDiff.lineBeforeMin = 500; }
'clipBlankRightMax': 1000,
if (wDiff.blankBeforeMax === undefined) { wDiff.blankBeforeMax = 1000; }
'clipBlankRightMin': 500,
if (wDiff.blankBeforeMin === undefined) { wDiff.blankBeforeMin = 500; }
'clipCharsRight': 500,
if (wDiff.charsBefore === undefined) { wDiff.charsBefore = 500; }
 
// charactersMaximum afternumber diffof taglines to search for nextclip heading, paragraph, line break, or charactersposition
'clipLinesRightMax': 10,
if (wDiff.headingAfter === undefined) { wDiff.headingAfter = 1500; }
'clipLinesLeftMax': 10,
if (wDiff.paragraphAfterMax === undefined) { wDiff.paragraphAfterMax = 1500; }
if (wDiff.paragraphAfterMin === undefined) { wDiff.paragraphAfterMin = 500; }
if (wDiff.lineAfterMax === undefined) { wDiff.lineAfterMax = 1000; }
if (wDiff.lineAfterMin === undefined) { wDiff.lineAfterMin = 500; }
if (wDiff.blankAfterMax === undefined) { wDiff.blankAfterMax = 1000; }
if (wDiff.blankAfterMin === undefined) { wDiff.blankAfterMin = 500; }
if (wDiff.charsAfter === undefined) { wDiff.charsAfter = 500; }
 
// Skip clipping if ranges are too close
// lines before and after diff tag to search for previous heading, paragraph, line break, cut characters
'clipSkipLines': 5,
if (wDiff.linesBeforeMax === undefined) { wDiff.linesBeforeMax = 10; }
'clipSkipChars': 1000,
if (wDiff.linesAfterMax === undefined) { wDiff.linesAfterMax = 10; }
 
// Css stylesheet
// maximal fragment distance to join close fragments
'cssMarkLeft': '◀',
if (wDiff.fragmentJoinLines === undefined) { wDiff.fragmentJoinLines = 5; }
'cssMarkRight': '▶',
if (wDiff.fragmentJoinChars === undefined) { wDiff.fragmentJoinChars = 1000; }
'stylesheet':
 
// Insert
//
'.wikEdDiffInsert {' +
// css classes
'font-weight: bold; background-color: #bbddff; ' +
//
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'} ' +
'.wikEdDiffInsertBlank { background-color: #66bbff; } ' +
'.wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; } ' +
 
// Delete
if (wDiff.stylesheet === undefined) {
'.wikEdDiffDelete {' +
wDiff.stylesheet =
'.wDiffTabfont-weight:before { content: "→"bold; background-color: #bbb; font-size: smallerffe49c; }' +
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'.wDiffNewline:before { content: " "; }' +
'} ' +
'.wDiffMarkRight:before { content: "▶"; }' +
'.wDiffMarkLeft:before wikEdDiffDeleteBlank { contentbackground-color: "◀"#ffd064; } ' +
'.wDiffDeletewikEdDiffFragment:hover .wikEdDiffDeleteBlank { font-weight: bold; background-color: #ffe49c; color:} #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
'.wDiffInsert { font-weight: bold; background-color: #bbddff; color: #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
'.wDiffBlockLeft { font-weight: bold; background-color: #e8e8e8; border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; }' +
'.wDiffBlockRight { font-weight: bold; background-color: #e8e8e8; border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; }' +
'.wDiffMarkLeft { font-weight: bold; background-color: #ffe49c; color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; }' +
'.wDiffMarkRight { font-weight: bold; background-color: #ffe49c; color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; }' +
'.wDiffFragment { white-space: pre-wrap; background: #fff; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; }' +
'.wDiffContainer { }' +
'.wDiffNoChange { white-space: pre-wrap; background: #f0f0f0; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; }' +
'.wDiffSeparator { margin-bottom: 1em; }' +
'.wDiffBlock { }' +
'.wDiffBlock0 { background-color: #ffff60; }' +
'.wDiffBlock1 { background-color: #c0ff60; }' +
'.wDiffBlock2 { background-color: #ffd8ff; }' +
'.wDiffBlock3 { background-color: #a0ffff; }' +
'.wDiffBlock4 { background-color: #ffe840; }' +
'.wDiffBlock5 { background-color: #bbccff; }' +
'.wDiffBlock6 { background-color: #ffaaff; }' +
'.wDiffBlock7 { background-color: #ffbbbb; }' +
'.wDiffBlock8 { background-color: #a0e8a0; }' +
'.wDiffMark { }' +
'.wDiffMark0 { color: #ffff60; }' +
'.wDiffMark1 { color: #c0ff60; }' +
'.wDiffMark2 { color: #ffd8ff; }' +
'.wDiffMark3 { color: #a0ffff; }' +
'.wDiffMark4 { color: #ffd840; }' +
'.wDiffMark5 { color: #bbccff; }' +
'.wDiffMark6 { color: #ff99ff; }' +
'.wDiffMark7 { color: #ff9999; }' +
'.wDiffMark8 { color: #90d090; }' +
'.wDiffBlockHighlight { background-color: #777; color: #fff; border: solid #777; border-width: 1px 0; }' +
'.wDiffMarkHighlight { background-color: #777; color: #fff; }';
}
 
// Block
//
'.wikEdDiffBlock {' +
// css styles
'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.styleContainer === undefined) { wDiff.styleContainer = ''; }
'.wikEdDiffMarkLeft, .wikEdDiffMarkRight {' +
if (wDiff.styleDelete === undefined) { wDiff.styleDelete = ''; }
'font-weight: bold; background-color: #ffe49c; ' +
if (wDiff.styleInsert === undefined) { wDiff.styleInsert = ''; }
'color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; ' +
if (wDiff.styleBlockLeft === undefined) { wDiff.styleBlockLeft = ''; }
'} ' +
if (wDiff.styleBlockRight === undefined) { wDiff.styleBlockRight = ''; }
'.wikEdDiffMarkLeft:before { content: "{cssMarkLeft}"; } ' +
if (wDiff.styleBlockHighlight === undefined) { wDiff.styleBlockHighlight = ''; }
'.wikEdDiffMarkRight:before { content: "{cssMarkRight}"; } ' +
if (wDiff.styleBlockColor === undefined) { wDiff.styleBlockColor = []; }
'.wikEdDiffMarkLeft.wikEdDiffNoUnicode:before { content: "<"; } ' +
if (wDiff.styleMarkLeft === undefined) { wDiff.styleMarkLeft = ''; }
'.wikEdDiffMarkRight.wikEdDiffNoUnicode:before { content: ">"; } ' +
if (wDiff.styleMarkRight === undefined) { wDiff.styleMarkRight = ''; }
'.wikEdDiffMark { background-color: #e8e8e8; color: #666; } ' +
if (wDiff.styleMarkColor === undefined) { wDiff.styleMarkColor = []; }
'.wikEdDiffMark0 { background-color: #ffff60; } ' +
if (wDiff.styleNewline === undefined) { wDiff.styleNewline = ''; }
'.wikEdDiffMark1 { background-color: #c8f880; } ' +
if (wDiff.styleTab === undefined) { wDiff.styleTab = ''; }
'.wikEdDiffMark2 { background-color: #ffd0f0; } ' +
if (wDiff.styleFragment === undefined) { wDiff.styleFragment = ''; }
'.wikEdDiffMark3 { background-color: #a0ffff; } ' +
if (wDiff.styleNoChange === undefined) { wDiff.styleNoChange = ''; }
'.wikEdDiffMark4 { background-color: #fff860; } ' +
if (wDiff.styleSeparator === undefined) { wDiff.styleSeparator = ''; }
'.wikEdDiffMark5 { background-color: #b0c0ff; } ' +
if (wDiff.styleOmittedChars === undefined) { wDiff.styleOmittedChars = ''; }
'.wikEdDiffMark6 { background-color: #e0c0ff; } ' +
'.wikEdDiffMark7 { background-color: #ffa8a8; } ' +
'.wikEdDiffMark8 { background-color: #98e898; } ' +
'.wikEdDiffMarkHighlight { background-color: #777; color: #fff; } ' +
 
// Wrappers
//
'.wikEdDiffContainer { } ' +
// html for core diff
'.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
// dynamic replacements: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
'.wikEdDiffNewline:before { content: "¶"; color: transparent; } ' +
// class plus html comment are required indicators for wDiff.ShortenOutput()
'.wikEdDiffBlock:hover .wikEdDiffNewline:before { color: #aaa; } ' +
if (wDiff.blockEvent === undefined) { wDiff.blockEvent = ' onmouseover="wDiff.BlockHandler(undefined, this, \'mouseover\');"'; }
'.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.htmlContainerStart === undefined) { wDiff.htmlContainerStart = '<div class="wDiffContainer" style="' + wDiff.styleContainer + '">'; }
'.wikEdDiffTab { position: relative; } ' +
if (wDiff.htmlContainerEnd === undefined) { wDiff.htmlContainerEnd = '</div>'; }
'.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.htmlDeleteStart === undefined) { wDiff.htmlDeleteStart = '<span class="wDiffDelete" style="' + wDiff.styleDelete + '" title="−">'; }
'.wikEdDiffSpace { position: relative; } ' +
if (wDiff.htmlDeleteEnd === undefined) { wDiff.htmlDeleteEnd = '</span><!--wDiffDelete-->'; }
'.wikEdDiffSpaceSymbol { position: absolute; top: -0.2em; left: -0.05em; } ' +
'.wikEdDiffSpaceSymbol:before { content: "·"; color: transparent; } ' +
'.wikEdDiffBlock:hover .wikEdDiffSpaceSymbol:before { color: #999; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffSpaceSymbol:before { color: transparent; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffSpaceSymbol:before { color: #ddd; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffInsert:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #888; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffDelete:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #999; } ' +
 
// Error
if (wDiff.htmlInsertStart === undefined) { wDiff.htmlInsertStart = '<span class="wDiffInsert" style="' + wDiff.styleInsert + '" title="+">'; }
'.wikEdDiffError .wikEdDiffFragment,' +
if (wDiff.htmlInsertEnd === undefined) { wDiff.htmlInsertEnd = '</span><!--wDiffInsert-->'; }
'.wikEdDiffError .wikEdDiffNoChange' +
'{ background: #faa; }'
};
 
/** Add regular expressions to configuration settings. */
if (wDiff.htmlBlockLeftStart === undefined) { wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft wDiffBlock{class}" style="' + wDiff.styleBlockLeft + '{block}" title="▶" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
if (wDiff.htmlBlockLeftEnd === undefined) { wDiff.htmlBlockLeftEnd = '</span><!--wDiffBlockLeft-->'; }
 
this.config.regExp = {
if (wDiff.htmlBlockRightStart === undefined) { wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight wDiffBlock{class}" style="' + wDiff.styleBlockRight + '{block}" title="◀" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
if (wDiff.htmlBlockRightEnd === undefined) { wDiff.htmlBlockRightEnd = '</span><!--wDiffBlockRight-->'; }
 
// RegExps for splitting text
if (wDiff.htmlMarkRight === undefined) { wDiff.htmlMarkRight = '<span class="wDiffMarkRight wDiffMark{class}" style="' + wDiff.styleMarkRight + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->'; }
'split': {
if (wDiff.htmlMarkLeft === undefined) { wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft wDiffMark{class}" style="' + wDiff.styleMarkLeft + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->'; }
 
// Split into paragraphs, after double newlines
if (wDiff.htmlNewline === undefined) { wDiff.htmlNewline = '<span class="wDiffNewline" style="' + wDiff.styleNewline + '"></span>\n'; }
'paragraph': new RegExp(
if (wDiff.htmlTab === undefined) { wDiff.htmlTab = '<span class="wDiffTab" style="' + wDiff.styleTab + '">\t</span>'; }
'(\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
']',
'g'
),
 
// Split into lines
//
'line': new RegExp(
// html for shorten output
'\\r\\n|\\n|\\r|[' +
//
this.config.regExpNewLinesAll +
']',
'g'
),
 
// Split into sentences /[^ ].*?[.!?:;]+(?= |$)/
if (wDiff.htmlFragmentStart === undefined) { wDiff.htmlFragmentStart = '<pre class="wDiffFragment" style="' + wDiff.styleFragment + '">'; }
'sentence': new RegExp(
if (wDiff.htmlFragmentEnd === undefined) { wDiff.htmlFragmentEnd = '</pre>'; }
'[^' +
this.config.regExpBlanks +
'].*?[.!?:;' +
this.config.regExpFullStops +
this.config.regExpExclamationMarks +
this.config.regExpQuestionMarks +
']+(?=[' +
this.config.regExpBlanks +
']|$)',
'g'
),
 
// Split into inline chunks
if (wDiff.htmlNoChange === undefined) { wDiff.htmlNoChange = '<pre class="wDiffNoChange" style="' + wDiff.styleNoChange + '" title="="></pre>'; }
'chunk': new RegExp(
if (wDiff.htmlSeparator === undefined) { wDiff.htmlSeparator = '<div class="wDiffSeparator" style="' + wDiff.styleSeparator + '"></div>'; }
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
if (wDiff.htmlOmittedChars === undefined) { wDiff.htmlOmittedChars = '<span class="wDiffOmittedChars" style="' + wDiff.styleOmittedChars + '">…</span>'; }
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
 
// Split into words, multi-char markup, and chars
// regExpLetters speed-up: \\w+
'word': new RegExp(
'(\\w+|[_' +
this.config.regExpLetters +
'])+([\'’][_' +
this.config.regExpLetters +
']*)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.',
'g'
),
 
// Split into chars
//
'character': /./g
// javascript handler for output code, compatible with IE 8
},
//
 
// RegExp to detect blank tokens
// wDiff.BlockHandler: event handler for block and mark elements
'blankOnlyToken': new RegExp(
if (wDiff.BlockHandler === undefined) { wDiff.BlockHandler = function (event, element, type) {
'[^' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']'
),
 
// RegExps for sliding gaps: newlines and space/word breaks
// IE compatibility
'slideStop': new RegExp(
if ( (event === undefined) && (window.event !== undefined) ) {
'[' +
event = window.event;
this.config.regExpNewLinesAll +
}
this.config.regExpNewParagraph +
']$'
),
'slideBorder': new RegExp(
'[' +
this.config.regExpBlanks +
']$'
),
 
// RegExps for counting words
// get mark/block elements
'countWords': new RegExp(
var number = element.id.replace(/\D/g, '');
'(\\w+|[_' +
var block = document.getElementById('wDiffBlock' + number);
this.config.regExpLetters +
var mark = document.getElementById('wDiffMark' + number);
'])+([\'’][_' +
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
// highlight corresponding mark/block pairs
'blankBlock': /^([^\t\S]+|[^\t])$/,
if (type == 'mouseover') {
element.onmouseover = null;
element.onmouseout = function (event) { wDiff.BlockHandler(event, element, 'mouseout'); };
element.onclick = function (event) { wDiff.BlockHandler(event, element, 'click'); };
block.className += ' wDiffBlockHighlight';
mark.className += ' wDiffMarkHighlight';
}
 
// RegExps for clipping
// remove mark/block highlighting
'clipLine': new RegExp(
if ( (type == 'mouseout') || (type == 'click') ) {
'[' + this.config.regExpNewLinesAll +
element.onmouseout = null;
this.config.regExpNewParagraph +
element.onmouseover = function (event) { wDiff.BlockHandler(event, element, 'mouseover'); };
']+',
'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. */
// getElementsByClassName
var container = element.parentNode;
var spans = container.getElementsByTagName('span');
for (var i = 0; i < spans.length; i ++) {
if ( ( (spans[i] != block) && (spans[i] != mark) ) || (type != 'click') ) {
if (spans[i].className.indexOf(' wDiffBlockHighlight') != -1) {
spans[i].className = spans[i].className.replace(/ wDiffBlockHighlight/g, '');
}
else if (spans[i].className.indexOf(' wDiffMarkHighlight') != -1) {
spans[i].className = spans[i].className.replace(/ wDiffMarkHighlight/g, '');
}
}
}
}
 
this.config.msg = {
// scroll to corresponding mark/block element
'wiked-diff-empty': '(No difference)',
if (type == 'click') {
'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!'
};
 
/**
// get corresponding element
* Add output html fragments to configuration settings.
var corrElement;
* Dynamic replacements:
if (element == block) {
* {number}: class/color/block/mark/id number
corrElement = mark;
* {title}: title attribute (popup)
}
* {nounicode}: noUnicodeSymbols fallback
else {
*/
corrElement = block;
this.config.htmlCode = {
}
'noChangeStart':
'<div class="wikEdDiffNoChange" title="' +
this.config.msg['wiked-diff-same'] +
'">',
'noChangeEnd': '</div>',
 
'containerStart': '<div class="wikEdDiffContainer" id="wikEdDiffContainer">',
// getOffsetTop
'containerEnd': '</div>',
var corrElementPos = 0;
var node = corrElement;
do {
corrElementPos += node.offsetTop;
} while ( (node = node.offsetParent) !== null );
 
'fragmentStart': '<pre class="wikEdDiffFragment" style="white-space: pre-wrap;">',
// scroll element under mouse cursor
'fragmentEnd': '</pre>',
var top;
'separator': '<div class="wikEdDiffSeparator"></div>',
if (window.pageXOffset !== undefined) {
top = window.pageXOffset;
}
else {
document.documentElement.scrollTop;
}
 
'insertStart':
var cursor;
'<span class="wikEdDiffInsert" title="' +
if (event.pageY !== undefined) {
this.config.msg['wiked-diff-ins'] +
cursor = event.pageY;
} '">',
'insertStartBlank':
else if (event.clientY !== undefined) {
'<span class="wikEdDiffInsert wikEdDiffInsertBlank" title="' +
cursor = event.clientY + top;
this.config.msg['wiked-diff-ins'] +
}
'">',
'insertEnd': '</span>',
 
'deleteStart':
var line = 12;
'<span class="wikEdDiffDelete" title="' +
if (window.getComputedStyle !== undefined) {
this.config.msg['wiked-diff-del'] +
line = parseInt(window.getComputedStyle(corrElement).getPropertyValue('line-height'));
} '">',
'deleteStartBlank':
window.scroll(0, corrElementPos + top - cursor + line / 2);
'<span class="wikEdDiffDelete wikEdDiffDeleteBlank" title="' +
}
this.config.msg['wiked-diff-del'] +
return;
'">',
}; }
'deleteEnd': '</span>',
 
'blockStart':
//
'<span class="wikEdDiffBlock"' +
// start of diff code
'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':
// wDiff.Init: initialize wDiff
'<span class="wikEdDiffMarkRight{nounicode}"' +
// called from: on code load
'title="{title}" id="wikEdDiffMark{number}"' +
// calls: wDiff.AddStyleSheet()
'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>',
wDiff.Init = function () {
'tab': '<span class="wikEdDiffTab"><span class="wikEdDiffTabSymbol"></span>\t</span>',
'space': '<span class="wikEdDiffSpace"><span class="wikEdDiffSpaceSymbol"></span> </span>',
 
'omittedChars': '<span class="wikEdDiffOmittedChars">…</span>',
// compatibility fixes for old names of functions
window.StringDiff = wDiff.Diff;
window.WDiffString = wDiff.Diff;
window.WDiffShortenOutput = wDiff.ShortenOutput;
 
'errorStart': '<div class="wikEdDiffError" title="Error: diff not consistent with versions!">',
// shortcut to wikEd.Debug()
'errorEnd': '</div>'
if (WED === undefined) {
};
if (typeof console == 'object') {
 
WED = console.log;
/*
* Add JavaScript event handler function to configuration settings
* Highlights corresponding block and mark elements on hover and jumps between them on click
* Code for use in non-jQuery environments and legacy browsers (at least IE 8 compatible)
*
* @option Event|undefined event Browser event if available
* @option element Node DOM node
* @option type string Event type
*/
this.config.blockHandler = function ( event, element, type ) {
 
// IE compatibility
if ( event === undefined && window.event !== undefined ) {
event = window.event;
}
 
else {
// Get mark/block elements
WED = window.alert;
var number = element.id.replace( /\D/g, '' );
var block = document.getElementById( 'wikEdDiffBlock' + number );
var mark = document.getElementById( 'wikEdDiffMark' + number );
if ( block === null || mark === null ) {
return;
}
}
 
// Highlight corresponding mark/block pairs
// add styles to head
if ( type === 'mouseover' ) {
wDiff.AddStyleSheet(wDiff.stylesheet);
element.onmouseover = null;
element.onmouseout = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'mouseout' );
};
element.onclick = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'click' );
};
block.className += ' wikEdDiffBlockHighlight';
mark.className += ' wikEdDiffMarkHighlight';
}
 
// Remove mark/block highlighting
// add block handler to head if running under Greasemonkey
if (typeof GM_infotype === 'objectmouseout' || type === 'click' ) {
element.onmouseout = null;
var script = 'var wDiff; if (wDiff === undefined) { wDiff = {}; } wDiff.BlockHandler = ' + wDiff.BlockHandler.toString();
element.onmouseover = function ( event ) {
wDiff.AddScript(script);
window.wikEdDiffBlockHandler( event, element, 'mouseover' );
}
};
return;
};
 
// Reset, allow outside container (e.g. legend)
if ( type !== 'click' ) {
block.className = block.className.replace( / wikEdDiffBlockHighlight/g, '' );
mark.className = mark.className.replace( / wikEdDiffMarkHighlight/g, '' );
 
// GetElementsByClassName
// wDiff.Diff: main method
var container = document.getElementById( 'wikEdDiffContainer' );
// input: oldString, newString, strings containing the texts to be diffed
if ( container !== null ) {
// called from: user code
var spans = container.getElementsByTagName( 'span' );
// calls: wDiff.Split(), wDiff.SplitRefine(), wDiff.CalculateDiff(), wDiff.DetectBlocks(), wDiff.AssembleDiff()
var spansLength = spans.length;
// returns: diff html code, call wDiff.ShortenOutput() for shortening this output
for ( var i = 0; i < spansLength; i ++ ) {
if ( spans[i] !== block && spans[i] !== mark ) {
if ( spans[i].className.indexOf( ' wikEdDiffBlockHighlight' ) !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffBlockHighlight/g, '' );
}
else if ( spans[i].className.indexOf( ' wikEdDiffMarkHighlight') !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffMarkHighlight/g, '' );
}
}
}
}
}
}
 
// Scroll to corresponding mark/block element
wDiff.Diff = function (oldString, newString) {
if ( type === 'click' ) {
 
// Get corresponding element
var diff = '';
var corrElement;
if ( element === block ) {
corrElement = mark;
}
else {
corrElement = block;
}
 
// Get element height (getOffsetTop)
// IE / Mac fix
var corrElementPos = 0;
oldString = oldString.replace(/\r\n?/g, '\n');
var node = corrElement;
newString = newString.replace(/\r\n?/g, '\n');
do {
corrElementPos += node.offsetTop;
} while ( ( node = node.offsetParent ) !== null );
 
// Get scroll height
// prepare text data object
var text = {top;
if ( window.pageYOffset !== undefined ) {
newText: {
top = window.pageYOffset;
string: newString,
tokens: [],}
first:else null,{
top = document.documentElement.scrollTop;
last: null
},
 
oldText: {
// Get cursor pos
string: oldString,
tokens:var [],cursor;
if ( event.pageY !== undefined ) {
first: null,
cursor = event.pageY;
last: null
},
else if ( event.clientY !== undefined ) {
diff: ''
cursor = event.clientY + top;
}
 
// Get line height
var line = 12;
if ( window.getComputedStyle !== undefined ) {
line = parseInt( window.getComputedStyle( corrElement ).getPropertyValue( 'line-height' ) );
}
 
// Scroll element under mouse cursor
window.scroll( 0, corrElementPos + top - cursor + line / 2 );
}
return;
};
 
/** Internal data structures. */
// trap trivial changes: no change
if (oldString == newString) {
text.diff = wDiff.HtmlEscape(newString);
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var WikEdDiffText newText New text version object with text and token list */
// trap trivial changes: old text deleted
this.newText = null;
if ( (oldString === null) || (oldString.length === 0) ) {
text.diff = wDiff.htmlInsertStart + wDiff.HtmlEscape(newString) + wDiff.htmlInsertEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var WikEdDiffText oldText Old text version object with text and token list */
// trap trivial changes: new text deleted
this.oldText = null;
if ( (newString === null) || (newString.length === 0) ) {
text.diff = wDiff.htmlDeleteStart + wDiff.HtmlEscape(oldString) + wDiff.htmlDeleteEnd;
wDiff.HtmlFormat(text);
return text.diff;
}
 
/** @var object symbols Symbols table for whole text at all refinement levels */
// new symbols object
var this.symbols = {
token: [],
hashhashTable: {},
linked: false
};
 
/** @var array bordersDown Matched region borders downwards */
// split new and old text into paragraps
this.bordersDown = [];
wDiff.Split(text.newText, 'paragraph');
wDiff.Split(text.oldText, 'paragraph');
 
/** @var array bordersUp Matched region borders upwards */
// calculate diff
this.bordersUp = [];
wDiff.CalculateDiff(text, symbols, 'paragraph');
 
/** @var array blocks Block data (consecutive text tokens) in new text order */
// refine different paragraphs into sentences
this.blocks = [];
wDiff.SplitRefine(text.newText, 'sentence');
wDiff.SplitRefine(text.oldText, 'sentence');
 
/** @var int maxWords Maximal detected word count of all linked blocks */
// calculate refined diff
this.maxWords = 0;
wDiff.CalculateDiff(text, symbols, 'sentence');
 
/** @var array groups Section blocks that are consecutive in old text order */
// refine different paragraphs into chunks
this.groups = [];
wDiff.SplitRefine(text.newText, 'chunk');
wDiff.SplitRefine(text.oldText, 'chunk');
 
/** @var array sections Block sections with no block move crosses outside a section */
// calculate refined diff
this.sections = [];
wDiff.CalculateDiff(text, symbols, 'chunk');
 
/** @var object timer Debug timer array: string 'label' => float milliseconds. */
// refine different sentences into words
this.timer = {};
wDiff.SplitRefine(text.newText, 'word');
wDiff.SplitRefine(text.oldText, 'word');
 
/** @var array recursionTimer Count time spent in recursion level in milliseconds. */
// calculate refined diff information with recursion for unresolved gaps
this.recursionTimer = [];
wDiff.CalculateDiff(text, symbols, 'word', true);
 
//** bubbleOutput updata. gaps*/
wDiff.BubbleUpGaps(text.newText, text.oldText);
wDiff.BubbleUpGaps(text.oldText, text.newText);
 
/** @var bool error Unit tests have detected a diff error */
// split tokens into chars in selected unresolved gaps
this.error = false;
if (wDiff.charDiff === true) {
wDiff.SplitRefineChars(text);
 
/** @var array fragments Diff fragment list for markup, abstraction layer for customization */
// calculate refined diff information with recursion for unresolved gaps
this.fragments = [];
wDiff.CalculateDiff(text, symbols, 'character', true);
}
 
/** @var string html Html code of diff */
// bubble up gaps
this.html = '';
wDiff.BubbleUpGaps(text.newText, text.oldText);
wDiff.BubbleUpGaps(text.oldText, text.newText);
 
// enumerate tokens lists
wDiff.EnumerateTokens(text.newText);
wDiff.EnumerateTokens(text.oldText);
 
/**
// detect moved blocks
* Constructor, initialize settings, load js and css.
var blocks = [];
*
var groups = [];
* @param[in] object wikEdDiffConfig Custom customization settings
wDiff.DetectBlocks(text, blocks, groups);
* @param[out] object config Settings
*/
 
this.init = function () {
// assemble diff blocks into formatted html text
diff = wDiff.AssembleDiff(text, blocks, groups);
 
// Import customizations from wikEdDiffConfig{}
return diff;
if ( typeof wikEdDiffConfig === 'object' ) {
};
this.deepCopy( wikEdDiffConfig, this.config );
}
 
// Add CSS stylescheet
this.addStyleSheet( this.config.stylesheet );
 
// Load block handler script
// wDiff.Split: split text into paragraph, sentence, or word tokens
if ( this.config.showBlockMoves === true ) {
// input: text (text.newText or text.oldText), object containing text data and strings; regExp, regular expression for splitting text into tokens; token, tokens index of token to be split
// changes: text (text.newText or text.oldText): text.tokens list, text.first, text.last
// called from: wDiff.Diff()
 
// Add block handler to head if running under Greasemonkey
wDiff.Split = function (text, level, token) {
if ( typeof GM_info === 'object' ) {
var script = 'var wikEdDiffBlockHandler = ' + this.config.blockHandler.toString() + ';';
this.addScript( script );
}
else {
window.wikEdDiffBlockHandler = this.config.blockHandler;
}
}
return;
};
 
var prev = null;
var next = null;
var current = text.tokens.length;
var first = current;
var string = '';
 
/**
// split full text or specified token
* Main diff method.
if (token === undefined) {
*
string = text.string;
* @param string oldString Old text version
}
* @param string newString New text version
else {
* @param[out] array fragment
prev = text.tokens[token].prev;
* Diff fragment list ready for markup, abstraction layer for customized diffs
next = text.tokens[token].next;
* @param[out] string html Html code of diff
string = text.tokens[token].token;
* @return string Html code of diff
}
*/
this.diff = function ( oldString, newString ) {
 
// Start total timer
// split text into tokens, regExp match as separator
if ( this.config.timer === true ) {
var number = 0;
this.time( 'total' );
var split = [];
var regExpMatch;
var lastIndex = 0;
while ( (regExpMatch = wDiff.regExpSplit[level].exec(string)) !== null) {
if (regExpMatch.index > lastIndex) {
split.push(string.substring(lastIndex, regExpMatch.index));
}
split.push(regExpMatch[0]);
lastIndex = wDiff.regExpSplit[level].lastIndex;
}
if (lastIndex < string.length) {
split.push(string.substring(lastIndex));
}
 
// Start diff timer
// cycle trough new tokens
if ( this.config.timer === true ) {
for (var i = 0; i < split.length; i ++) {
this.time( 'diff' );
}
 
// Reset error flag
// insert current item, link to previous
this.error = false;
text.tokens[current] = {
token: split[i],
prev: prev,
next: null,
link: null,
number: null,
parsed: false,
unique: false
};
number ++;
 
// linkStrip previoustrailing itemnewline to(.js currentonly)
if (prev !this.config.stripTrailingNewline === true null) {
if ( newString.substr( -1 ) === '\n' && oldString.substr( -1 === '\n' ) ) {
text.tokens[prev].next = current;
newString = newString.substr( 0, newString.length - 1 );
oldString = oldString.substr( 0, oldString.length - 1 );
}
}
prev = current;
current ++;
}
 
// Load version strings into WikEdDiffText objects
// connect last new item and existing next item
this.newText = new WikEdDiff.WikEdDiffText( newString, this );
if ( (number > 0) && (token !== undefined) ) {
this.oldText = new WikEdDiff.WikEdDiffText( oldString, this );
if (prev !== null) {
 
text.tokens[prev].next = next;
// Trap trivial changes: no change
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;
}
 
if (next !== null) {
// Trap trivial changes: old text deleted
text.tokens[next].prev = prev;
if (
this.oldText.text === '' || (
this.oldText.text === '\n' &&
( this.newText.text.charAt( this.newText.text.length - 1 ) === '\n' )
)
) {
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
// set text first and last token index
if (number > 0) {
this.newText.text === '' || (
this.newText.text === '\n' &&
( this.oldText.text.charAt( this.oldText.text.length - 1 ) === '\n' )
)
) {
this.html =
this.config.htmlCode.containerStart +
this.config.htmlCode.fragmentStart +
this.config.htmlCode.deleteStart +
this.htmlEscape( this.oldText.text ) +
this.config.htmlCode.deleteEnd +
this.config.htmlCode.fragmentEnd +
this.config.htmlCode.containerEnd;
return this.html;
}
 
// initialSplit new and old text splitinto paragraps
if (token this.config.timer === undefinedtrue ) {
textthis.firsttime( ='paragraph 0split' );
}
text.last = prev;
this.newText.splitText( 'paragraph' );
this.oldText.splitText( 'paragraph' );
if ( this.config.timer === true ) {
this.timeEnd( 'paragraph split' );
}
 
// Calculate diff
// first or last token has been split
this.calculateDiff( 'line' );
else {
 
if (token == text.first) {
// Refine different paragraphs into lines
text.first = first;
if ( this.config.timer === true ) {
}
this.time( 'line split' );
if (token == text.last) {
}
text.last = prev;
this.newText.splitRefine( 'line' );
}
this.oldText.splitRefine( 'line' );
if ( this.config.timer === true ) {
this.timeEnd( 'line split' );
}
}
return;
};
 
// Calculate refined diff
this.calculateDiff( 'line' );
 
// Refine different lines into sentences
// 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( 'sentence split' );
// called from: wDiff.Diff()
}
// calls: wDiff.Split()
this.newText.splitRefine( 'sentence' );
this.oldText.splitRefine( 'sentence' );
if ( this.config.timer === true ) {
this.timeEnd( 'sentence split' );
}
 
// Calculate refined diff
wDiff.SplitRefine = function (text, regExp) {
this.calculateDiff( 'sentence' );
 
// Refine different sentences into chunks
// cycle through tokens list
if ( this.config.timer === true ) {
var i = text.first;
this.time( 'chunk split' );
while ( (i !== null) && (text.tokens[i] !== null) ) {
}
this.newText.splitRefine( 'chunk' );
this.oldText.splitRefine( 'chunk' );
if ( this.config.timer === true ) {
this.timeEnd( 'chunk split' );
}
 
// Calculate refined diff
// refine unique unmatched tokens into smaller tokens
this.calculateDiff( 'chunk' );
if (text.tokens[i].link === null) {
 
wDiff.Split(text, regExp, i);
// Refine different chunks into words
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' );
}
i = text.tokens[i].next;
}
return;
};
 
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( 'word', true );
 
// Slide gaps
// wDiff.SplitRefineChars: split tokens into chars in the following unresolved regions (gaps):
if ( this.config.timer === true ) {
// - one token became separated by space, dash, or any string
this.time( 'word slide' );
// - same number of tokens in gap and strong similarity of all tokens:
}
// - addition or deletion of flanking strings in tokens
this.slideGaps( this.newText, this.oldText );
// - addition or deletion of internal string in tokens
this.slideGaps( this.oldText, this.newText );
// - same length and at least 50 % identity
if ( this.config.timer === true ) {
// - same start or end, same text longer than different text
this.timeEnd( 'word slide' );
// - 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
 
// Split tokens into chars
wDiff.SplitRefineChars = function (text) {
if ( this.config.charDiff === true ) {
 
// Split tokens into chars in selected unresolved gaps
//
if ( this.config.timer === true ) {
// find corresponding gaps
this.time( 'character split' );
//
}
this.splitRefineChars();
if ( this.config.timer === true ) {
this.timeEnd( 'character split' );
}
 
// Calculate refined diff information with recursion for unresolved gaps
// cycle trough new text tokens list
this.calculateDiff( 'character', true );
var gaps = [];
var gap = null;
var i = text.newText.first;
var j = text.oldText.first;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
 
// getSlide token linksgaps
if ( this.config.timer === true ) {
var newLink = text.newText.tokens[i].link;
this.time( 'character slide' );
var oldLink = null;
}
if (j !== null) {
oldLinkthis.slideGaps( =this.newText, textthis.oldText.tokens[j].link );
this.slideGaps( this.oldText, this.newText );
if ( this.config.timer === true ) {
this.timeEnd( 'character slide' );
}
}
 
// Free memory
// start of gap in new and old
this.symbols = undefined;
if ( (gap === null) && (newLink === null) && (oldLink === null) ) {
this.bordersDown = undefined;
gap = gaps.length;
this.bordersUp = undefined;
gaps.push({
this.newText.words = undefined;
newFirst: i,
this.oldText.words = undefined;
newLast: i,
 
newTokens: 1,
// Enumerate token lists
oldFirst: j,
this.newText.enumerateTokens();
oldLast: j,
this.oldText.enumerateTokens();
oldTokens: null,
 
charSplit: null
// Detect moved blocks
});
if ( this.config.timer === true ) {
this.time( 'blocks' );
}
this.detectBlocks();
if ( this.config.timer === true ) {
this.timeEnd( 'blocks' );
}
 
// Free memory
// count chars and tokens in gap
this.newText.tokens = undefined;
else if ( (gap !== null) && (newLink === null) ) {
this.oldText.tokens = undefined;
gaps[gap].newLast = i;
 
gaps[gap].newTokens ++;
// Assemble blocks into fragment table
this.getDiffFragments();
 
// Free memory
this.blocks = undefined;
this.groups = undefined;
this.sections = undefined;
 
// Stop diff timer
if ( this.config.timer === true ) {
this.timeEnd( 'diff' );
}
 
// gapUnit endedtests
else if ( (gapthis.config.unitTesting !== null) && (newLink !== null)true ) {
 
gap = null;
// Test diff to test consistency between input and output
if ( this.config.timer === true ) {
this.time( 'unit tests' );
}
this.unitTests();
if ( this.config.timer === true ) {
this.timeEnd( 'unit tests' );
}
}
 
// next list elementsClipping
if (newLink !this.config.fullDiff === false null) {
 
j = text.oldText.tokens[newLink].next;
// Clipping unchanged sections from unmoved block text
if ( this.config.timer === true ) {
this.time( 'clip' );
}
this.clipDiffFragments();
if ( this.config.timer === true ) {
this.timeEnd( 'clip' );
}
}
i = text.newText.tokens[i].next;
}
 
// Create html formatted diff code from diff fragments
// cycle trough gaps and add old text gap data
if ( this.config.timer === true ) {
for (var gap = 0; gap < gaps.length; gap ++) {
this.time( 'html' );
}
this.getDiffHtml();
if ( this.config.timer === true ) {
this.timeEnd( 'html' );
}
 
// No change
// cycle trough old text tokens list
if ( this.html === '' ) {
var j = gaps[gap].oldFirst;
this.html =
while ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.oldText.tokens[j].link === 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;
}
 
// Add error indicator
// count old chars and tokens in gap
if ( this.error === true ) {
gaps[gap].oldLast = j;
this.html = this.config.htmlCode.errorStart + this.html + this.config.htmlCode.errorEnd;
gaps[gap].oldTokens ++;
}
 
// Stop total timer
j = text.oldText.tokens[j].next;
if ( this.config.timer === true ) {
this.timeEnd( 'total' );
}
}
 
return this.html;
//
};
// select gaps of identical token number and strong similarity of all tokens
//
 
for (var gap = 0; gap < gaps.length; gap ++) {
var charSplit = true;
 
/**
// not same gap length
* Split tokens into chars in the following unresolved regions (gaps):
if (gaps[gap].newTokens != gaps[gap].oldTokens) {
* - 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. */
// one word became separated by space, dash, or any string
 
if ( (gaps[gap].newTokens == 1) && (gaps[gap].oldTokens == 3) ) {
// Cycle through new text tokens list
if (text.newText.tokens[ gaps[gap].newFirst ].token != text.oldText.tokens[ gaps[gap].oldFirst ].token + text.oldText.tokens[ gaps[gap].oldLast ].token ) {
var gaps = [];
continue;
var gap = null;
}
var i = this.newText.first;
var j = this.oldText.first;
while ( i !== null ) {
 
// Get token links
var newLink = this.newText.tokens[i].link;
var oldLink = null;
if ( j !== null ) {
oldLink = this.oldText.tokens[j].link;
}
 
else if ( (gaps[gap].oldTokens == 1) && (gaps[gap].newTokens == 3) ) {
// Start of gap in new and old
if (text.oldText.tokens[ gaps[gap].oldFirst ].token != text.newText.tokens[ gaps[gap].newFirst ].token + text.newText.tokens[ gaps[gap].newLast ].token ) {
if ( gap === null && newLink === null && oldLink === null ) {
continue;
gap = gaps.length;
}
gaps.push( {
newFirst: i,
newLast: i,
newTokens: 1,
oldFirst: j,
oldLast: j,
oldTokens: null,
charSplit: null
} );
}
 
else {
// Count chars and tokens in gap
continue;
else if ( gap !== null && newLink === null ) {
gaps[gap].newLast = i;
gaps[gap].newTokens ++;
}
 
// Gap ended
else if ( gap !== null && newLink !== null ) {
gap = null;
}
 
// Next list elements
if ( newLink !== null ) {
j = this.oldText.tokens[newLink].next;
}
i = this.newText.tokens[i].next;
}
 
// cycleCycle troughthrough newgaps textand tokensadd listold andtext setgap charSplitdata
var igapsLength = gaps[gap].newFirstlength;
for ( var jgap = gaps[0; gap].oldFirst < gapsLength; gap ++ ) {
while (i !== null) {
var newToken = text.newText.tokens[i].token;
var oldToken = text.oldText.tokens[j].token;
 
// getCycle shorterthrough andold longertext tokentokens list
var shorterTokenj = gaps[gap].oldFirst;
while (
var longerToken;
j !== null &&
if (newToken.length < oldToken.length) {
this.oldText.tokens[j] !== null &&
shorterToken = newToken;
this.oldText.tokens[j].link === null
longerToken = oldToken;
}) {
 
else {
// Count old chars and tokens in gap
shorterToken = oldToken;
longerTokengaps[gap].oldLast = newTokenj;
gaps[gap].oldTokens ++;
 
j = this.oldText.tokens[j].next;
}
}
 
/** Select gaps of identical token number and strong similarity of all tokens. */
// not same token length
if (newToken.length != oldToken.length) {
 
var gapsLength = gaps.length;
// test for addition or deletion of internal string in tokens
for ( var gap = 0; gap < gapsLength; gap ++ ) {
var charSplit = true;
 
// Not same gap length
// find number of identical chars from left
if ( gaps[gap].newTokens !== gaps[gap].oldTokens ) {
var left = 0;
 
while (left < shorterToken.length) {
// One word became separated by space, dash, or any string
if (newToken.charAt(left) != oldToken.charAt(left)) {
if ( gaps[gap].newTokens === 1 && gaps[gap].oldTokens === 3 ) {
break;
var token = this.newText.tokens[ gaps[gap].newFirst ].token;
var tokenFirst = this.oldText.tokens[ gaps[gap].oldFirst ].token;
var tokenLast = this.oldText.tokens[ gaps[gap].oldLast ].token;
if (
token.indexOf( tokenFirst ) !== 0 ||
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
left ++;
}
else if ( gaps[gap].oldTokens === 1 && gaps[gap].newTokens === 3 ) {
 
var token = this.oldText.tokens[ gaps[gap].oldFirst ].token;
// find number of identical chars from right
var tokenFirst = this.newText.tokens[ gaps[gap].newFirst ].token;
var right = 0;
var tokenLast = this.newText.tokens[ gaps[gap].newLast ].token;
while (right < shorterToken.length) {
if (
if (newToken.charAt(newToken.length - 1 - right) != oldToken.charAt(oldToken.length - 1 - right)) {
token.indexOf( tokenFirst ) !== 0 ||
break;
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
right ++;
}
else {
continue;
}
gaps[gap].charSplit = true;
}
 
// Cycle through new text tokens list and set charSplit
// no simple insertion or deletion of internal string
else {
if (left + right != shorterToken.length) {
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
while ( i !== null ) {
var newToken = this.newText.tokens[i].token;
var oldToken = this.oldText.tokens[j].token;
 
// Get shorter and longer token
// not addition or deletion of flanking strings in tokens (smaller token not part of larger token)
ifvar (longerToken.indexOf(shorterToken) == -1) {;
var longerToken;
if ( newToken.length < oldToken.length ) {
shorterToken = newToken;
longerToken = oldToken;
}
else {
shorterToken = oldToken;
longerToken = newToken;
}
 
// Not same text at start or end shorter than differenttoken textlength
if ( (left < shorterTokennewToken.length /!== 2) && (right < shorterTokenoldToken.length / 2) ) {
 
// doTest notfor splitaddition intoor charsdeletion thisof gapinternal string in tokens
 
charSplit = false;
// Find number of identical chars from left
break;
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;
}
}
}
}
}
}
 
// sameSame token length
else if ( newToken !== oldToken ) {
 
// tokensTokens less than 50 % identical
var ident = 0;
for ( var postokenLength = 0; pos < shorterToken.length; pos ++) {
if for (shorterToken.charAt( var pos) == longerToken.charAt(0; pos) < tokenLength; pos ++ ) {
if ( shorterToken.charAt( pos ) === longerToken.charAt( pos ) ) {
ident ++;
ident ++;
}
}
if ( ident / shorterToken.length < 0.49 ) {
 
// Do not split into chars this gap
charSplit = false;
break;
}
}
}
if (ident/shorterToken.length < 0.49) {
 
// doNext notlist split into chars this gapelements
if ( i === gaps[gap].newLast ) {
charSplit = false;
break;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
gaps[gap].charSplit = charSplit;
}
 
// next list elements
if (i == gaps[gap].newLast) {
break;
}
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
gaps[gap].charSplit = charSplit;
}
 
/** Refine words into chars in selected gaps. */
//
// refine words into chars in selected gaps
//
 
for ( var gapgapsLength = 0; gap < gaps.length; gap ++) {
iffor (gaps[ var gap].charSplit === true0; gap < gapsLength; gap ++ ) {
if ( gaps[gap].charSplit === true ) {
 
// cycleCycle troughthrough new text tokens list, link spaces, and split into chars
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
var newGapLength = i - gaps[gap].newLast;
while (i !== null) {
var newTokenoldGapLength = text.newText.tokensj - gaps[igap].tokenoldLast;
while ( i !== null || j !== null ) {
var oldToken = text.oldText.tokens[j].token;
 
// linkLink identical tokens (spaces) to keep char refinement to words
if (newToken == oldToken) {
newGapLength === oldGapLength &&
text.newText.tokens[i].link = j;
text this.oldTextnewText.tokens[ji].linktoken === i;this.oldText.tokens[j].token
} ) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
 
// refine differentRefine words into chars
else {
if ( i !== null ) {
wDiff.Split(text.newText, 'character', i);
wDiff this.Split(textnewText.oldText,splitText( 'character', ji );
}
if ( j !== null ) {
this.oldText.splitText( 'character', j );
}
}
 
// nextNext list elements
if ( i === gaps[gap].newLast ) {
break i = null;
}
if ( j === gaps[gap].oldLast ) {
j = null;
}
if ( i !== null ) {
i = this.newText.tokens[i].next;
}
if ( j !== null ) {
j = this.oldText.tokens[j].next;
}
}
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
}
return;
}
};
 
// WED('Gap', wDiff.DebugGaps(gaps));
 
/**
return;
* Move gaps with ambiguous identical fronts to last newline border or otherwise last word border.
};
*
* @param[in/out] wikEdDiffText text, textLinked These two are newText and oldText
*/
this.slideGaps = function ( text, textLinked ) {
 
var regExpSlideBorder = this.config.regExp.slideBorder;
var regExpSlideStop = this.config.regExp.slideStop;
 
// Cycle through tokens list
// wDiff.BubbleUpGaps: move gaps with ambiguous identical fronts and backs up
var i = text.first;
// start ambiguous gap borders after line breaks and text section closing characters
var gapStart = null;
// changes: text (text.newText or text.oldText) .tokens list
while ( i !== null ) {
// called from: wDiff.Diff()
 
// Remember gap start
wDiff.BubbleUpGaps = function (text, textLinked) {
if ( gapStart === null && text.tokens[i].link === null ) {
gapStart = i;
}
 
// Find gap end
// cycle through tokens list
else if ( gapStart !== null && text.tokens[i].link !== null ) {
var i = text.first;
var gapStartgapFront = nullgapStart;
while var (gapBack (i !== null) && (text.tokens[i] !== null) ) {.prev;
 
// Slide down as deep as possible
// remember gap start
var front = gapFront;
if ( (gapStart === null) && (text.tokens[i].link === null) ) {
var back = text.tokens[gapBack].next;
gapStart = i;
} if (
front !== null &&
back !== null &&
text.tokens[front].link === null &&
text.tokens[back].link !== null &&
text.tokens[front].token === text.tokens[back].token
) {
text.tokens[front].link = text.tokens[back].link;
textLinked.tokens[ text.tokens[front].link ].link = front;
text.tokens[back].link = null;
 
gapFront = text.tokens[gapFront].next;
// find gap end
else gapBack if ( (gapStart !== null) && (text.tokens[igapBack].link !== null) ) {next;
 
front = text.tokens[front].next;
// bubble down as deep as possible
back = text.tokens[back].next;
var front = gapStart;
}
var back = i;
while (
(front !== null) && (back !== null) &&
(text.tokens[front].link === null) && (text.tokens[back].link !== null) &&
(text.tokens[front].token === text.tokens[back].token)
) {
text.tokens[front].link = text.tokens[back].link;
textLinked.tokens[ text.tokens[front].link ].link = front;
text.tokens[back].link = null;
front = text.tokens[front].next;
back = text.tokens[back].next;
}
 
// testTest baloonslide up, remember last line break or closingword textborder
var frontStopfront = nulltext.tokens[gapFront].prev;
var back = gapBack;
front = text.tokens[front].prev;
back var gapFrontBlankTest = regExpSlideBorder.test( text.tokens[backgapFront].prevtoken );
var frontTestfrontStop = front;
if ( text.tokens[back].link === null ) {
var backTest = back;
while (
(frontTest front !== null) && (backTest !== null) &&
back !== null &&
(text.tokens[frontTest].link !== null) && (text.tokens[backTest].link === null) &&
( text.tokens[frontTestfront].tokenlink !== text.tokens[backTest].token)null &&
text.tokens[front].token === text.tokens[back].token
) {
) {
frontTest = text.tokens[frontTest].prev;
if ( front !== null ) {
backTest = text.tokens[backTest].prev;
 
if (wDiff.regExpBubbleStop.test(text.tokens[frontTest].token) === true) {
frontStop // =Stop frontTest;at line break
if ( regExpSlideStop.test( text.tokens[front].token ) === true ) {
break;
frontStop = front;
break;
}
 
// Stop at first word border (blank/word or word/blank)
if (
regExpSlideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
frontStop = front;
}
}
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
}
else if ( (frontStop === null) && (wDiff.regExpBubbleClosing.test(text.tokens[frontTest].token) === true) ) {
frontStop = frontTest;
}
}
 
// actuallyActually bubbleslide up to last line break or closing textstop
var front = text.tokens[gapFront].prev;
if (frontStop !== null) {
var back = gapBack;
while (
(front !== null) && (
back !== null) && (
front !== frontStop) &&
(text.tokens[front].link !== null) && (
text.tokens[back].link === null) &&
(text.tokens[front].token === text.tokens[back].token)
) {
text.tokens[back].link = text.tokens[front].link;
textLinked.tokens[ text.tokens[back].link ].link = back;
text.tokens[front].link = null;
 
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
gapStart = null;
}
gapStarti = nulltext.tokens[i].next;
}
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; level: 'paragraph', 'sentence', 'word', or 'character'
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
// pass 3: connect unique matched tokens
// pass 4: connect adjacent identical tokens downwards
// pass 5: connect adjacent identical tokens upwards
// recursively diff still unresolved regions downwards
// recursively diff still unresolved regions upwards
 
wDiff.CalculateDiff = function (text, symbols, level, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
 
// set defaults
if (newStart === undefined) { newStart = text.newText.first; }
if (newEnd === undefined) { newEnd = text.newText.last; }
if (oldStart === undefined) { oldStart = text.oldText.first; }
if (oldEnd === undefined) { oldEnd = text.oldText.last; }
if (recursionLevel === undefined) { recursionLevel = 0; }
 
// limit recursion depth
if (recursionLevel > 10) {
return;
}
 
//
// pass 1: parse new text into symbol table
//
 
// cycle trough new text tokens list
var i = newStart;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
 
// add new entry to symbol table
var token = text.newText.tokens[i].token;
if (Object.prototype.hasOwnProperty.call(symbols.hash, token) === false) {
var current = symbols.token.length;
symbols.hash[token] = current;
symbols.token[current] = {
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
};
bordersDown = [];
bordersUp = [];
}
 
// or update existing entry
else {
 
// Updated versions of linked region borders
// increment token counter for new text
var hashToArraybordersUpNext = symbols.hash[token];
var bordersDownNext = [];
symbols.token[hashToArray].newCount ++;
}
 
/**
// next list element
* Pass 1: parse new text into symbol table.
if (i == newEnd) {
break; */
}
i = text.newText.tokens[i].next;
}
 
// Cycle through new text tokens list
//
var i = newStart;
// pass 2: parse old text into symbol table
while ( i !== null ) {
//
if ( this.newText.tokens[i].link === null ) {
 
// Add new entry to symbol table
// cycle trough old text tokens list
var token = this.newText.tokens[i].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: 1,
oldCount: 0,
newToken: i,
oldToken: null
} );
}
 
// addOr newupdate existing entry to symbol table
else {
var token = text.oldText.tokens[j].token;
if (Object.prototype.hasOwnProperty.call(symbols.hash, token) === false) {
var current = symbols.token.length;
symbols.hash[token] = current;
symbols.token[current] = {
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
};
}
 
// Increment token counter for new text
// or update existing entry
var hashToArray = symbols.hashTable[token];
else {
symbols.token[hashToArray].newCount ++;
}
}
 
// incrementStop tokenafter countergap forif old textrecursing
else if ( recursionLevel > 0 ) {
var hashToArray = symbols.hash[token];
break;
symbols.token[hashToArray].oldCount ++;
}
 
// addGet next token number for old text
if ( up === false ) {
symbols.token[hashToArray].oldToken = j;
i = this.newText.tokens[i].next;
}
else {
i = this.newText.tokens[i].prev;
}
}
 
/**
// next list element
* Pass 2: parse old text into symbol table.
if (j === oldEnd) {
break; */
}
j = text.oldText.tokens[j].next;
}
 
// Cycle through old text tokens list
//
var j = oldStart;
// pass 3: connect unique tokens
while ( j !== null ) {
//
if ( this.oldText.tokens[j].link === null ) {
 
// cycleAdd troughnew entry to symbol arraytable
for ( var itoken = 0; i < symbolsthis.oldText.tokens[j].token.length; i ++) {
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
symbols.hashTable[token] = symbols.token.length;
symbols.token.push( {
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
} );
}
 
// Or update existing entry
// find tokens in the symbol table that occur only once in both versions
else {
if ( (symbols.token[i].newCount == 1) && (symbols.token[i].oldCount == 1) ) {
var newToken = symbols.token[i].newToken;
var oldToken = symbols.token[i].oldToken;
 
// Increment token counter for old text
// do not use spaces as unique markers
var hashToArray = symbols.hashTable[token];
if (/^\s+$/.test(text.newText.tokens[newToken].token) === false) {
symbols.token[hashToArray].oldCount ++;
 
// connectAdd fromtoken newnumber tofor old and from old to newtext
symbols.token[hashToArray].oldToken = j;
if (text.newText.tokens[newToken].link === null) {
text.newText.tokens[newToken].link = oldToken;
text.oldText.tokens[oldToken].link = newToken;
symbols.linked = true;
if ( (level != 'character') && (recursionLevel === 0) ) {
text.newText.tokens[newToken].unique = true;
text.oldText.tokens[oldToken].unique = true;
}
}
}
 
// Stop after gap if recursing
else if ( recursionLevel > 0 ) {
break;
}
 
// Get next token
if ( up === false ) {
j = this.oldText.tokens[j].next;
}
else {
j = this.oldText.tokens[j].prev;
}
}
}
 
/**
//
// pass* 4Pass 3: connect adjacent identicalunique tokens downwards.
*/
//
 
// Cycle through symbol array
// cycle trough new text tokens list
var symbolsLength = symbols.token.length;
if (symbols.linked === true) {
for ( var i = 0; i < symbolsLength; i ++ ) {
var i = text.newText.first;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
var iNext = text.newText.tokens[i].next;
 
// Find tokens in the symbol table that occur only once in both versions
// find already connected pairs
if ( symbols.token[i].newCount === 1 && symbols.token[i].oldCount === 1 ) {
var j = text.newText.tokens[i].link;
var newToken = symbols.token[i].newToken;
if (j !== null) {
var jNextoldToken = textsymbols.oldText.tokenstoken[ji].nextoldToken;
var newTokenObj = this.newText.tokens[newToken];
var oldTokenObj = this.oldText.tokens[oldToken];
 
// checkConnect iffrom thenew followingto tokensold areand notfrom yetold connectedto new
if ( (iNextnewTokenObj.link !== null) && (jNext !== null) ) {
if ( (text.newText.tokens[iNext].link === null) && (text.oldText.tokens[jNext].link === null) ) {
 
// connectDo ifnot theuse followingspaces tokensas areunique the samemarkers
if (
if (text.newText.tokens[iNext].token == text.oldText.tokens[jNext].token) {
this.config.regExp.blankOnlyToken.test( newTokenObj.token ) === true
text.newText.tokens[iNext].link = jNext;
) {
text.oldText.tokens[jNext].link = iNext;
 
}
// Link new and old tokens
}
newTokenObj.link = oldToken;
}
oldTokenObj.link = newToken;
}
i symbols.linked = iNexttrue;
}
 
// Save linked region borders
//
bordersDown.push( [newToken, oldToken] );
// pass 5: connect adjacent identical tokens upwards
bordersUp.push( [newToken, oldToken] );
//
 
// cycleCheck troughif newtoken textcontains tokensunique listword
if ( recursionLevel === 0 ) {
var i = text.newText.last;
var unique = false;
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
if ( level === 'character' ) {
var iNext = text.newText.tokens[i].prev;
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 jwordsLength = textwords.newText.tokens[i].linklength;
if (j !=wordsLength >= nullthis.config.blockMinLength ) {
unique = true;
var jNext = text.oldText.tokens[j].prev;
}
 
// checkUnique if theit preceedingcontains 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 preceeding tokens are the same
if ( unique === true ) {
if (text.newText.tokens[iNext].token == text.oldText.tokens[jNext].token) {
text newTokenObj.newText.tokens[iNext].linkunique = jNexttrue;
text oldTokenObj.oldText.tokens[jNext].linkunique = iNexttrue;
}
}
}
}
}
i = iNext;
}
 
// Continue passes only if unique tokens have been linked previously
// refine by recursively diffing unresolved regions caused by addition of common tokens around sequences of common tokens, only at word level split
if ( (recurse === true) && (wDiffsymbols.recursiveDifflinked === true) ) {
 
//**
// recursively* diffPass still4: unresolvedconnect regionsadjacent identical tokens downwards.
/ */
 
// cycleCycle troughthrough list of linked new text tokens list
var ibordersLength = newStartbordersDown.length;
for ( var match = 0; match < bordersLength; match ++ ) {
var j = oldStart;
var i = bordersDown[match][0];
var j = bordersDown[match][1];
 
// Next down
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
var iMatch = i;
var jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
 
// Cycle through new text list gap region downwards
// get j from previous tokens match
while (
var iPrev = text.newText.tokens[i].prev;
if (iPrev i !== null) {&&
j !== null &&
var jPrev = text.newText.tokens[iPrev].link;
ifthis.newText.tokens[i].link (jPrev !=== null) {&&
j = textthis.oldText.tokens[jPrevj].next;link === null
}) {
}
 
// Connect if same token
// check for the start of an unresolved sequence
if ( (j !== null) && (text.oldText.tokens[j] !== null) && (textthis.newText.tokens[i].linktoken === null) && (textthis.oldText.tokens[j].link === null)token ) {
this.newText.tokens[i].link = j;
 
this.oldText.tokens[j].link = i;
// determine the limits of of the unresolved new sequence
var iStart = i;
var iEnd = null;
var iLength = 0;
var iNext = i;
while ( (iNext !== null) && (text.newText.tokens[iNext].link === null) ) {
iEnd = iNext;
iLength ++;
if (iEnd == newEnd) {
break;
}
iNext = text.newText.tokens[iNext].next;
}
 
// determineNot thea limitsmatch ofyet, ofmaybe thein unresolvednext oldrefinement sequencelevel
varelse jStart = j;{
bordersDownNext.push( [iMatch, jMatch] );
var jEnd = null;
var jLength = 0 break;
var jNext = j;
while ( (jNext !== null) && (text.oldText.tokens[jNext].link === null) ) {
jEnd = jNext;
jLength ++;
if (jEnd == oldEnd) {
break;
}
jNext = text.oldText.tokens[jNext].next;
}
 
// recursivelyNext difftoken the unresolved sequencedown
iMatch = i;
if ( (iLength > 1) || (jLength > 1) ) {
jMatch = j;
 
i = this.newText.tokens[i].next;
// new symbols object for sub-region
j = this.oldText.tokens[j].next;
var symbolsRecurse = {
token: [],
hash: {},
linked: false
};
wDiff.CalculateDiff(text, symbolsRecurse, level, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
}
i = iEnd;
}
 
// next list element
if (i == newEnd) {
break;
}
i = text.newText.tokens[i].next;
}
 
//**
// recursively* diffPass still5: unresolvedconnect regionsadjacent identical tokens upwards.
/ */
 
// cycleCycle troughthrough list of connected new text tokens list
var ibordersLength = newEndbordersUp.length;
for ( var match = 0; match < bordersLength; match ++ ) {
var j = oldEnd;
var i = bordersUp[match][0];
while ( (i !== null) && (text.newText.tokens[i] !== null) ) {
var j = bordersUp[match][1];
 
// getNext j from next matched tokensup
var iPreviMatch = text.newText.tokens[i].next;
ifvar (iPrevjMatch !== null) {j;
var jPrevi = textthis.newText.tokens[iPrevi].linkprev;
j = this.oldText.tokens[j].prev;
if (jPrev !== null) {
j = text.oldText.tokens[jPrev].prev;
}
}
 
// checkCycle forthrough thenew starttext ofgap anregion unresolved sequenceupwards
while (
if ( (j !== null) && (text.oldText.tokens[j] !== null) && (text.newText.tokens[i].link === null) && (text.oldText.tokens[j].link === null) ) {
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
 
// Connect if same token
// determine the limits of of the unresolved new sequence
if ( this.newText.tokens[i].token === this.oldText.tokens[j].token ) {
var iStart = null;
var iEnd this.newText.tokens[i].link = ij;
var iLength this.oldText.tokens[j].link = 0i;
var iNext = i;
while ( (iNext !== null) && (text.newText.tokens[iNext].link === null) ) {
iStart = iNext;
iLength ++;
if (iStart == newStart) {
break;
}
iNext = text.newText.tokens[iNext].prev;
}
 
// determineNot thea limitsmatch ofyet, ofmaybe thein unresolvednext oldrefinement sequencelevel
varelse jStart = null;{
bordersUpNext.push( [iMatch, jMatch] );
var jEnd = j;
var jLength = 0 break;
var jNext = j;
while ( (jNext !== null) && (text.oldText.tokens[jNext].link === null) ) {
jStart = jNext;
jLength ++;
if (jStart == oldStart) {
break;
}
jNext = text.oldText.tokens[jNext].prev;
}
 
// recursivelyNext difftoken the unresolved sequenceup
iMatch = i;
if ( (iLength > 1) || (jLength > 1) ) {
jMatch = j;
 
i = this.newText.tokens[i].prev;
// new symbols object for sub-region
j = this.oldText.tokens[j].prev;
var symbolsRecurse = {
token: [],
hash: {},
linked: false
};
wDiff.CalculateDiff(text, symbolsRecurse, level, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
}
i = iStart;
}
 
// next list element
if (i == newStart) {
break;
}
i = text.newText.tokens[i].prev;
}
}
}
return;
};
 
/**
* Connect adjacent identical tokens downwards from text start.
* Treat boundary as connected, stop after first connected token.
*/
 
// Only for full text diff
// wDiff.DetectBlocks: extract block data for inserted, deleted, or moved blocks from diff data in text object
if ( recursionLevel === 0 && repeating === false ) {
// 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: = + =-= = -= = + = = = = =
 
// From start
wDiff.DetectBlocks = function (text, blocks, groups) {
var i = this.newText.first;
var j = this.oldText.first;
var iMatch = null;
var jMatch = null;
 
// Cycle through old text tokens down
// WED('text.oldText', wDiff.DebugText(text.oldText));
// Connect identical tokens, stop after first connected token
// WED('text.newText', wDiff.DebugText(text.newText));
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
// collect identical corresponding ('same') blocks from old text and sort by new text
i = this.newText.last;
wDiff.GetSameBlocks(text, blocks);
j = this.oldText.last;
iMatch = null;
jMatch = null;
 
// Cycle through old text tokens up
// collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
// Connect identical tokens, stop after first connected token
var sections = [];
while (
wDiff.GetSections(blocks, sections);
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] );
}
}
 
// Save updated linked region borders to object
// find groups of continuous old text blocks
if ( recursionLevel === 0 && repeating === false ) {
wDiff.GetGroups(blocks, groups);
this.bordersDown = bordersDownNext;
this.bordersUp = bordersUpNext;
}
 
// Merge local updated linked region borders into object
// set longest sequence of increasing groups in sections as fixed (not moved)
else {
wDiff.SetFixed(blocks, groups, sections);
this.bordersDown = this.bordersDown.concat( bordersDownNext );
this.bordersUp = this.bordersUp.concat( bordersUpNext );
}
 
// collect deletion ('del') blocks from old text
wDiff.GetDelBlocks(text, blocks);
 
/**
// position 'del' blocks into new text order
* Repeat once with empty symbol table to link hidden unresolved common tokens in cross-overs.
wDiff.PositionDelBlocks(blocks);
* ("and" in "and this a and b that" -> "and this a and b that")
*/
 
if ( repeating === false && this.config.repeatedDiff === true ) {
// sort blocks by new text token number and update groups
var repeat = true;
wDiff.SortBlocks(blocks, groups);
this.calculateDiff( level, recurse, repeat, newStart, oldStart, up, recursionLevel );
}
 
/**
// convert groups to insertions/deletions if maximal block length is too short
* Refine by recursively diffing not linked regions with new symbol table.
if (wDiff.blockMinLength > 0) {
* At word and character level only.
var unlinked = wDiff.UnlinkBlocks(text, blocks, groups);
* Helps against gaps caused by addition of common tokens around sequences of common tokens.
*/
 
if (
// repeat from start after conversion
if (unlinked recurse === true) {&&
this.config['recursiveDiff'] === true &&
wDiff.GetSameBlocks(text, blocks);
recursionLevel < this.config.recursionMax
wDiff.GetSections(blocks, sections);
) {
wDiff.GetGroups(blocks, groups);
wDiff.SetFixed(blocks, groups, sections);
wDiff.GetDelBlocks(text, blocks);
wDiff.PositionDelBlocks(blocks);
}
}
 
/**
// collect insertion ('ins') blocks from new text
* Recursively diff gap downwards.
wDiff.GetInsBlocks(text, blocks);
*/
 
// Cycle through list of linked region borders
// sort blocks by new text token number and update groups
var bordersLength = bordersDownNext.length;
wDiff.SortBlocks(blocks, groups);
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersDownNext[match][0];
var j = bordersDownNext[match][1];
 
// Next token down
// set group numbers of 'ins' and 'del' blocks
i = this.newText.tokens[i].next;
wDiff.SetInsDelGroups(blocks, groups);
j = this.oldText.tokens[j].next;
 
// Start recursion at first gap token pair
// mark original positions of moved groups
if (
wDiff.MarkMoved(groups);
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 );
}
}
 
/**
// set moved block colors
* Recursively diff gap upwards.
wDiff.ColorMoved(groups);
*/
 
// Cycle through list of linked region borders
// WED('Groups', wDiff.DebugGroups(groups));
var bordersLength = bordersUpNext.length;
// WED('Blocks', wDiff.DebugBlocks(blocks));
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersUpNext[match][0];
var j = bordersUpNext[match][1];
 
// Next token up
return;
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 );
}
}
}
}
 
// Stop timers
// wDiff.GetSameBlocks: collect identical corresponding ('same') blocks from old text and sort by new text
if ( this.config.timer === true && repeating === false ) {
// called from: DetectBlocks()
if ( this.recursionTimer[recursionLevel] === undefined ) {
// changes: creates blocks
this.recursionTimer[recursionLevel] = 0;
}
this.recursionTimer[recursionLevel] += this.timeEnd( level + recursionLevel, true );
}
if ( this.config.timer === true && repeating === false && recursionLevel === 0 ) {
this.timeRecursionEnd( level );
this.timeEnd( level );
}
 
return;
wDiff.GetSameBlocks = function (text, blocks) {
};
 
// clear blocks array
blocks.splice(0);
 
/**
// cycle through old text to find matched (linked) blocks
* Main method for processing raw diff data, extracting deleted, inserted, and moved blocks.
var j = text.oldText.first;
*
var i = null;
* Scheme of blocks, sections, and groups (old block numbers):
while (j !== null) {
* 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 () {
 
// skipDebug 'del' blockslog
whileif ( (j !== null) && (text.oldTextthis.tokens[j]config.linkdebug === null)true ) {
j = textthis.oldText.tokens[j].nextdebugText( 'Old text' );
this.newText.debugText( 'New text' );
}
 
// Collect identical corresponding ('=') blocks from old text and sort by new text
// get 'same' block
this.getSameBlocks();
if (j !== null) {
i = text.oldText.tokens[j].link;
var iStart = i;
var jStart = j;
 
// Collect independent block sections with no block move crosses outside a section
// detect matching blocks ('same')
this.getSections();
var count = 0;
var unique = false;
var chars = 0;
var string = '';
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
var token = text.oldText.tokens[j].token;
count ++;
unique = unique || text.newText.tokens[i].unique;
chars += token.length;
string += token;
i = text.newText.tokens[i].next;
j = text.oldText.tokens[j].next;
}
 
// saveFind groups of continuous old text 'same' blockblocks
this.getGroups();
blocks.push({
oldBlock: blocks.length,
newBlock: null,
oldNumber: text.oldText.tokens[jStart].number,
newNumber: text.newText.tokens[iStart].number,
oldStart: jStart,
count: count,
unique: unique,
words: wDiff.WordCount(string),
chars: chars,
type: 'same',
section: null,
group: null,
fixed: null,
string: string
});
}
}
 
// Set longest sequence of increasing groups in sections as fixed (not moved)
// sort blocks by new text token number
this.setFixed();
blocks.sort(function(a, b) {
return a.newNumber - b.newNumber;
});
 
// Convert groups to insertions/deletions if maximum block length is too short
// number blocks in new text order
// Only for (varmore blockcomplex =texts 0;that blockactually <have blocks.length; blockof ++)minimum {block length
var unlinkCount = 0;
blocks[block].newBlock = block;
if (
}
this.config.unlinkBlocks === true &&
return;
this.config.blockMinLength > 0 &&
};
this.maxWords >= this.config.blockMinLength
) {
if ( this.config.timer === true ) {
this.time( 'total unlinking' );
}
 
// Repeat as long as unlinking is possible
var unlinked = true;
while ( unlinked === true && unlinkCount < this.config.unlinkMax ) {
 
// Convert '=' to '+'/'-' pairs
// wDiff.GetSections: collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
unlinked = this.unlinkBlocks();
// called from: DetectBlocks()
// changes: creates sections, blocks[].section
 
// Start over after conversion
wDiff.GetSections = function (blocks, sections) {
if ( unlinked === true ) {
unlinkCount ++;
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
 
// Repeat block detection from start
// clear sections array
this.maxWords = 0;
sections.splice(0);
this.getSameBlocks();
this.getSections();
this.getGroups();
this.setFixed();
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'total unlinking' );
}
}
 
// cycleCollect throughdeletion ('-') blocks from old text
this.getDelBlocks();
for (var block = 0; block < blocks.length; block ++) {
 
// Position '-' blocks into new text order
var sectionStart = block;
this.positionDelBlocks();
var sectionEnd = block;
 
// Collect insertion ('+') blocks from new text
var oldMax = blocks[sectionStart].oldNumber;
this.getInsBlocks();
var sectionOldMax = oldMax;
 
// Set group numbers of '+' blocks
// check right
this.setInsGroups();
for (var j = sectionStart + 1; j < blocks.length; j ++) {
 
// Mark original positions of moved groups
// check for crossing over to the left
this.insertMarks();
if (blocks[j].oldNumber > oldMax) {
 
oldMax = blocks[j].oldNumber;
// Debug log
}
if ( this.config.timer === true || this.config.debug === true ) {
else if (blocks[j].oldNumber < sectionOldMax) {
console.log( 'Unlink count: ', unlinkCount );
sectionEnd = j;
sectionOldMax = oldMax;
}
}
if ( this.config.debug === true ) {
this.debugGroups( 'Groups' );
this.debugBlocks( 'Blocks' );
}
return;
};
 
// save crossing sections
if (sectionEnd > sectionStart) {
 
/**
// save section to block
* Collect identical corresponding matching ('=') blocks from old text and sort by new text.
for (var i = sectionStart; i <= sectionEnd; i ++) {
*
blocks[i].section = sections.length;
* @param[in] WikEdDiffText newText, oldText Text objects
}
* @param[in/out] array blocks Blocks table object
*/
this.getSameBlocks = function () {
 
if ( this.config.timer === true ) {
// save section
this.time( 'getSameBlocks' );
sections.push({
blockStart: sectionStart,
blockEnd: sectionEnd,
deleted: false
});
block = sectionEnd;
}
}
return;
};
 
var blocks = this.blocks;
 
// Clear blocks array
// wDiff.GetGroups: find groups of continuous old text blocks
blocks.splice( 0 );
// called from: DetectBlocks()
// changes: creates groups, blocks[].group
 
// Cycle through old text to find connected (linked, matched) blocks
wDiff.GetGroups = function (blocks, groups) {
var j = this.oldText.first;
var i = null;
while ( j !== null ) {
 
// Skip '-' blocks
// clear groups array
while ( j !== null && this.oldText.tokens[j].link === null ) {
groups.splice(0);
j = this.oldText.tokens[j].next;
}
 
// Get '=' block
// cycle through blocks
if ( j !== null ) {
for (var block = 0; block < blocks.length; block ++) {
i = this.oldText.tokens[j].link;
if (blocks[block].deleted === true) {
continue var iStart = i;
var jStart = j;
}
var groupStart = block;
var groupEnd = block;
var oldBlock = blocks[groupStart].oldBlock;
 
// Detect matching blocks ('=')
// get word and char count of block
var count = 0;
var words = wDiff.WordCount(blocks[block].string);
var maxWordsunique = wordsfalse;
var uniquetext = false'';
while ( i !== null && j !== null && this.oldText.tokens[j].link === i ) {
var chars = blocks[block].chars;
text += this.oldText.tokens[j].token;
 
count ++;
// check right
if ( this.newText.tokens[i].unique === true ) {
for (var i = groupEnd + 1; i < blocks.length; i ++) {
unique = true;
}
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
 
// checkSave forold crossingtext over'=' to the leftblock
if ( blocks[i].oldBlock != oldBlock + 1)push( {
oldBlock: blocks.length,
break;
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
} );
}
}
oldBlock = blocks[i].oldBlock;
 
// getSort wordblocks andby charnew counttext oftoken blocknumber
if (blocks[i].wordssort( function( a, >b maxWords) {
return a.newNumber - b.newNumber;
maxWords = blocks[i].words;
} );
 
unique = unique || blocks[i].unique;
words// +=Number blocks[i].words; in new text order
charsvar blocksLength += blocks[i].charslength;
for ( var block = 0; block < blocksLength; block ++ ) {
groupEnd = i;
blocks[block].newBlock = block;
}
 
if ( this.config.timer === true ) {
// save crossing group
this.timeEnd( 'getSameBlocks' );
if (groupEnd >= groupStart) {
}
return;
};
 
// set groups outside sections as fixed
var fixed = false;
if (blocks[groupStart].section === null) {
fixed = true;
}
 
/**
// save group to block
* Collect independent block sections with no block move crosses
for (var i = groupStart; i <= groupEnd; i ++) {
* outside a section for per-section determination of non-moving fixed groups.
blocks[i].group = groups.length;
*
blocks[i].fixed = fixed;
* @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 ) {
// save group
this.time( 'getSections' );
groups.push({
oldNumber: blocks[groupStart].oldNumber,
blockStart: groupStart,
blockEnd: groupEnd,
unique: unique,
maxWords: maxWords,
words: words,
chars: chars,
fixed: fixed,
moved: [],
movedFrom: null,
color: null,
diff: ''
});
block = groupEnd;
}
}
return;
};
 
var blocks = this.blocks;
var sections = this.sections;
 
// Clear sections array
// wDiff.SetFixed: set longest sequence of increasing groups in sections as fixed (not moved)
sections.splice( 0 );
// called from: DetectBlocks()
// calls: wDiff.FindMaxPath()
// changes: groups[].fixed, blocks[].fixed
 
// Cycle through blocks
wDiff.SetFixed = function (blocks, groups, sections) {
var blocksLength = blocks.length;
for ( var block = 0; block < blocksLength; block ++ ) {
 
var sectionStart = block;
// cycle through sections
var sectionEnd = block;
for (var section = 0; section < sections.length; section ++) {
var blockStart = sections[section].blockStart;
var blockEnd = sections[section].blockEnd;
 
var groupStartoldMax = blocks[blockStartsectionStart].groupoldNumber;
var groupEndsectionOldMax = blocks[blockEnd].groupoldMax;
 
// Check right
// recusively find path of groups in increasing old group order with longest char length
for ( var j = sectionStart + 1; j < blocksLength; j ++ ) {
 
// startCheck atfor eachcrossing groupover ofto sectionthe left
if ( blocks[j].oldNumber > oldMax ) {
var cache = [];
oldMax = blocks[j].oldNumber;
var maxChars = 0;
}
var maxPath = null;
else if ( blocks[j].oldNumber < sectionOldMax ) {
for (var i = groupStart; i <= groupEnd; i ++) {
sectionEnd = j;
var pathObj = wDiff.FindMaxPath(i, [], 0, cache, groups, groupEnd);
sectionOldMax = oldMax;
if (pathObj.chars > maxChars) {
}
maxPath = pathObj.path;
maxChars = pathObj.chars;
}
}
 
// markSave fixedcrossing groupssections
if ( sectionEnd > sectionStart ) {
for (var i = 0; i < maxPath.length; i ++) {
var group = maxPath[i];
groups[group].fixed = true;
 
// markSave fixedsection blocksto block
for ( var blocki = groups[group].blockStartsectionStart; blocki <= groups[group].blockEndsectionEnd; blocki ++ ) {
blocks[blocki].fixedsection = truesections.length;
}
 
// Save section
sections.push( {
blockStart: sectionStart,
blockEnd: sectionEnd
} );
block = sectionEnd;
}
}
if ( this.config.timer === true ) {
}
this.timeEnd( 'getSections' );
return;
};
return;
};
 
 
/**
// wDiff.FindMaxPath: recusively find path of groups in increasing old group order with longest char length
* Find groups of continuous old text blocks.
// input: start, path start group; path, array of path groups; chars, char count of path; cache, cached sub-path lengths; groups, groups, group object; groupEnd, last group
*
// returns: returnObj, contains path and length
* @param[out] array groups Groups table object
// called from: wDiff.SetFixed()
* @param[in/out] array blocks Blocks table object, group property
// calls: itself recursively
*/
this.getGroups = function () {
 
if ( this.config.timer === true ) {
wDiff.FindMaxPath = function (start, path, chars, cache, groups, groupEnd) {
this.time( 'getGroups' );
}
 
var blocks = this.blocks;
// add current path point
var pathLocalgroups = paththis.slice()groups;
pathLocal.push(start);
chars = chars + groups[start].chars;
 
// Clear groups array
// last group, terminate recursion
groups.splice( 0 );
var returnObj = { path: pathLocal, chars: chars };
if (start == groupEnd) {
return returnObj;
}
 
// Cycle through blocks
// find longest sub-path
var maxCharsblocksLength = 0blocks.length;
for ( var block = 0; block < blocksLength; block ++ ) {
var oldNumber = groups[start].oldNumber;
var groupStart = block;
for (var i = start + 1; i <= groupEnd; i ++) {
var groupEnd = block;
var oldBlock = blocks[groupStart].oldBlock;
 
// onlyGet inword increasingand oldchar groupcount orderof block
var words = this.wordCount( blocks[block].text );
if (groups[i].oldNumber < oldNumber) {
var maxWords = words;
continue;
var unique = blocks[block].unique;
}
var chars = blocks[block].chars;
 
// Check right
// get longest sub-path from cache
for ( var i = groupEnd + 1; i < blocksLength; i ++ ) {
if (cache[start] !== undefined) {
returnObj = cache[start];
}
 
// Check for crossing over to the left
// get longest sub-path by recursion
if ( blocks[i].oldBlock !== oldBlock + 1 ) {
else {
break;
var pathObj = wDiff.FindMaxPath(i, pathLocal, chars, cache, groups, groupEnd);
}
oldBlock = blocks[i].oldBlock;
 
// Get word and char count of block
// select longest sub-path
if (pathObj blocks[i].charswords > maxCharsmaxWords ) {
returnObj maxWords = pathObjblocks[i].words;
}
if ( blocks[i].unique === true ) {
unique = true;
}
words += blocks[i].words;
chars += blocks[i].chars;
groupEnd = i;
}
}
}
 
// Save crossing group
// save longest path to cache
if (cache[i] groupEnd >=== undefinedgroupStart ) {
cache[start] = returnObj;
}
return returnObj;
};
 
// Set groups outside sections as fixed
var fixed = false;
if ( blocks[groupStart].section === null ) {
fixed = true;
}
 
// Save group to block
// wDiff.GetDelBlocks: collect deletion ('del') blocks from old text
for ( var i = groupStart; i <= groupEnd; i ++ ) {
// called from: DetectBlocks()
blocks[i].group = groups.length;
// changes: blocks
blocks[i].fixed = fixed;
}
 
// Save group
wDiff.GetDelBlocks = function (text, blocks) {
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;
 
// cycleSet throughglobal oldword textcount toof findlongest matched (linked) blocksblock
if ( maxWords > this.maxWords ) {
var j = text.oldText.first;
this.maxWords = maxWords;
var i = null;
}
while (j !== null) {
}
 
// collect 'del' blocks
var oldStart = j;
var count = 0;
var string = '';
while ( (j !== null) && (text.oldText.tokens[j].link === null) ) {
count ++;
string += text.oldText.tokens[j].token;
j = text.oldText.tokens[j].next;
}
if ( this.config.timer === true ) {
 
this.timeEnd( 'getGroups' );
// save old text 'del' block
if (count !== 0) {
blocks.push({
oldBlock: null,
newBlock: null,
oldNumber: text.oldText.tokens[oldStart].number,
newNumber: null,
oldStart: oldStart,
count: count,
unique: false,
words: null,
chars: null,
type: 'del',
section: null,
group: null,
fixed: null,
string: string
});
}
return;
};
 
 
// skip 'same' block
/**
if (j !== null) {
* Set longest sequence of increasing groups in sections as fixed (not moved).
i = text.oldText.tokens[j].link;
*
while ( (i !== null) && (j !== null) && (text.oldText.tokens[j].link == i) ) {
* @param[in] array sections Sections table object
i = text.newText.tokens[i].next;
* @param[in/out] array groups Groups table object, fixed property
j = text.oldText.tokens[j].next;
* @param[in/out] array blocks Blocks table object, fixed property
}
*/
this.setFixed = function () {
 
if ( this.config.timer === true ) {
this.time( 'setFixed' );
}
}
return;
};
 
var blocks = this.blocks;
var groups = this.groups;
var sections = this.sections;
 
// Cycle through sections
// wDiff.PositionDelBlocks: position 'del' blocks into new text order
var sectionsLength = sections.length;
// called from: DetectBlocks()
for ( var section = 0; section < sectionsLength; section ++ ) {
// changes: blocks[].section/group/fixed/newNumber
var blockStart = sections[section].blockStart;
//
var blockEnd = sections[section].blockEnd;
// 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
 
var groupStart = blocks[blockStart].group;
wDiff.PositionDelBlocks = function (blocks) {
var groupEnd = blocks[blockEnd].group;
 
// Recusively find path of groups in increasing old group order with longest char length
// sort shallow copy of blocks by oldNumber
var blocksOldcache = blocks.slice()[];
var maxChars = 0;
blocksOld.sort(function(a, b) {
var maxPath = null;
return a.oldNumber - b.oldNumber;
});
 
// Start at each group of section
// cycle through 'del' blocks in old text order
for ( var blockOldi = 0groupStart; blockOldi <= blocksOld.lengthgroupEnd; blockOldi ++ ) {
var pathObj = this.findMaxPath( i, groupEnd, cache );
var delBlock = blocksOld[blockOld];
if (delBlock pathObj.typechars > !=maxChars 'del') {
maxPath = pathObj.path;
continue;
maxChars = pathObj.chars;
}
}
}
 
// getMark oldfixed text prev blockgroups
var maxPathLength = maxPath.length;
var prevBlock;
for ( var i = 0; i < maxPathLength; i ++ ) {
if (blockOld > 0) {
var group = maxPath[i];
prevBlock = blocks[ blocksOld[blockOld - 1].newBlock ];
groups[group].fixed = true;
}
 
// getMark oldfixed text next blockblocks
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
var nextBlock;
blocks[block].fixed = true;
if (blockOld < blocksOld.length - 1) {
}
nextBlock = blocks[ blocksOld[blockOld + 1].newBlock ];
}
}
if ( this.config.timer === true ) {
 
this.timeEnd( 'setFixed' );
// move after prev block if fixed
var neighbor;
if ( (prevBlock !== undefined) && (prevBlock.fixed === true) ) {
neighbor = prevBlock;
delBlock.newNumber = neighbor.newNumber + 0.1;
}
return;
};
 
// move before next block if fixed
else if ( (nextBlock !== undefined) && (nextBlock.fixed === true) ) {
neighbor = nextBlock;
delBlock.newNumber = neighbor.newNumber - 0.1;
}
 
/**
// move after prev block if existent
* Recusively find path of groups in increasing old group order with longest char length.
else if (prevBlock !== undefined) {
*
neighbor = prevBlock;
* @param int start Path start group
delBlock.newNumber = neighbor.newNumber + 0.1;
* @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;
// move before next block
else if (nextBlock !== undefined) {
neighbor = nextBlock;
delBlock.newNumber = neighbor.newNumber - 0.1;
}
 
// moveFind beforelongest first blocksub-path
var maxChars = 0;
else {
var oldNumber = groups[start].oldNumber;
delBlock.newNumber = -0.1;
var returnObj = { path: [], chars: 0};
}
for ( var i = start + 1; i <= groupEnd; i ++ ) {
 
// updateOnly 'del'in blockincreasing withold neighborgroup dataorder
if ( groups[i].oldNumber < oldNumber ) {
if (neighbor !== undefined) {
continue;
delBlock.section = neighbor.section;
}
delBlock.group = neighbor.group;
 
delBlock.fixed = neighbor.fixed;
// Get longest sub-path from cache (deep copy)
var pathObj;
if ( cache[i] !== undefined ) {
pathObj = { path: cache[i].path.slice(), chars: cache[i].chars };
}
 
// Get longest sub-path by recursion
else {
pathObj = this.findMaxPath( i, groupEnd, cache );
}
 
// Select longest sub-path
if ( pathObj.chars > maxChars ) {
maxChars = pathObj.chars;
returnObj = pathObj;
}
}
}
return;
};
 
// Add current start to path
returnObj.path.unshift( start );
returnObj.chars += groups[start].chars;
 
// Save path to cache (deep copy)
// wDiff.UnlinkBlocks: convert 'same' blocks in groups into 'ins'/'del' pairs if too short
if ( cache[start] === undefined ) {
// called from: DetectBlocks()
cache[start] = { path: returnObj.path.slice(), chars: returnObj.chars };
// changes: text.newText/oldText[].link
}
// returns: true if text tokens were unlinked
 
return returnObj;
};
 
wDiff.UnlinkBlocks = function (text, blocks, groups) {
 
/**
var unlinked = false;
* Convert matching '=' blocks in groups into insertion/deletion ('+'/'-') pairs
* if too short and too common.
* Prevents fragmentated diffs for very different versions.
*
* @param[in] array blocks Blocks table object
* @param[in/out] WikEdDiffText newText, oldText Text object, linked property
* @param[in/out] array groups Groups table object
* @return bool True if text tokens were unlinked
*/
this.unlinkBlocks = function () {
 
var blocks = this.blocks;
// cycle through groups
for ( var groupgroups = 0; group < this.groups.length; group ++) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
 
// Cycle through groups
// no block in group is at least blockMinLength words long
var unlinked = false;
if (groups[group].maxWords < wDiff.blockMinLength) {
var groupsLength = groups.length;
for ( var group = 0; group < groupsLength; group ++ ) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
 
// unlinkUnlink whole moved group if itno containsblock nois uniqueat matchedleast tokenblockMinLength words long and unique
if ( (groups[group].fixedmaxWords ===< false)this.config.blockMinLength && (groups[group].unique === false) ) {
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === 'same=' ) {
wDiffthis.UnlinkSingleBlockunlinkSingleBlock( blocks[block], text);
unlinked = true;
}
Line 1,887 ⟶ 2,562:
}
 
// Otherwise unlink block flanks
else {
 
// unlinkUnlink blocks from start if preceded by 'del'
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( (block > 0) && (blocks[block - 1].type === 'del') && (blocks[block].type == 'same') ) {
 
// stopStop unlinking if more than one word or a unique word
if ( (blocks[block].words > 1) || ( (blocks[block].words == 1) && (blocks[block].unique === true) ) ) {
break;
}
wDiffthis.UnlinkSingleBlockunlinkSingleBlock( blocks[block], text);
unlinked = true;
blockStart = block;
Line 1,904 ⟶ 2,579:
}
 
// unlinkUnlink blocks from end if followed by 'del'
for ( var block = blockEnd; block > blockStart; block -- ) {
if ( (blockEnd < blocks.length - 1) && (blocks[block + 1].type === 'del') && (blocks[block].type == 'same') ) {
 
// stopStop unlinking if more than one word or a unique word
if (
if ( (blocks[block].words > 1) || ( (blocks[block].words == 1) && (blocks[block].unique === true) ) ) {
blocks[block].words > 1 ||
( blocks[block].words === 1 && blocks[block].unique === true )
) {
break;
}
wDiffthis.UnlinkSingleBlockunlinkSingleBlock( blocks[block], text);
unlinked = true;
}
Line 1,918 ⟶ 2,596:
}
}
return unlinked;
}
};
return unlinked;
};
 
 
/**
// wDiff.UnlinkBlock: un-link text tokens of single block, converting them into 'ins'/'del' pairs
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs.
// called from: wDiff.UnlinkBlocks()
*
// changes: text.newText/oldText[].link
* @param[in] array blocks Blocks table object
* @param[out] WikEdDiffText newText, oldText Text objects, link property
*/
this.unlinkSingleBlock = function ( block ) {
 
// Cycle through old text
wDiff.UnlinkSingleBlock = function (block, text) {
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
 
// Unlink tokens
// cycle through old text
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
var j = block.oldStart;
this.oldText.tokens[j].link = null;
for (var count = 0; count < block.count; count ++) {
j = this.oldText.tokens[j].next;
}
return;
};
 
// unlink tokens
text.newText.tokens[ text.oldText.tokens[j].link ].link = null;
text.oldText.tokens[j].link = null;
j = text.oldText.tokens[j].next;
}
return;
};
 
/**
* Collect deletion ('-') blocks from old text.
*
* @param[in] WikEdDiffText oldText Old Text object
* @param[out] array blocks Blocks table object
*/
this.getDelBlocks = function () {
 
if ( this.config.timer === true ) {
// wDiff.GetInsBlocks: collect insertion ('ins') blocks from new text
this.time( 'getDelBlocks' );
// called from: DetectBlocks()
}
// changes: blocks
 
var blocks = this.blocks;
wDiff.GetInsBlocks = function (text, blocks) {
 
// cycleCycle through newold text to find insertionconnected (linked, matched) blocks
var ij = textthis.newTextoldText.first;
while var (i !== null) {;
while ( j !== null ) {
 
// Collect '-' blocks
// jump over linked (matched) block
var oldStart = j;
while ( (i !== null) && (text.newText.tokens[i].link !== null) ) {
i = text.newText.tokens[i].next;
}
 
// detect insertion blocks ('ins')
if (i !== null) {
var iStart = i;
var count = 0;
var stringtext = '';
while ( (ij !== null) && (textthis.newTextoldText.tokens[ij].link === null) ) {
count ++;
stringtext += textthis.newTextoldText.tokens[ij].token;
ij = textthis.newTextoldText.tokens[ij].next;
}
 
// saveSave newold text 'ins-' block
if ( count !== 0 ) {
blocks.push({
oldBlock:blocks.push( null,{
newBlock oldBlock: null,
oldNumber newBlock: null,
newNumber oldNumber: textthis.newTextoldText.tokens[iStartoldStart].number,
oldStart newNumber: null,
count oldStart: countoldStart,
unique count: false count,
words unique: nullfalse,
chars words: null,
type chars: 'ins'text.length,
section type: null '-',
group section: null,
fixed group: null,
string fixed: string null,
moved: null,
});
text: text
} );
}
 
// Skip '=' blocks
if ( j !== null ) {
i = this.oldText.tokens[j].link;
while ( i !== null && j !== null && this.oldText.tokens[j].link === i ) {
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
}
if ( this.config.timer === true ) {
}
this.timeEnd( 'getDelBlocks' );
return;
};
return;
};
 
 
/**
// wDiff.SortBlocks: sort blocks by new text token number and update groups
* Position deletion '-' blocks into new text order.
// called from: DetectBlocks()
* Deletion blocks move with fixed reference:
// changes: blocks
* Old: 1 D 2 1 D 2
* / \ / \ \
* New: 1 D 2 1 D 2
* Fixed: * *
* newNumber: 1 1 2 2
*
* Marks '|' and deletions '-' get newNumber of reference block
* and are sorted around it by old text number.
*
* @param[in/out] array blocks Blocks table, newNumber, section, group, and fixed properties
*
*/
this.positionDelBlocks = function () {
 
if ( this.config.timer === true ) {
wDiff.SortBlocks = function (blocks, groups) {
this.time( 'positionDelBlocks' );
}
 
var blocks = this.blocks;
// sort by newNumber
var groups = this.groups;
blocks.sort(function(a, b) {
return a.newNumber - b.newNumber;
});
 
// Sort shallow copy of blocks by oldNumber
// cycle through blocks and update groups with new block numbers
var groupblocksOld = nullblocks.slice();
blocksOld.sort( function( a, b ) {
for (var block = 0; block < blocks.length; block ++) {
return a.oldNumber - b.oldNumber;
var blockGroup = blocks[block].group;
} );
if (blockGroup !== null) {
 
if (blockGroup != group) {
group// =Cycle through blocks[block].group; in old text order
var blocksOldLength = blocksOld.length;
groups[group].blockStart = block;
for ( var block = 0; block < blocksOldLength; block ++ ) {
groups[group].oldNumber = blocks[block].oldNumber;
var delBlock = blocksOld[block];
 
// '-' block only
if ( delBlock.type !== '-' ) {
continue;
}
groups[blockGroup].blockEnd = block;
}
}
return;
};
 
// Find fixed '=' reference block from original block position to position '-' block
// Similar to position marks '|' code
 
// Get old text prev block
// wDiff.SetInsDelGroups: set group numbers of 'ins' and 'del' blocks
var prevBlockNumber = null;
// called from: DetectBlocks()
var prevBlock = null;
// changes: groups, blocks[].fixed/group
if ( block > 0 ) {
prevBlockNumber = blocksOld[block - 1].newBlock;
prevBlock = blocks[prevBlockNumber];
}
 
// Get old text next block
wDiff.SetInsDelGroups = function (blocks, groups) {
var nextBlockNumber = null;
var nextBlock = null;
if ( block < blocksOld.length - 1 ) {
nextBlockNumber = blocksOld[block + 1].newBlock;
nextBlock = blocks[nextBlockNumber];
}
 
// Move after prev block if fixed
// set group numbers of 'ins' and 'del' blocks inside existing groups
var refBlock = null;
for (var group = 0; group < groups.length; group ++) {
if ( prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
var fixed = groups[group].fixed;
refBlock = prevBlock;
for (var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++) {
if (blocks[block].group === null) {
blocks[block].group = group;
blocks[block].fixed = fixed;
}
}
}
 
// Move before next block if fixed
// add remaining 'ins' and 'del' blocks to groups
else if ( nextBlock !== null && nextBlock.type === '=' && nextBlock.fixed === true ) {
refBlock = nextBlock;
}
 
// Move after prev block if not start of group
// cycle through blocks
else if (
for (var block = 0; block < blocks.length; block ++) {
prevBlock !== null &&
prevBlock.type === '=' &&
prevBlockNumber !== groups[ prevBlock.group ].blockEnd
) {
refBlock = prevBlock;
}
 
// Move before next block if not start of group
// skip existing groups
else if (
if (blocks[block].group === null) {
nextBlock !== null &&
blocks[block].group = groups.length;
nextBlock.type === '=' &&
var fixed = blocks[block].fixed;
nextBlockNumber !== groups[ nextBlock.group ].blockStart
) {
refBlock = nextBlock;
}
 
// Move after closest previous fixed block
// save group
groups.push(else {
for ( var fixed = block; fixed >= 0; fixed -- ) {
oldNumber: blocks[block].oldNumber,
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
blockStart: block,
refBlock = blocksOld[fixed];
blockEnd: block,
break;
unique: false,
}
maxWords: null,
}
words: null,
}
chars: null,
 
fixed: fixed,
moved:// Move before first [],block
movedFrom:if ( refBlock === null, ) {
color:delBlock.newNumber = null,-1;
}
diff: ''
 
});
// Update '-' block data
else {
delBlock.newNumber = refBlock.newNumber;
delBlock.section = refBlock.section;
delBlock.group = refBlock.group;
delBlock.fixed = refBlock.fixed;
}
}
}
return;
};
 
// Sort '-' blocks in and update groups
this.sortBlocks();
 
if ( this.config.timer === true ) {
// wDiff.MarkMoved: mark original positions of moved groups
this.timeEnd( 'positionDelBlocks' );
// called from: DetectBlocks()
}
// changes: groups[].moved/movedFrom
return;
// moved block marks at original positions relative to fixed groups:
};
// groups: 3 7
// 1 <| | (no next smaller fixed)
// 5 |< |
// |> 5 |
// | 5 <|
// | >| 5
// | |> 9 (no next larger fixed)
// fixed: * *
// mark direction: groups[movedGroup].blockStart < groups[group].blockStart
// group side: groups[movedGroup].oldNumber < groups[group].oldNumber
 
wDiff.MarkMoved = function (groups) {
 
/**
// cycle through groups (moved group)
* Collect insertion ('+') blocks from new text.
for (var movedGroup = 0; movedGroup < groups.length; movedGroup ++) {
*
if (groups[movedGroup].fixed !== false) {
* @param[in] WikEdDiffText newText New Text object
continue;
* @param[out] array blocks Blocks table object
*/
this.getInsBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'getInsBlocks' );
}
var movedOldNumber = groups[movedGroup].oldNumber;
 
var blocks = this.blocks;
// find closest fixed groups
var nextSmallerNumber = null;
var nextSmallerGroup = null;
var nextLargerNumber = null;
var nextLargerGroup = null;
 
// cycleCycle through groupsnew (originaltext positions)to find insertion blocks
var i = this.newText.first;
for (var group = 0; group < groups.length; group ++) {
ifwhile ( (groups[group].fixedi !== true) || (group == movedGroup)null ) {
continue;
}
 
// Jump over linked (matched) block
// find fixed group with closest smaller oldNumber
while ( i !== null && this.newText.tokens[i].link !== null ) {
var oldNumber = groups[group].oldNumber;
i = this.newText.tokens[i].next;
if ( (oldNumber < movedOldNumber) && ( (nextSmallerNumber === null) || (oldNumber > nextSmallerNumber) ) ) {
nextSmallerNumber = oldNumber;
nextSmallerGroup = group;
}
 
// Detect insertion blocks ('+')
// find fixed group with closest larger oldNumber
if ( i !== null ) {
if ( (oldNumber > movedOldNumber) && ( (nextLargerNumber === null) || (oldNumber < nextLargerNumber) ) ) {
nextLargerNumbervar iStart = oldNumberi;
nextLargerGroupvar count = group0;
var text = '';
while ( i !== null && this.newText.tokens[i].link === null ) {
count ++;
text += this.newText.tokens[i].token;
i = this.newText.tokens[i].next;
}
 
// Save new text '+' block
blocks.push( {
oldBlock: null,
newBlock: null,
oldNumber: null,
newNumber: this.newText.tokens[iStart].number,
oldStart: null,
count: count,
unique: false,
words: null,
chars: text.length,
type: '+',
section: null,
group: null,
fixed: null,
moved: null,
text: text
} );
}
}
 
// Sort '+' blocks in and update groups
// no larger fixed group, moved right
this.sortBlocks();
var movedFrom = '';
if (nextLargerGroup === null) {
movedFrom = 'left';
}
 
if ( this.config.timer === true ) {
// no smaller fixed group, moved right
this.timeEnd( 'getInsBlocks' );
else if (nextSmallerGroup === null) {
movedFrom = 'right';
}
return;
};
 
// group moved from between two closest fixed neighbors, moved left or right depending on char distance
else {
var rightChars = 0;
for (var group = nextSmallerGroup + 1; group < movedGroup; group ++) {
rightChars += groups[group].chars;
}
var leftChars = 0;
for (var group = movedGroup + 1; group < nextLargerGroup; group ++) {
leftChars += groups[group].chars;
}
 
/**
// moved right
* Sort blocks by new text token number and update groups.
if (rightChars <= leftChars) {
*
movedFrom = 'left';
* @param[in/out] array groups Groups table object
* @param[in/out] array blocks Blocks table object
*/
this.sortBlocks = function () {
 
var blocks = this.blocks;
var groups = this.groups;
 
// Sort by newNumber, then by old number
blocks.sort( function( a, b ) {
var comp = a.newNumber - b.newNumber;
if ( comp === 0 ) {
comp = a.oldNumber - b.oldNumber;
}
return comp;
} );
 
// Cycle through blocks and update groups with new block numbers
// moved left
var group = null;
else {
var blocksLength = blocks.length;
movedFrom = 'right';
for ( var block = 0; block < blocksLength; block ++ ) {
var blockGroup = blocks[block].group;
if ( blockGroup !== null ) {
if ( blockGroup !== group ) {
group = blocks[block].group;
groups[group].blockStart = block;
groups[group].oldNumber = blocks[block].oldNumber;
}
groups[blockGroup].blockEnd = block;
}
}
return;
};
 
 
// check for null-moves
/**
if (movedFrom == 'left') {
* Set group numbers of insertion '+' blocks.
if (groups[nextSmallerGroup].blockEnd + 1 != groups[movedGroup].blockStart) {
*
groups[nextSmallerGroup].moved.push(movedGroup);
* @param[in/out] array groups Groups table object
groups[movedGroup].movedFrom = nextSmallerGroup;
* @param[in/out] array blocks Blocks table object, fixed and group properties
}
*/
this.setInsGroups = function () {
 
if ( this.config.timer === true ) {
this.time( 'setInsGroups' );
}
 
else if (movedFrom == 'right') {
var blocks = this.blocks;
if (groups[movedGroup].blockEnd + 1 != groups[nextLargerGroup].blockStart) {
var groups = this.groups;
groups[nextLargerGroup].moved.push(movedGroup);
 
groups[movedGroup].movedFrom = nextLargerGroup;
// Set group numbers of '+' blocks inside existing groups
var groupsLength = groups.length;
for ( var group = 0; group < groupsLength; group ++ ) {
var fixed = groups[group].fixed;
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
if ( blocks[block].group === null ) {
blocks[block].group = group;
blocks[block].fixed = fixed;
}
}
}
}
 
// cycleAdd throughremaining groups, sort'+' blocks movedto fromnew here by old numbergroups
for (var group = 0; group < groups.length; group ++) {
var moved = groups[group].moved;
if (moved !== null) {
moved.sort(function(a, b) {
return groups[a].oldNumber - groups[b].oldNumber;
});
}
}
return;
};
 
// Cycle through blocks
var blocksLength = blocks.length;
for ( var block = 0; block < blocksLength; block ++ ) {
 
// Skip existing groups
// wDiff.ColorMoved: set moved block color numbers
if ( blocks[block].group === null ) {
// called from: DetectBlocks()
blocks[block].group = groups.length;
// changes: groups[].color
 
// Save new single-block group
wDiff.ColorMoved = function (groups) {
groups.push( {
oldNumber: blocks[block].oldNumber,
blockStart: block,
blockEnd: block,
unique: blocks[block].unique,
maxWords: blocks[block].words,
words: blocks[block].words,
chars: blocks[block].chars,
fixed: blocks[block].fixed,
movedFrom: null,
color: null
} );
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'setInsGroups' );
}
return;
};
 
// cycle through groups
var moved = [];
for (var group = 0; group < groups.length; group ++) {
moved = moved.concat(groups[group].moved);
}
 
/**
// sort moved array by old number
* Mark original positions of moved groups.
moved.sort(function(a, b) {
* Scheme: moved block marks at original positions relative to fixed groups:
return groups[a].oldNumber - groups[b].oldNumber;
* Groups: 3 7
});
* 1 <| | (no next smaller fixed)
* 5 |< |
* |> 5 |
* | 5 <|
* | >| 5
* | |> 9 (no next larger fixed)
* Fixed: * *
*
* Mark direction: groups.movedGroup.blockStart < groups.group.blockStart
* Group side: groups.movedGroup.oldNumber < groups.group.oldNumber
*
* Marks '|' and deletions '-' get newNumber of reference block
* and are sorted around it by old text number.
*
* @param[in/out] array groups Groups table object, movedFrom property
* @param[in/out] array blocks Blocks table object
*/
this.insertMarks = function () {
 
if ( this.config.timer === true ) {
// set color
this.time( 'insertMarks' );
var color = 0;
for (var i = 0; i < moved.length; i ++) {
var movedGroup = moved[i];
if (wDiff.showBlockMoves === true) {
groups[movedGroup].color = color;
color ++;
}
}
return;
};
 
var blocks = this.blocks;
var groups = this.groups;
var moved = [];
var color = 1;
 
// Make shallow copy of blocks
// wDiff.AssembleDiff: process diff data into formatted html text
var blocksOld = blocks.slice();
// input: text, object containing text tokens list; blocks, array containing block type; groups, array containing fixed (not moved), color, and moved mark data
// returns: diff html string
// called from: wDiff.Diff()
// calls: wDiff.HtmlCustomize(), wDiff.HtmlFormat()
 
// Enumerate copy
wDiff.AssembleDiff = function (text, blocks, groups) {
var blocksOldLength = blocksOld.length;
for ( var i = 0; i < blocksOldLength; i ++ ) {
blocksOld[i].number = i;
}
 
// Sort copy by oldNumber
//
blocksOld.sort( function( a, b ) {
// create group diffs
var comp = a.oldNumber - b.oldNumber;
//
if ( comp === 0 ) {
comp = a.newNumber - b.newNumber;
}
return comp;
} );
 
// Create lookup table: original to sorted
// cycle through groups
var lookupSorted = [];
for (var group = 0; group < groups.length; group ++) {
for ( var i = 0; i < blocksOldLength; i ++ ) {
var color = groups[group].color;
lookupSorted[ blocksOld[i].number ] = i;
var blockStart = groups[group].blockStart;
}
var blockEnd = groups[group].blockEnd;
var diff = '';
 
// Cycle through groups (moved group)
// check for colored block and move direction
var blockFromgroupsLength = nullgroups.length;
for ( var moved = 0; moved < groupsLength; moved ++ ) {
if (color !== null) {
var movedGroup = groups[moved];
if (groups[ groups[group].movedFrom ].blockStart < blockStart) {
if ( movedGroup.fixed !== false ) {
blockFrom = 'left';
continue;
}
var movedOldNumber = movedGroup.oldNumber;
else {
 
blockFrom = 'right';
// Find fixed '=' reference block from original block position to position '|' block
// Similar to position deletions '-' code
 
// Get old text prev block
var prevBlock = null;
var block = lookupSorted[ movedGroup.blockStart ];
if ( block > 0 ) {
prevBlock = blocksOld[block - 1];
}
}
 
// addGet coloredold blocktext startnext markupblock
var nextBlock = null;
if (blockFrom == 'left') {
var block = lookupSorted[ movedGroup.blockEnd ];
diff += wDiff.HtmlCustomize(wDiff.htmlBlockLeftStart, color);
if ( block < blocksOld.length - 1 ) {
}
nextBlock = blocksOld[block + 1];
else if (blockFrom == 'right') {
}
diff += wDiff.HtmlCustomize(wDiff.htmlBlockRightStart, color);
}
 
// Move after prev block if fixed
// cycle through blocks
var refBlock = null;
for (var block = blockStart; block <= blockEnd; block ++) {
if ( prevBlock !== null && prevBlock.type === '=' && prevBlock.fixed === true ) {
var type = blocks[block].type;
refBlock = prevBlock;
var string = blocks[block].string;
}
 
// htmlMove escapebefore textnext stringblock if fixed
else if ( nextBlock !== null && nextBlock.type === '=' && nextBlock.fixed === true ) {
string = wDiff.HtmlEscape(string);
refBlock = nextBlock;
}
 
// Find closest fixed block to the left
// add 'same' (unchanged) text
if (type == 'same')else {
for ( var fixed = lookupSorted[ movedGroup.blockStart ] - 1; fixed >= 0; fixed -- ) {
if (color !== null) {
if ( blocksOld[fixed].type === '=' && blocksOld[fixed].fixed === true ) {
string = string.replace(/\n/g, wDiff.htmlNewline);
refBlock = blocksOld[fixed];
break;
}
}
diff += string;
}
 
// addGet 'del'position textof new mark block
var newNumber;
else if (type == 'del') {
var markGroup;
string = string.replace(/\n/g, wDiff.htmlNewline);
diff += wDiff.htmlDeleteStart + string + wDiff.htmlDeleteEnd;
}
 
// No smaller fixed block, moved right from before first block
// add 'ins' text
else if (type refBlock === null 'ins') {
newNumber = -1;
string = string.replace(/\n/g, wDiff.htmlNewline);
markGroup = groups.length;
diff += wDiff.htmlInsertStart + string + wDiff.htmlInsertEnd;
 
// Save new single-mark-block group
groups.push( {
oldNumber: 0,
blockStart: blocks.length,
blockEnd: blocks.length,
unique: false,
maxWords: null,
words: null,
chars: 0,
fixed: null,
movedFrom: null,
color: null
} );
}
else {
newNumber = refBlock.newNumber;
markGroup = refBlock.group;
}
 
// Insert '|' block
blocks.push( {
oldBlock: null,
newBlock: null,
oldNumber: movedOldNumber,
newNumber: newNumber,
oldStart: null,
count: null,
unique: null,
words: null,
chars: 0,
type: '|',
section: null,
group: markGroup,
fixed: true,
moved: moved,
text: ''
} );
 
// Set group color
movedGroup.color = color;
movedGroup.movedFrom = markGroup;
color ++;
}
 
// Sort '|' blocks in and update groups
// add colored block end markup
this.sortBlocks();
if (blockFrom == 'left') {
 
diff += wDiff.htmlBlockLeftEnd;
if ( this.config.timer === true ) {
}
this.timeEnd( 'insertMarks' );
else if (blockFrom == 'right') {
diff += wDiff.htmlBlockRightEnd;
}
return;
};
 
groups[group].diff = diff;
}
 
//**
* Collect diff fragment list for markup, create abstraction layer for customized diffs.
// mark original block positions
* Adds the following fagment types:
//
* '=', '-', '+' same, deletion, insertion
* '<', '>' mark left, mark right
* '(<', '(>', ')' block start and end
* '[', ']' fragment start and end
* '{', '}' container start and end
*
* @param[in] array groups Groups table object
* @param[in] array blocks Blocks table object
* @param[out] array fragments Fragments array, abstraction layer for diff code
*/
this.getDiffFragments = function () {
 
var blocks = this.blocks;
// cycle through groups
for ( var groupgroups = 0; group < this.groups.length; group ++) {
var movedfragments = groups[group]this.movedfragments;
 
// cycleMake throughshallow listcopy of groups movedand fromsort hereby blockStart
var leftMarksgroupsSort = ''groups.slice();
groupsSort.sort( function( a, b ) {
var rightMarks = '';
return a.blockStart - b.blockStart;
for (var i = 0; i < moved.length; i ++) {
} );
var movedGroup = moved[i];
var markColor = groups[movedGroup].color;
var mark = '';
 
// getCycle movedthrough block textgroups
var movedTextgroupsSortLength = ''groupsSort.length;
for ( var blockgroup = groups[movedGroup].blockStart0; blockgroup <= groups[movedGroup].blockEndgroupsSortLength; blockgroup ++ ) {
var blockStart = groupsSort[group].blockStart;
if (blocks[block].type != 'ins') {
movedTextvar blockEnd += blocksgroupsSort[blockgroup].stringblockEnd;
 
// Add moved block start
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
} );
}
 
// Cycle through blocks
// display as deletion at original position
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if (wDiff.showBlockMoves === false) {
var type = blocks[block].type;
mark = wDiff.htmlDeleteStart + wDiff.HtmlEscape(movedText) + wDiff.htmlDeleteEnd;
}
 
// Add '=' unchanged text and moved block
// get mark direction
if ( type === '=' || type === '-' || type === '+' ) {
else {
fragments.push( {
if (groups[movedGroup].blockStart < groups[group].blockStart) {
text: blocks[block].text,
mark = wDiff.htmlMarkLeft;
type: type,
color: color
} );
}
else {
mark = wDiff.htmlMarkRight;
}
mark = wDiff.HtmlCustomize(mark, markColor, movedText);
}
 
// Add '<' and '>' marks
else if ( type === '|' ) {
var movedGroup = groups[ blocks[block].moved ];
 
// getGet sidemark of group to marktext
var markText = '';
if (groups[movedGroup].oldNumber < groups[group].oldNumber) {
leftMarks for += mark;(
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
} );
}
}
 
else {
// Add moved block end
rightMarks += mark;
if ( color !== null ) {
fragments.push( {
text: '',
type: ' )',
color: color
} );
}
}
groups[group].diff = leftMarks + groups[group].diff + rightMarks;
}
 
// Cycle through fragments, join consecutive fragments of same type (i.e. '-' blocks)
//
var fragmentsLength = fragments.length;
// join diffs
for ( var fragment = 1; fragment < fragmentsLength; fragment ++ ) {
//
 
// Check if joinable
// make shallow copy of groups and sort by blockStart
if (
var groupsSort = groups.slice();
fragments[fragment].type === fragments[fragment - 1].type &&
groupsSort.sort(function(a, b) {
fragments[fragment].color === fragments[fragment - 1].color &&
return a.blockStart - b.blockStart;
fragments[fragment].text !== '' && fragments[fragment - 1].text !== ''
});
) {
 
// Join and splice
// cycle through sorted groups and assemble diffs
fragments[fragment - 1].text += fragments[fragment].text;
for (var group = 0; group < groupsSort.length; group ++) {
fragments.splice( fragment, 1 );
text.diff += groupsSort[group].diff;
fragment --;
}
}
}
 
// Enclose in containers
// WED('Groups', wDiff.DebugGroups(groups));
fragments.unshift( { text: '', type: '{', color: null }, { text: '', type: '[', color: null } );
fragments.push( { text: '', type: ']', color: null }, { text: '', type: '}', color: null } );
 
return;
// keep newlines and multiple spaces
};
wDiff.HtmlFormat(text);
 
// WED('text.diff', text.diff);
 
/**
return text.diff;
* Clip unchanged sections from unmoved block text.
};
* Adds the following fagment types:
* '~', ' ~', '~ ' omission indicators
* '[', ']', ',' fragment start and end, fragment separator
*
* @param[in/out] array fragments Fragments array, abstraction layer for diff code
*/
this.clipDiffFragments = function () {
 
var fragments = this.fragments;
 
// Skip if only one fragment in containers, no change
//
if ( fragments.length === 5 ) {
// wDiff.HtmlCustomize: customize move indicator html: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
return;
// input: text (html or css code)
}
// returns: customized text
// called from: wDiff.AssembleDiff()
 
wDiff.HtmlCustomize = function (text, number, title) {
 
// Min length for clipping right
if (wDiff.coloredBlocks === true) {
var blockStyleminRight = wDiffthis.styleBlockColor[number]config.clipHeadingRight;
if ( this.config.clipParagraphRightMin < minRight ) {
if (blockStyle === undefined) {
minRight = this.config.clipParagraphRightMin;
blockStyle = '';
}
if ( this.config.clipLineRightMin < minRight ) {
var markStyle = wDiff.styleMarkColor[number];
minRight = this.config.clipLineRightMin;
if (markStyle === undefined) {
}
markStyle = '';
if ( this.config.clipBlankRightMin < minRight ) {
minRight = this.config.clipBlankRightMin;
}
if ( this.config.clipCharsRight < minRight ) {
minRight = this.config.clipCharsRight;
}
text = text.replace(/\{block\}/g, ' ' + blockStyle);
text = text.replace(/\{mark\}/g, ' ' + markStyle);
text = text.replace(/\{class\}/g, number);
}
else {
text = text.replace(/\{block\}|\{mark\}|\{class\}/g, '');
}
text = text.replace(/\{number\}/g, number);
 
// Min length for clipping left
// shorten title text, replace {title}
var minLeft = this.config.clipHeadingLeft;
if ( (title !== undefined) && (title !== '') ) {
if ( this.config.clipParagraphLeftMin < minLeft ) {
var max = 512;
minLeft = this.config.clipParagraphLeftMin;
var end = 128;
}
var gapMark = ' [...] ';
if (title this.lengthconfig.clipLineLeftMin < >minLeft max) {
minLeft = this.config.clipLineLeftMin;
title = title.substr(0, max - gapMark.length - end) + gapMark + title.substr(title.length - end);
}
if ( this.config.clipBlankLeftMin < minLeft ) {
minLeft = this.config.clipBlankLeftMin;
}
if ( this.config.clipCharsLeft < minLeft ) {
minLeft = this.config.clipCharsLeft;
}
title = wDiff.HtmlEscape(title);
title = title.replace(/\t/g, '&nbsp;&nbsp;');
title = title.replace(/ /g, '&nbsp;&nbsp;');
text = text.replace(/\{title\}/, ' title="' + title + '"');
}
else {
text = text.replace(/\{title\}/, '');
}
return text;
};
 
// Cycle through fragments
var fragmentsLength = fragments.length;
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
 
// Skip if not an unmoved and unchanged block
//
var type = fragments[fragment].type;
// wDiff.HtmlEscape: replace html-sensitive characters in output text with character entities
var color = fragments[fragment].color;
// input: text
if ( type !== '=' || color !== null ) {
// returns: escaped text
continue;
// called from: wDiff.Diff(), wDiff.AssembleDiff()
}
 
// Skip if too short for clipping
wDiff.HtmlEscape = function (text) {
var text = fragments[fragment].text;
var textLength = text.length;
if ( textLength < minRight && textLength < minLeft ) {
continue;
}
 
// Get line positions including start and end
text = text.replace(/&/g, '&amp;');
var lines = [];
text = text.replace(/</g, '&lt;');
var lastIndex = null;
text = text.replace(/>/g, '&gt;');
var regExpMatch;
text = text.replace(/"/g, '&quot;');
while ( ( regExpMatch = this.config.regExp.clipLine.exec( text ) ) !== null ) {
return (text);
lines.push( regExpMatch.index );
};
lastIndex = this.config.regExp.clipLine.lastIndex;
}
if ( lines[0] !== 0 ) {
lines.unshift( 0 );
}
if ( lastIndex !== textLength ) {
lines.push( textLength );
}
 
// Get heading positions
var headings = [];
var headingsEnd = [];
while ( ( regExpMatch = this.config.regExp.clipHeading.exec( text ) ) !== null ) {
headings.push( regExpMatch.index );
headingsEnd.push( regExpMatch.index + regExpMatch[0].length );
}
 
// Get paragraph positions including start and end
//
var paragraphs = [];
// wDiff.HtmlFormat: tidy html, keep newlines and multiple spaces, add container
var lastIndex = null;
// changes: text.diff
while ( ( regExpMatch = this.config.regExp.clipParagraph.exec( text ) ) !== null ) {
// called from: wDiff.Diff(), wDiff.AssembleDiff()
paragraphs.push( regExpMatch.index );
lastIndex = this.config.regExp.clipParagraph.lastIndex;
}
if ( paragraphs[0] !== 0 ) {
paragraphs.unshift( 0 );
}
if ( lastIndex !== textLength ) {
paragraphs.push( textLength );
}
 
// Determine ranges to keep on left and right side
wDiff.HtmlFormat = function (text) {
var rangeRight = null;
var rangeLeft = null;
var rangeRightType = '';
var rangeLeftType = '';
 
// Find clip pos from left, skip for first non-container block
text.diff = text.diff.replace(/<\/(\w+)><!--wDiff(Delete|Insert)--><\1\b[^>]*\bclass="wDiff\2"[^>]*>/g, '');
if ( fragment !== 2 ) {
text.diff = text.diff.replace(/\t/g, wDiff.htmlTab);
text.diff = wDiff.htmlContainerStart + wDiff.htmlFragmentStart + text.diff + wDiff.htmlFragmentEnd + wDiff.htmlContainerEnd;
return;
};
 
// Maximum lines to search from left
var rangeLeftMax = textLength;
if ( this.config.clipLinesLeftMax < lines.length ) {
rangeLeftMax = lines[this.config.clipLinesLeftMax];
}
 
// Find first heading from left
// wDiff.ShortenOutput: shorten diff html by removing unchanged parts
if ( rangeLeft === null ) {
// input: diff html string from wDiff.Diff()
var headingsLength = headingsEnd.length;
// returns: shortened html with removed unchanged passages indicated by (...) or separator
for ( var j = 0; j < headingsLength; j ++ ) {
if ( headingsEnd[j] > this.config.clipHeadingLeft || headingsEnd[j] > rangeLeftMax ) {
break;
}
rangeLeft = headingsEnd[j];
rangeLeftType = 'heading';
break;
}
}
 
// Find first paragraph from left
wDiff.ShortenOutput = function (html) {
if ( rangeLeft === null ) {
var paragraphsLength = paragraphs.length;
for ( var j = 0; j < paragraphsLength; j ++ ) {
if (
paragraphs[j] > this.config.clipParagraphLeftMax ||
paragraphs[j] > rangeLeftMax
) {
break;
}
if ( paragraphs[j] > this.config.clipParagraphLeftMin ) {
rangeLeft = paragraphs[j];
rangeLeftType = 'paragraph';
break;
}
}
}
 
// Find first line break from left
var diff = '';
if ( rangeLeft === null ) {
var linesLength = lines.length;
for ( var j = 0; j < linesLength; j ++ ) {
if ( lines[j] > this.config.clipLineLeftMax || lines[j] > rangeLeftMax ) {
break;
}
if ( lines[j] > this.config.clipLineLeftMin ) {
rangeLeft = lines[j];
rangeLeftType = 'line';
break;
}
}
}
 
// Find first blank from left
// empty text
if ( (html === undefined) || (htmlrangeLeft === '')null ) {
this.config.regExp.clipBlank.lastIndex = this.config.clipBlankLeftMin;
return '';
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
// remove container by non-regExp replace
if ( rangeLeft === null ) {
html = html.replace(wDiff.htmlContainerStart, '');
if ( this.config.clipCharsLeft < rangeLeftMax ) {
html = html.replace(wDiff.htmlFragmentStart, '');
rangeLeft = this.config.clipCharsLeft;
html = html.replace(wDiff.htmlFragmentEnd, '');
rangeLeftType = 'chars';
html = html.replace(wDiff.htmlContainerEnd, '');
}
}
 
// Fixed number of lines from left
// scan for diff html tags
if ( rangeLeft === null ) {
var regExpDiff = /<\w+\b[^>]*\bclass="[^">]*?\bwDiff(MarkLeft|MarkRight|BlockLeft|BlockRight|Delete|Insert)\b[^">]*"[^>]*>(.|\n)*?<!--wDiff\1-->/g;
rangeLeft = rangeLeftMax;
var tagsStart = [];
rangeLeftType = 'fixed';
var tagsEnd = [];
}
var i = 0;
}
var regExpMatch;
 
// Find clip pos from right, skip for last non-container block
// save tag positions
while if ( (regExpMatchfragment !== regExpDifffragments.exec(html))length !==- null3 ) {
 
// Maximum lines to search from right
// combine consecutive diff tags
var rangeRightMin = 0;
if ( (i > 0) && (tagsEnd[i - 1] == regExpMatch.index) ) {
if ( lines.length >= this.config.clipLinesRightMax ) {
tagsEnd[i - 1] = regExpMatch.index + regExpMatch[0].length;
rangeRightMin = lines[lines.length - this.config.clipLinesRightMax];
}
else { }
tagsStart[i] = regExpMatch.index;
tagsEnd[i] = regExpMatch.index + regExpMatch[0].length;
i ++;
}
}
 
// Find last heading from right
// no diff tags detected
if (tagsStart.length rangeRight === 0null ) {
for ( var j = headings.length - 1; j >= 0; j -- ) {
return wDiff.htmlNoChange;
if (
}
headings[j] < textLength - this.config.clipHeadingRight ||
headings[j] < rangeRightMin
) {
break;
}
rangeRight = headings[j];
rangeRightType = 'heading';
break;
}
}
 
// Find last paragraph from right
// define regexps
if ( rangeRight === null ) {
var regExpLine = /^(\n+|.)|(\n+|.)$|\n+/g;
for ( var j = paragraphs.length - 1; j >= 0 ; j -- ) {
var regExpHeading = /(^|\n)(<[^>]+>)*(==+.+?==+|\{\||\|\}).*?\n?/g;
if (
var regExpParagraph = /^(\n\n+|.)|(\n\n+|.)$|\n\n+/g;
paragraphs[j] < textLength - this.config.clipParagraphRightMax ||
var regExpBlank = /(<[^>]+>)*\s+/g;
paragraphs[j] < rangeRightMin
) {
break;
}
if ( paragraphs[j] < textLength - this.config.clipParagraphRightMin ) {
rangeRight = paragraphs[j];
rangeRightType = 'paragraph';
break;
}
}
}
 
// getFind last line positionsbreak from right
if ( rangeRight === null ) {
var regExpMatch;
for ( var j = lines.length - 1; j >= []0; j -- ) {
if (
while ( (regExpMatch = regExpLine.exec(html)) !== null) {
lines[j] < textLength - this.config.clipLineRightMax ||
lines.push(regExpMatch.index);
lines[j] < rangeRightMin
}
) {
break;
}
if ( lines[j] < textLength - this.config.clipLineRightMin ) {
rangeRight = lines[j];
rangeRightType = 'line';
break;
}
}
}
 
// Find last blank from right
// get heading positions
if ( rangeRight === null ) {
var headings = [];
var startPos = textLength - this.config.clipBlankRightMax;
var headingsEnd = [];
if ( startPos < rangeRightMin ) {
while ( (regExpMatch = regExpHeading.exec(html)) !== null ) {
startPos = rangeRightMin;
headings.push(regExpMatch.index);
}
headingsEnd.push(regExpMatch.index + regExpMatch[0].length);
this.config.regExp.clipBlank.lastIndex = startPos;
}
var lastPos = null;
while ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if ( regExpMatch.index > textLength - this.config.clipBlankRightMin ) {
if ( lastPos !== null ) {
rangeRight = lastPos;
rangeRightType = 'blank';
}
break;
}
lastPos = regExpMatch.index;
}
}
 
// Fixed number of chars from right
// get paragraph positions
if ( rangeRight === null ) {
var paragraphs = [];
if ( textLength - this.config.clipCharsRight > rangeRightMin ) {
while ( (regExpMatch = regExpParagraph.exec(html)) !== null ) {
rangeRight = textLength - this.config.clipCharsRight;
paragraphs.push(regExpMatch.index);
rangeRightType = 'chars';
}
}
}
 
// Fixed number of lines from right
// determine fragment border positions around diff tags
if ( rangeRight === null ) {
var lineMaxBefore = 0;
rangeRight = rangeRightMin;
var headingBefore = 0;
rangeRightType = 'fixed';
var paragraphBefore = 0;
}
var lineBefore = 0;
}
 
// Check if we skip clipping if ranges are close together
var lineMaxAfter = 0;
if ( rangeLeft !== null && rangeRight !== null ) {
var headingAfter = 0;
var paragraphAfter = 0;
var lineAfter = 0;
 
// Skip if overlapping ranges
var rangeStart = [];
if ( rangeLeft > rangeRight ) {
var rangeEnd = [];
continue;
var rangeStartType = [];
}
var rangeEndType = [];
 
// Skip if chars too close
// cycle through diff tag start positions
var skipChars = rangeRight - rangeLeft;
for (var i = 0; i < tagsStart.length; i ++) {
if ( skipChars < this.config.clipSkipChars ) {
var tagStart = tagsStart[i];
continue;
var tagEnd = tagsEnd[i];
}
 
// maximalSkip if lines to search before difftoo tagclose
var rangeStartMinskipLines = 0;
for ( var jlinesLength = lineMaxBefore; j < lines.length - 1; j ++) {
if for (tagStart var j = 0; j < lines[linesLength; j ++ 1]) {
if ( lines[j] -> wDiff.linesBeforeMaxrangeRight || skipLines >= 0this.config.clipSkipLines ) {
break;
rangeStartMin = lines[j - wDiff.linesBeforeMax];
}
if ( lines[j] > rangeLeft ) {
skipLines ++;
}
}
if ( skipLines < this.config.clipSkipLines ) {
continue;
}
lineMaxBefore = j;
break;
}
}
 
// findSkip lastif headingnothing beforeto diff tagclip
if (rangeStart[i] rangeLeft === undefinednull && rangeRight === null ) {
continue;
for (var j = headingBefore; j < headings.length - 1; j ++) {
}
if (headings[j] > tagStart) {
 
break;
// Split left text
var textLeft = null;
var omittedLeft = null;
if ( rangeLeft !== null ) {
textLeft = text.slice( 0, rangeLeft );
 
// Remove trailing empty lines
textLeft = textLeft.replace( this.config.regExp.clipTrimNewLinesLeft, '' );
 
// Get omission indicators, remove trailing blanks
if ( rangeLeftType === 'chars' ) {
omittedLeft = '~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
else if (headings[j +rangeLeftType 1]=== >'blank' tagStart) {
omittedLeft = ' ~';
if ( (headings[j] > tagStart - wDiff.headingBefore) && (headings[j] > rangeStartMin) ) {
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
rangeStart[i] = headings[j];
rangeStartType[i] = 'heading';
headingBefore = j;
}
break;
}
}
}
 
// Split right text
// find last paragraph before diff tag
var textRight = null;
if (rangeStart[i] === undefined) {
var omittedRight = null;
for (var j = paragraphBefore; j < paragraphs.length - 1; j ++) {
if (paragraphs[j] >rangeRight !== null tagStart) {
textRight = text.slice( rangeRight );
break;
 
// Remove leading empty lines
textRight = textRight.replace( this.config.regExp.clipTrimNewLinesRight, '' );
 
// Get omission indicators, remove leading blanks
if ( rangeRightType === 'chars' ) {
omittedRight = '~';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
else if ( rangeRightType === 'blank' ) {
if (paragraphs[j + 1] > tagStart - wDiff.paragraphBeforeMin) {
omittedRight = '~ ';
if ( (paragraphs[j] > tagStart - wDiff.paragraphBeforeMax) && (paragraphs[j] > rangeStartMin) ) {
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
rangeStart[i] = paragraphs[j];
rangeStartType[i] = 'paragraph';
paragraphBefore = j;
}
break;
}
}
}
 
// Remove split element
// find last line break before diff tag
fragments.splice( fragment, 1 );
if (rangeStart[i] === undefined) {
fragmentsLength --;
for (var j = lineBefore; j < lines.length - 1; j ++) {
 
if (lines[j + 1] > tagStart - wDiff.lineBeforeMin) {
// Add left text to fragments list
if ( (lines[j] > tagStart - wDiff.lineBeforeMax) && (lines[j] > rangeStartMin) ) {
if ( rangeLeft !== null ) {
rangeStart[i] = lines[j];
fragments.splice( fragment ++, 0, { text: textLeft, type: '=', color: null } );
rangeStartType[i] = 'line';
lineBeforefragmentsLength = j++;
if ( omittedLeft !== null ) {
}
fragments.splice( fragment ++, 0, { text: '', type: omittedLeft, color: null } );
break;
fragmentsLength ++;
}
}
}
 
// Add fragment container and separator to list
// find last blank before diff tag
if ( rangeLeft !== null && rangeRight !== null ) {
if (rangeStart[i] === undefined) {
fragments.splice( fragment ++, 0, { text: '', type: ']', color: null } );
var lastPos = tagStart - wDiff.blankBeforeMax;
fragments.splice( fragment ++, 0, { text: '', type: ',', color: null } );
if (lastPos < rangeStartMin) {
fragments.splice( fragment ++, 0, { text: '', type: '[', color: null } );
lastPos = rangeStartMin;
fragmentsLength += 3;
}
 
regExpBlank.lastIndex = lastPos;
// Add right text to fragments list
while ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
if ( rangeRight !== null ) {
if (regExpMatch.index > tagStart - wDiff.blankBeforeMin) {
if ( omittedRight !== null ) {
rangeStart[i] = lastPos;
fragments.splice( fragment ++, 0, { text: '', type: omittedRight, color: null } );
rangeStartType[i] = 'blank';
breakfragmentsLength ++;
}
fragments.splice( fragment ++, 0, { text: textRight, type: '=', color: null } );
lastPos = regExpMatch.index;
fragmentsLength ++;
}
}
 
// Debug log
// fixed number of chars before diff tag
if (rangeStart[i] this.config.debug === undefinedtrue ) {
this.debugFragments( 'Fragments' );
if (tagStart - wDiff.charsBefore > rangeStartMin) {
rangeStart[i] = tagStart - wDiff.charsBefore;
rangeStartType[i] = 'chars';
}
}
 
return;
// fixed number of lines before diff tag
};
if (rangeStart[i] === undefined) {
 
rangeStart[i] = rangeStartMin;
 
rangeStartType[i] = 'lines';
/**
* Create html formatted diff code from diff fragments.
*
* @param[in] array fragments Fragments array, abstraction layer for diff code
* @param string|undefined version
* Output version: 'new' or 'old': only text from new or old version, used for unit tests
* @param[out] string html Html code of diff
*/
this.getDiffHtml = function ( version ) {
 
var fragments = this.fragments;
 
// No change, only one unchanged block in containers
if ( fragments.length === 5 && fragments[2].type === '=' ) {
this.html = '';
return;
}
 
// Cycle through fragments
// maximal lines to search after diff tag
var rangeEndMaxhtmlFragments = html.length[];
for (var jfragmentsLength = lineMaxAfter; j < linesfragments.length; j ++) {
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
if (lines[j] > tagEnd) {
var text = fragments[fragment].text;
if (j + wDiff.linesAfterMax < lines.length) {
var type = fragments[fragment].type;
rangeEndMax = lines[j + wDiff.linesAfterMax];
var color = fragments[fragment].color;
var html = '';
 
// Test if text is blanks-only or a single character
var blank = false;
if ( text !== '' ) {
blank = this.config.regExp.blankBlock.test( text );
}
 
// Add container start markup
if ( type === '{' ) {
html = this.config.htmlCode.containerStart;
}
 
// Add container end markup
else if ( type === '}' ) {
html = this.config.htmlCode.containerEnd;
}
 
// Add fragment start markup
if ( type === '[' ) {
html = this.config.htmlCode.fragmentStart;
}
 
// Add fragment end markup
else if ( type === ']' ) {
html = this.config.htmlCode.fragmentEnd;
}
 
// Add fragment separator markup
else if ( type === ',' ) {
html = this.config.htmlCode.separator;
}
 
// Add omission markup
if ( type === '~' ) {
html = this.config.htmlCode.omittedChars;
}
 
// Add omission markup
if ( type === ' ~' ) {
html = ' ' + this.config.htmlCode.omittedChars;
}
 
// Add omission markup
if ( type === '~ ' ) {
html = this.config.htmlCode.omittedChars + ' ';
}
 
// Add colored left-pointing block start markup
else if ( type === '(<' ) {
if ( version !== 'old' ) {
 
// Get title
var title;
if ( this.config.noUnicodeSymbols === true ) {
title = this.config.msg['wiked-diff-block-left-nounicode'];
}
else {
title = this.config.msg['wiked-diff-block-left'];
}
 
// Get html
if ( this.config.coloredBlocks === true ) {
html = this.config.htmlCode.blockColoredStart;
}
else {
html = this.config.htmlCode.blockStart;
}
html = this.htmlCustomize( html, color, title );
}
lineMaxAfter = j;
break;
}
}
 
// Add colored right-pointing block start markup
// find first heading after diff tag
else if (rangeEnd[i] type === undefined'(>' ) {
if ( version !== 'old' ) {
for (var j = headingAfter; j < headingsEnd.length; j ++) {
 
if (headingsEnd[j] > tagEnd) {
// Get title
if ( (headingsEnd[j] < tagEnd + wDiff.headingAfter) && (headingsEnd[j] < rangeEndMax) ) {
var title;
rangeEnd[i] = headingsEnd[j];
if ( this.config.noUnicodeSymbols === true ) {
rangeEndType[i] = 'heading';
title = this.config.msg['wiked-diff-block-right-nounicode'];
paragraphAfter = j;
}
break;else {
title = this.config.msg['wiked-diff-block-right'];
}
 
// Get html
if ( this.config.coloredBlocks === true ) {
html = this.config.htmlCode.blockColoredStart;
}
else {
html = this.config.htmlCode.blockStart;
}
html = this.htmlCustomize( html, color, title );
}
}
}
 
// findAdd firstcolored paragraphblock afterend diff tagmarkup
else if (rangeEnd[i] type === undefined' )' ) {
if ( version !== 'old' ) {
for (var j = paragraphAfter; j < paragraphs.length; j ++) {
html = this.config.htmlCode.blockEnd;
if (paragraphs[j] > tagEnd + wDiff.paragraphAfterMin) {
}
if ( (paragraphs[j] < tagEnd + wDiff.paragraphAfterMax) && (paragraphs[j] < rangeEndMax) ) {
}
rangeEnd[i] = paragraphs[j];
 
rangeEndType[i] = 'paragraph';
// Add '=' (unchanged) text and moved block
paragraphAfter = j;
if ( type === '=' ) {
text = this.htmlEscape( text );
if ( color !== null ) {
if ( version !== 'old' ) {
html = this.markupBlanks( text, true );
}
break;}
else {
html = this.markupBlanks( text );
}
}
}
 
// Add '-' text
// find first line break after diff tag
else if (rangeEnd[i] type === undefined'-' ) {
for if (var jversion !== lineAfter; j < lines.length; j'new' ++) {
 
if (lines[j] > tagEnd + wDiff.lineAfterMin) {
// For old version skip '-' inside moved group
if ( (lines[j] < tagEnd + wDiff.lineAfterMax) && (lines[j] < rangeEndMax) ) {
if ( version !== 'old' || color === null ) {
rangeEnd[i] = lines[j];
rangeEndType[i]text = 'line'this.htmlEscape( text );
text = this.markupBlanks( text, true );
lineAfter = j;
if ( blank === true ) {
html = this.config.htmlCode.deleteStartBlank;
}
else {
html = this.config.htmlCode.deleteStart;
}
html += text + this.config.htmlCode.deleteEnd;
}
break;
}
}
}
 
// findAdd blank'+' after diff tagtext
else if (rangeEnd[i] type === undefined'+' ) {
if ( version !== 'old' ) {
regExpBlank.lastIndex = tagEnd + wDiff.blankAfterMin;
text = this.htmlEscape( text );
if ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
text = this.markupBlanks( text, true );
if ( (regExpMatch.index < tagEnd + wDiff.blankAfterMax) && (regExpMatch.index < rangeEndMax) ) {
if ( blank === true ) {
rangeEnd[i] = regExpMatch.index;
html = this.config.htmlCode.insertStartBlank;
rangeEndType[i] = 'blank';
}
else {
html = this.config.htmlCode.insertStart;
}
html += text + this.config.htmlCode.insertEnd;
}
}
 
// Add '<' and '>' code
else if ( type === '<' || type === '>' ) {
if ( version !== 'new' ) {
 
// Display as deletion at original position
if ( this.config.showBlockMoves === false || version === 'old' ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
if ( version === 'old' ) {
if ( this.config.coloredBlocks === true ) {
html =
this.htmlCustomize( this.config.htmlCode.blockColoredStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
else {
html =
this.htmlCustomize( this.config.htmlCode.blockStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
}
else {
if ( blank === true ) {
html =
this.config.htmlCode.deleteStartBlank +
text +
this.config.htmlCode.deleteEnd;
}
else {
html = this.config.htmlCode.deleteStart + text + this.config.htmlCode.deleteEnd;
}
}
}
 
// Display as mark
else {
if ( type === '<' ) {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markLeftColored, color, text );
}
else {
html = this.htmlCustomize( this.config.htmlCode.markLeft, color, text );
}
}
else {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markRightColored, color, text );
}
else {
html = this.htmlCustomize( this.config.htmlCode.markRight, color, text );
}
}
}
}
}
htmlFragments.push( html );
}
 
// Join fragments
// fixed number of chars after diff tag
this.html = htmlFragments.join( '' );
if (rangeEnd[i] === undefined) {
 
if (tagEnd + wDiff.charsAfter < rangeEndMax) {
return;
rangeEnd[i] = tagEnd + wDiff.charsAfter;
};
rangeEndType[i] = 'chars';
 
 
/**
* Customize html code fragments.
* Replaces:
* {number}: class/color/block/mark/id number
* {title}: title attribute (popup)
* {nounicode}: noUnicodeSymbols fallback
* input: html, number: block number, title: title attribute (popup) text
*
* @param string html Html code to be customized
* @return string Customized html code
*/
this.htmlCustomize = function ( html, number, title ) {
 
// Replace {number} with class/color/block/mark/id number
html = html.replace( /\{number\}/g, number);
 
// Replace {nounicode} with wikEdDiffNoUnicode class name
if ( this.config.noUnicodeSymbols === true ) {
html = html.replace( /\{nounicode\}/g, ' wikEdDiffNoUnicode');
}
else {
html = html.replace( /\{nounicode\}/g, '');
}
 
// Shorten title text, replace {title}
if ( title !== undefined ) {
var max = 512;
var end = 128;
var gapMark = ' [...] ';
if ( title.length > max ) {
title =
title.substr( 0, max - gapMark.length - end ) +
gapMark +
title.substr( title.length - end );
}
title = this.htmlEscape( title );
title = title.replace( /\t/g, '&nbsp;&nbsp;');
title = title.replace( / /g, '&nbsp;&nbsp;');
html = html.replace( /\{title\}/, title);
}
return html;
};
 
 
// fixed number of lines after diff tag
/**
if (rangeEnd[i] === undefined) {
* Replace html-sensitive characters in output text with character entities.
rangeEnd[i] = rangeEndMax;
*
rangeEndType[i] = 'lines';
* @param string html Html code to be escaped
* @return string Escaped html code
*/
this.htmlEscape = function ( html ) {
 
html = html.replace( /&/g, '&amp;');
html = html.replace( /</g, '&lt;');
html = html.replace( />/g, '&gt;');
html = html.replace( /"/g, '&quot;');
return html;
};
 
 
/**
* Markup tabs, newlines, and spaces in diff fragment text.
*
* @param bool highlight Highlight newlines and spaces in addition to tabs
* @param string html Text code to be marked-up
* @return string Marked-up text
*/
this.markupBlanks = function ( html, highlight ) {
 
if ( highlight === true ) {
html = html.replace( / /g, this.config.htmlCode.space);
html = html.replace( /\n/g, this.config.htmlCode.newline);
}
html = html.replace( /\t/g, this.config.htmlCode.tab);
}
return html;
};
 
// remove overlaps, join close fragments
var fragmentStart = [];
var fragmentEnd = [];
var fragmentStartType = [];
var fragmentEndType = [];
fragmentStart[0] = rangeStart[0];
fragmentEnd[0] = rangeEnd[0];
fragmentStartType[0] = rangeStartType[0];
fragmentEndType[0] = rangeEndType[0];
var j = 1;
for (var i = 1; i < rangeStart.length; i ++) {
 
/**
// get lines between fragments
* Count real words in text.
var lines = 0;
*
if (fragmentEnd[j - 1] < rangeStart[i]) {
* @param string text Text for word counting
var join = html.substring(fragmentEnd[j - 1], rangeStart[i]);
* @return int Number of words in text
lines = (join.match(/\n/g) || []).length;
*/
this.wordCount = function ( text ) {
 
return ( text.match( this.config.regExp.countWords ) || [] ).length;
};
 
 
/**
* Test diff code for consistency with input versions.
* Prints results to debug console.
*
* @param[in] WikEdDiffText newText, oldText Text objects
*/
this.unitTests = function () {
 
// Check if output is consistent with new text
this.getDiffHtml( 'new' );
var diff = this.html.replace( /<[^>]*>/g, '');
var text = this.htmlEscape( this.newText.text );
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with new text version!'
);
this.error = true;
console.log( 'new text:\n', text );
console.log( 'new diff:\n', diff );
}
else {
console.log( 'OK: wikEdDiff unit test passed: diff consistent with new text.' );
}
 
// Check if output is consistent with old text
if ( (rangeStart[i] > fragmentEnd[j - 1] + wDiff.fragmentJoinChars) || (lines > wDiff.fragmentJoinLines) ) {
this.getDiffHtml( 'old' );
fragmentStart[j] = rangeStart[i];
var diff = this.html.replace( /<[^>]*>/g, '');
fragmentEnd[j] = rangeEnd[i];
var text = this.htmlEscape( this.oldText.text );
fragmentStartType[j] = rangeStartType[i];
if ( diff !== text ) {
fragmentEndType[j] = rangeEndType[i];
console.log(
j ++;
'Error: wikEdDiff unit test failure: diff not consistent with old text version!'
);
this.error = true;
console.log( 'old text:\n', text );
console.log( 'old diff:\n', diff );
}
else {
console.log( 'OK: wikEdDiff unit test passed: diff consistent with old text.' );
fragmentEnd[j - 1] = rangeEnd[i];
fragmentEndType[j - 1] = rangeEndType[i];
}
}
 
return;
// assemble the fragments
};
for (var i = 0; i < fragmentStart.length; i ++) {
 
// get text fragment
var fragment = html.substring(fragmentStart[i], fragmentEnd[i]);
fragment = fragment.replace(/^\n+|\n+$/g, '');
 
/**
// add inline marks for omitted chars and words
* Dump blocks object to browser console.
if (fragmentStart[i] > 0) {
*
if (fragmentStartType[i] == 'chars') {
* @param string name Block name
fragment = wDiff.htmlOmittedChars + fragment;
* @param[in] array blocks Blocks table object
*/
this.debugBlocks = function ( name, blocks ) {
 
if ( blocks === undefined ) {
blocks = this.blocks;
}
var dump =
'\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq' +
'\twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \ttext\n';
var blocksLength = blocks.length;
for ( var i = 0; i < blocksLength; i ++ ) {
dump +=
i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' +
blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \t' + blocks[i].oldStart + ' \t' +
blocks[i].count + ' \t' + blocks[i].unique + ' \t' + blocks[i].words + ' \t' +
blocks[i].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' +
blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + blocks[i].moved + ' \t' +
this.debugShortenText( blocks[i].text ) + '\n';
}
console.log( name + ':\n' + dump );
};
 
 
/**
* Dump groups object to browser console.
*
* @param string name Group name
* @param[in] array groups Groups table object
*/
this.debugGroups = function ( name, groups ) {
 
if ( groups === undefined ) {
groups = this.groups;
}
var dump =
'\ni \toldNm \tblSta \tblEnd \tuniq \tmaxWo' +
'\twords \tchars \tfixed \toldNm \tmFrom \tcolor\n';
var groupsLength = groupsLength;
for ( var i = 0; i < groups.length; i ++ ) {
dump +=
i + ' \t' + groups[i].oldNumber + ' \t' + groups[i].blockStart + ' \t' +
groups[i].blockEnd + ' \t' + groups[i].unique + ' \t' + groups[i].maxWords + ' \t' +
groups[i].words + ' \t' + groups[i].chars + ' \t' + groups[i].fixed + ' \t' +
groups[i].oldNumber + ' \t' + groups[i].movedFrom + ' \t' + groups[i].color + '\n';
}
console.log( name + ':\n' + dump );
};
 
 
/**
* Dump fragments array to browser console.
*
* @param string name Fragments name
* @param[in] array fragments Fragments array
*/
this.debugFragments = function ( name ) {
 
var fragments = this.fragments;
var dump = '\ni \ttype \tcolor \ttext\n';
var fragmentsLength = fragments.length;
for ( var i = 0; i < fragmentsLength; i ++ ) {
dump +=
i + ' \t"' + fragments[i].type + '" \t' + fragments[i].color + ' \t' +
this.debugShortenText( fragments[i].text, 120, 40 ) + '\n';
}
console.log( name + ':\n' + dump );
};
 
 
/**
* Dump borders array to browser console.
*
* @param string name Arrays name
* @param[in] array border Match border array
*/
this.debugBorders = function ( name, borders ) {
 
var dump = '\ni \t[ new \told ]\n';
var bordersLength = borders.length;
for ( var i = 0; i < bordersLength; i ++ ) {
dump += i + ' \t[ ' + borders[i][0] + ' \t' + borders[i][1] + ' ]\n';
}
console.log( name, dump );
};
 
 
/**
* Shorten text for dumping.
*
* @param string text Text to be shortened
* @param int max Max length of (shortened) text
* @param int end Length of trailing fragment of shortened text
* @return string Shortened text
*/
this.debugShortenText = function ( text, max, end ) {
 
if ( typeof text !== 'string' ) {
text = text.toString();
}
text = text.replace( /\n/g, '\\n');
text = text.replace( /\t/g, ' ');
if ( max === undefined ) {
max = 50;
}
if ( end === undefined ) {
end = 15;
}
if ( text.length > max ) {
text = text.substr( 0, max - 1 - end ) + '…' + text.substr( text.length - end );
}
return '"' + text + '"';
};
 
 
/**
* Start timer 'label', analogous to JavaScript console timer.
* Usage: this.time( 'label' );
*
* @param string label Timer label
* @param[out] array timer Current time in milliseconds (float)
*/
this.time = function ( label ) {
 
this.timer[label] = new Date().getTime();
return;
};
 
 
/**
* Stop timer 'label', analogous to JavaScript console timer.
* Logs time in milliseconds since start to browser console.
* Usage: this.timeEnd( 'label' );
*
* @param string label Timer label
* @param bool noLog Do not log result
* @return float Time in milliseconds
*/
this.timeEnd = function ( label, noLog ) {
 
var diff = 0;
if ( this.timer[label] !== undefined ) {
var start = this.timer[label];
var stop = new Date().getTime();
diff = stop - start;
this.timer[label] = undefined;
if ( noLog !== true ) {
console.log( label + ': ' + diff.toFixed( 2 ) + ' ms' );
}
}
else if (fragmentStartType[i] == 'blank') {
return diff;
fragment = wDiff.htmlOmittedChars + ' ' + fragment;
};
 
 
/**
* Log recursion timer results to browser console.
* Usage: this.timeRecursionEnd();
*
* @param string text Text label for output
* @param[in] array recursionTimer Accumulated recursion times
*/
this.timeRecursionEnd = function ( text ) {
 
if ( this.recursionTimer.length > 1 ) {
 
// Subtract times spent in deeper recursions
var timerEnd = this.recursionTimer.length - 1;
for ( var i = 0; i < timerEnd; i ++ ) {
this.recursionTimer[i] -= this.recursionTimer[i + 1];
}
 
// Log recursion times
var timerLength = this.recursionTimer.length;
for ( var i = 0; i < timerLength; i ++ ) {
console.log( text + ' recursion ' + i + ': ' + this.recursionTimer[i].toFixed( 2 ) + ' ms' );
}
}
this.recursionTimer = [];
if (fragmentEnd[i] < html.length) {
return;
if (fragmentStartType[i] == 'chars') {
};
fragment = fragment + wDiff.htmlOmittedChars;
 
 
/**
* Log variable values to debug console.
* Usage: this.debug( 'var', var );
*
* @param string name Object identifier
* @param mixed|undefined name Object to be logged
*/
this.debug = function ( name, object ) {
 
if ( object === undefined ) {
console.log( name );
}
else {
console.log( name + ': ' + object );
}
return;
};
 
 
/**
* Add script to document head.
*
* @param string code JavaScript code
*/
this.addScript = function ( code ) {
 
if ( document.getElementById( 'wikEdDiffBlockHandler' ) === null ) {
var script = document.createElement( 'script' );
script.id = 'wikEdDiffBlockHandler';
if ( script.innerText !== undefined ) {
script.innerText = code;
}
else {
else if (fragmentStartType[i] == 'blank') {
script.textContent = code;
fragment = fragment + ' ' + wDiff.htmlOmittedChars;
}
document.getElementsByTagName( 'head' )[0].appendChild( script );
}
return;
};
 
// remove leading and trailing empty lines
fragment = fragment.replace(/^\n+|\n+$/g, '');
 
/**
// add fragment separator
* Add stylesheet to document head, cross-browser >= IE6.
if (i > 0) {
*
diff += wDiff.htmlSeparator;
* @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 {
style.appendChild( document.createTextNode( css ) );
}
document.getElementsByTagName( 'head' )[0].appendChild( style );
}
return;
};
 
// encapsulate span errors
diff += wDiff.htmlFragmentStart + fragment + wDiff.htmlFragmentEnd;
}
 
/**
// add to container
* Recursive deep copy from target over source for customization import.
diff = wDiff.htmlContainerStart + diff + wDiff.htmlContainerEnd;
*
* @param object source Source object
* @param object target Target object
*/
this.deepCopy = function ( source, target ) {
 
for ( var key in source ) {
// WED('diff', diff);
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
return diff;
this.init();
};
 
 
//**
* Data and methods for single text version (old or new one).
// wDiff.AddScript: add script to head
*
//
* @class WikEdDiffText
*/
WikEdDiff.WikEdDiffText = function ( text, parent ) {
 
/** @var WikEdDiff parent Parent object for configuration settings and debugging methods */
wDiff.AddScript = function (code) {
this.parent = parent;
 
/** @var string text Text of this version */
var script = document.createElement('script');
this.text = null;
script.id = 'wDiffBlockHandler';
if (script.innerText !== undefined) {
script.innerText = code;
}
else {
script.textContent = code;
}
document.getElementsByTagName('head')[0].appendChild(script);
return;
};
 
/** @var array tokens Tokens list */
this.tokens = [];
 
/** @var int first, last First and last index of tokens list */
//
this.first = null;
// wDiff.AddStyleSheet: add CSS rules to new style sheet, cross-browser >= IE6
this.last = null;
//
 
/** @var array words Word counts for version text */
wDiff.AddStyleSheet = function (css) {
this.words = {};
 
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet !== undefined) {
style.styleSheet.cssText = css;
}
else {
style.appendChild( document.createTextNode(css) );
}
document.getElementsByTagName('head')[0].appendChild(style);
return;
};
 
/**
* Constructor, initialize text object.
*
* @param string text Text of version
* @param WikEdDiff parent Parent, for configuration settings and debugging methods
*/
this.init = function () {
 
if ( typeof text !== 'string' ) {
//
text = text.toString();
// wDiff.WordCount: count words in string
}
//
 
// IE / Mac fix
wDiff.WordCount = function (string) {
this.text = text.replace( /\r\n?/g, '\n');
 
// Parse and count words and chunks for identification of unique real words
return (string.match(wDiff.regExpWordCount) || []).length;
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;
};
 
 
//**
* Parse and count words and chunks for identification of unique words.
// wDiff.DebugText: dump text (text.oldText or text.newText) object
*
//
* @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 );
wDiff.DebugText = function (text) {
if ( regExpMatch !== null ) {
var dump = 'first: ' + text.first + '\tlast: ' + text.last + '\n';
var matchLength = regExpMatch.length;
dump += '\ni \tlink \t(prev \tnext) \t#num \t"token"\n';
for (var i = 0; i < matchLength; i ++) {
var i = text.first;
var word = regExpMatch[i];
while ( (i !== null) && (text.tokens[i] !== null) ) {
if ( Object.prototype.hasOwnProperty.call( this.words, word ) === false ) {
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';
this.words[word] = 1;
i = text.tokens[i].next;
}
}
else {
return dump;
this.words[word] ++;
};
}
}
}
return;
};
 
 
//**
* Split text into paragraph, line, sentence, chunk, word, or character tokens.
// wDiff.DebugBlocks: dump blocks 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.DebugBlocks = function (blocks) {
var next = null;
var dump = '\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq \twords \tchars \ttype \tsect \tgroup \tfixed \tstring\n';
for ( var icurrent = 0; i < blocksthis.tokens.length; i ++) {
var first = current;
dump += i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' + blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \t' + blocks[i].oldStart + ' \t' + blocks[i].count + ' \t' + blocks[i].unique + ' \t' + blocks[i].words + ' \t' + blocks[i].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' + blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + wDiff.DebugShortenString(blocks[i].string) + '\n';
var text = '';
}
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.DebugGroups: dump groups 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.DebugGroups = function (groups) {
var splitLength = split.length;
var dump = '\ni \tblSta \tblEnd \tuniq \tmaxWo \twords \tchars \tfixed \toldNm \tmFrom \tcolor \tmoved \tdiff\n';
for ( var i = 0; i < groups.lengthsplitLength; i ++ ) {
dump += i + ' \t' + groups[i].blockStart + ' \t' + groups[i].blockEnd + ' \t' + groups[i].unique + ' \t' + groups[i].maxWords + ' \t' + groups[i].words + ' \t' + groups[i].chars + ' \t' + groups[i].fixed + ' \t' + groups[i].oldNumber + ' \t' + groups[i].movedFrom + ' \t' + groups[i].color + ' \t' + groups[i].moved.toString() + ' \t' + wDiff.DebugShortenString(groups[i].diff) + '\n';
}
return dump;
};
 
// 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.DebugGaps: dump gaps object
this.tokens[prev].next = current;
//
}
prev = current;
current ++;
}
 
// Connect last new item and existing next item
wDiff.DebugGaps = function (gaps) {
if ( number > 0 && token !== undefined ) {
var dump = '\ni \tnFirs \tnLast \tnTok \toFirs \toLast \toTok \tcharSplit\n';
for if (var iprev !== 0; i < gaps.length; inull ++) {
this.tokens[prev].next = next;
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 ( 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.DebugShortenString: shorten string for debugging
this.first = 0;
//
this.last = prev;
}
 
// First or last token has been split
wDiff.DebugShortenString = function (string) {
else {
if (string === null) {
if ( token === this.first ) {
return 'null';
this.first = first;
}
}
string = string.replace(/\n/g, '\\n');
if ( token === this.last ) {
var max = 100;
this.last = prev;
if (string.length > max) {
}
string = string.substr(0, max - 1 - 30) + '…' + string.substr(string.length - 30);
}
}
return '"' + string + '"';
return;
};
};
 
 
/**
// initialize wDiff
* Split unique unmatched tokens into smaller tokens.
wDiff.Init();
*
* @param string level Level of splitting: line, sentence, chunk, or word
* @param[in] array tokens Tokens list
*/
this.splitRefine = function ( regExp ) {
 
// Cycle through tokens list
var i = this.first;
while ( i !== null ) {
 
// Refine unique unmatched tokens into smaller tokens
if ( this.tokens[i].link === null ) {
this.splitText( regExp, i );
}
i = this.tokens[i].next;
}
return;
};
 
 
/**
* Enumerate text token list before detecting blocks.
*
* @param[out] array tokens Tokens list
*/
this.enumerateTokens = function () {
 
// Enumerate tokens list
var number = 0;
var i = this.first;
while ( i !== null ) {
this.tokens[i].number = number;
number ++;
i = this.tokens[i].next;
}
return;
};
 
 
/**
* Dump tokens object to browser console.
*
* @param string name Text name
* @param[in] int first, last First and last index of tokens list
* @param[in] array tokens Tokens list
*/
this.debugText = function ( name ) {
 
var tokens = this.tokens;
var dump = 'first: ' + this.first + '\tlast: ' + this.last + '\n';
dump += '\ni \tlink \t(prev \tnext) \tuniq \t#num \t"token"\n';
var i = this.first;
while ( i !== null ) {
dump +=
i + ' \t' + tokens[i].link + ' \t(' + tokens[i].prev + ' \t' + tokens[i].next + ') \t' +
tokens[i].unique + ' \t#' + tokens[i].number + ' \t' +
parent.debugShortenText( tokens[i].token ) + '\n';
i = tokens[i].next;
}
console.log( name + ':\n' + dump );
return;
};
 
 
// Initialize WikEdDiffText object
this.init();
};
 
// </syntaxhighlight>