User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.1.3 (September 23, 2014) fix del and mark positioning, update sort, fix slide up to border, added error flag, new option unlinkBlocks
another background that could use some darkmode friendly color
 
(16 intermediate revisions by 3 users not shown)
Line 2:
 
// ==UserScript==
// @name wDiffwikEd diff
// @version 1.12.34
// @date SeptemberOctober 23, 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 */
Additional features:
 
* Word (token) types have been optimized for MediaWiki source texts
* Resolution down to characters level
* Highlighting of moved blocks and their original position marks
* Stepwise split (paragraphs, sentences, words, chars)
* Recursive diff
* Additional post-pass-5 code for resolving islands caused by common tokens at the border of sequences of common tokens
* Block move detection and visualization
* Minimizing length of moved vs. static blocks
* Sliding of ambiguous unresolved regions to next line break
* Optional omission of unchanged irrelevant parts from the output
* Fully customizable
* Well commented and documented code
 
This code is used by the MediaWiki in-browser text editors [[en:User:Cacycle/editor]] and [[en:User:Cacycle/wikEd]]
and the enhanced diff view tool wikEdDiff [[en:User:Cacycle/wikEd]].
 
Usage:
var diffHtml = wDiff.diff(oldString, newString, full);
 
Datastructures (abbreviations from publication):
 
wDiff: namespace object (global)
.configurations see top of code below for configuration and customization options
.error result has not passed unit tests
 
class Text: diff text object (new or old version)
.string: text
.words{}: word count hash
.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 TextDiff: diff object
.newText, new text
.oldText: old text
.html: diff html
 
.symbols: object for symbols table data
.token[]: associative array (hash) of parsed tokens for passes 1 - 3, points to symbol[i]
.symbol[]: array of objects that hold token counters and pointers:
.newCount: new text token counter (NC)
.oldCount: old text token counter (OC)
.newToken: token index in text.newText.tokens
.oldToken: token index in text.oldText.tokens
.linked: flag: at least one unique token pair has been linked
 
.blocks[]: array of objects that holds block (consecutive text tokens) data in order of the new text
.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 matched token
.words: word count
.chars: char length
.type: 'same', 'del', 'ins', 'mark'
.section: section number
.group: group number of block
.fixed: belongs to a fixed (not moved) group
.moved: 'mark' block associated moved block group number
.string: string of block tokens
 
.groups[]: section blocks that are consecutive in old text
.oldNumber: first block oldNumber
.blockStart: first block index
.blockEnd: last block index
.unique: contains unique matched token
.maxWords: word count of longest 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
 
*/
 
// JSHint options: W004: is already defined, W100: character may get silently deleted
/* jshint -W004, -W100, newcap: false, browser: true, jquery: true, sub: true, bitwise: true, curly: true, evil: true, forin: true, freeze: true, globalstrict: true, immed: true, latedef: true, loopfunc: true, quotmark: single, strict: true, undef: true */
/* global console */
 
// turnTurn on ECMAScript 5 strict mode
'use strict';
 
//** defineDefine global objects. */
var wikEdDiffConfig;
var wDiff; if (wDiff === undefined) { wDiff = {}; }
var WED;
 
//
// start of configuration and customization settings
//
 
//**
// * corewikEd diff settingsmain class.
*
//
* @class WikEdDiff
*/
var WikEdDiff = function () {
 
/** @var array config Configuration and customization settings. */
// enable block move layout with highlighted blocks and marks at their original positions
this.config = {
if (wDiff.showBlockMoves === undefined) { wDiff.showBlockMoves = true; }
 
/** Core diff settings (with default values). */
// further resolve replacements character-wise from start and end
if (wDiff.charDiff === undefined) { wDiff.charDiff = true; }
 
/**
// enable recursive diff to resolve problematic sequences
* @var bool config.fullDiff
if (wDiff.recursiveDiff === undefined) { wDiff.recursiveDiff = true; }
* Show complete un-clipped diff text (false)
*/
'fullDiff': false,
 
/**
// unlink blocks if too short and too common
* @var bool config.showBlockMoves
if (wDiff.unlinkBlocks === undefined) { wDiff.unlinkBlocks = true; }
* Enable block move layout with highlighted blocks and marks at the original positions (true)
*/
'showBlockMoves': true,
 
/**
// minimal number of real words for a moved block
* @var bool config.charDiff
if (wDiff.blockMinLength === undefined) { wDiff.blockMinLength = 3; }
* Enable character-refined diff (true)
*/
'charDiff': true,
 
/**
// display blocks in different colors
* @var bool config.repeatedDiff
if (wDiff.coloredBlocks === undefined) { wDiff.coloredBlocks = false; }
* Enable repeated diff to resolve problematic sequences (true)
*/
'repeatedDiff': true,
 
/**
// show debug infos and stats
* @var bool config.recursiveDiff
if (wDiff.debug === undefined) { wDiff.debug = false; }
* Enable recursive diff to resolve problematic sequences (true)
*/
'recursiveDiff': true,
 
/**
// show debug timing results
* @var int config.recursionMax
if (wDiff.debugTime === undefined) { wDiff.debugTime = false; }
* Maximum recursion depth (10)
*/
'recursionMax': 10,
 
/**
// run unit tests
* @var bool config.unlinkBlocks
if (wDiff.unitTesting === undefined) { wDiff.unitTesting = false; }
* Reject blocks if they are too short and their words are not unique,
* prevents fragmentated diffs for very different versions (true)
*/
'unlinkBlocks': true,
 
/**
// UniCode letter support for regexps, from http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
* @var int config.unlinkMax
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'); }
* Maximum number of rejection cycles (5)
*/
'unlinkMax': 5,
 
/**
// new line characters without and with '\n' and '\r'
* @var int config.blockMinLength
if (wDiff.newLines === undefined) { wDiff.newLines = '\\u0085\\u2028'; }
* Reject blocks if shorter than this number of real words (3)
if (wDiff.newLinesAll === undefined) { wDiff.newLinesAll = '\\n\\r\\u0085\\u2028'; }
*/
'blockMinLength': 3,
 
/**
// full stops without '.'
* @var bool config.coloredBlocks
if (wDiff.fullStops === undefined) { wDiff.fullStops = '058906D40701070209640DF41362166E180318092CF92CFE2E3C3002A4FFA60EA6F3FE52FF0EFF61'.replace(/(\w{4})/g, '\\u$1'); }
* Display blocks in differing colors (rainbow color scheme) (false)
*/
'coloredBlocks': false,
 
/**
// new paragraph characters without '\n' and '\r'
* @var bool config.coloredBlocks
if (wDiff.newParagraph === undefined) { wDiff.newParagraph = '\\u2029'; }
* Do not use UniCode block move marks (legacy browsers) (false)
*/
'noUnicodeSymbols': false,
 
/**
// exclamation marks without '!'
* @var bool config.stripTrailingNewline
if (wDiff.exclamationMarks === undefined) { wDiff.exclamationMarks = '01C301C301C3055C055C07F919441944203C203C20482048FE15FE57FF01'.replace(/(\w{4})/g, '\\u$1'); }
* Strip trailing newline off of texts (true in .js, false in .php)
*/
'stripTrailingNewline': true,
 
/**
// question marks without '?'
* @var bool config.debug
if (wDiff.questionMarks === undefined) { wDiff.questionMarks = '037E055E061F13671945204720492CFA2CFB2E2EA60FA6F7FE56FF1F'.replace(/(\w{4})/g, '\\u$1') + '\\u11143'; }
* Show debug infos and stats (block, group, and fragment data) in debug console (false)
*/
'debug': false,
 
/**
// regExps for splitting text (included separators)
* @var bool config.timer
if (wDiff.regExpSplit === undefined) {
* Show timing results in debug console (false)
wDiff.regExpSplit = {
*/
'timer': false,
 
/**
// paragraphs: after double newlines
* @var bool config.unitTesting
paragraph: new RegExp('(.|\\n)*?((\\r\\n|\\n|\\r){2,}|[' + wDiff.newParagraph + '])+', 'g'),
* Run unit tests to prove correct working, display results in debug console (false)
*/
'unitTesting': false,
 
/** RegExp character classes. */
// sentences: after newlines and .spaces
sentence: new RegExp('[^' + wDiff.newLinesAll + ']*?([.!?;]+[^\\S' + wDiff.newLinesAll + ']+|[' + wDiff.fullStops + wDiff.exclamationMarks + wDiff.questionMarks + ']+[^\\S' + wDiff.newLinesAll + ']*|[' + wDiff.newLines + ']|\\r\\n|\\n|\\r)', 'g'),
 
// UniCode letter support for regexps
// inline chunks
// From http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
// [[wiki link]] | {{template}} | [ext. link] |<html> | [[wiki link| | {{template| | url
'regExpLetters':
chunk: /\[\[[^\[\]\n]+\]\]|\{\{[^\{\}\n]+\}\}|\[[^\[\]\n]+\]|<\/?[^<>\[\]\{\}\n]+>|\[\[[^\[\]\|\n]+\]\]\||\{\{[^\{\}\|\n]+\||\b((https?:|)\/\/)[^\x00-\x20\s"\[\]\x7f]+/g,
'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
// words, multi-char markup, and chars
'regExpNewLines': '\\u0085\\u2028',
word: new RegExp('[' + wDiff.letters + ']+([\'’_]?[' + wDiff.letters + ']+)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.', 'g'),
'regExpNewLinesAll': '\\n\\r\\u0085\\u2028',
 
// Breaking white space characters without \n, \r, and \f
// chars
'regExpBlanks': ' \\t\\x0b\\u2000-\\u200b\\u202f\\u205f\\u3000',
character: /./g
};
}
 
// Full stops without '.'
// regExps for sliding gaps: newlines and space/word breaks
'regExpFullStops':
if (wDiff.regExpSlideStop === undefined) { wDiff.regExpSlideStop = new RegExp('[\\n\\r' + wDiff.newLines + ']$'); }
'\\u0589\\u06D4\\u0701\\u0702\\u0964\\u0DF4\\u1362\\u166E\\u1803\\u1809' +
if (wDiff.regExpSlideBorder === undefined) { wDiff.regExpSlideBorder = new RegExp('[ \\t' + wDiff.newLinesAll + wDiff.newParagraph + '\\x0C\\x0b]$'); }
'\\u2CF9\\u2CFE\\u2E3C\\u3002\\uA4FF\\uA60E\\uA6F3\\uFE52\\uFF0E\\uFF61',
 
// New paragraph characters without \n and \r
// regExps for counting words
'regExpNewParagraph': '\\f\\u2029',
if (wDiff.regExpWord === undefined) { wDiff.regExpWord = new RegExp('[' + wDiff.letters + ']+([\'’_]?[' + wDiff.letters + ']+)*', 'g'); }
if (wDiff.regExpChunk === undefined) { wDiff.regExpChunk = wDiff.regExpSplit.chunk; }
 
// Exclamation marks without '!'
// regExp detecting blank-only and single-char blocks
'regExpExclamationMarks':
if (wDiff.regExpBlankBlock === undefined) { wDiff.regExpBlankBlock = /^([^\t\S]+|[^\t])$/; }
'\\u01C3\\u01C3\\u01C3\\u055C\\u055C\\u07F9\\u1944\\u1944' +
'\\u203C\\u203C\\u2048\\u2048\\uFE15\\uFE57\\uFF01',
 
// Question marks without '?'
//
'regExpQuestionMarks':
// shorten output settings
'\\u037E\\u055E\\u061F\\u1367\\u1945\\u2047\\u2049' +
//
'\\u2CFA\\u2CFB\\u2E2E\\uA60F\\uA6F7\\uFE56\\uFF1F',
 
/** Clip settings. */
// characters before diff tag to search for previous heading, paragraph, line break, cut characters
if (wDiff.headingBefore === undefined) { wDiff.headingBefore = 1500; }
if (wDiff.paragraphBeforeMax === undefined) { wDiff.paragraphBeforeMax = 1500; }
if (wDiff.paragraphBeforeMin === undefined) { wDiff.paragraphBeforeMin = 500; }
if (wDiff.lineBeforeMax === undefined) { wDiff.lineBeforeMax = 1000; }
if (wDiff.lineBeforeMin === undefined) { wDiff.lineBeforeMin = 500; }
if (wDiff.blankBeforeMax === undefined) { wDiff.blankBeforeMax = 1000; }
if (wDiff.blankBeforeMin === undefined) { wDiff.blankBeforeMin = 500; }
if (wDiff.charsBefore === undefined) { wDiff.charsBefore = 500; }
 
// Find clip position: characters from right
// characters after diff tag to search for next heading, paragraph, line break, or characters
'clipHeadingLeft': 1500,
if (wDiff.headingAfter === undefined) { wDiff.headingAfter = 1500; }
'clipParagraphLeftMax': 1500,
if (wDiff.paragraphAfterMax === undefined) { wDiff.paragraphAfterMax = 1500; }
'clipParagraphLeftMin': 500,
if (wDiff.paragraphAfterMin === undefined) { wDiff.paragraphAfterMin = 500; }
'clipLineLeftMax': 1000,
if (wDiff.lineAfterMax === undefined) { wDiff.lineAfterMax = 1000; }
'clipLineLeftMin': 500,
if (wDiff.lineAfterMin === undefined) { wDiff.lineAfterMin = 500; }
'clipBlankLeftMax': 1000,
if (wDiff.blankAfterMax === undefined) { wDiff.blankAfterMax = 1000; }
'clipBlankLeftMin': 500,
if (wDiff.blankAfterMin === undefined) { wDiff.blankAfterMin = 500; }
'clipCharsLeft': 500,
if (wDiff.charsAfter === undefined) { wDiff.charsAfter = 500; }
 
// Find clip position: characters from right
// lines before and after diff tag to search for previous heading, paragraph, line break, cut characters
'clipHeadingRight': 1500,
if (wDiff.linesBeforeMax === undefined) { wDiff.linesBeforeMax = 10; }
'clipParagraphRightMax': 1500,
if (wDiff.linesAfterMax === undefined) { wDiff.linesAfterMax = 10; }
'clipParagraphRightMin': 500,
'clipLineRightMax': 1000,
'clipLineRightMin': 500,
'clipBlankRightMax': 1000,
'clipBlankRightMin': 500,
'clipCharsRight': 500,
 
// Maximum number of lines to search for clip position
// maximal fragment distance to join close fragments
'clipLinesRightMax': 10,
if (wDiff.fragmentJoinLines === undefined) { wDiff.fragmentJoinLines = 5; }
'clipLinesLeftMax': 10,
if (wDiff.fragmentJoinChars === undefined) { wDiff.fragmentJoinChars = 1000; }
 
// Skip clipping if ranges are too close
//
'clipSkipLines': 5,
// css classes
'clipSkipChars': 1000,
//
 
// Css stylesheet
if (wDiff.symbolMarkLeft === undefined) { wDiff.symbolMarkLeft = '◀'; }
'cssMarkLeft': '◀',
if (wDiff.symbolMarkRight === undefined) { wDiff.symbolMarkRight = '▶'; }
'cssMarkRight': '▶',
if (wDiff.stylesheet === undefined) {
wDiff. 'stylesheet =':
 
// insertInsert
'.wikEdDiffInsert {' +
'.wDiffInsert { font-weight: bold; background-color: #bbddff; color: #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
'.wDiffInsertBlankfont-weight: {bold; background-color: #66bbffbbddff; }' +
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'.wDiffFragment:hover .wDiffInsertBlank { background-color: #bbddff; }' +
'} ' +
'.wikEdDiffInsertBlank { background-color: #66bbff; } ' +
'.wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; } ' +
 
// deleteDelete
'.wikEdDiffDelete {' +
'.wDiffDelete { font-weight: bold; background-color: #ffe49c; color: #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
'.wDiffDeleteBlankfont-weight: {bold; background-color: #ffd064ffe49c; }' +
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; ' +
'.wDiffFragment:hover .wDiffDeleteBlank { background-color: #ffe49c; }' +
'} ' +
'.wikEdDiffDeleteBlank { background-color: #ffd064; } ' +
'.wikEdDiffFragment:hover .wikEdDiffDeleteBlank { background-color: #ffe49c; } ' +
 
// blockBlock
'.wikEdDiffBlock {' +
'.wDiffBlockLeft, .wDiffBlockRight { font-weight: bold; background-color: #e8e8e8; border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; }' +
'font-weight: bold; background-color: #e8e8e8; ' +
'.wDiffBlock { }' +
'border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; ' +
'.wDiffBlock0 { background-color: #ffff80; }' +
'} ' +
'.wDiffBlock1 { background-color: #d0ff80; }' +
'.wDiffBlock2wikEdDiffBlock { background-color: #ffd8f0000; } ' +
'.wDiffBlock3wikEdDiffBlock0 { background-color: #c0ffffffff80; } ' +
'.wDiffBlock4wikEdDiffBlock1 { background-color: #fff888d0ff80; } ' +
'.wDiffBlock5wikEdDiffBlock2 { background-color: #bbccffffd8f0; } ' +
'.wDiffBlock6wikEdDiffBlock3 { background-color: #e8c8ffc0ffff; } ' +
'.wDiffBlock7wikEdDiffBlock4 { background-color: #ffbbbbfff888; } ' +
'.wDiffBlock8wikEdDiffBlock5 { background-color: #a0e8a0bbccff; } ' +
'.wDiffBlockHighlightwikEdDiffBlock6 { background-color: #777e8c8ff; color:} #fff; border: solid #777; border-width: 1px 0; }' +
'.wikEdDiffBlock7 { background-color: #ffbbbb; } ' +
'.wikEdDiffBlock8 { background-color: #a0e8a0; } ' +
'.wikEdDiffBlockHighlight {' +
'background-color: #777; color: #fff; ' +
'border: solid #777; border-width: 1px 0; ' +
'} ' +
 
// markMark
'.wikEdDiffMarkLeft, .wikEdDiffMarkRight {' +
'.wDiffMarkLeft, .wDiffMarkRight { font-weight: bold; background-color: #ffe49c; color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; }' +
'font-weight: bold; background-color: #ffe49c; ' +
'.wDiffMarkRight:before { content: "' + wDiff.symbolMarkRight + '"; }' +
'color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; ' +
'.wDiffMarkLeft:before { content: "' + wDiff.symbolMarkLeft + '"; }' +
'} ' +
'.wDiffMark { background-color: #e8e8e8; color: #666; }' +
'.wikEdDiffMarkLeft:before { content: "{cssMarkLeft}"; } ' +
'.wDiffMark0 { background-color: #ffff60; }' +
'.wikEdDiffMarkRight:before { content: "{cssMarkRight}"; } ' +
'.wDiffMark1 { background-color: #c8f880; }' +
'.wikEdDiffMarkLeft.wikEdDiffNoUnicode:before { content: "<"; } ' +
'.wDiffMark2 { background-color: #ffd0f0; }' +
'.wikEdDiffMarkRight.wikEdDiffNoUnicode:before { content: ">"; } ' +
'.wDiffMark3 { background-color: #a0ffff; }' +
'.wDiffMark4wikEdDiffMark { background-color: #fff860e8e8e8; color: #666; } ' +
'.wDiffMark5wikEdDiffMark0 { background-color: #b0c0ffffff60; } ' +
'.wDiffMark6wikEdDiffMark1 { background-color: #e0c0ffc8f880; } ' +
'.wDiffMark7wikEdDiffMark2 { background-color: #ffa8a8ffd0f0; } ' +
'.wDiffMark8wikEdDiffMark3 { background-color: #98e898a0ffff; } ' +
'.wDiffMarkHighlightwikEdDiffMark4 { background-color: #777fff860; color:} #fff; }' +
'.wikEdDiffMark5 { background-color: #b0c0ff; } ' +
'.wikEdDiffMark6 { background-color: #e0c0ff; } ' +
'.wikEdDiffMark7 { background-color: #ffa8a8; } ' +
'.wikEdDiffMark8 { background-color: #98e898; } ' +
'.wikEdDiffMarkHighlight { background-color: #777; color: #fff; } ' +
 
// wrappersWrappers
'.wDiffContainerwikEdDiffContainer { } ' +
'.wikEdDiffFragment {' +
'.wDiffFragment { white-space: pre-wrap; background: #fff; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; }' +
'.wDiffNoChange { white-space: pre-wrap; background-color: var(--background-color-base, #f0f0f0fff); 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.5em1em; margin: 1em 0; }' +
'} ' +
'.wDiffSeparator { margin-bottom: 1em; }' +
'.wikEdDiffNoChange { background: var(--background-color-interactive, #eaecf0); border: 1px #bbb solid; border-radius: 0.5em; ' +
'.wDiffOmittedChars { }' +
'line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; ' +
'text-align: center; ' +
'} ' +
'.wikEdDiffSeparator { margin-bottom: 1em; } ' +
'.wikEdDiffOmittedChars { } ' +
 
// newlineNewline
'.wDiffNewlinewikEdDiffNewline:before { content: "¶"; color: transparent; } ' +
'.wDiffBlockHighlightwikEdDiffBlock:hover .wDiffNewlinewikEdDiffNewline:before { color: transparent#aaa; } ' +
'.wDiffBlockHighlight:hoverwikEdDiffBlockHighlight .wDiffNewlinewikEdDiffNewline:before { color: #ccctransparent; } ' +
'.wDiffBlockHighlightwikEdDiffBlockHighlight:hover .wDiffInsert .wDiffNewline:before, .wDiffInsert:hover .wDiffNewlinewikEdDiffNewline:before { color: #999ccc; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffNewline:before, ' +
'.wDiffBlockHighlight:hover .wDiffDelete .wDiffNewline:before, .wDiffDelete:hover .wDiffNewline:before { color: #aaa; }' +
'.wikEdDiffInsert:hover .wikEdDiffNewline:before' +
'{ color: #999; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffNewline:before, ' +
'.wikEdDiffDelete:hover .wikEdDiffNewline:before' +
'{ color: #aaa; } ' +
 
// tabTab
'.wDiffTabwikEdDiffTab { position: relative; } ' +
'.wDiffTabSymbolwikEdDiffTabSymbol { position: absolute; top: -0.2em; } ' +
'.wDiffTabSymbolwikEdDiffTabSymbol:before { content: "→"; font-size: smaller; color: transparent; color: #ccc; } ' +
'.wDiffBlockLeftwikEdDiffBlock .wDiffTabSymbol:before, .wDiffBlockRight .wDiffTabSymbolwikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wDiffBlockHighlightwikEdDiffBlockHighlight .wDiffTabSymbolwikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wDiffInsertwikEdDiffInsert .wDiffTabSymbolwikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wDiffDeletewikEdDiffDelete .wDiffTabSymbolwikEdDiffTabSymbol:before { color: #bbb; } ' +
 
// spaceSpace
'.wDiffSpacewikEdDiffSpace { position: relative; } ' +
'.wDiffSpaceSymbolwikEdDiffSpaceSymbol { position: absolute; top: -0.2em; left: -0.05em; } ' +
'.wDiffSpaceSymbolwikEdDiffSpaceSymbol:before { content: "·"; color: transparent; } ' +
'.wDiffBlockHighlightwikEdDiffBlock:hover .wDiffSpaceSymbolwikEdDiffSpaceSymbol:before { color: transparent#999; } ' +
'.wDiffBlockHighlight:hoverwikEdDiffBlockHighlight .wDiffSpaceSymbolwikEdDiffSpaceSymbol:before { color: #dddtransparent; } ' +
'.wDiffBlockHighlightwikEdDiffBlockHighlight:hover .wDiffInsert .wDiffSpaceSymbol:before, .wDiffInsert:hover .wDiffSpaceSymbolwikEdDiffSpaceSymbol:before { color: #888ddd; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffSpaceSymbol:before,' +
'.wDiffBlockHighlight:hover .wDiffDelete .wDiffSpaceSymbol:before, .wDiffDelete:hover .wDiffSpaceSymbol:before { color: #999; }' +
'.wikEdDiffInsert:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #888; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffDelete:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #999; } ' +
 
// errorError
'.wikEdDiffError .wikEdDiffFragment,' +
'.wDiffError .wDiffContainer, .wDiffError .wDiffFragment, .wDiffError .wDiffNoChange { background: #faa; }';
'.wikEdDiffError .wikEdDiffNoChange' +
}
'{ background: #faa; }'
};
 
/** Add regular expressions to configuration settings. */
//
// css styles
//
 
this.config.regExp = {
if (wDiff.styleInsert === undefined) { wDiff.styleInsert = ''; }
if (wDiff.styleDelete === undefined) { wDiff.styleDelete = ''; }
if (wDiff.styleInsertBlank === undefined) { wDiff.styleInsertBlank = ''; }
if (wDiff.styleDeleteBlank === undefined) { wDiff.styleDeleteBlank = ''; }
if (wDiff.styleBlock === undefined) { wDiff.styleBlock = ''; }
if (wDiff.styleBlockLeft === undefined) { wDiff.styleBlockLeft = ''; }
if (wDiff.styleBlockRight === undefined) { wDiff.styleBlockRight = ''; }
if (wDiff.styleBlockHighlight === undefined) { wDiff.styleBlockHighlight = ''; }
if (wDiff.styleBlockColor === undefined) { wDiff.styleBlockColor = []; }
if (wDiff.styleMark === undefined) { wDiff.styleMark = ''; }
if (wDiff.styleMarkLeft === undefined) { wDiff.styleMarkLeft = ''; }
if (wDiff.styleMarkRight === undefined) { wDiff.styleMarkRight = ''; }
if (wDiff.styleMarkColor === undefined) { wDiff.styleMarkColor = []; }
if (wDiff.styleContainer === undefined) { wDiff.styleContainer = ''; }
if (wDiff.styleFragment === undefined) { wDiff.styleFragment = ''; }
if (wDiff.styleNoChange === undefined) { wDiff.styleNoChange = ''; }
if (wDiff.styleSeparator === undefined) { wDiff.styleSeparator = ''; }
if (wDiff.styleOmittedChars === undefined) { wDiff.styleOmittedChars = ''; }
if (wDiff.styleError === undefined) { wDiff.styleError = ''; }
if (wDiff.styleNewline === undefined) { wDiff.styleNewline = ''; }
if (wDiff.styleTab === undefined) { wDiff.styleTab = ''; }
if (wDiff.styleTabSymbol === undefined) { wDiff.styleTabSymbol = ''; }
if (wDiff.styleSpace === undefined) { wDiff.styleSpace = ''; }
if (wDiff.styleSpaceSymbol === undefined) { wDiff.styleSpaceSymbol = ''; }
 
// RegExps for splitting text
//
'split': {
// output html
//
 
// Split into paragraphs, after double newlines
// dynamic replacements: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
'paragraph': new RegExp(
// class plus html comment are required indicators for TextDiff.shortenOutput()
'(\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
']',
'g'
),
 
// Split into lines
if (wDiff.blockEvent === undefined) { wDiff.blockEvent = ' onmouseover="wDiff.blockHandler(undefined, this, \'mouseover\');"'; }
'line': new RegExp(
'\\r\\n|\\n|\\r|[' +
this.config.regExpNewLinesAll +
']',
'g'
),
 
// Split into sentences /[^ ].*?[.!?:;]+(?= |$)/
if (wDiff.htmlContainerStart === undefined) { wDiff.htmlContainerStart = '<div class="wDiffContainer" id="wDiffContainer" style="' + wDiff.styleContainer + '">'; }
'sentence': new RegExp(
if (wDiff.htmlContainerEnd === undefined) { wDiff.htmlContainerEnd = '</div>'; }
'[^' +
this.config.regExpBlanks +
'].*?[.!?:;' +
this.config.regExpFullStops +
this.config.regExpExclamationMarks +
this.config.regExpQuestionMarks +
']+(?=[' +
this.config.regExpBlanks +
']|$)',
'g'
),
 
// Split into inline chunks
if (wDiff.htmlInsertStart === undefined) { wDiff.htmlInsertStart = '<span class="wDiffInsert" style="' + wDiff.styleInsert + '" title="+">'; }
'chunk': new RegExp(
if (wDiff.htmlInsertStartBlank === undefined) { wDiff.htmlInsertStartBlank = '<span class="wDiffInsert wDiffInsertBlank" style="' + wDiff.styleInsertBlank + '" title="+">'; }
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
if (wDiff.htmlInsertEnd === undefined) { wDiff.htmlInsertEnd = '</span><!--wDiffInsert-->'; }
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
 
// Split into words, multi-char markup, and chars
if (wDiff.htmartlDeleteSt === undefined) { wDiff.htmlDeleteStart = '<span class="wDiffDelete" style="' + wDiff.styleDelete + '" title="−">'; }
// regExpLetters speed-up: \\w+
if (wDiff.htmlDeleteStartBlank === undefined) { wDiff.htmlDeleteStartBlank = '<span class="wDiffDelete wDiffDeleteBlank" style="' + wDiff.styleDeleteBlank + '" title="−">'; }
'word': new RegExp(
if (wDiff.htmlDeleteEnd === undefined) { wDiff.htmlDeleteEnd = '</span><!--wDiffDelete-->'; }
'(\\w+|[_' +
this.config.regExpLetters +
'])+([\'’][_' +
this.config.regExpLetters +
']*)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.',
'g'
),
 
// Split into chars
if (wDiff.htmlBlockLeftStart === undefined) {
'character': /./g
if (wDiff.coloredBlocks === false) {
},
wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft" style="' + wDiff.styleBlockLeft + '" title="' + wDiff.symbolMarkLeft + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>';
}
else {
wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft wDiffBlock wDiffBlock{class}" style="' + wDiff.styleBlockLeft + wDiff.styleBlock + '{block}" title="' + wDiff.symbolMarkLeft + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>';
}
}
if (wDiff.htmlBlockLeftEnd === undefined) { wDiff.htmlBlockLeftEnd = '</span><!--wDiffBlockLeft-->'; }
 
// RegExp to detect blank tokens
if (wDiff.htmlBlockRightStart === undefined) {
'blankOnlyToken': new RegExp(
if (wDiff.coloredBlocks === false) {
'[^' +
wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight" style="' + wDiff.styleBlockRight + '" title="' + wDiff.symbolMarkRight + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>';
this.config.regExpBlanks +
}
this.config.regExpNewLinesAll +
else {
this.config.regExpNewParagraph +
wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight wDiffBlock wDiffBlock{class}" style="' + wDiff.styleBlockRight + wDiff.styleBlock + '{block}" title="' + wDiff.symbolMarkRight + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>';
']'
}
),
}
if (wDiff.htmlBlockRightEnd === undefined) { wDiff.htmlBlockRightEnd = '</span><!--wDiffBlockRight-->'; }
 
// RegExps for sliding gaps: newlines and space/word breaks
if (wDiff.htmlMarkLeft === undefined) {
'slideStop': new RegExp(
if (wDiff.coloredBlocks === false) {
'[' +
wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft" style="' + wDiff.styleMarkLeft + '"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->';
this.config.regExpNewLinesAll +
}
this.config.regExpNewParagraph +
else {
']$'
wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft wDiffMark wDiffMark{class}" style="' + wDiff.styleMarkLeft + wDiff.styleMark + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->';
),
}
'slideBorder': new RegExp(
}
'[' +
if (wDiff.htmlMarkRight === undefined) {
this.config.regExpBlanks +
if (wDiff.coloredBlocks === false) {
']$'
wDiff.htmlMarkRight = '<span class="wDiffMarkRight" style="' + wDiff.styleMarkRight + '"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->';
),
}
else {
wDiff.htmlMarkRight = '<span class="wDiffMarkRight wDiffMark wDiffMark{class}" style="' + wDiff.styleMarkRight + wDiff.styleMark + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->';
}
}
 
// RegExps for counting words
if (wDiff.htmlNewline === undefined) { wDiff.htmlNewline = '<span class="wDiffNewline" style="' + wDiff.styleNewline + '">\n</span>'; }
'countWords': new RegExp(
if (wDiff.htmlTab === undefined) { wDiff.htmlTab = '<span class="wDiffTab" style="' + wDiff.styleTab + '"><span class="wDiffTabSymbol" style="' + wDiff.styleTabSymbol + '"></span>\t</span>'; }
'(\\w+|[_' +
if (wDiff.htmlSpace === undefined) { wDiff.htmlSpace = '<span class="wDiffSpace" style="' + wDiff.styleSpace + '"><span class="wDiffSpaceSymbol" style="' + wDiff.styleSpaceSymbol + '"></span> </span>'; }
this.config.regExpLetters +
'])+([\'’][_' +
this.config.regExpLetters +
']*)*',
'g'
),
'countChunks': new RegExp(
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'
),
 
// RegExp detecting blank-only and single-char blocks
// shorten output
'blankBlock': /^([^\t\S]+|[^\t])$/,
 
// RegExps for clipping
if (wDiff.htmlFragmentStart === undefined) { wDiff.htmlFragmentStart = '<pre class="wDiffFragment" style="' + wDiff.styleFragment + '">'; }
'clipLine': new RegExp(
if (wDiff.htmlFragmentEnd === undefined) { wDiff.htmlFragmentEnd = '</pre>'; }
'[' + this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g'
),
'clipHeading': new RegExp(
'( ^|\\n)(==+.+?==+|\\{\\||\\|\\}).*?(?=\\n|$)', 'g' ),
'clipParagraph': new RegExp(
'( (\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
'])+',
'g'
),
'clipBlank': new RegExp(
'[' +
this.config.regExpBlanks + ']+',
'g'
),
'clipTrimNewLinesLeft': new RegExp(
'[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimNewLinesRight': new RegExp(
'^[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g'
),
'clipTrimBlanksLeft': new RegExp(
'[' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimBlanksRight': new RegExp(
'^[' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g'
)
};
 
/** Add messages to configuration settings. */
if (wDiff.htmlNoChange === undefined) { wDiff.htmlNoChange = '<pre class="wDiffNoChange" style="' + wDiff.styleNoChange + '" title="="></pre>'; }
if (wDiff.htmlSeparator === undefined) { wDiff.htmlSeparator = '<div class="wDiffSeparator" style="' + wDiff.styleSeparator + '"></div>'; }
if (wDiff.htmlOmittedChars === undefined) { wDiff.htmlOmittedChars = '<span class="wDiffOmittedChars" style="' + wDiff.styleOmittedChars + '">…</span>'; }
 
this.config.msg = {
if (wDiff.htmlErrorStart === undefined) { wDiff.htmlErrorStart = '<div class="wDiffError" style="' + wDiff.styleError + '">'; }
'wiked-diff-empty': '(No difference)',
if (wDiff.htmlErrorEnd === undefined) { wDiff.htmlErrorEnd = '</div>'; }
'wiked-diff-same': '=',
'wiked-diff-ins': '+',
'wiked-diff-del': '-',
'wiked-diff-block-left': '◀',
'wiked-diff-block-right': '▶',
'wiked-diff-block-left-nounicode': '<',
'wiked-diff-block-right-nounicode': '>',
'wiked-diff-error': 'Error: diff not consistent with versions!'
};
 
//**
* Add output html fragments to configuration settings.
// javascript handler for output code, IE 8 compatible
* Dynamic replacements:
//
* {number}: class/color/block/mark/id number
* {title}: title attribute (popup)
* {nounicode}: noUnicodeSymbols fallback
*/
this.config.htmlCode = {
'noChangeStart':
'<div class="wikEdDiffNoChange" title="' +
this.config.msg['wiked-diff-same'] +
'">',
'noChangeEnd': '</div>',
 
'containerStart': '<div class="wikEdDiffContainer" id="wikEdDiffContainer">',
// wDiff.blockHandler: event handler for block and mark elements
'containerEnd': '</div>',
if (wDiff.blockHandler === undefined) { wDiff.blockHandler = function (event, element, type) {
 
'fragmentStart': '<pre class="wikEdDiffFragment" style="white-space: pre-wrap;">',
// IE compatibility
'fragmentEnd': '</pre>',
if ( (event === undefined) && (window.event !== undefined) ) {
'separator': '<div class="wikEdDiffSeparator"></div>',
event = window.event;
}
 
'insertStart':
// get mark/block elements
'<span class="wikEdDiffInsert" title="' +
var number = element.id.replace(/\D/g, '');
this.config.msg['wiked-diff-ins'] +
var block = document.getElementById('wDiffBlock' + number);
'">',
var mark = document.getElementById('wDiffMark' + number);
'insertStartBlank':
'<span class="wikEdDiffInsert wikEdDiffInsertBlank" title="' +
this.config.msg['wiked-diff-ins'] +
'">',
'insertEnd': '</span>',
 
'deleteStart':
// highlight corresponding mark/block pairs
'<span class="wikEdDiffDelete" title="' +
if (type == 'mouseover') {
this.config.msg['wiked-diff-del'] +
element.onmouseover = null;
'">',
element.onmouseout = function (event) { wDiff.blockHandler(event, element, 'mouseout'); };
'deleteStartBlank':
element.onclick = function (event) { wDiff.blockHandler(event, element, 'click'); };
'<span class="wikEdDiffDelete wikEdDiffDeleteBlank" title="' +
block.className += ' wDiffBlockHighlight';
this.config.msg['wiked-diff-del'] +
mark.className += ' wDiffMarkHighlight';
'">',
}
'deleteEnd': '</span>',
 
'blockStart':
// remove mark/block highlighting
'<span class="wikEdDiffBlock"' +
if ( (type == 'mouseout') || (type == 'click') ) {
'title="{title}" id="wikEdDiffBlock{number}"' +
element.onmouseout = null;
element. 'onmouseover = function "wikEdDiffBlockHandler(event) { wDiff.blockHandler(eventundefined, elementthis, \'mouseover\'); };">',
'blockColoredStart':
'<span class="wikEdDiffBlock wikEdDiffBlock wikEdDiffBlock{number}"' +
'title="{title}" id="wikEdDiffBlock{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
'blockEnd': '</span>',
 
'markLeft':
// getElementsByClassName
'<span class="wikEdDiffMarkLeft{nounicode}"' +
var container = document.getElementById('wDiffContainer');
'title="{title}" id="wikEdDiffMark{number}"' +
var spans = container.getElementsByTagName('span');
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
for (var i = 0; i < spans.length; i ++) {
'markLeftColored':
if ( ( (spans[i] != block) && (spans[i] != mark) ) || (type != 'click') ) {
'<span class="wikEdDiffMarkLeft{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
if (spans[i].className.indexOf(' wDiffBlockHighlight') != -1) {
'title="{title}" id="wikEdDiffMark{number}"' +
spans[i].className = spans[i].className.replace(/ wDiffBlockHighlight/g, '');
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
}
else if (spans[i].className.indexOf(' wDiffMarkHighlight') != -1) {
spans[i].className = spans[i].className.replace(/ wDiffMarkHighlight/g, '');
}
}
}
}
 
'markRight':
// scroll to corresponding mark/block element
'<span class="wikEdDiffMarkRight{nounicode}"' +
if (type == 'click') {
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
'markRightColored':
'<span class="wikEdDiffMarkRight{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
 
'newline': '<span class="wikEdDiffNewline">\n</span>',
// get corresponding element
'tab': '<span class="wikEdDiffTab"><span class="wikEdDiffTabSymbol"></span>\t</span>',
var corrElement;
'space': '<span class="wikEdDiffSpace"><span class="wikEdDiffSpaceSymbol"></span> </span>',
if (element == block) {
corrElement = mark;
}
else {
corrElement = block;
}
 
'omittedChars': '<span class="wikEdDiffOmittedChars">…</span>',
// get element height (getOffsetTop)
var corrElementPos = 0;
var node = corrElement;
do {
corrElementPos += node.offsetTop;
} while ( (node = node.offsetParent) !== null );
 
'errorStart': '<div class="wikEdDiffError" title="Error: diff not consistent with versions!">',
// get scroll height
'errorEnd': '</div>'
var top;
};
if (window.pageYOffset !== undefined) {
 
top = window.pageYOffset;
/*
}
* Add JavaScript event handler function to configuration settings
else {
* Highlights corresponding block and mark elements on hover and jumps between them on click
top = document.documentElement.scrollTop;
* 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;
}
 
// getGet cursormark/block poselements
var number = element.id.replace( /\D/g, '' );
var cursor;
var block = document.getElementById( 'wikEdDiffBlock' + number );
if (event.pageY !== undefined) {
var mark = document.getElementById( 'wikEdDiffMark' + number );
cursor = event.pageY;
if ( block === null || mark === null ) {
}
return;
else if (event.clientY !== undefined) {
cursor = event.clientY + top;
}
 
// Highlight corresponding mark/block pairs
// get line height
if ( type === 'mouseover' ) {
var line = 12;
element.onmouseover = null;
if (window.getComputedStyle !== undefined) {
element.onmouseout = function ( event ) {
line = parseInt(window.getComputedStyle(corrElement).getPropertyValue('line-height'));
window.wikEdDiffBlockHandler( event, element, 'mouseout' );
};
element.onclick = function ( event ) {
window.wikEdDiffBlockHandler( event, element, 'click' );
};
block.className += ' wikEdDiffBlockHighlight';
mark.className += ' wikEdDiffMarkHighlight';
}
 
// Remove mark/block highlighting
// scroll element under mouse cursor
if ( type === 'mouseout' || type === 'click' ) {
window.scroll(0, corrElementPos + top - cursor + line / 2);
element.onmouseout = null;
}
element.onmouseover = function ( event ) {
return;
window.wikEdDiffBlockHandler( event, element, 'mouseover' );
}; }
};
 
// Reset, allow outside container (e.g. legend)
if ( type !== 'click' ) {
block.className = block.className.replace( / wikEdDiffBlockHighlight/g, '' );
mark.className = mark.className.replace( / wikEdDiffMarkHighlight/g, '' );
 
// GetElementsByClassName
//
var container = document.getElementById( 'wikEdDiffContainer' );
// end of configuration and customization settings
if ( container !== null ) {
//
var spans = container.getElementsByTagName( 'span' );
var spansLength = spans.length;
for ( var i = 0; i < spansLength; i ++ ) {
if ( spans[i] !== block && spans[i] !== mark ) {
if ( spans[i].className.indexOf( ' wikEdDiffBlockHighlight' ) !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffBlockHighlight/g, '' );
}
else if ( spans[i].className.indexOf( ' wikEdDiffMarkHighlight') !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffMarkHighlight/g, '' );
}
}
}
}
}
}
 
// Scroll to corresponding mark/block element
if ( type === 'click' ) {
 
// Get corresponding element
// wDiff.init(): initialize wDiff
var corrElement;
// called from: on code load
if ( element === block ) {
// calls: .addStyleSheet(), .addScript()
corrElement = mark;
}
else {
corrElement = block;
}
 
// Get element height (getOffsetTop)
wDiff.init = function () {
var corrElementPos = 0;
var node = corrElement;
do {
corrElementPos += node.offsetTop;
} while ( ( node = node.offsetParent ) !== null );
 
// Get scroll height
wDiff.error = false;
var top;
if ( window.pageYOffset !== undefined ) {
top = window.pageYOffset;
}
else {
top = document.documentElement.scrollTop;
}
 
// Get cursor pos
// legacy for short time
var cursor;
wDiff.Diff = wDiff.diff;
if ( event.pageY !== undefined ) {
cursor = event.pageY;
}
else if ( event.clientY !== undefined ) {
cursor = event.clientY + top;
}
 
// addGet stylesline to headheight
var line = 12;
wDiff.addStyleSheet(wDiff.stylesheet);
if ( window.getComputedStyle !== undefined ) {
line = parseInt( window.getComputedStyle( corrElement ).getPropertyValue( 'line-height' ) );
}
 
// Scroll element under mouse cursor
// add block handler to head if running under Greasemonkey
window.scroll( 0, corrElementPos + top - cursor + line / 2 );
if (typeof GM_info == 'object') {
var script = 'var wDiff; if (wDiff === undefined) { wDiff = {}; } wDiff.blockHandler = ' + wDiff.blockHandler.toString();
wDiff.addScript(script);
}
return;
};
 
 
// wDiff.diff(): main method of wDiff, runs the diff and shortens the output
// called from: user land
// calls: new TextDiff, TextDiff.shortenOutput(), this.unitTests()
 
wDiff.diff = function (oldString, newString, full) {
 
wDiff.error = false;
 
// create text diff object
var textDiff = new wDiff.TextDiff(oldString, newString, this);
 
// legacy for short time
wDiff.textDiff = textDiff;
wDiff.ShortenOutput = wDiff.textDiff.shortenOutput;
 
// start timer
if (wDiff.debugTime === true) {
console.time('diff');
}
 
// run the diff
textDiff.diff();
 
// start timer
if (wDiff.debugTime === true) {
console.timeEnd('diff');
}
 
// shorten output
if (full !== true) {
 
// start timer
if (wDiff.debugTime === true) {
console.time('shorten');
}
return;
};
 
/** Internal data structures. */
textDiff.shortenOutput();
 
/** @var WikEdDiffText newText New text version object with text and token list */
// stop timer
this.newText = null;
if (wDiff.debugTime === true) {
console.timeEnd('shorten');
}
}
 
/** @var WikEdDiffText oldText Old text version object with text and token list */
// stop timer
this.oldText = null;
if (wDiff.debugTime === true) {
console.timeEnd('diff');
}
 
/** @var object symbols Symbols table for whole text at all refinement levels */
// run unit tests
this.symbols = {
if (wDiff.unitTesting === true) {
token: [],
wDiff.unitTests(textDiff);
hashTable: {},
}
linked: false
return textDiff.html;
};
 
/** @var array bordersDown Matched region borders downwards */
this.bordersDown = [];
 
/** @var array bordersUp Matched region borders upwards */
// wDiff.unitTests(): test diff for consistency between input and output
this.bordersUp = [];
// input: textDiff: text diff object after calling .diff()
// called from: .diff()
 
/** @var array blocks Block data (consecutive text tokens) in new text order */
wDiff.unitTests = function (textDiff) {
this.blocks = [];
 
/** @var int maxWords Maximal detected word count of all linked blocks */
// start timer
this.maxWords = 0;
if (wDiff.debugTime === true) {
console.time('unit tests');
}
 
/** @var array groups Section blocks that are consecutive in old text order */
var html = textDiff.html;
this.groups = [];
 
/** @var array sections Block sections with no block move crosses outside a section */
// check if output is consistent with new text
this.sections = [];
textDiff.assembleDiff('new');
var diff = textDiff.html.replace(/<[^>]*>/g, '');
var text = textDiff.htmlEscape(textDiff.newText.string);
if (diff != text) {
console.log('Error: wDiff unit test failure: output not consistent with new text');
wDiff.error = true;
console.log('new text:\n', text);
console.log('new diff:\n', diff);
}
else {
console.log('OK: wDiff unit test passed: output consistent with new text');
}
 
/** @var object timer Debug timer array: string 'label' => float milliseconds. */
// check if output is consistent with old text
this.timer = {};
textDiff.assembleDiff('old');
var diff = textDiff.html.replace(/<[^>]*>/g, '');
var text = textDiff.htmlEscape(textDiff.oldText.string);
if (diff != text) {
console.log('Error: wDiff unit test failure: output not consistent with old text');
wDiff.error = true;
console.log('old text:\n', text);
console.log('old diff:\n', diff);
}
else {
console.log('OK: wDiff unit test passed: output consistent with old text');
}
 
/** @var array recursionTimer Count time spent in recursion level in milliseconds. */
if (wDiff.error === true) {
this.recursionTimer = [];
html = wDiff.htmlErrorStart + html + wDiff.htmlErrorEnd;
}
textDiff.html = html;
 
/** Output data. */
// stop timer
if (wDiff.debugTime === true) {
console.timeEnd('unit tests');
}
return;
};
 
/** @var bool error Unit tests have detected a diff error */
this.error = false;
 
/** @var array fragments Diff fragment list for markup, abstraction layer for customization */
//
this.fragments = [];
// wDiff.Text class: data and methods for single text version (old or new)
// called from: TextDiff.init()
//
 
/** @var string html Html code of diff */
wDiff.Text = function (string, parent) {
this.html = '';
 
this.parent = parent;
this.string = null;
this.tokens = [];
this.first = null;
this.last = null;
this.words = {};
 
 
//**
// Text.init():* Constructor, initialize textsettings, objectload js and css.
*
//
* @param[in] object wikEdDiffConfig Custom customization settings
* @param[out] object config Settings
*/
 
this.init = function () {
 
// Import customizations from wikEdDiffConfig{}
if (typeof string != 'string') {
if ( typeof wikEdDiffConfig === 'object' ) {
string = string.toString();
this.deepCopy( wikEdDiffConfig, this.config );
}
 
// IEAdd /CSS Mac fixstylescheet
this.stringaddStyleSheet( = stringthis.replace(/\r\n?/g,config.stylesheet '\n');
 
// Load block handler script
this.wordParse(wDiff.regExpWord);
if ( this.config.showBlockMoves === true ) {
this.wordParse(wDiff.regExpChunk);
return;
};
 
// Add block handler to head if running under Greasemonkey
 
if ( typeof GM_info === 'object' ) {
// Text.wordParse(): parse and count words and chunks for identification of unique words
var script = 'var wikEdDiffBlockHandler = ' + this.config.blockHandler.toString() + ';';
// called from: .init()
this.addScript( script );
// changes: .words
 
this.wordParse = function (regExp) {
 
var regExpMatch;
while ( (regExpMatch = regExp.exec(this.string)) !== null) {
var word = regExpMatch[0];
if (this.words[word] === undefined) {
this.words[word] = 1;
}
else {
window.wikEdDiffBlockHandler = this.config.blockHandler;
this.words[word] ++;
}
}
Line 713 ⟶ 915:
 
 
/**
// Text.split(): split text into paragraph, sentence, or word tokens
* Main diff method.
// input: regExp, regular expression for splitting text into tokens; token, tokens index of token to be split
*
// called from: TextDiff.diff(), .splitRefine()
* @param string oldString Old text version
// changes: .tokens list, .first, .last
* @param string newString New text version
* @param[out] array fragment
* Diff fragment list ready for markup, abstraction layer for customized diffs
* @param[out] string html Html code of diff
* @return string Html code of diff
*/
this.diff = function ( oldString, newString ) {
 
// Start total timer
this.split = function (level, token) {
if ( this.config.timer === true ) {
this.time( 'total' );
}
 
var// prevStart =diff null;timer
if ( this.config.timer === true ) {
var next = null;
this.time( 'diff' );
var current = this.tokens.length;
var first = current;
var string = '';
 
// split full text or specified token
if (token === undefined) {
string = this.string;
}
else {
prev = this.tokens[token].prev;
next = this.tokens[token].next;
string = this.tokens[token].token;
}
 
// Reset error flag
// split text into tokens, regExp match as separator
var numberthis.error = 0false;
 
var split = [];
// Strip trailing newline (.js only)
var regExpMatch;
if ( this.config.stripTrailingNewline === true ) {
var lastIndex = 0;
if ( newString.substr( -1 ) === '\n' && oldString.substr( -1 === '\n' ) ) {
while ( (regExpMatch = wDiff.regExpSplit[level].exec(string)) !== null) {
newString = newString.substr( 0, newString.length - 1 );
if (regExpMatch.index > lastIndex) {
oldString = oldString.substr( 0, oldString.length - 1 );
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));
}
 
// Load version strings into WikEdDiffText objects
// cycle trough new tokens
this.newText = new WikEdDiff.WikEdDiffText( newString, this );
for (var i = 0; i < split.length; i ++) {
this.oldText = new WikEdDiff.WikEdDiffText( oldString, this );
 
// Trap trivial changes: no change
// insert current item, link to previous
if ( this.tokens[current]newText.text === this.oldText.text ) {
this.html =
token: split[i],
this.config.htmlCode.containerStart +
prev: prev,
this.config.htmlCode.noChangeStart +
next: null,
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
link: null,
this.config.htmlCode.noChangeEnd +
number: null,
this.config.htmlCode.containerEnd;
unique: false
return this.html;
};
}
number ++;
 
// Trap trivial changes: old text deleted
// link previous item to current
if (prev !== null) {
this.tokens[prev]oldText.nexttext === '' || current;(
this.oldText.text === '\n' &&
}
( this.newText.text.charAt( this.newText.text.length - 1 ) === '\n' )
prev = current;
current ++;)
) {
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
// connect last new item and existing next item
if (
if ( (number > 0) && (token !== undefined) ) {
this.newText.text === '' || (
if (prev !== null) {
this.tokens[prev]newText.nexttext === '\n' next;&&
( this.oldText.text.charAt( this.oldText.text.length - 1 ) === '\n' )
}
)
if (next !== null) {
) {
this.tokens[next].prev = prev;
}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;
}
 
// setSplit text firstnew and lastold text tokeninto indexparagraps
if (number >this.config.timer === true 0) {
this.time( 'paragraph split' );
}
this.newText.splitText( 'paragraph' );
this.oldText.splitText( 'paragraph' );
if ( this.config.timer === true ) {
this.timeEnd( 'paragraph split' );
}
 
// initialCalculate text splitdiff
this.calculateDiff( 'line' );
if (token === undefined) {
this.first = 0;
this.last = prev;
}
 
// Refine different paragraphs into lines
// first or last token has been split
if ( this.config.timer === true ) {
else {
this.time( 'line split' );
if (token == this.first) {
}
this.first = first;
this.newText.splitRefine( 'line' );
}
this.oldText.splitRefine( 'line' );
if (token == this.last) {
if ( this.lastconfig.timer === true ) prev;{
this.timeEnd( 'line split' );
}
}
}
return;
};
 
// Calculate refined diff
this.calculateDiff( 'line' );
 
// Refine different lines into sentences
// Text.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: TextDiff.diff()
}
// calls: .split()
this.newText.splitRefine( 'sentence' );
this.oldText.splitRefine( 'sentence' );
if ( this.config.timer === true ) {
this.timeEnd( 'sentence split' );
}
 
// Calculate refined diff
this.splitRefine = function (regExp) {
this.calculateDiff( 'sentence' );
 
// Refine different sentences into chunks
// cycle through tokens list
varif i =( this.first;config.timer === true ) {
this.time( 'chunk split' );
while ( (i !== null) && (this.tokens[i] !== null) ) {
}
 
this.newText.splitRefine( 'chunk' );
// refine unique unmatched tokens into smaller tokens
if (this.tokens[i]oldText.linksplitRefine( ==='chunk' null) {;
if ( this.split(regExp,config.timer === true i); {
this.timeEnd( 'chunk split' );
}
i = this.tokens[i].next;
}
return;
};
 
// Calculate refined diff
this.calculateDiff( 'chunk' );
 
// Refine different chunks into words
// Text.enumerateTokens(): enumerate text token list
if ( this.config.timer === true ) {
// called from: TextDiff.diff()
this.time( 'word split' );
// changes: .tokens list
}
this.newText.splitRefine( 'word' );
this.oldText.splitRefine( 'word' );
if ( this.config.timer === true ) {
this.timeEnd( 'word split' );
}
 
// Calculate refined diff information with recursion for unresolved gaps
this.enumerateTokens = function () {
this.calculateDiff( 'word', true );
 
// enumerateSlide tokens listgaps
if ( this.config.timer === true ) {
var number = 0;
this.time( 'word slide' );
var i = this.first;
}
while ( (i !== null) && (this.tokens[i] !== null) ) {
this.tokens[i]slideGaps( this.numbernewText, =this.oldText number);
this.slideGaps( this.oldText, this.newText );
number ++;
iif =( this.tokens[i]config.next;timer === true ) {
this.timeEnd( 'word slide' );
}
return;
};
 
// Split tokens into chars
if ( this.config.charDiff === true ) {
 
// Split tokens into chars in selected unresolved gaps
// Text.debugText(): dump text object for debugging
if ( this.config.timer === true ) {
// input: text: title
this.time( 'character split' );
}
this.splitRefineChars();
if ( this.config.timer === true ) {
this.timeEnd( 'character split' );
}
 
// Calculate refined diff information with recursion for unresolved gaps
this.debugText = function (text) {
this.calculateDiff( 'character', true );
 
// Slide gaps
var dump = 'first: ' + this.first + '\tlast: ' + this.last + '\n';
if ( this.config.timer === true ) {
dump += '\ni \tlink \t(prev \tnext) \tuniq \t#num \t"token"\n';
this.time( 'character slide' );
var i = this.first;
}
while ( (i !== null) && (this.tokens[i] !== null) ) {
this.slideGaps( this.newText, this.oldText );
dump += i + ' \t' + this.tokens[i].link + ' \t(' + this.tokens[i].prev + ' \t' + this.tokens[i].next + ') \t' + this.tokens[i].unique + ' \t#' + this.tokens[i].number + ' \t' + parent.debugShortenString(this.tokens[i].token) + '\n';
ithis.slideGaps( =this.oldText, this.tokens[i].nextnewText );
if ( this.config.timer === true ) {
this.timeEnd( 'character slide' );
}
}
console.log(text + ':\n' + dump);
return;
};
 
// Free memory
this.symbols = undefined;
this.bordersDown = undefined;
this.bordersUp = undefined;
this.newText.words = undefined;
this.oldText.words = undefined;
 
// Enumerate token lists
// initialize text object
this.initnewText.enumerateTokens();
this.oldText.enumerateTokens();
};
 
// Detect moved blocks
if ( this.config.timer === true ) {
this.time( 'blocks' );
}
this.detectBlocks();
if ( this.config.timer === true ) {
this.timeEnd( 'blocks' );
}
 
// Free memory
//
this.newText.tokens = undefined;
// wDiff.TextDiff class: main wDiff class, includes all data structures and methods required for a diff
this.oldText.tokens = undefined;
// called from: wDiff.diff()
//
 
// Assemble blocks into fragment table
wDiff.TextDiff = function (oldString, newString) {
this.getDiffFragments();
 
// Free memory
this.newText = null;
this.oldTextblocks = nullundefined;
this.blocksgroups = []undefined;
this.groupssections = []undefined;
this.sections = [];
this.html = '';
 
// Stop diff timer
if ( this.config.timer === true ) {
this.timeEnd( 'diff' );
}
 
// Unit tests
//
if ( this.config.unitTesting === true ) {
// TextDiff.init(): initialize diff object
//
 
// Test diff to test consistency between input and output
this.init = function () {
if ( this.config.timer === true ) {
this.time( 'unit tests' );
}
this.unitTests();
if ( this.config.timer === true ) {
this.timeEnd( 'unit tests' );
}
}
 
// Clipping
this.newText = new wDiff.Text(newString, this);
if ( this.config.fullDiff === false ) {
this.oldText = new wDiff.Text(oldString, this);
return;
};
 
// 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' );
}
}
 
// Create html formatted diff code from diff fragments
// TextDiff.diff(): main method
if ( this.config.timer === true ) {
// input: version: 'new', 'old', show only one marked-up version, .oldString, .newString
this.time( 'html' );
// called from: wDiff.diff()
}
// calls: Text.split(), Text.splitRefine(), .calculateDiff(), .slideGaps(), .enumerateTokens(), .detectBlocks(), .assembleDiff()
this.getDiffHtml();
// changes: .html
if ( this.config.timer === true ) {
 
this.timeEnd( 'html' );
this.diff = function (version) {
 
// trap trivial changes: no change
if (this.newText.string == this.oldText.string) {
return;
}
 
// No change
// trap trivial changes: old text deleted
if ( this.html === '' ) {
if ( (this.oldText.string === '') || ( (this.oldText.string == '\n') && (this.newText.string.charAt(this.newText.string.length - 1) == '\n') ) ) {
this.html =
this.html = wDiff.htmlInsertStart + this.htmlEscape(this.newText.string) + wDiff.htmlInsertEnd;
this.config.htmlCode.containerStart +
return;
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd +
this.config.htmlCode.containerEnd;
}
 
// Add error indicator
// trap trivial changes: new text deleted
if ( this.error === true ) {
if ( (this.newText.string === '') || ( (this.newText.string == '\n') && (this.oldText.string.charAt(this.oldText.string.length - 1) == '\n') ) ) {
this.html = wDiffthis.config.htmlCode.htmlDeleteStarterrorStart + this.htmlEscape(this.oldText.string)html + wDiffthis.config.htmlCode.htmlDeleteEnderrorEnd;
return;
}
 
// newStop symbolstotal objecttimer
if ( this.config.timer === true ) {
var symbols = {
this.timeEnd( 'total' );
token: [],
hash: {},
linked: false
};
 
// split new and old text into paragraps
this.newText.split('paragraph');
this.oldText.split('paragraph');
 
// calculate diff
this.calculateDiff(symbols, 'paragraph');
 
// refine different paragraphs into sentences
this.newText.splitRefine('sentence');
this.oldText.splitRefine('sentence');
 
// calculate refined diff
this.calculateDiff(symbols, 'sentence');
 
// refine different paragraphs into chunks
this.newText.splitRefine('chunk');
this.oldText.splitRefine('chunk');
 
// calculate refined diff
this.calculateDiff(symbols, 'chunk');
 
// refine different sentences into words
this.newText.splitRefine('word');
this.oldText.splitRefine('word');
 
// calculate refined diff information with recursion for unresolved gaps
this.calculateDiff(symbols, 'word', false, true);
 
// slide gaps
this.slideGaps(this.newText, this.oldText);
this.slideGaps(this.oldText, this.newText);
 
// split tokens into chars in selected unresolved gaps
if (wDiff.charDiff === true) {
this.splitRefineChars();
 
// calculate refined diff information with recursion for unresolved gaps
this.calculateDiff(symbols, 'character', false, true);
 
// slide gaps
this.slideGaps(this.newText, this.oldText);
this.slideGaps(this.oldText, this.newText);
}
 
return this.html;
// enumerate token lists
this.newText.enumerateTokens();
this.oldText.enumerateTokens();
 
// detect moved blocks
this.detectBlocks();
 
// assemble diff blocks into formatted html
this.assembleDiff(version);
 
if (wDiff.debug === true) {
console.log('HTML:\n', this.html);
}
return;
};
 
 
/**
// TextDiff.splitRefineChars(): split tokens into chars in the following unresolved regions (gaps):
* Split tokens into chars in the following unresolved regions (gaps):
// - one token became connected or separated by space or dash (or any token)
* - 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:
// * - additionSame or deletionnumber of flanking stringstokens in gap and strong similarity of all tokens:
// * - additionAddition or deletion of internalflanking stringstrings in tokens
// * - sameAddition lengthor anddeletion atof leastinternal 50string %in identitytokens
* - Same length and at least 50 % identity
// - same start or end, same text longer than different text
* - Same start or end, same text longer than different text
// - same length and at least 50 % identity
// * identicalIdentical tokens including space separators will be linked,
* resulting in word-wise char-level diffs
*
// changes: text (text.newText or text.oldText) .tokens list
* @param[in/out] WikEdDiffText newText, oldText Text object tokens list
// called from: .diff()
*/
// calls: Text.split()
// steps:
// find corresponding gaps
// select gaps of identical token number and strong similarity in all tokens
// refine words into chars in selected gaps
 
this.splitRefineChars = function () {
 
/** Find corresponding gaps. */
//
// find corresponding gaps
//
 
// cycleCycle troughthrough new text tokens list
var gaps = [];
var gap = null;
var i = this.newText.first;
var j = this.oldText.first;
while ( (i !== null) && (this.newText.tokens[i] !== null) ) {
 
// getGet token links
var newLink = this.newText.tokens[i].link;
var oldLink = null;
if ( j !== null ) {
oldLink = this.oldText.tokens[j].link;
}
 
// startStart of gap in new and old
if ( (gap === null) && (newLink === null) && (oldLink === null) ) {
gap = gaps.length;
gaps.push( {
newFirst: i,
newLast: i,
Line 1,036 ⟶ 1,238:
oldTokens: null,
charSplit: null
} );
}
 
// countCount chars and tokens in gap
else if ( (gap !== null) && (newLink === null) ) {
gaps[gap].newLast = i;
gaps[gap].newTokens ++;
}
 
// gapGap ended
else if ( (gap !== null) && (newLink !== null) ) {
gap = null;
}
 
// nextNext list elements
if ( newLink !== null ) {
j = this.oldText.tokens[newLink].next;
}
Line 1,057 ⟶ 1,259:
}
 
// cycleCycle troughthrough gaps and add old text gap data
for (var gapgapsLength = 0; gap < gaps.length; gap ++) {
for ( var gap = 0; gap < gapsLength; gap ++ ) {
 
// cycleCycle troughthrough old text tokens list
var j = gaps[gap].oldFirst;
while (
while ( (j !== null) && (this.oldText.tokens[j] !== null) && (this.oldText.tokens[j].link === null) ) {
j !== null &&
this.oldText.tokens[j] !== null &&
this.oldText.tokens[j].link === null
) {
 
// countCount old chars and tokens in gap
gaps[gap].oldLast = j;
gaps[gap].oldTokens ++;
Line 1,072 ⟶ 1,279:
}
 
/** Select gaps of identical token number and strong similarity of all tokens. */
//
// select gaps of identical token number and strong similarity of all tokens
//
 
for (var gapgapsLength = 0; gap < gaps.length; gap ++) {
for ( var gap = 0; gap < gapsLength; gap ++ ) {
var charSplit = true;
 
// notNot same gap length
if ( gaps[gap].newTokens !== gaps[gap].oldTokens ) {
 
// oneOne word became separated by space, dash, or any string
if ( (gaps[gap].newTokens === 1) && (gaps[gap].oldTokens === 3) ) {
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 (
if ( (token.indexOf(tokenFirst) !== 0) || (token.indexOf(tokenLast) != token.length - tokenLast.length) ) {
token.indexOf( tokenFirst ) !== 0 ||
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
}
else if ( (gaps[gap].oldTokens === 1) && (gaps[gap].newTokens === 3) ) {
var token = this.oldText.tokens[ gaps[gap].oldFirst ].token;
var tokenFirst = this.newText.tokens[ gaps[gap].newFirst ].token;
var tokenLast = this.newText.tokens[ gaps[gap].newLast ].token;
if (
if ( (token.indexOf(tokenFirst) !== 0) || (token.indexOf(tokenLast) != token.length - tokenLast.length) ) {
token.indexOf( tokenFirst ) !== 0 ||
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
Line 1,105 ⟶ 1,317:
}
 
// cycleCycle troughthrough new text tokens list and set charSplit
else {
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;
 
// getGet shorter and longer token
var shorterToken;
var longerToken;
if ( newToken.length < oldToken.length ) {
shorterToken = newToken;
longerToken = oldToken;
Line 1,125 ⟶ 1,337:
}
 
// notNot same token length
if ( newToken.length !== oldToken.length ) {
 
// testTest for addition or deletion of internal string in tokens
 
// findFind number of identical chars from left
var left = 0;
while ( left < shorterToken.length ) {
if ( newToken.charAt( left ) !== oldToken.charAt( left ) ) {
break;
}
Line 1,139 ⟶ 1,351:
}
 
// findFind 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;
}
Line 1,148 ⟶ 1,363:
}
 
// noNo simple insertion or deletion of internal string
if ( left + right !== shorterToken.length ) {
 
// notNot addition or deletion of flanking strings in tokens
// (smallerSmaller token not part of larger token)
if ( longerToken.indexOf( shorterToken ) === -1 ) {
 
// sameSame text at start or end shorter than different text
if ( (left < shorterToken.length / 2) && (right < shorterToken.length / 2) ) {
 
// doDo not split into chars in this gap
charSplit = false;
break;
Line 1,165 ⟶ 1,381:
}
 
// sameSame token length
else if ( newToken !== oldToken ) {
 
// tokensTokens less than 50 % identical
var ident = 0;
for (var postokenLength = 0; pos < shorterToken.length; pos ++) {
iffor (shorterToken.charAt( var pos) == longerToken.charAt(0; pos) < tokenLength; pos ++ ) {
if ( shorterToken.charAt( pos ) === longerToken.charAt( pos ) ) {
ident ++;
}
}
if ( ident / shorterToken.length < 0.49 ) {
 
// doDo not split into chars this gap
charSplit = false;
break;
Line 1,183 ⟶ 1,400:
}
 
// nextNext list elements
if ( i === gaps[gap].newLast ) {
break;
}
Line 1,194 ⟶ 1,411:
}
 
/** 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;
var oldGapLength = j - gaps[gap].oldLast;
while ( (i !== null) || (j !== null) ) {
 
// linkLink identical tokens (spaces) to keep char refinement to words
if (
if ( (newGapLength == oldGapLength) && (this.newText.tokens[i].token == this.oldText.tokens[j].token) ) {
newGapLength === oldGapLength &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
 
// refineRefine words into chars
else {
if ( i !== null ) {
this.newText.splitsplitText( 'character', i );
}
if ( j !== null ) {
this.oldText.splitsplitText( 'character', j );
}
}
 
// nextNext list elements
if ( i === gaps[gap].newLast ) {
i = null;
}
if ( j === gaps[gap].oldLast ) {
j = null;
}
if ( i !== null ) {
i = this.newText.tokens[i].next;
}
if ( j !== null ) {
j = this.oldText.tokens[j].next;
}
Line 1,244 ⟶ 1,463:
 
 
/**
// TextDiff.slideGaps(): move gaps with ambiguous identical fronts to last newline or, if absent, last word border
* Move gaps with ambiguous identical fronts to last newline border or otherwise last word border.
// called from: .diff(), .detectBlocks()
*
// changes: .newText/.oldText .tokens list
* @param[in/out] wikEdDiffText text, textLinked These two are newText and oldText
*/
this.slideGaps = function ( text, textLinked ) {
 
var regExpSlideBorder = this.config.regExp.slideBorder;
this.slideGaps = function (text, textLinked) {
var regExpSlideStop = this.config.regExp.slideStop;
 
// cycleCycle through tokens list
var i = text.first;
var gapStart = null;
while ( (i !== null) && (text.tokens[i] !== null) ) {
 
// rememberRemember gap start
if ( (gapStart === null) && (text.tokens[i].link === null) ) {
gapStart = i;
}
 
// findFind gap end
else if ( (gapStart !== null) && (text.tokens[i].link !== null) ) {
var gapFront = gapStart;
var gapBack = text.tokens[i].prev;
 
// slideSlide down as deep as possible
var front = gapFront;
var back = text.tokens[gapBack].next;
if (
(front !== null) && (
back !== null) &&
(text.tokens[front].link === null) && (
text.tokens[back].link !== null) &&
(text.tokens[front].token === text.tokens[back].token)
) {
text.tokens[front].link = text.tokens[back].link;
Line 1,284 ⟶ 1,509:
}
 
// testTest slide up, remember last line break or word border
var front = text.tokens[gapFront].prev;
var back = gapBack;
var gapFrontBlankTest = wDiff.regExpSlideBorder.test( text.tokens[gapFront].token );
var frontStop = front;
if ( text.tokens[back].link === null ) {
while (
(front !== null) && (
back !== null) &&
(text.tokens[front].link !== null) &&
(text.tokens[front].token === text.tokens[back].token)
) {
if ( front !== null ) {
 
// Stop at line break
front = text.tokens[front].prev;
back if =( regExpSlideStop.test( text.tokens[backfront].prev;token ) === true ) {
frontStop = front;
break;
}
 
// stopStop at linefirst breakword border (blank/word or word/blank)
if (
if (wDiff.regExpSlideStop.test(text.tokens[front].token) === true) {
regExpSlideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
frontStop = front;
break frontStop = front;
}
 
// stop at first word border (blank/word or word/blank)
if (wDiff.regExpSlideBorder.test(text.tokens[front].token) !== gapFrontBlankTest) {
frontStop = front;
}
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
}
 
// actuallyActually slide up to stop
var front = text.tokens[gapFront].prev;
var back = gapBack;
while (
(front !== null) && (
back !== null) && (
front !== frontStop) &&
(text.tokens[front].link !== null) && (
text.tokens[back].link === null) &&
(text.tokens[front].token === text.tokens[back].token)
) {
text.tokens[back].link = text.tokens[front].link;
Line 1,335 ⟶ 1,566:
 
 
/**
// TextDiff.calculateDiff(): calculate diff information, can be called repeatedly during refining
* Calculate diff information, can be called repeatedly during refining.
// input: level: 'paragraph', 'sentence', 'chunk', 'word', or 'character'
* Links corresponding tokens from old and new text.
// optionally for recursive calls: recurse, newStart, newEnd, oldStart, oldEnd (tokens list indexes), recursionLevel
* Steps:
// called from: .diff()
* Pass 1: parse new text into symbol table
// calls: itself recursively
* Pass 2: parse old text into symbol table
// changes: .oldText/.newText.tokens[].link, links corresponding tokens from old and new text
* Pass 3: connect unique matching tokens
// steps:
* Pass 4: connect adjacent identical tokens downwards
// pass 1: parse new text into symbol table
* Pass 5: connect adjacent identical tokens upwards
// pass 2: parse old text into symbol table
* Repeat with empty symbol table (against crossed-over gaps)
// pass 3: connect unique matched tokens
* Recursively diff still unresolved regions downwards with empty symbol table
// pass 4: connect adjacent identical tokens downwards
* Recursively diff still unresolved regions upwards with empty symbol table
// pass 5: connect adjacent identical tokens upwards
*
// recursively diff still unresolved regions downwards
* @param array symbols Symbol table object
// recursively diff still unresolved regions upwards
* @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
this.calculateDiff = function (symbols, level, repeat, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
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; }
 
// startStart timertimers
if ( (wDiffthis.debugTimeconfig.timer === true) && (repeatrepeating !=== true)false && (recursionLevel === undefined)0 ) {
consolethis.time( level );
}
if ( this.config.timer === true && repeating === false ) {
this.time( level + recursionLevel );
}
 
// Get object symbols table and linked region borders
// set defaults
var symbols;
if (newStart === undefined) { newStart = this.newText.first; }
var bordersDown;
if (newEnd === undefined) { newEnd = this.newText.last; }
var bordersUp;
if (oldStart === undefined) { oldStart = this.oldText.first; }
if (oldEnd recursionLevel === undefined)0 {&& oldEndrepeating === this.oldText.last;false }) {
symbols = this.symbols;
if (recursionLevel === undefined) { recursionLevel = 0; }
bordersDown = this.bordersDown;
bordersUp = this.bordersUp;
}
 
// Create empty local symbols table and linked region borders arrays
// limit recursion depth
else {
if (recursionLevel > 10) {
return;symbols = {
token: [],
hashTable: {},
linked: false
};
bordersDown = [];
bordersUp = [];
}
 
//
// pass 1: parse new text into symbol table
//
 
// cycleUpdated troughversions newof textlinked tokensregion listborders
var bordersUpNext = [];
var bordersDownNext = [];
 
/**
* Pass 1: parse new text into symbol table.
*/
 
// Cycle through new text tokens list
var i = newStart;
while ( (i !== null ) &&{
if ( this.newText.tokens[i].link !=== null) ) {
if (this.newText.tokens[i].link === null) {
 
// addAdd new entry to symbol table
var token = this.newText.tokens[i].token;
if ( Object.prototype.hasOwnProperty.call( symbols.hashhashTable, token ) === false ) {
var currentsymbols.hashTable[token] = symbols.token.length;
symbols.hash[token].push( = current;{
symbols.token[current] = {
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
} );
}
 
// orOr update existing entry
else {
 
// incrementIncrement token counter for new text
var hashToArray = symbols.hashhashTable[token];
symbols.token[hashToArray].newCount ++;
}
}
 
// nextStop listafter elementgap if recursing
else if (i ==recursionLevel > 0 newEnd) {
break;
}
 
i = this.newText.tokens[i].next;
// Get next token
if ( up === false ) {
i = this.newText.tokens[i].next;
}
else {
i = this.newText.tokens[i].prev;
}
}
 
//**
// pass* Pass 2: parse old text into symbol table.
/ */
 
// cycleCycle troughthrough old text tokens list
var j = oldStart;
while ( (j !== null ) &&{
if ( this.oldText.tokens[j].link !=== null) ) {
if (this.oldText.tokens[j].link === null) {
 
// addAdd new entry to symbol table
var token = this.oldText.tokens[j].token;
if ( Object.prototype.hasOwnProperty.call( symbols.hashhashTable, token ) === false ) {
var currentsymbols.hashTable[token] = symbols.token.length;
symbols.hash[token].push( = current;{
symbols.token[current] = {
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
} );
}
 
// orOr update existing entry
else {
 
// incrementIncrement token counter for old text
var hashToArray = symbols.hashhashTable[token];
symbols.token[hashToArray].oldCount ++;
 
// addAdd token number for old text
symbols.token[hashToArray].oldToken = j;
}
}
 
// nextStop listafter elementgap if recursing
else if (j ===recursionLevel > 0 oldEnd) {
break;
}
 
j = this.oldText.tokens[j].next;
// Get next token
if ( up === false ) {
j = this.oldText.tokens[j].next;
}
else {
j = this.oldText.tokens[j].prev;
}
}
 
//**
// pass* Pass 3: connect unique tokens.
/ */
 
// cycleCycle troughthrough symbol array
for (var isymbolsLength = 0; i < symbols.token.length; i ++) {
for ( var i = 0; i < symbolsLength; i ++ ) {
 
// findFind tokens in the symbol table that occur only once in both versions
if ( (symbols.token[i].newCount === 1) && (symbols.token[i].oldCount === 1) ) {
var newToken = symbols.token[i].newToken;
var oldToken = symbols.token[i].oldToken;
var newTokenObj = this.newText.tokens[newToken];
var oldTokenObj = this.oldText.tokens[oldToken];
 
// connectConnect from new to old and from old to new
if (this.newText.tokens[newToken] newTokenObj.link === null ) {
 
// doDo not use spaces as unique markers
if (
if (/^\s+$/.test(this.newText.tokens[newToken].token) === false) {
this.config.regExp.blankOnlyToken.test( newTokenObj.token ) === true
) {
 
// Link new and old tokens
this.newText.tokens[newToken].link = oldToken;
this.oldText.tokens[oldToken]newTokenObj.link = newTokenoldToken;
oldTokenObj.link = newToken;
symbols.linked = true;
 
// checkSave iflinked tokenregion contains unique wordborders
bordersDown.push( [newToken, oldToken] );
if (recursionLevel === 0) {
bordersUp.push( [newToken, oldToken] );
 
// Check if token contains unique word
if ( recursionLevel === 0 ) {
var unique = false;
if ( level === 'character' ) {
unique = true;
}
else {
var token = this.newText.tokens[newToken]newTokenObj.token;
var words =
var words = (token.match(wDiff.regExpWord) || []).concat(token.match(wDiff.regExpChunk) || []);
( token.match( this.config.regExp.countWords ) || [] ).concat(
( token.match( this.config.regExp.countChunks ) || [] )
);
 
// uniqueUnique if longer than min block length
ifvar (words.lengthwordsLength >= wDiffwords.blockMinLength) {length;
if ( wordsLength >= this.config.blockMinLength ) {
unique = true;
}
 
// uniqueUnique if it contains at least one unique word
else {
for ( var wordi = 0; wordi < words.lengthwordsLength; wordi ++ ) {
ifvar ( (this.oldText.words[ words[word] ] == 1) && (this.newText.words[ words[wordi] ] == 1) ) {;
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;
Line 1,496 ⟶ 1,800:
}
 
// setSet unique
if ( unique === true ) {
this.newText.tokens[newToken]newTokenObj.unique = true;
this.oldText.tokens[oldToken]oldTokenObj.unique = true;
}
}
Line 1,507 ⟶ 1,811:
}
 
// continueContinue passes only if unique tokens have been linked previously
if ( symbols.linked === true ) {
 
//**
// pass* Pass 4: connect adjacent identical tokens downwards.
/ */
 
// getCycle surroundingthrough connectedlist of linked new text tokens
var ibordersLength = newStartbordersDown.length;
for ( var match = 0; match < bordersLength; match ++ ) {
if (this.newText.tokens[i].prev !== null) {
var i = this.newText.tokensbordersDown[imatch][0].prev;
var j = bordersDown[match][1];
}
var iStop = newEnd;
if (this.newText.tokens[iStop].next !== null) {
iStop = this.newText.tokens[iStop].next;
}
var j = null;
 
// cycle trough new text tokens listNext down
do 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
// connected pair
while (
var link = this.newText.tokens[i].link;
if (link i !== null) {&&
j !== null &&
j = this.oldText.tokens[link].next;
this.newText.tokens[i].link === null &&
}
this.oldText.tokens[j].link === null
) {
 
// connectConnect if tokenssame are the sametoken
else if ( (j !== 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;
}
 
// Not a match yet, maybe in next refinement level
else {
bordersDownNext.push( [iMatch, jMatch] );
break;
}
 
// Next token down
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
 
// not same**
* Pass 5: connect adjacent identical tokens upwards.
else {
j = null;*/
}
i = this.newText.tokens[i].next;
} while (i !== iStop);
 
// Cycle through list of connected new text tokens
//
var bordersLength = bordersUp.length;
// pass 5: connect adjacent identical tokens upwards
for ( var match = 0; match < bordersLength; match ++ ) {
//
var i = bordersUp[match][0];
var j = bordersUp[match][1];
 
// Next up
// get surrounding connected tokens
var iiMatch = newEndi;
var jMatch = j;
if (this.newText.tokens[i].next !== null) {
i = this.newText.tokens[i].nextprev;
j = this.oldText.tokens[j].prev;
}
var iStop = newStart;
if (this.newText.tokens[iStop].prev !== null) {
iStop = this.newText.tokens[iStop].prev;
}
var j = null;
 
// cycleCycle troughthrough new text tokensgap listregion upupwards
do while {(
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null
) {
 
// connectedConnect pairif same token
var if link =( this.newText.tokens[i].link;token === this.oldText.tokens[j].token ) {
if ( this.newText.tokens[i].link !== null) {j;
j = this.oldText.tokens[linkj].prevlink = i;
}
 
// Not a match yet, maybe in next refinement level
// connect if tokens are the same
else {
else if ( (j !== null) && (this.oldText.tokens[j].link === null) && (this.newText.tokens[i].token == this.oldText.tokens[j].token) ) {
bordersUpNext.push( [iMatch, jMatch] );
this.newText.tokens[i].link = j;
break;
this.oldText.tokens[j].link = i;
}
 
// Next token up
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
}
 
// not same**
* Connect adjacent identical tokens downwards from text start.
else {
* Treat boundary as connected, stop after first connected token.
j = null;
} */
i = this.newText.tokens[i].prev;
} while (i !== iStop);
 
// Only for full text diff
//
if ( recursionLevel === 0 && repeating === false ) {
// connect adjacent identical tokens downwards from text start, treat boundary as connected, stop after first connected token
//
 
// onlyFrom for full text diffstart
if ( (newStart == this.newText.first) && (newEnd == this.newText.last) ) {
 
// from start
var i = this.newText.first;
var j = this.oldText.first;
var iMatch = null;
var jMatch = null;
 
// cycleCycle troughthrough newold text tokens list down,
// connectConnect identical tokens, stop after first connected token
while (
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) ) {
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;
jiMatch = this.oldText.tokens[j].nexti;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
if ( iMatch !== null ) {
bordersDownNext.push( [iMatch, jMatch] );
}
 
// fromFrom end
var i = this.newText.last;
var j = this.oldText.last;
iMatch = null;
jMatch = null;
 
// cycleCycle troughthrough old text tokens list up,
// connectConnect identical tokens, stop after first connected token
while (
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) ) {
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;
jiMatch = this.oldText.tokens[j].previ;
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
//
if ( recursionLevel === 0 && repeating === false ) {
// repeat with empty symbol table to link hidden unresolved common tokens in cross-overs ("and" in "and this a and b that" -> "and this a and b that")
this.bordersDown = bordersDownNext;
//
this.bordersUp = bordersUpNext;
}
 
// newMerge emptylocal symbolsupdated linked region borders into object
else {
if (repeat !== true) {
this.bordersDown = this.bordersDown.concat( bordersDownNext );
var symbolsRepeat = {
this.bordersUp = this.bordersUp.concat( bordersUpNext );
token: [],
hash: {},
linked: false
};
this.calculateDiff(symbolsRepeat, level, true, false, newStart, newEnd, oldStart, oldEnd);
}
 
//
// refine by recursively diffing unresolved regions caused by addition of common tokens around sequences of common tokens, only at word level split
//
 
/**
if ( (recurse === true) && (wDiff.recursiveDiff === true) ) {
* Repeat once with empty symbol table to link hidden unresolved common tokens in cross-overs.
* ("and" in "and this a and b that" -> "and this a and b that")
*/
 
if ( repeating === false && this.config.repeatedDiff === true ) {
//
var repeat = true;
// recursively diff still unresolved regions downwards
this.calculateDiff( level, recurse, repeat, newStart, oldStart, up, recursionLevel );
//
}
 
/**
// cycle trough new text tokens list
* Refine by recursively diffing not linked regions with new symbol table.
var i = newStart;
* At word and character level only.
var j = oldStart;
* Helps against gaps caused by addition of common tokens around sequences of common tokens.
*/
 
if (
while ( (i !== null) && (this.newText.tokens[i] !== null) ) {
recurse === true &&
this.config['recursiveDiff'] === true &&
recursionLevel < this.config.recursionMax
) {
 
/**
// get j from previous tokens match
* Recursively diff gap downwards.
var iPrev = this.newText.tokens[i].prev;
*/
if (iPrev !== null) {
var jPrev = this.newText.tokens[iPrev].link;
if (jPrev !== null) {
j = this.oldText.tokens[jPrev].next;
}
}
 
// checkCycle forthrough the startlist of anlinked unresolvedregion sequenceborders
var bordersLength = bordersDownNext.length;
if ( (j !== null) && (this.oldText.tokens[j] !== null) && (this.newText.tokens[i].link === null) && (this.oldText.tokens[j].link === null) ) {
for ( match = 0; match < bordersLength; match ++ ) {
var i = bordersDownNext[match][0];
var j = bordersDownNext[match][1];
 
// Next token down
// determine the limits of the unresolved new sequence
var iStarti = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
var iEnd = null;
var iLength = 0;
var iNext = i;
while ( (iNext !== null) && (this.newText.tokens[iNext].link === null) ) {
iEnd = iNext;
iLength ++;
if (iEnd == newEnd) {
break;
}
iNext = this.newText.tokens[iNext].next;
}
 
// determineStart therecursion limitsat offirst thegap unresolvedtoken old sequencepair
varif jStart = j;(
var jEndi !== null; &&
var jLengthj !== null 0;&&
this.newText.tokens[i].link === null &&
var jNext = j;
while ( (jNext !== null) && (this.oldText.tokens[jNextj].link === null) ) {
jEnd) = jNext;{
jLengthvar ++repeat = false;
ifvar (jEnddirUp == oldEnd) {false;
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
break;
}
jNext = this.oldText.tokens[jNext].next;
}
 
// recursively diff the unresolved sequence
if ( (iLength > 1) || (jLength > 1) ) {
 
// new symbols object for sub-region
var symbolsRecurse = {
token: [],
hash: {},
linked: false
};
this.calculateDiff(symbolsRecurse, level, false, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
}
i = iEnd;
}
 
// next list element
if (i == newEnd) {
break;
}
i = this.newText.tokens[i].next;
}
 
//**
// recursively* Recursively diff still unresolved regionsgap upwards.
/ */
 
// cycleCycle troughthrough newlist textof tokenslinked listregion borders
var ibordersLength = newEndbordersUpNext.length;
for ( match = 0; match < bordersLength; match ++ ) {
var j = oldEnd;
var i = bordersUpNext[match][0];
while ( (i !== null) && (this.newText.tokens[i] !== null) ) {
var j = bordersUpNext[match][1];
 
// getNext jtoken from next matched tokensup
var iPrevi = this.newText.tokens[i].nextprev;
j = this.oldText.tokens[j].prev;
if (iPrev !== null) {
var jPrev = this.newText.tokens[iPrev].link;
if (jPrev !== null) {
j = this.oldText.tokens[jPrev].prev;
}
}
 
// checkStart forrecursion theat startfirst ofgap antoken unresolved sequencepair
if (
if ( (j !== null) && (this.oldText.tokens[j] !== null) && (this.newText.tokens[i].link === null) && (this.oldText.tokens[j].link === null) ) {
i !== null &&
 
j !== null &&
// determine the limits of the unresolved new sequence
var iStartthis.newText.tokens[i].link === null; &&
var iEndthis.oldText.tokens[j].link === i;null
var) iLength = 0;{
var iNextrepeat = ifalse;
var dirUp = true;
while ( (iNext !== null) && (this.newText.tokens[iNext].link === null) ) {
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
iStart = iNext;
iLength ++;
if (iStart == newStart) {
break;
}
iNext = this.newText.tokens[iNext].prev;
}
 
// determine the limits of the unresolved old sequence
var jStart = null;
var jEnd = j;
var jLength = 0;
var jNext = j;
while ( (jNext !== null) && (this.oldText.tokens[jNext].link === null) ) {
jStart = jNext;
jLength ++;
if (jStart == oldStart) {
break;
}
jNext = this.oldText.tokens[jNext].prev;
}
 
// recursively diff the unresolved sequence
if ( (iLength > 1) || (jLength > 1) ) {
 
// new symbols object for sub-region
var symbolsRecurse = {
token: [],
hash: {},
linked: false
};
this.calculateDiff(symbolsRecurse, level, false, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
}
i = iStart;
}
 
// next list element
if (i == newStart) {
break;
}
i = this.newText.tokens[i].prev;
}
}
}
 
// stopStop timertimers
if ( (wDiffthis.debugTimeconfig.timer === true) && (repeat !== true) && (recursionLevelrepeating === 0)false ) {
if ( this.recursionTimer[recursionLevel] === undefined ) {
console.timeEnd(level);
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;
};
 
 
/**
// TextDiff.detectBlocks(): main method for extracting deleted, inserted, and moved blocks from raw diff data
* Main method for processing raw diff data, extracting deleted, inserted, and moved blocks.
// called from: .diff()
*
// calls: .getSameBlocks(), .getSections(), .getGroups(), .setFixed(), getDelBlocks(), .positionDelBlocks(), .unlinkBlocks(), .getInsBlocks(), .setInsGroups(), .insertMarks()
* Scheme of blocks, sections, and groups (old block numbers):
// input:
* Old: 1 2 3D4 5E6 7 8 9 10 11
// text: object containing text tokens list
* | ‾/-/_ X | >|< |
// blocks: empty array for block data
* New: 1 I 3D4 2 E6 5 N 7 10 9 8 11
// groups: empty array for group data
* Section: 0 0 0 1 1 2 2 2
// changes: .text, .blocks, .groups
* Group: 0 10 111 2 33 4 11 5 6 7 8 9
// scheme of blocks, sections, and groups (old block numbers):
// * oldFixed: . 1 +++ - 2 3D4++ - 5E6 . 7 . - 8 9- 10 11+
// * Type: = . =-= = | ‾/-/_ X = = . |= = >|<= = |=
*
// new: 1 I 3D4 2 E6 5 N 7 10 9 8 11
* @param[out] array groups Groups table object
// section: 0 0 0 1 1 2 2 2
* @param[out] array blocks Blocks table object
// group: 0 10 111 2 33 4 11 5 6 7 8 9
* @param[in/out] WikEdDiffText newText, oldText Text object tokens list
// fixed: + +++ - ++ - + + - - +
*/
// type: = + =-= = -= = + = = = = =
 
this.detectBlocks = function () {
 
// Debug log
if (wDiff.debug === true) {
if ( this.oldTextconfig.debugText('Olddebug === true text'); {
this.newTextoldText.debugText( 'NewOld text' );
this.newText.debugText( 'New text' );
}
 
// collectCollect identical corresponding ('same=') blocks from old text and sort by new text
this.getSameBlocks();
 
// collectCollect independent block sections (with no old/newblock move crosses outside section)a for per-section determination of non-moving (fixed) groups
this.getSections();
 
// findFind groups of continuous old text blocks
this.getGroups();
 
// setSet longest sequence of increasing groups in sections as fixed (not moved)
this.setFixed();
 
// Convert groups to insertions/deletions if maximum block length is too short
// collect deletion ('del') blocks from old text
// Only for more complex texts that actually have blocks of minimum block length
this.getDelBlocks();
var unlinkCount = 0;
if (
this.config.unlinkBlocks === true &&
this.config.blockMinLength > 0 &&
this.maxWords >= this.config.blockMinLength
) {
if ( this.config.timer === true ) {
this.time( 'total unlinking' );
}
 
// Repeat as long as unlinking is possible
// position 'del' blocks into new text order
var unlinked = true;
this.positionDelBlocks();
while ( unlinked === true && unlinkCount < this.config.unlinkMax ) {
 
// Convert '=' to '+'/'-' pairs
// convert groups to insertions/deletions if maximal block length is too short
var unlink = 0;
if ( (wDiff.unlinkBlocks = true) && (wDiff.blockMinLength > 0) ) {
 
// repeat as long as unlinking is possible
var unlinked = false;
do {
 
// convert 'same' to 'ins'/'del' pairs
unlinked = this.unlinkBlocks();
 
// startStart over after conversion
if ( unlinked === true ) {
unlinkunlinkCount ++;
this.slideGaps( this.newText, this.oldText );
this.slideGaps( this.oldText, this.newText );
 
// repeatRepeat block detection from start
this.maxWords = 0;
this.getSameBlocks();
this.getSections();
this.getGroups();
this.setFixed();
this.getDelBlocks();
this.positionDelBlocks();
}
}
} while (unlinked === true);
if ( this.config.timer === true ) {
this.timeEnd( 'total unlinking' );
}
}
 
// collectCollect insertiondeletion ('ins-') blocks from newold text
this.getDelBlocks();
 
// Position '-' blocks into new text order
this.positionDelBlocks();
 
// Collect insertion ('+') blocks from new text
this.getInsBlocks();
 
// setSet group numbers of 'ins+' blocks
this.setInsGroups();
 
// markMark original positions of moved groups
this.insertMarks();
 
// Debug log
if (wDiff.debug === true) {
if ( this.config.timer === true || this.config.debug === true ) {
console.log('Unlinked: ', unlink);
console.log( 'Unlink count: ', unlinkCount );
this.debugGroups('Groups');
}
this.debugBlocks('Blocks');
if ( this.config.debug === true ) {
this.debugGroups( 'Groups' );
this.debugBlocks( 'Blocks' );
}
return;
Line 1,877 ⟶ 2,171:
 
 
/**
// TextDiff.getSameBlocks(): collect identical corresponding ('same') blocks from old text and sort by new text
* Collect identical corresponding matching ('=') blocks from old text and sort by new text.
// called from: .detectBlocks()
*
// calls: .wordCount()
* @param[in] WikEdDiffText newText, oldText Text objects
// changes: .blocks
* @param[in/out] array blocks Blocks table object
 
*/
this.getSameBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'getSameBlocks' );
}
 
var blocks = this.blocks;
 
// clearClear blocks array
blocks.splice( 0 );
 
// cycleCycle through old text to find matchedconnected (linked, matched) blocks
var j = this.oldText.first;
var i = null;
while ( j !== null ) {
 
// skipSkip 'del-' blocks
while ( (j !== null) && (this.oldText.tokens[j].link === null) ) {
j = this.oldText.tokens[j].next;
}
 
// getGet 'same=' block
if ( j !== null ) {
i = this.oldText.tokens[j].link;
var iStart = i;
var jStart = j;
 
// detectDetect matching blocks ('same=')
var count = 0;
var unique = false;
var stringtext = '';
while ( (i !== null) && (j !== null) && (this.oldText.tokens[j].link === i) ) {
var tokentext += this.oldText.tokens[j].token;
count ++;
if ( this.newText.tokens[i].unique === true ) {
unique = true;
}
string += token;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
 
// saveSave old text 'same=' block
blocks.push( {
oldBlock: blocks.length,
newBlock: null,
Line 1,929 ⟶ 2,227:
count: count,
unique: unique,
words: this.wordCount(string text ),
chars: stringtext.length,
type: 'same=',
section: null,
group: null,
fixed: null,
moved: null,
stringtext: string text
} );
}
}
 
// sortSort blocks by new text token number
blocks.sort( function( a, b ) {
return a.newNumber - b.newNumber;
} );
 
// numberNumber blocks in new text order
for (var blockblocksLength = 0; block < blocks.length; block ++) {
for ( var block = 0; block < blocksLength; block ++ ) {
blocks[block].newBlock = block;
}
 
if ( this.config.timer === true ) {
this.timeEnd( 'getSameBlocks' );
}
return;
Line 1,954 ⟶ 2,257:
 
 
/**
// TextDiff.getSections(): collect independent block sections (no old/new crosses outside section) for per-section determination of non-moving (fixed) groups
* Collect independent block sections with no block move crosses
// called from: .detectBlocks()
* outside a section for per-section determination of non-moving fixed groups.
// changes: creates sections, blocks[].section
*
 
* @param[out] array sections Sections table object
* @param[in/out] array blocks Blocks table object, section property
*/
this.getSections = function () {
 
if ( this.config.timer === true ) {
this.time( 'getSections' );
}
 
var blocks = this.blocks;
var sections = this.sections;
 
// clearClear sections array
sections.splice( 0 );
 
// cycleCycle through blocks
for (var blockblocksLength = 0; block < blocks.length; block ++) {
for ( var block = 0; block < blocksLength; block ++ ) {
 
var sectionStart = block;
Line 1,975 ⟶ 2,286:
var sectionOldMax = oldMax;
 
// checkCheck right
for ( var j = sectionStart + 1; j < blocks.lengthblocksLength; j ++ ) {
 
// checkCheck for crossing over to the left
if ( blocks[j].oldNumber > oldMax ) {
oldMax = blocks[j].oldNumber;
}
else if ( blocks[j].oldNumber < sectionOldMax ) {
sectionEnd = j;
sectionOldMax = oldMax;
Line 1,988 ⟶ 2,299:
}
 
// saveSave crossing sections
if ( sectionEnd > sectionStart ) {
 
// saveSave section to block
for ( var i = sectionStart; i <= sectionEnd; i ++ ) {
blocks[i].section = sections.length;
}
 
// saveSave section
sections.push( {
blockStart: sectionStart,
blockEnd: sectionEnd
} );
block = sectionEnd;
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getSections' );
}
return;
Line 2,008 ⟶ 2,322:
 
 
/**
// TextDiff.getGroups(): find groups of continuous old text blocks
* Find groups of continuous old text blocks.
// called from: .detectBlocks()
*
// calls: .wordCount()
* @param[out] array groups Groups table object
// changes: creates .groups, .blocks[].group
* @param[in/out] array blocks Blocks table object, group property
 
*/
this.getGroups = function () {
 
if ( this.config.timer === true ) {
this.time( 'getGroups' );
}
 
var blocks = this.blocks;
var groups = this.groups;
 
// clearClear groups array
groups.splice( 0 );
 
// cycleCycle through blocks
for (var blockblocksLength = 0; block < blocks.length; block ++) {
for ( var block = 0; block < blocksLength; block ++ ) {
var groupStart = block;
var groupEnd = block;
var oldBlock = blocks[groupStart].oldBlock;
 
// getGet word and char count of block
var words = this.wordCount( blocks[block].stringtext );
var maxWords = words;
var unique = blocks[block].unique;
var chars = blocks[block].chars;
 
// checkCheck right
for ( var i = groupEnd + 1; i < blocks.lengthblocksLength; i ++ ) {
 
// checkCheck for crossing over to the left
if ( blocks[i].oldBlock !== oldBlock + 1 ) {
break;
}
oldBlock = blocks[i].oldBlock;
 
// getGet word and char count of block
if ( blocks[i].words > maxWords ) {
maxWords = blocks[i].words;
}
if ( blocks[i].unique === true ) {
unique = true;
}
Line 2,054 ⟶ 2,374:
}
 
// saveSave crossing group
if ( groupEnd >= groupStart ) {
 
// setSet groups outside sections as fixed
var fixed = false;
if ( blocks[groupStart].section === null ) {
fixed = true;
}
 
// saveSave group to block
for ( var i = groupStart; i <= groupEnd; i ++ ) {
blocks[i].group = groups.length;
blocks[i].fixed = fixed;
}
 
// saveSave group
groups.push( {
oldNumber: blocks[groupStart].oldNumber,
blockStart: groupStart,
Line 2,081 ⟶ 2,401:
movedFrom: null,
color: null
} );
block = groupEnd;
 
// Set global word count of longest linked block
if ( maxWords > this.maxWords ) {
this.maxWords = maxWords;
}
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getGroups' );
}
return;
Line 2,089 ⟶ 2,417:
 
 
/**
// TextDiff.setFixed(): set longest sequence of increasing groups in sections as fixed (not moved)
* Set longest sequence of increasing groups in sections as fixed (not moved).
// called from: .detectBlocks()
*
// calls: .findMaxPath()
* @param[in] array sections Sections table object
// changes: .groups[].fixed, .blocks[].fixed
* @param[in/out] array groups Groups table object, fixed property
 
* @param[in/out] array blocks Blocks table object, fixed property
*/
this.setFixed = function () {
 
if ( this.config.timer === true ) {
this.time( 'setFixed' );
}
 
var blocks = this.blocks;
Line 2,100 ⟶ 2,434:
var sections = this.sections;
 
// cycleCycle through sections
for (var sectionsectionsLength = 0; section < sections.length; section ++) {
for ( var section = 0; section < sectionsLength; section ++ ) {
var blockStart = sections[section].blockStart;
var blockEnd = sections[section].blockEnd;
Line 2,108 ⟶ 2,443:
var groupEnd = blocks[blockEnd].group;
 
// recusivelyRecusively find path of groups in increasing old group order with longest char length
var cache = [];
var maxChars = 0;
var maxPath = null;
 
// startStart at each group of section
for ( var i = groupStart; i <= groupEnd; i ++ ) {
var pathObj = this.findMaxPath( i, groupEnd, cache );
if ( pathObj.chars > maxChars ) {
maxPath = pathObj.path;
maxChars = pathObj.chars;
Line 2,122 ⟶ 2,457:
}
 
// markMark fixed groups
for (var imaxPathLength = 0; i < maxPath.length; i ++) {
for ( var i = 0; i < maxPathLength; i ++ ) {
var group = maxPath[i];
groups[group].fixed = true;
 
// markMark fixed blocks
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
blocks[block].fixed = true;
}
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'setFixed' );
}
return;
Line 2,137 ⟶ 2,476:
 
 
/**
// TextDiff.findMaxPath(): recusively find path of groups in increasing old group order with longest char length
* Recusively find path of groups in increasing old group order with longest char length.
// input: start: path start group, path: array of path groups, chars: char count of path, cache: cached sub-path lengths, groupEnd: last group
*
// called from: .setFixed()
* @param int start Path start group
// calls: itself recursively
* @param int groupEnd Path last group
// returns: returnObj, contains path and length
* @param array cache Cache object, contains returnObj for start
 
* @return array returnObj Contains path and char length
this.findMaxPath = function (start, groupEnd, cache) {
*/
this.findMaxPath = function ( start, groupEnd, cache ) {
 
var groups = this.groups;
 
// findFind longest sub-path
var maxChars = 0;
var oldNumber = groups[start].oldNumber;
var returnObj = { path: [], chars: 0};
for ( var i = start + 1; i <= groupEnd; i ++ ) {
 
// onlyOnly in increasing old group order
if ( groups[i].oldNumber < oldNumber ) {
continue;
}
 
// getGet longest sub-path from cache (deep copy)
var pathObj;
if ( cache[i] !== undefined ) {
pathObj = { path: cache[i].path.slice(), chars: cache[i].chars };
}
 
// getGet longest sub-path by recursion
else {
pathObj = this.findMaxPath( i, groupEnd, cache );
}
 
// selectSelect longest sub-path
if ( pathObj.chars > maxChars ) {
maxChars = pathObj.chars;
returnObj = pathObj;
Line 2,176 ⟶ 2,517:
}
 
// addAdd current start to path
returnObj.path.unshift( start );
returnObj.chars += groups[start].chars;
 
// saveSave path to cache (deep copy)
if ( cache[start] === undefined ) {
cache[start] = { path: returnObj.path.slice(), chars: returnObj.chars };
}
Line 2,189 ⟶ 2,530:
 
 
/**
// TextDiff.getDelBlocks(): collect deletion ('del') blocks from old text
* Convert matching '=' blocks in groups into insertion/deletion ('+'/'-') pairs
// called from: .detectBlocks()
* if too short and too common.
// changes: .blocks
* Prevents fragmentated diffs for very different versions.
*
* @param[in] array blocks Blocks table object
* @param[in/out] WikEdDiffText newText, oldText Text object, linked property
* @param[in/out] array groups Groups table object
* @return bool True if text tokens were unlinked
*/
this.unlinkBlocks = function () {
 
var blocks = this.blocks;
var groups = this.groups;
 
// Cycle through groups
var unlinked = false;
var groupsLength = groups.length;
for ( var group = 0; group < groupsLength; group ++ ) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
 
// Unlink whole group if no block is at least blockMinLength words long and unique
if ( groups[group].maxWords < this.config.blockMinLength && groups[group].unique === false ) {
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === '=' ) {
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
 
// Otherwise unlink block flanks
else {
 
// Unlink blocks from start
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === '=' ) {
 
// Stop unlinking if more than one word or a unique word
if ( blocks[block].words > 1 || blocks[block].unique === true ) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
blockStart = block;
}
}
 
// Unlink blocks from end
for ( var block = blockEnd; block > blockStart; block -- ) {
if ( blocks[block].type === '=' ) {
 
// Stop unlinking if more than one word or a unique word
if (
blocks[block].words > 1 ||
( blocks[block].words === 1 && blocks[block].unique === true )
) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
}
return unlinked;
};
 
 
/**
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs.
*
* @param[in] array blocks Blocks table object
* @param[out] WikEdDiffText newText, oldText Text objects, link property
*/
this.unlinkSingleBlock = function ( block ) {
 
// Cycle through old text
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
 
// Unlink tokens
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
this.oldText.tokens[j].link = null;
j = this.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 ) {
this.time( 'getDelBlocks' );
}
 
var blocks = this.blocks;
 
// cycleCycle through old text to find matchedconnected (linked, matched) blocks
var j = this.oldText.first;
var i = null;
while ( j !== null ) {
 
// collectCollect 'del-' blocks
var oldStart = j;
var count = 0;
var stringtext = '';
while ( (j !== null) && (this.oldText.tokens[j].link === null) ) {
count ++;
stringtext += this.oldText.tokens[j].token;
j = this.oldText.tokens[j].next;
}
 
// saveSave old text 'del-' block
if ( count !== 0 ) {
blocks.push( {
oldBlock: null,
newBlock: null,
Line 2,223 ⟶ 2,661:
unique: false,
words: null,
chars: stringtext.length,
type: 'del-',
section: null,
group: null,
fixed: null,
moved: null,
stringtext: string text
} );
}
 
// skipSkip 'same=' 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;
Line 2,246 ⟶ 2,687:
 
 
/**
// TextDiff.positionDelBlocks(): position 'del' blocks into new text order
* Position deletion '-' blocks into new text order.
// called from: .detectBlocks()
* Deletion blocks move with fixed reference:
// calls: .sortBlocks()
* Old: 1 D 2 1 D 2
// changes: .blocks[].section/group/fixed/newNumber
* / \ / \ \
//
* New: 1 D 2 1 D 2
// deletion blocks move with fixed reference (new number +/- 0.1):
// * oldFixed: * 1 D 2 1 D 2*
// * newNumber: 1 1 / 2 \ / \ \2
*
// new: 1 D 2 1 D 2
* Marks '|' and deletions '-' get newNumber of reference block
// fixed: * *
* and are sorted around it by old text number.
// new number: 1 1 2 2
*
// 'mark' and 'del' get new number of reference block and are sorted around it by old text number
* @param[in/out] array blocks Blocks table, newNumber, section, group, and fixed properties
 
*
*/
this.positionDelBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'positionDelBlocks' );
}
 
var blocks = this.blocks;
var groups = this.groups;
 
// sortSort shallow copy of blocks by oldNumber
var blocksOld = blocks.slice();
blocksOld.sort( function( a, b ) {
return a.oldNumber - b.oldNumber;
} );
 
// cycleCycle through blocks in old text order
for (var blockblocksOldLength = 0; block < blocksOld.length; block ++) {
for ( var block = 0; block < blocksOldLength; block ++ ) {
var delBlock = blocksOld[block];
 
// 'del-' block only
if ( delBlock.type !== 'del-' ) {
continue;
}
 
// findFind fixed 'same=' reference block from original block position to position 'del-' block,
// similarSimilar to position marks 'mark|' code
 
// getGet old text prev block
var prevBlockNumber = null;
var prevBlock = null;
if ( block > 0 ) {
prevBlockNumber = blocksOld[block - 1].newBlock;
prevBlock = blocks[prevBlockNumber];
}
 
// getGet old text next block
var nextBlockNumber = null;
var nextBlock = null;
if ( block < blocksOld.length - 1 ) {
nextBlockNumber = blocksOld[block + 1].newBlock;
nextBlock = blocks[nextBlockNumber];
}
 
// moveMove after prev block if fixed
var refBlock = null;
if ( (prevBlock !== null) && (prevBlock.type === 'same=') && (prevBlock.fixed === true) ) {
refBlock = prevBlock;
}
 
// moveMove before next block if fixed
else if ( (nextBlock !== null) && (nextBlock.type === 'same=') && (nextBlock.fixed === true) ) {
refBlock = nextBlock;
}
 
// moveMove after prev block if not start of group
else if (
else if ( (prevBlock !== null) && (prevBlock.type == 'same') && (prevBlockNumber != groups[ prevBlock.group ].blockEnd) ) {
prevBlock !== null &&
prevBlock.type === '=' &&
prevBlockNumber !== groups[ prevBlock.group ].blockEnd
) {
refBlock = prevBlock;
}
 
// moveMove before next block if not start of group
else if (
else if ( (nextBlock !== null) && (nextBlock.type == 'same') && (nextBlockNumber != groups[ nextBlock.group ].blockStart) ) {
nextBlock !== null &&
nextBlock.type === '=' &&
nextBlockNumber !== groups[ nextBlock.group ].blockStart
) {
refBlock = nextBlock;
}
 
// moveMove after closest previous fixed block
else {
for ( var fixed = block; fixed >= 0; fixed -- ) {
if ( (blocksOld[fixed].type === 'same=') && (blocksOld[fixed].fixed === true) ) {
refBlock = blocksOld[fixed];
break;
Line 2,328 ⟶ 2,785:
}
 
// moveMove before first block
if ( refBlock === null ) {
delBlock.newNumber = -1;
}
 
// updateUpdate 'del-' block data
else {
delBlock.newNumber = refBlock.newNumber;
Line 2,342 ⟶ 2,799:
}
 
// sortSort 'del-' blocks in and update groups
this.sortBlocks();
 
if ( this.config.timer === true ) {
this.timeEnd( 'positionDelBlocks' );
}
return;
};
 
 
/**
// TextDiff.unlinkBlocks(): convert 'same' blocks in groups into 'ins'/'del' pairs if too short and too common
* Collect insertion ('+') blocks from new text.
// called from: .detectBlocks()
*
// calls: .unlinkSingleBlock()
* @param[in] WikEdDiffText newText New Text object
// changes: .newText/oldText[].link
* @param[out] array blocks Blocks table object
// returns: true if text tokens were unlinked
*/
this.getInsBlocks = function () {
 
if ( this.unlinkBlocksconfig.timer === functiontrue () {
this.time( 'getInsBlocks' );
 
var blocks = this.blocks;
var groups = this.groups;
 
// cycle through groups
var unlinked = false;
for (var group = 0; group < groups.length; group ++) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
 
// unlink whole group if no block is at least blockMinLength words long and unique
if ( (groups[group].maxWords < wDiff.blockMinLength) && (groups[group].unique === false) ) {
for (var block = blockStart; block <= blockEnd; block ++) {
if (blocks[block].type == 'same') {
this.unlinkSingleBlock(blocks[block]);
unlinked = true;
}
}
}
 
// otherwise unlink block flanks
else {
 
// unlink blocks from start
for (var block = blockStart; block <= blockEnd; block ++) {
if (blocks[block].type == 'same') {
 
// stop unlinking if more than one word or a unique word
if ( (blocks[block].words > 1) || (blocks[block].unique === true) ) {
break;
}
this.unlinkSingleBlock(blocks[block]);
unlinked = true;
blockStart = block;
}
}
 
// unlink blocks from end
for (var block = blockEnd; block > blockStart; block --) {
if ( (blocks[block].type == 'same') ) {
 
// stop unlinking if more than one word or a unique word
if ( (blocks[block].words > 1) || ( (blocks[block].words == 1) && (blocks[block].unique === true) ) ) {
break;
}
this.unlinkSingleBlock(blocks[block]);
unlinked = true;
}
}
}
}
return unlinked;
};
 
 
// TextDiff.unlinkBlock(): unlink text tokens of single block, converting them into 'ins'/'del' pair
// called from: .unlinkBlocks()
// changes: text.newText/oldText[].link
 
this.unlinkSingleBlock = function (block) {
 
// cycle through old text
var j = block.oldStart;
for (var count = 0; count < block.count; count ++) {
 
// unlink tokens
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
this.oldText.tokens[j].link = null;
j = this.oldText.tokens[j].next;
}
return;
};
 
 
// TextDiff.getInsBlocks(): collect insertion ('ins') blocks from new text
// called from: .detectBlocks()
// calls: .sortBlocks()
// changes: .blocks
 
this.getInsBlocks = function () {
 
var blocks = this.blocks;
 
// cycleCycle through new text to find insertion blocks
var i = this.newText.first;
while ( i !== null ) {
 
// jumpJump over linked (matched) block
while ( (i !== null) && (this.newText.tokens[i].link !== null) ) {
i = this.newText.tokens[i].next;
}
 
// detectDetect insertion blocks ('ins+')
if ( i !== null ) {
var iStart = i;
var count = 0;
var stringtext = '';
while ( (i !== null) && (this.newText.tokens[i].link === null) ) {
count ++;
stringtext += this.newText.tokens[i].token;
i = this.newText.tokens[i].next;
}
 
// saveSave new text 'ins+' block
blocks.push( {
oldBlock: null,
newBlock: null,
Line 2,469 ⟶ 2,853:
unique: false,
words: null,
chars: stringtext.length,
type: 'ins+',
section: null,
group: null,
fixed: null,
moved: null,
stringtext: string text
} );
}
}
 
// sortSort 'ins+' blocks in and update groups
this.sortBlocks();
 
if ( this.config.timer === true ) {
this.timeEnd( 'getInsBlocks' );
}
return;
};
 
 
/**
// TextDiff.sortBlocks(): sort blocks by new text token number and update groups
* Sort blocks by new text token number and update groups.
// called from: .positionDelBlocks(), .getInsBlocks(), .insertMarks()
*
// changes: .blocks, .groups
* @param[in/out] array groups Groups table object
 
* @param[in/out] array blocks Blocks table object
*/
this.sortBlocks = function () {
 
Line 2,496 ⟶ 2,885:
var groups = this.groups;
 
// sortSort 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;
} );
 
// cycleCycle through blocks and update groups with new block numbers
var group = null;
for (var blockblocksLength = 0; block < blocks.length; block ++) {
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;
Line 2,522 ⟶ 2,912:
 
 
/**
// TextDiff.setInsGroups: set group numbers of 'ins' blocks
* Set group numbers of insertion '+' blocks.
// called from: .detectBlocks()
*
// changes: .groups, .blocks[].fixed/group
* @param[in/out] array groups Groups table object
 
* @param[in/out] array blocks Blocks table object, fixed and group properties
*/
this.setInsGroups = function () {
 
if ( this.config.timer === true ) {
this.time( 'setInsGroups' );
}
 
var blocks = this.blocks;
var groups = this.groups;
 
// setSet group numbers of 'ins+' blocks inside existing groups
for (var groupgroupsLength = 0; group < groups.length; group ++) {
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;
Line 2,542 ⟶ 2,939:
}
 
// addAdd remaining 'ins+' blocks to new groups
 
// cycleCycle through blocks
for (var blockblocksLength = 0; block < blocks.length; block ++) {
for ( var block = 0; block < blocksLength; block ++ ) {
 
// skipSkip existing groups
if ( blocks[block].group === null ) {
blocks[block].group = groups.length;
 
// saveSave new single-block group
groups.push( {
oldNumber: blocks[block].oldNumber,
blockStart: block,
Line 2,563 ⟶ 2,961:
movedFrom: null,
color: null
} );
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'setInsGroups' );
}
return;
Line 2,570 ⟶ 2,971:
 
 
/**
// TextDiff.insertMarks(): mark original positions of moved groups
* Mark original positions of moved groups.
// called from: .detectBlocks()
* Scheme: moved block marks at original positions relative to fixed groups:
// changes: .groups[].movedFrom
* Groups: 3 7
// moved block marks at original positions relative to fixed groups:
// * groups: 3 1 <| 7 | (no next smaller fixed)
// * 1 <| 5 |< (no next smaller fixed)|
// * 5 |< |> 5 |
// * |> 5 5 <|
// * | 5 < >| 5
// * | > |> 9 (no next larger 5fixed)
// * Fixed: * | |> 9 (no next larger fixed)*
*
// fixed: * *
// * markMark direction: groups.movedGroup.blockStart < .groups[.group].blockStart
// * groupGroup side: groups.movedGroup.oldNumber < .groups[.group].oldNumber
*
// 'mark' and 'del' get new number of reference block and are sorted around it by old text number
* Marks '|' and deletions '-' get newNumber of reference block
 
* and are sorted around it by old text number.
*
* @param[in/out] array groups Groups table object, movedFrom property
* @param[in/out] array blocks Blocks table object
*/
this.insertMarks = function () {
 
if ( this.config.timer === true ) {
this.time( 'insertMarks' );
}
 
var blocks = this.blocks;
Line 2,593 ⟶ 3,003:
var color = 1;
 
// makeMake shallow copy of blocks
var blocksOld = blocks.slice();
 
// enumerateEnumerate copy
for (var iblocksOldLength = 0; i < blocksOld.length; i ++) {
for ( var i = 0; i < blocksOldLength; i ++ ) {
blocksOld[i].number = i;
}
 
// sortSort copy by oldNumber
blocksOld.sort( function( a, b ) {
var comp = a.oldNumber - b.oldNumber;
if ( comp === 0 ) {
comp = a.newNumber - b.newNumber;
}
return comp;
} );
 
// this.debugGroups('insertMarks after Groups');
this.debugBlocks('blocksOld', blocksOld);
 
// createCreate lookup table: original to sorted
var lookupSorted = [];
for ( var i = 0; i < blocksOld.lengthblocksOldLength; i ++ ) {
lookupSorted[ blocksOld[i].number ] = i;
}
 
// cycleCycle through groups (moved group)
for (var movedgroupsLength = 0; moved < groups.length; moved ++) {
for ( var moved = 0; moved < groupsLength; moved ++ ) {
var movedGroup = groups[moved];
if ( movedGroup.fixed !== false ) {
continue;
}
var movedOldNumber = movedGroup.oldNumber;
 
// findFind fixed 'same=' reference block from original block position to position 'mark|' block,
// similarSimilar to position deletions 'del-' code
 
// getGet old text prev block
var prevBlock = null;
var block = lookupSorted[ groups[moved]movedGroup.blockStart ];
if ( block > 0 ) {
prevBlock = blocksOld[block - 1];
}
 
// getGet old text next block
var nextBlock = null;
var block = lookupSorted[ groups[moved]movedGroup.blockEnd ];
if ( block < blocksOld.length - 1 ) {
nextBlock = blocksOld[block + 1];
}
 
// moveMove after prev block if fixed
var refBlock = null;
if ( (prevBlock !== null) && (prevBlock.type === 'same=') && (prevBlock.fixed === true) ) {
refBlock = prevBlock;
}
 
// moveMove before next block if fixed
else if ( (nextBlock !== null) && (nextBlock.type === 'same=') && (nextBlock.fixed === true) ) {
refBlock = nextBlock;
}
 
// findFind closest fixed block to the left
else {
for ( var fixed = lookupSorted[ groups[moved]movedGroup.blockStart ] - 1; fixed >= 0; fixed -- ) {
if ( (blocksOld[fixed].type === 'same=') && (blocksOld[fixed].fixed === true) ) {
refBlock = blocksOld[fixed];
break;
Line 2,664 ⟶ 3,074:
}
 
// getGet position of new mark block
var newNumber;
var markGroup;
 
// noNo smaller fixed block, moved right from before first block
if ( refBlock === null ) {
newNumber = -1;
markGroup = groups.length;
 
// saveSave new single-mark-block group
groups.push( {
oldNumber: 0,
blockStart: blocks.length,
Line 2,685 ⟶ 3,095:
movedFrom: null,
color: null
} );
}
else {
Line 2,692 ⟶ 3,102:
}
 
// insertInsert 'mark|' block
blocks.push( {
oldBlock: null,
newBlock: null,
Line 2,703 ⟶ 3,113:
words: null,
chars: 0,
type: 'mark|',
section: null,
group: markGroup,
fixed: true,
moved: moved,
stringtext: ''
} );
 
// setSet group color
movedGroup.color = color;
movedGroup.movedFrom = markGroup;
Line 2,717 ⟶ 3,127:
}
 
// sortSort 'mark|' blocks in and update groups
this.sortBlocks();
 
if ( this.config.timer === true ) {
this.timeEnd( 'insertMarks' );
}
return;
};
 
 
/**
// TextDiff.assembleDiff(): create html formatted diff text from block and group data
* Collect diff fragment list for markup, create abstraction layer for customized diffs.
// input: version: 'new', 'old', show only one marked-up version
* Adds the following fagment types:
// returns: diff html string
* '=', '-', '+' same, deletion, insertion
// called from: .diff()
* '<', '>' mark left, mark right
// calls: .htmlCustomize(), .htmlEscape(), .htmlFormatBlock(), .htmlFormat()
* '(<', '(>', ')' block start and end
 
* '[', ']' fragment start and end
this.assembleDiff = function (version) {
* '{', '}' 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;
var groups = this.groups;
var fragments = this.fragments;
 
// makeMake shallow copy of groups and sort by blockStart
var groupsSort = groups.slice();
groupsSort.sort( function( a, b ) {
return a.blockStart - b.blockStart;
} );
 
// Cycle through groups
//
var groupsSortLength = groupsSort.length;
// create group diffs
for ( var group = 0; group < groupsSortLength; group ++ ) {
//
 
// cycle through groups
var htmlFrags = [];
for (var group = 0; group < groupsSort.length; group ++) {
var color = groupsSort[group].color;
var blockStart = groupsSort[group].blockStart;
var blockEnd = groupsSort[group].blockEnd;
 
// checkAdd for coloredmoved block and move directionstart
var moveDircolor = nullgroupsSort[group].color;
if ( color !== null ) {
var type;
var groupUnSort = blocks[blockStart].group;
if ( groupsSort[group].movedFrom < groupUnSortblocks[ blockStart ].group ) {
moveDirtype = 'left(<';
}
else {
moveDirtype = 'right(>';
}
fragments.push( {
text: '',
type: type,
color: color
} );
}
 
// addCycle coloredthrough block start markupblocks
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if (version != 'old') {
var htmltype = ''blocks[block].type;
 
if (moveDir == 'left') {
// Add '=' unchanged text and moved block
html = this.htmlCustomize(wDiff.htmlBlockLeftStart, color);
if ( type === '=' || type === '-' || type === '+' ) {
fragments.push( {
text: blocks[block].text,
type: type,
color: color
} );
}
 
else if (moveDir == 'right') {
// Add '<' and '>' marks
html = this.htmlCustomize(wDiff.htmlBlockRightStart, color);
else if ( type === '|' ) {
var movedGroup = groups[ blocks[block].moved ];
 
// Get mark text
var markText = '';
for (
var movedBlock = movedGroup.blockStart;
movedBlock <= movedGroup.blockEnd;
movedBlock ++
) {
if ( blocks[movedBlock].type === '=' || blocks[movedBlock].type === '-' ) {
markText += blocks[movedBlock].text;
}
}
 
// Get mark direction
var markType;
if ( movedGroup.blockStart < blockStart ) {
markType = '<';
}
else {
markType = '>';
}
 
// Add mark
fragments.push( {
text: markText,
type: markType,
color: movedGroup.color
} );
}
htmlFrags.push(html);
}
 
// cycleAdd throughmoved blocksblock end
forif (var blockcolor != blockStart; block <= blockEnd; blocknull ++) {
fragments.push( {
var html = '';
text: '',
var type = blocks[block].type;
type: ' )',
var string = blocks[block].string;
color: color
} );
}
}
 
// Cycle through fragments, join consecutive fragments of same type (i.e. '-' blocks)
// html escape text string
var fragmentsLength = fragments.length;
string = this.htmlEscape(string);
for ( var fragment = 1; fragment < fragmentsLength; fragment ++ ) {
 
// Check if joinable
// add 'same' (unchanged) text and moved block
if (type == 'same') {
fragments[fragment].type === fragments[fragment - 1].type &&
if (color !== null) {
fragments[fragment].color === fragments[fragment - 1].color &&
if (version != 'old') {
fragments[fragment].text !== '' && fragments[fragment - 1].text !== ''
html = this.htmlFormatBlock(string);
) {
 
// Join and splice
fragments[fragment - 1].text += fragments[fragment].text;
fragments.splice( fragment, 1 );
fragment --;
}
}
 
// Enclose in containers
fragments.unshift( { text: '', type: '{', color: null }, { text: '', type: '[', color: null } );
fragments.push( { text: '', type: ']', color: null }, { text: '', type: '}', color: null } );
 
return;
};
 
 
/**
* Clip unchanged sections from unmoved block text.
* Adds the following fagment types:
* '~', ' ~', '~ ' omission indicators
* '[', ']', ',' fragment start and end, fragment separator
*
* @param[in/out] array fragments Fragments array, abstraction layer for diff code
*/
this.clipDiffFragments = function () {
 
var fragments = this.fragments;
 
// Skip if only one fragment in containers, no change
if ( fragments.length === 5 ) {
return;
}
 
// Min length for clipping right
var minRight = this.config.clipHeadingRight;
if ( this.config.clipParagraphRightMin < minRight ) {
minRight = this.config.clipParagraphRightMin;
}
if ( this.config.clipLineRightMin < minRight ) {
minRight = this.config.clipLineRightMin;
}
if ( this.config.clipBlankRightMin < minRight ) {
minRight = this.config.clipBlankRightMin;
}
if ( this.config.clipCharsRight < minRight ) {
minRight = this.config.clipCharsRight;
}
 
// Min length for clipping left
var minLeft = this.config.clipHeadingLeft;
if ( this.config.clipParagraphLeftMin < minLeft ) {
minLeft = this.config.clipParagraphLeftMin;
}
if ( this.config.clipLineLeftMin < minLeft ) {
minLeft = this.config.clipLineLeftMin;
}
if ( this.config.clipBlankLeftMin < minLeft ) {
minLeft = this.config.clipBlankLeftMin;
}
if ( this.config.clipCharsLeft < minLeft ) {
minLeft = this.config.clipCharsLeft;
}
 
// 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;
var color = fragments[fragment].color;
if ( type !== '=' || color !== null ) {
continue;
}
 
// Skip if too short for clipping
var text = fragments[fragment].text;
var textLength = text.length;
if ( textLength < minRight && textLength < minLeft ) {
continue;
}
 
// Get line positions including start and end
var lines = [];
var lastIndex = null;
var regExpMatch;
while ( ( regExpMatch = this.config.regExp.clipLine.exec( text ) ) !== null ) {
lines.push( regExpMatch.index );
lastIndex = this.config.regExp.clipLine.lastIndex;
}
if ( lines[0] !== 0 ) {
lines.unshift( 0 );
}
if ( lastIndex !== textLength ) {
lines.push( textLength );
}
 
// 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 = [];
var lastIndex = null;
while ( ( regExpMatch = this.config.regExp.clipParagraph.exec( text ) ) !== null ) {
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
var rangeRight = null;
var rangeLeft = null;
var rangeRightType = '';
var rangeLeftType = '';
 
// Find clip pos from left, skip for first non-container block
if ( fragment !== 2 ) {
 
// Maximum lines to search from left
var rangeLeftMax = textLength;
if ( this.config.clipLinesLeftMax < lines.length ) {
rangeLeftMax = lines[this.config.clipLinesLeftMax];
}
 
// Find first heading from left
if ( rangeLeft === null ) {
var headingsLength = headingsEnd.length;
for ( var j = 0; j < headingsLength; j ++ ) {
if ( headingsEnd[j] > this.config.clipHeadingLeft || headingsEnd[j] > rangeLeftMax ) {
break;
}
rangeLeft = headingsEnd[j];
rangeLeftType = 'heading';
break;
}
else {}
 
html = string;
// Find first paragraph from left
if ( rangeLeft === null ) {
var paragraphsLength = paragraphs.length;
for ( var j = 0; j < paragraphsLength; j ++ ) {
if (
paragraphs[j] > this.config.clipParagraphLeftMax ||
paragraphs[j] > rangeLeftMax
) {
break;
}
if ( paragraphs[j] > this.config.clipParagraphLeftMin ) {
rangeLeft = paragraphs[j];
rangeLeftType = 'paragraph';
break;
}
}
}
 
// addFind 'del'first textline &&break (blocks[block].fixedfrom == true)left
else if ( (typerangeLeft == 'del') && (version != 'new')null ) {
var linesLength = lines.length;
 
// for old( var j = 0; j version< skiplinesLength; 'del'j inside++ moved) group{
if ( (versionlines[j] !=> 'old')this.config.clipLineLeftMax || (colorlines[j] ===> null)rangeLeftMax ) {
break;
if (wDiff.regExpBlankBlock.test(string) === true) {
html = wDiff.htmlDeleteStartBlank;
}
if ( lines[j] > this.config.clipLineLeftMin ) {
else {
htmlrangeLeft = wDiff.htmlDeleteStartlines[j];
rangeLeftType = 'line';
break;
}
html += this.htmlFormatBlock(string) + wDiff.htmlDeleteEnd;
}
}
 
// addFind 'ins'first textblank from left
else if ( (typerangeLeft == 'ins') && (version != 'old')null ) {
this.config.regExp.clipBlank.lastIndex = this.config.clipBlankLeftMin;
if (wDiff.regExpBlankBlock.test(string) === true) {
if ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
html = wDiff.htmlInsertStartBlank;
if (
regExpMatch.index < this.config.clipBlankLeftMax &&
regExpMatch.index < rangeLeftMax
) {
rangeLeft = regExpMatch.index;
rangeLeftType = 'blank';
}
}
else {}
 
html = wDiff.htmlInsertStart;
// Fixed number of chars from left
if ( rangeLeft === null ) {
if ( this.config.clipCharsLeft < rangeLeftMax ) {
rangeLeft = this.config.clipCharsLeft;
rangeLeftType = 'chars';
}
html += this.htmlFormatBlock(string) + wDiff.htmlInsertEnd;
}
 
// addFixed 'mark'number codeof lines from left
else if ( (typerangeLeft == 'mark') && (version != 'new')null ) {
var movedrangeLeft = blocks[block].movedrangeLeftMax;
var movedGrouprangeLeftType = groups[moved]'fixed';
}
var markColor = movedGroup.color;
}
 
// Find clip pos from right, skip for last non-container block
if ( fragment !== fragments.length - 3 ) {
 
// getMaximum movedlines blockto textsearch ('same'from and 'del')right
var stringrangeRightMin = ''0;
if ( lines.length >= this.config.clipLinesRightMax ) {
for (var mark = movedGroup.blockStart; mark <= movedGroup.blockEnd; mark ++) {
rangeRightMin = lines[lines.length - this.config.clipLinesRightMax];
if ( (blocks[mark].type == 'same') || (blocks[mark].type == 'del') ) {
}
string += blocks[mark].string;
 
// Find last heading from right
if ( rangeRight === null ) {
for ( var j = headings.length - 1; j >= 0; j -- ) {
if (
headings[j] < textLength - this.config.clipHeadingRight ||
headings[j] < rangeRightMin
) {
break;
}
rangeRight = headings[j];
rangeRightType = 'heading';
break;
}
}
 
// displayFind aslast deletionparagraph atfrom original positionright
if ( (wDiff.showBlockMovesrangeRight === false) || (version == 'old')null ) {
for ( var j = paragraphs.length - 1; j >= 0 ; j -- ) {
string = this.htmlEscape(string);
if (
string = this.htmlFormatBlock(string);
paragraphs[j] < textLength - this.config.clipParagraphRightMax ||
if (version == 'old') {
paragraphs[j] < rangeRightMin
if (movedGroup.blockStart < groupsSort[group].blockStart) {
) {
html = this.htmlCustomize(wDiff.htmlBlockLeftStart, markColor) + string + wDiff.htmlBlockLeftEnd;
}break;
else {
html = this.htmlCustomize(wDiff.htmlBlockRightStart, markColor) + string + wDiff.htmlBlockRightEnd;
}
}
if ( paragraphs[j] < textLength - this.config.clipParagraphRightMin ) {
else {
rangeRight = paragraphs[j];
if (wDiff.regExpBlankBlock.test(string) === true) {
rangeRightType = 'paragraph';
html = wDiff.htmlDeleteStartBlank + string + wDiff.htmlDeleteEnd;
}break;
else {
html = wDiff.htmlDeleteStart + string + wDiff.htmlDeleteEnd;
}
}
}
}
 
// displayFind aslast mark,line getbreak markfrom directionright
if ( rangeRight === null ) {
else {
for ( var j = lines.length - 1; j >= 0; j -- ) {
if (movedGroup.blockStart < groupsSort[group].blockStart) {
if (
html = this.htmlCustomize(wDiff.htmlMarkLeft, markColor, string);
lines[j] < textLength - this.config.clipLineRightMax ||
lines[j] < rangeRightMin
) {
break;
}
if ( lines[j] < textLength - this.config.clipLineRightMin ) {
else {
rangeRight = lines[j];
html = this.htmlCustomize(wDiff.htmlMarkRight, markColor, string);
rangeRightType = 'line';
break;
}
}
}
htmlFrags.push(html);
}
 
// addFind coloredlast blockblank endfrom markupright
if (version !rangeRight === null 'old') {
var startPos = textLength - this.config.clipBlankRightMax;
var html = '';
if (moveDir ==startPos < rangeRightMin 'left') {
html startPos = wDiff.htmlBlockLeftEndrangeRightMin;
}
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;
}
}
 
else if (moveDir == 'right') {
// Fixed number of chars from right
html = wDiff.htmlBlockRightEnd;
if ( rangeRight === null ) {
if ( textLength - this.config.clipCharsRight > rangeRightMin ) {
rangeRight = textLength - this.config.clipCharsRight;
rangeRightType = 'chars';
}
}
 
// Fixed number of lines from right
if ( rangeRight === null ) {
rangeRight = rangeRightMin;
rangeRightType = 'fixed';
}
htmlFrags.push(html);
}
}
 
// Check if we skip clipping if ranges are close together
// join fragments
if ( rangeLeft !== null && rangeRight !== null ) {
this.html = htmlFrags.join('');
 
// Skip if overlapping ranges
// markup newlines and spaces in blocks
if ( rangeLeft > rangeRight ) {
this.htmlFormat();
continue;
}
 
// Skip if chars too close
return;
var skipChars = rangeRight - rangeLeft;
};
if ( skipChars < this.config.clipSkipChars ) {
 
continue;
 
}
//
// TextDiff.htmlCustomize(): customize move indicator html: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
// input: text (html or css code), number: block number, title: title attribute (popup) text
// returns: customized text
// called from: .assembleDiff()
 
this.htmlCustomize = function (text, number, title) {
 
// Skip if lines too close
if (wDiff.coloredBlocks === true) {
var blockStyleskipLines = wDiff.styleBlockColor[number]0;
var linesLength = lines.length;
if (blockStyle === undefined) {
for ( var j = 0; j < linesLength; j ++ ) {
blockStyle = '';
if ( lines[j] > rangeRight || skipLines > this.config.clipSkipLines ) {
break;
}
if ( lines[j] > rangeLeft ) {
skipLines ++;
}
}
if ( skipLines < this.config.clipSkipLines ) {
continue;
}
}
var markStyle = wDiff.styleMarkColor[number];
if (markStyle === undefined) {
markStyle = '';
}
text = text.replace(/\{block\}/g, ' ' + blockStyle);
text = text.replace(/\{mark\}/g, ' ' + markStyle);
text = text.replace(/\{class\}/g, number);
}
else {
text = text.replace(/\{block\}|\{mark\}|\{class\}/g, '');
}
text = text.replace(/\{number\}/g, number);
 
// shortenSkip titleif text,nothing replaceto {title}clip
if ( (titlerangeLeft !=== undefined)null && (titlerangeRight !=== '')null ) {
var max = 512 continue;
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;');
text = text.replace(/\{title\}/, ' title="' + title + '"');
}
else {
text = text.replace(/\{title\}/, '');
}
return text;
};
 
// Split left text
var textLeft = null;
var omittedLeft = null;
if ( rangeLeft !== null ) {
textLeft = text.slice( 0, rangeLeft );
 
// Remove trailing empty lines
// TextDiff.htmlEscape(): replace html-sensitive characters in output text with character entities
textLeft = textLeft.replace( this.config.regExp.clipTrimNewLinesLeft, '' );
// input: html text
// returns: escaped html text
// called from: .diff(), .assembleDiff()
 
// Get omission indicators, remove trailing blanks
this.htmlEscape = function (html) {
if ( rangeLeftType === 'chars' ) {
omittedLeft = '~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
else if ( rangeLeftType === 'blank' ) {
omittedLeft = ' ~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
}
 
// Split right text
html = html.replace(/&/g, '&amp;');
var textRight = null;
html = html.replace(/</g, '&lt;');
var omittedRight = null;
html = html.replace(/>/g, '&gt;');
if ( rangeRight !== null ) {
html = html.replace(/"/g, '&quot;');
textRight = text.slice( rangeRight );
return (html);
};
 
// Remove leading empty lines
textRight = textRight.replace( this.config.regExp.clipTrimNewLinesRight, '' );
 
// Get omission indicators, remove leading blanks
// TextDiff.htmlFormatBlock(): markup newlines and spaces in blocks
if ( rangeRightType === 'chars' ) {
// input: string
omittedRight = '~';
// returns: formatted string
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
// called from: .diff(), .assembleDiff()
}
else if ( rangeRightType === 'blank' ) {
omittedRight = '~ ';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
}
 
// Remove split element
this.htmlFormatBlock = function (string) {
fragments.splice( fragment, 1 );
fragmentsLength --;
 
// Add left text to fragments list
// spare blanks in tags
if ( rangeLeft !== null ) {
string = string.replace(/(<[^>]*>)|( )/g, function (p, p1, p2) {
fragments.splice( fragment ++, 0, { text: textLeft, type: '=', color: null } );
if (p2 == ' ') {
fragmentsLength ++;
return wDiff.htmlSpace;
if ( omittedLeft !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedLeft, color: null } );
fragmentsLength ++;
}
}
return p1;
});
string = string.replace(/\n/g, wDiff.htmlNewline);
return string;
};
 
// Add fragment container and separator to list
if ( rangeLeft !== null && rangeRight !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: ']', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: ',', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: '[', color: null } );
fragmentsLength += 3;
}
 
// Add right text to fragments list
// TextDiff.htmlFormat(): markup tabs, add container
if ( rangeRight !== null ) {
// changes: .diff
if ( omittedRight !== null ) {
// called from: .diff(), .assembleDiff()
fragments.splice( fragment ++, 0, { text: '', type: omittedRight, color: null } );
fragmentsLength ++;
}
fragments.splice( fragment ++, 0, { text: textRight, type: '=', color: null } );
fragmentsLength ++;
}
}
 
// Debug log
this.htmlFormat = function () {
if ( this.config.debug === true ) {
this.debugFragments( 'Fragments' );
}
 
this.html = this.html.replace(/\t/g, wDiff.htmlTab);
this.html = wDiff.htmlContainerStart + wDiff.htmlFragmentStart + this.html + wDiff.htmlFragmentEnd + wDiff.htmlContainerEnd;
return;
};
 
 
/**
// TextDiff.shortenOutput(): shorten diff html by removing unchanged sections
// input:* diffCreate html stringformatted diff code from .diff() fragments.
*
// returns: shortened html with removed unchanged passages indicated by (...) or separator
* @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;
this.shortenOutput = function () {
 
// No change, only one unchanged block in containers
var html = this.html;
if ( fragments.length === 5 && fragments[2].type === '=' ) {
var diff = '';
this.html = '';
return;
}
 
// Cycle through fragments
// remove container by non-regExp replace
var htmlFragments = [];
html = html.replace(wDiff.htmlContainerStart, '');
var fragmentsLength = fragments.length;
html = html.replace(wDiff.htmlFragmentStart, '');
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
html = html.replace(wDiff.htmlFragmentEnd, '');
var text = fragments[fragment].text;
html = html.replace(wDiff.htmlContainerEnd, '');
var type = fragments[fragment].type;
var color = fragments[fragment].color;
var html = '';
 
// Test if text is blanks-only or a single character
// scan for diff html tags
var blank = false;
var regExpDiff = /<\w+\b[^>]*\bclass="[^">]*?\bwDiff(MarkLeft|MarkRight|BlockLeft|BlockRight|Delete|Insert)\b[^">]*"[^>]*>(.|\n)*?<!--wDiff\1-->/g;
if ( text !== '' ) {
var tagsStart = [];
blank = this.config.regExp.blankBlock.test( text );
var tagsEnd = [];
}
var i = 0;
 
// saveAdd tagcontainer positionsstart markup
if ( type === '{' ) {
var regExpMatch;
html = this.config.htmlCode.containerStart;
while ( (regExpMatch = regExpDiff.exec(html)) !== null ) {
}
 
// combineAdd consecutivecontainer diffend tagsmarkup
else if ( (i > 0) && (tagsEnd[i - 1]type === regExpMatch.index)'}' ) {
html = this.config.htmlCode.containerEnd;
tagsEnd[i - 1] = regExpMatch.index + regExpMatch[0].length;
}
 
else {
// Add fragment start markup
tagsStart[i] = regExpMatch.index;
if ( type === '[' ) {
tagsEnd[i] = regExpMatch.index + regExpMatch[0].length;
html = this.config.htmlCode.fragmentStart;
i ++;
}
}
 
// noAdd difffragment tagsend detectedmarkup
else if (tagsStart.length type === 0']' ) {
this. html = wDiffthis.config.htmlCode.htmlNoChangefragmentEnd;
return;}
}
 
// Add fragment separator markup
// define regexps
else if ( type === ',' ) {
var regExpLine = /^(\n+|.)|(\n+|.)$|\n+/g;
html = this.config.htmlCode.separator;
var regExpHeading = /(^|\n)(<[^>]+>)*(==+.+?==+|\{\||\|\}).*?\n?/g;
}
var regExpParagraph = /^(\n\n+|.)|(\n\n+|.)$|\n\n+/g;
var regExpBlank = /(<[^>]+>)*\s+/g;
 
// getAdd lineomission positionsmarkup
var if lines( type === '~' ) [];{
html = this.config.htmlCode.omittedChars;
var regExpMatch;
}
while ( (regExpMatch = regExpLine.exec(html)) !== null) {
lines.push(regExpMatch.index);
}
 
// getAdd headingomission positionsmarkup
var if headings( type === ' ~' ) [];{
html = ' ' + this.config.htmlCode.omittedChars;
var headingsEnd = [];
}
while ( (regExpMatch = regExpHeading.exec(html)) !== null ) {
headings.push(regExpMatch.index);
headingsEnd.push(regExpMatch.index + regExpMatch[0].length);
}
 
// getAdd paragraphomission positionsmarkup
if ( type === '~ ' ) {
var paragraphs = [];
html = this.config.htmlCode.omittedChars + ' ';
while ( (regExpMatch = regExpParagraph.exec(html)) !== null ) {
}
paragraphs.push(regExpMatch.index);
}
 
// Add colored left-pointing block start markup
// determine fragment border positions around diff tags
else if ( type === '(<' ) {
var lineMaxBefore = 0;
if ( version !== 'old' ) {
var headingBefore = 0;
var paragraphBefore = 0;
var lineBefore = 0;
 
// Get title
var lineMaxAfter = 0;
var headingAfter = 0title;
if ( this.config.noUnicodeSymbols === true ) {
var paragraphAfter = 0;
title = this.config.msg['wiked-diff-block-left-nounicode'];
var lineAfter = 0;
}
else {
title = this.config.msg['wiked-diff-block-left'];
}
 
// Get html
var rangeStart = [];
if ( this.config.coloredBlocks === true ) {
var rangeEnd = [];
html = this.config.htmlCode.blockColoredStart;
var rangeStartType = [];
}
var rangeEndType = [];
else {
 
html = this.config.htmlCode.blockStart;
// cycle through diff tag start positions
for (var i = 0; i < tagsStart.length; i ++) {
var tagStart = tagsStart[i];
var tagEnd = tagsEnd[i];
 
// maximal lines to search before diff tag
var rangeStartMin = 0;
for (var j = lineMaxBefore; j < lines.length - 1; j ++) {
if (tagStart < lines[j + 1]) {
if (j - wDiff.linesBeforeMax >= 0) {
rangeStartMin = lines[j - wDiff.linesBeforeMax];
}
html = this.htmlCustomize( html, color, title );
lineMaxBefore = j;
break;
}
}
 
// findAdd lastcolored headingright-pointing beforeblock diffstart tagmarkup
else if (rangeStart[i] type === undefined'(>' ) {
if ( version !== 'old' ) {
for (var j = headingBefore; j < headings.length - 1; j ++) {
 
if (headings[j] > tagStart) {
break;// Get title
var title;
if ( this.config.noUnicodeSymbols === true ) {
title = this.config.msg['wiked-diff-block-right-nounicode'];
}
else {
if (headings[j + 1] > tagStart) {
title = this.config.msg['wiked-diff-block-right'];
if ( (headings[j] > tagStart - wDiff.headingBefore) && (headings[j] > rangeStartMin) ) {
rangeStart[i] = headings[j];
rangeStartType[i] = 'heading';
headingBefore = j;
}
break;
}
}
}
 
// Get html
// find last paragraph before diff tag
if (rangeStart[i] this.config.coloredBlocks === undefinedtrue ) {
html = this.config.htmlCode.blockColoredStart;
for (var j = paragraphBefore; j < paragraphs.length - 1; j ++) {
if (paragraphs[j] > tagStart) {
break;
}
else {
if (paragraphs[j + 1] > tagStart - wDiff.paragraphBeforeMin) {
html = this.config.htmlCode.blockStart;
if ( (paragraphs[j] > tagStart - wDiff.paragraphBeforeMax) && (paragraphs[j] > rangeStartMin) ) {
rangeStart[i] = paragraphs[j];
rangeStartType[i] = 'paragraph';
paragraphBefore = j;
}
break;
}
html = this.htmlCustomize( html, color, title );
}
}
 
// findAdd lastcolored lineblock breakend before diff tagmarkup
else if (rangeStart[i] type === undefined' )' ) {
forif (var jversion !== lineBefore; j < lines.length - 1; j'old' ++) {
html = this.config.htmlCode.blockEnd;
if (lines[j + 1] > tagStart - wDiff.lineBeforeMin) {
if ( (lines[j] > tagStart - wDiff.lineBeforeMax) && (lines[j] > rangeStartMin) ) {
rangeStart[i] = lines[j];
rangeStartType[i] = 'line';
lineBefore = j;
}
break;
}
}
}
 
// findAdd last'=' blank(unchanged) beforetext diffand tagmoved block
if (rangeStart[i] type === undefined'=' ) {
text = this.htmlEscape( text );
var lastPos = tagStart - wDiff.blankBeforeMax;
if (lastPos <color !== null rangeStartMin) {
if ( version !== 'old' ) {
lastPos = rangeStartMin;
html = this.markupBlanks( text, true );
}
regExpBlank.lastIndex = lastPos;
while ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
if (regExpMatch.index > tagStart - wDiff.blankBeforeMin) {
rangeStart[i] = lastPos;
rangeStartType[i] = 'blank';
break;
}
lastPos = regExpMatch.index;
}
} else {
html = this.markupBlanks( text );
 
// fixed number of chars before diff tag
if (rangeStart[i] === undefined) {
if (tagStart - wDiff.charsBefore > rangeStartMin) {
rangeStart[i] = tagStart - wDiff.charsBefore;
rangeStartType[i] = 'chars';
}
}
 
// Add '-' text
// fixed number of lines before diff tag
else if (rangeStart[i] type === undefined'-' ) {
if ( version !== 'new' ) {
rangeStart[i] = rangeStartMin;
rangeStartType[i] = 'lines';
}
 
// For old version skip '-' inside moved group
// maximal lines to search after diff tag
if ( version !== 'old' || color === null ) {
var rangeEndMax = html.length;
text = this.htmlEscape( text );
for (var j = lineMaxAfter; j < lines.length; j ++) {
text = this.markupBlanks( text, true );
if (lines[j] > tagEnd) {
if (j +blank wDiff.linesAfterMax=== <true lines.length) {
html = this.config.htmlCode.deleteStartBlank;
rangeEndMax = lines[j + wDiff.linesAfterMax];
}
else {
html = this.config.htmlCode.deleteStart;
}
html += text + this.config.htmlCode.deleteEnd;
}
lineMaxAfter = j;
break;
}
}
 
// findAdd first'+' heading after diff tagtext
else if (rangeEnd[i] type === undefined'+' ) {
if ( version !== 'old' ) {
for (var j = headingAfter; j < headingsEnd.length; j ++) {
text = this.htmlEscape( text );
if (headingsEnd[j] > tagEnd) {
text = this.markupBlanks( text, true );
if ( (headingsEnd[j] < tagEnd + wDiff.headingAfter) && (headingsEnd[j] < rangeEndMax) ) {
if ( blank === true ) {
rangeEnd[i] = headingsEnd[j];
html = this.config.htmlCode.insertStartBlank;
rangeEndType[i] = 'heading';
paragraphAfter = j;
}
break;
}
else {
html = this.config.htmlCode.insertStart;
}
html += text + this.config.htmlCode.insertEnd;
}
}
 
// findAdd first'<' paragraphand after'>' diff tagcode
else if (rangeEnd[i] type === undefined'<' || type === '>' ) {
if ( version !== 'new' ) {
for (var j = paragraphAfter; j < paragraphs.length; j ++) {
 
if (paragraphs[j] > tagEnd + wDiff.paragraphAfterMin) {
// Display as deletion at original position
if ( (paragraphs[j] < tagEnd + wDiff.paragraphAfterMax) && (paragraphs[j] < rangeEndMax) ) {
if ( this.config.showBlockMoves === false || version === 'old' ) {
rangeEnd[i] = paragraphs[j];
rangeEndType[i]text = 'paragraph'this.htmlEscape( text );
text = this.markupBlanks( text, true );
paragraphAfter = j;
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;
}
}
break;
}
}
}
 
// Display as mark
// find first line break after diff tag
else {
if (rangeEnd[i] === undefined) {
for if (var jtype === lineAfter; j '<' lines.length; j ++) {
if (lines[j] >this.config.coloredBlocks tagEnd=== +true wDiff.lineAfterMin) {
html = this.htmlCustomize( this.config.htmlCode.markLeftColored, color, text );
if ( (lines[j] < tagEnd + wDiff.lineAfterMax) && (lines[j] < rangeEndMax) ) {
rangeEnd[i] = lines[j];}
rangeEndType[i]else = 'line';{
html = this.htmlCustomize( this.config.htmlCode.markLeft, color, text );
lineAfter = j;
}
}
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 );
}
}
break;
}
}
}
htmlFragments.push( html );
}
 
// Join fragments
// find blank after diff tag
this.html = htmlFragments.join( '' );
if (rangeEnd[i] === undefined) {
 
regExpBlank.lastIndex = tagEnd + wDiff.blankAfterMin;
return;
if ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
};
if ( (regExpMatch.index < tagEnd + wDiff.blankAfterMax) && (regExpMatch.index < rangeEndMax) ) {
 
rangeEnd[i] = regExpMatch.index;
 
rangeEndType[i] = 'blank';
/**
}
* 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 chars after diff tag
/**
if (rangeEnd[i] === undefined) {
* Replace html-sensitive characters in output text with character entities.
if (tagEnd + wDiff.charsAfter < rangeEndMax) {
*
rangeEnd[i] = tagEnd + wDiff.charsAfter;
* @param string html Html code to be escaped
rangeEndType[i] = 'chars';
* @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;
};
 
 
/**
* Count real words in text.
*
* @param string text Text for word counting
* @return int Number of words in text
*/
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
this.getDiffHtml( 'old' );
var diff = this.html.replace( /<[^>]*>/g, '');
var text = this.htmlEscape( this.oldText.text );
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with old text version!'
);
this.error = true;
console.log( 'old text:\n', text );
console.log( 'old diff:\n', diff );
}
else {
console.log( 'OK: wikEdDiff unit test passed: diff consistent with old text.' );
}
 
return;
};
 
 
/**
* Dump blocks object to browser console.
*
* @param string name Block name
* @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' );
}
}
return diff;
};
 
 
/**
* 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 ) {
 
// fixedSubtract numbertimes ofspent linesin afterdeeper diff tagrecursions
var timerEnd = this.recursionTimer.length - 1;
if (rangeEnd[i] === undefined) {
for ( var i = 0; i < timerEnd; i ++ ) {
rangeEnd[i] = rangeEndMax;
this.recursionTimer[i] -= this.recursionTimer[i + 1];
rangeEndType[i] = 'lines';
}
 
// 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 = [];
return;
};
 
// 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
* Log variable values to debug console.
var lines = 0;
* Usage: this.debug( 'var', var );
if (fragmentEnd[j - 1] < rangeStart[i]) {
*
var join = html.substring(fragmentEnd[j - 1], rangeStart[i]);
* @param string name Object identifier
lines = (join.match(/\n/g) || []).length;
* @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 {
script.textContent = code;
}
document.getElementsByTagName( 'head' )[0].appendChild( script );
}
return;
};
 
 
/**
* Add stylesheet to document head, cross-browser >= IE6.
*
* @param string css CSS code
*/
this.addStyleSheet = function ( css ) {
 
if ( document.getElementById( 'wikEdDiffStyles' ) === null ) {
 
// Replace mark symbols
css = css.replace( /\{cssMarkLeft\}/g, this.config.cssMarkLeft);
css = css.replace( /\{cssMarkRight\}/g, this.config.cssMarkRight);
 
var style = document.createElement( 'style' );
if ( (rangeStart[i] > fragmentEnd[j - 1] + wDiff.fragmentJoinChars) || (lines > wDiff.fragmentJoinLines) ) {
style.id = 'wikEdDiffStyles';
fragmentStart[j] = rangeStart[i];
fragmentEnd[j]style.type = rangeEnd[i]'text/css';
if ( style.styleSheet !== undefined ) {
fragmentStartType[j] = rangeStartType[i];
style.styleSheet.cssText = css;
fragmentEndType[j] = rangeEndType[i];
j ++;
}
else {
style.appendChild( document.createTextNode( css ) );
fragmentEnd[j - 1] = rangeEnd[i];
fragmentEndType[j - 1] = rangeEndType[i];
}
document.getElementsByTagName( 'head' )[0].appendChild( style );
}
return;
};
 
// assemble the fragments
for (var i = 0; i < fragmentStart.length; i ++) {
 
/**
// get text fragment
* Recursive deep copy from target over source for customization import.
var fragment = html.substring(fragmentStart[i], fragmentEnd[i]);
*
fragment = fragment.replace(/^\n+|\n+$/g, '');
* @param object source Source object
* @param object target Target object
*/
this.deepCopy = function ( source, target ) {
 
for ( var key in source ) {
// add inline marks for omitted chars and words
if ( Object.prototype.hasOwnProperty.call( source, key ) === true ) {
if (fragmentStart[i] > 0) {
if (fragmentStartType typeof source[ikey] === 'charsobject' ) {
this.deepCopy( source[key], target[key] );
fragment = wDiff.htmlOmittedChars + fragment;
}
else if (fragmentStartType[i] == 'blank') {
target[key] = source[key];
fragment = wDiff.htmlOmittedChars + ' ' + fragment;
}
}
if (fragmentEnd[i] < html.length) {
if (fragmentStartType[i] == 'chars') {
fragment = fragment + wDiff.htmlOmittedChars;
}
else if (fragmentStartType[i] == 'blank') {
fragment = fragment + ' ' + wDiff.htmlOmittedChars;
}
}
}
return;
};
 
// Initialze WikEdDiff object
// remove leading and trailing empty lines
this.init();
fragment = fragment.replace(/^\n+|\n+$/g, '');
};
 
// add fragment separator
if (i > 0) {
diff += wDiff.htmlSeparator;
}
 
/**
// add fragment wrapper
* Data and methods for single text version (old or new one).
diff += wDiff.htmlFragmentStart + fragment + wDiff.htmlFragmentEnd;
*
}
* @class WikEdDiffText
*/
WikEdDiff.WikEdDiffText = function ( text, parent ) {
 
/** @var WikEdDiff parent Parent object for configuration settings and debugging methods */
// add diff wrapper
this.parent = parent;
diff = wDiff.htmlContainerStart + diff + wDiff.htmlContainerEnd;
 
/** @var string text Text of this version */
this.html = diff;
this.text = null;
return;
};
 
/** @var array tokens Tokens list */
this.tokens = [];
 
/** @var int first, last First and last index of tokens list */
// wDiff.wordCount(): count words in string
this.first = null;
// called from: .getGroups(), .getSameBlocks()
this.last = null;
//
 
/** @var array words Word counts for version text */
this.wordCount = function (string) {
this.words = {};
 
return (string.match(wDiff.regExpWord) || []).length;
};
 
/**
* Constructor, initialize text object.
*
* @param string text Text of version
* @param WikEdDiff parent Parent, for configuration settings and debugging methods
*/
this.init = function () {
 
if ( typeof text !== 'string' ) {
// TextDiff.debugBlocks(): dump blocks object for debugging
text = text.toString();
// input: text: title, group: block object (optional)
}
//
 
// IE / Mac fix
this.debugBlocks = function (text, blocks) {
this.text = text.replace( /\r\n?/g, '\n');
 
// Parse and count words and chunks for identification of unique real words
if (blocks === undefined) {
if ( this.parent.config.timer === true ) {
blocks = this.blocks;
this.parent.time( 'wordParse' );
}
this.wordParse( this.parent.config.regExp.countWords );
var dump = '\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq \twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \tstring\n';
this.wordParse( this.parent.config.regExp.countChunks );
for (var i = 0; i < blocks.length; i ++) {
if ( this.parent.config.timer === true ) {
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.debugShortenString(blocks[i].string) + '\n';
this.parent.timeEnd( 'wordParse' );
}
return;
console.log(text + ':\n' + dump);
};
 
 
/**
// TextDiff.debugGroups(): dump groups object for debugging
* Parse and count words and chunks for identification of unique words.
// input: text: title, group: group object (optional)
*
//
* @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 );
this.debugGroups = function (text, groups) {
if ( regExpMatch !== null ) {
var matchLength = regExpMatch.length;
for (var i = 0; i < matchLength; i ++) {
var word = regExpMatch[i];
if ( Object.prototype.hasOwnProperty.call( this.words, word ) === false ) {
this.words[word] = 1;
}
else {
this.words[word] ++;
}
}
}
return;
};
 
 
if (groups === undefined) {
/**
groups = this.groups;
* Split text into paragraph, line, sentence, chunk, word, or character tokens.
*
* @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;
var next = null;
var current = this.tokens.length;
var first = current;
var text = '';
 
// Split full text or specified token
if ( token === undefined ) {
text = this.text;
}
else {
var dump = '\ni \toldNm \tblSta \tblEnd \tuniq \tmaxWo \twords \tchars \tfixed \toldNm \tmFrom \tcolor\n';
prev = this.tokens[token].prev;
for (var i = 0; i < groups.length; i ++) {
next = this.tokens[token].next;
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';
text = this.tokens[token].token;
}
console.log(text + ':\n' + dump);
};
 
// Split text into tokens, regExp match as separator
var number = 0;
var split = [];
var regExpMatch;
var lastIndex = 0;
var regExp = this.parent.config.regExp.split[level];
while ( ( regExpMatch = regExp.exec( text ) ) !== null ) {
if ( regExpMatch.index > lastIndex ) {
split.push( text.substring( lastIndex, regExpMatch.index ) );
}
split.push( regExpMatch[0] );
lastIndex = regExp.lastIndex;
}
if ( lastIndex < text.length ) {
split.push( text.substring( lastIndex ) );
}
 
// Cycle through new tokens
// TextDiff.debugShortenString(): shorten string for dumping
var splitLength = split.length;
// called from .debugBlocks, .debugGroups, Text.debugText
for ( var i = 0; i < splitLength; i ++ ) {
//
 
// Insert current item, link to previous
this.debugShortenString = function (string) {
this.tokens.push( {
token: split[i],
prev: prev,
next: null,
link: null,
number: null,
unique: false
} );
number ++;
 
// Link previous item to current
if (typeof string != 'string') {
if ( prev !== null ) {
string = string.toString();
this.tokens[prev].next = current;
}
prev = current;
current ++;
}
 
string = string.replace(/\n/g, '\\n');
// Connect last new item and existing next item
string = string.replace(/\t/g, ' ');
if ( number > 0 && token !== undefined ) {
var max = 100;
if (string.length >prev !== null max) {
this.tokens[prev].next = next;
string = string.substr(0, max - 1 - 30) + '…' + string.substr(string.length - 30);
}
if ( next !== null ) {
this.tokens[next].prev = prev;
}
}
 
return '"' + string + '"';
// Set text first and last token index
if ( number > 0 ) {
 
// Initial text split
if ( token === undefined ) {
this.first = 0;
this.last = prev;
}
 
// First or last token has been split
else {
if ( token === this.first ) {
this.first = first;
}
if ( token === this.last ) {
this.last = prev;
}
}
}
return;
};
 
 
/**
// initialze text diff object
* Split unique unmatched tokens into smaller tokens.
this.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
// wDiff.addScript(): add script to head
if ( this.tokens[i].link === null ) {
// called from: wDiff.init()
this.splitText( regExp, i );
//
}
i = this.tokens[i].next;
}
return;
};
 
wDiff.addScript = function (code) {
 
/**
var script = document.createElement('script');
* Enumerate text token list before detecting blocks.
script.id = 'wDiffBlockHandler';
*
if (script.innerText !== undefined) {
* @param[out] array tokens Tokens list
script.innerText = code;
*/
}
this.enumerateTokens = function () {
else {
script.textContent = code;
}
document.getElementsByTagName('head')[0].appendChild(script);
return;
};
 
// 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;
};
 
// wDiff.addStyleSheet(): add CSS rules to new style sheet, cross-browser >= IE6
// called from: wDiff.init()
//
 
/**
wDiff.addStyleSheet = function (css) {
* 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 style = document.createElement('style');
var dump = 'first: ' + this.first + '\tlast: ' + this.last + '\n';
style.type = 'text/css';
dump += '\ni \tlink \t(prev \tnext) \tuniq \t#num \t"token"\n';
if (style.styleSheet !== undefined) {
var i = this.first;
style.styleSheet.cssText = css;
while ( i !== null ) {
}
dump +=
else {
i + ' \t' + tokens[i].link + ' \t(' + tokens[i].prev + ' \t' + tokens[i].next + ') \t' +
style.appendChild( document.createTextNode(css) );
tokens[i].unique + ' \t#' + tokens[i].number + ' \t' +
}
parent.debugShortenText( tokens[i].token ) + '\n';
document.getElementsByTagName('head')[0].appendChild(style);
i = tokens[i].next;
return;
};
console.log( name + ':\n' + dump );
return;
};
 
 
// Initialize WikEdDiffText object
// initialize wDiff
wDiff this.init();
};
 
// </syntaxhighlight>