User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.1.6 (September 25, 2014) block hover css, rm debug
1.2.1 (October 09, 2014) update for synced one-to-one port with mediawiki:Extension:WikEdDiff
Line 3:
// ==UserScript==
// @name wikEd diff
// @version 1.12.60
// @date SeptemberOctober 2509, 2014
// @description improved word-based diff library with block move detection
// @homepage https://en.wikipedia.org/wiki/User:Cacycle/diff
Line 13:
 
/*
* 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, 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
* .html diff html
* .error flag: result has not passed unit tests
*
* symbols: object for symbols table data
* .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 matched 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 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
*
* .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
*
*/
 
Communications of the ACM 21(4):264 (1978)
http://doi.acm.org/10.1145/359460.359467
 
// JSHint options
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: true, 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 */
// show complete un-clipped diff text
this.config = {
if (wDiff.fullDiff === undefined) { wDiff.fullDiff = false; }
 
/** Core diff settings (with default values) */
// enable block move layout with highlighted blocks and marks at their original positions
if (wDiff.showBlockMoves === undefined) { wDiff.showBlockMoves = true; }
 
/**
// enable character-refined diff
* @var bool config.fullDiff
if (wDiff.charDiff === undefined) { wDiff.charDiff = true; }
* Show complete un-clipped diff text (false)
*/
'fullDiff': false,
 
/**
// enable recursive diff to resolve problematic sequences
* @var bool config.showBlockMoves
if (wDiff.recursiveDiff === undefined) { wDiff.recursiveDiff = true; }
* Enable block move layout with highlighted blocks and marks at their original positions (true)
*/
'showBlockMoves': true,
 
/**
// reject blocks if they are too short and their words are not unique
* @var bool config.charDiff
if (wDiff.unlinkBlocks === undefined) { wDiff.unlinkBlocks = true; }
* Enable character-refined diff (true)
*/
'charDiff': true,
 
/**
// reject blocks if shorter than this number of real words
* @var bool config.recursiveDiff
if (wDiff.blockMinLength === undefined) { wDiff.blockMinLength = 3; }
* Enable recursive diff to resolve problematic sequences (true)
*/
'recursiveDiff': true,
 
/**
// display blocks in differing colors (rainbow color scheme)
* @var int config.recursionMax
if (wDiff.coloredBlocks === undefined) { wDiff.coloredBlocks = false; }
* Maximum recursion depth (10)
*/
'recursionMax': 10,
 
/**
// show debug infos and stats (block and group data object) in browser console
* @var bool config.unlinkBlocks
if (wDiff.debug === undefined) { wDiff.debug = false; }
* Reject blocks if they are too short and their words are not unique,
* prevents fragmentated diffs for very different versions (true)
*/
'unlinkBlocks': true,
 
/**
// show timing results in browser console
* @var int config.unlinkMax
if (wDiff.timer === undefined) { wDiff.timer = false; }
* Maximum number of rejection cycles (5)
*/
'unlinkMax': 5,
 
/**
// run unit tests to prove correct working, display results in browser console
* @var int config.blockMinLength
if (wDiff.unitTesting === undefined) { wDiff.unitTesting = false; }
* Reject blocks if shorter than this number of real words (3)
*/
'blockMinLength': 3,
 
/**
// UniCode letter support for regexps, from http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
* @var bool config.coloredBlocks
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'); }
* Display blocks in differing colors (rainbow color scheme) (false)
*/
'coloredBlocks': false,
 
/**
// new line characters without and with '\n' and '\r'
* @var bool config.coloredBlocks
if (wDiff.newLines === undefined) { wDiff.newLines = '\\u0085\\u2028'; }
* Do not use UniCode block move marks (legacy browsers) (false)
if (wDiff.newLinesAll === undefined) { wDiff.newLinesAll = '\\n\\r\\u0085\\u2028'; }
*/
'noUnicodeSymbols': false,
 
/**
// full stops without '.'
* @var bool config.stripTrailingNewline
if (wDiff.fullStops === undefined) { wDiff.fullStops = '058906D40701070209640DF41362166E180318092CF92CFE2E3C3002A4FFA60EA6F3FE52FF0EFF61'.replace(/(\w{4})/g, '\\u$1'); }
* Strip trailing newline off of texts (true in .js, false in .php)
*/
'stripTrailingNewline': true,
 
/**
// new paragraph characters without '\n' and '\r'
* @var bool config.debug
if (wDiff.newParagraph === undefined) { wDiff.newParagraph = '\\u2029'; }
* Show debug infos and stats (block, group, and fragment data objects) in debug console (false)
*/
'debug': false,
 
/**
// exclamation marks without '!'
* @var bool config.timer
if (wDiff.exclamationMarks === undefined) { wDiff.exclamationMarks = '01C301C301C3055C055C07F919441944203C203C20482048FE15FE57FF01'.replace(/(\w{4})/g, '\\u$1'); }
* Show timing results in debug console (false)
*/
'timer': false,
 
/**
// question marks without '?'
* @var bool config.unitTesting
if (wDiff.questionMarks === undefined) { wDiff.questionMarks = '037E055E061F13671945204720492CFA2CFB2E2EA60FA6F7FE56FF1F'.replace(/(\w{4})/g, '\\u$1') + '\\u11143'; }
* Run unit tests to prove correct working, display results in debug console (false)
*/
'unitTesting': false,
 
/** RegExp character classes */
// regExps for splitting text (included separators)
if (wDiff.regExpSplit === undefined) {
wDiff.regExpSplit = {
 
// UniCode letter support for regexps, from http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
// paragraphs: after double newlines
'regExpLetters': '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' ),
paragraph: new RegExp('(.|\\n)*?((\\r\\n|\\n|\\r){2,}|[' + wDiff.newParagraph + '])+', 'g'),
 
// New line characters without and with \n and \r
// sentences: after newlines and .spaces
'regExpNewLines': '\\u0085\\u2028',
sentence: new RegExp('[^' + wDiff.newLinesAll + ']*?([.!?;]+[^\\S' + wDiff.newLinesAll + ']+|[' + wDiff.fullStops + wDiff.exclamationMarks + wDiff.questionMarks + ']+[^\\S' + wDiff.newLinesAll + ']*|[' + wDiff.newLines + ']|\\r\\n|\\n|\\r)', 'g'),
'regExpNewLinesAll': '\\n\\r\\u0085\\u2028',
 
// Breaking white space characters without \n, \r, and \f
// inline chunks
'regExpBlanks': ' \\t\\x0b\\u2000-\\u200b\\u202f\\u205f\\u3000',
// [[wiki link]] | {{template}} | [ext. link] |<html> | [[wiki link| | {{template| | url
chunk: /\[\[[^\[\]\n]+\]\]|\{\{[^\{\}\n]+\}\}|\[[^\[\]\n]+\]|<\/?[^<>\[\]\{\}\n]+>|\[\[[^\[\]\|\n]+\]\]\||\{\{[^\{\}\|\n]+\||\b((https?:|)\/\/)[^\x00-\x20\s"\[\]\x7f]+/g,
 
// Full stops without '.'
// words, multi-char markup, and chars
'regExpFullStops': '058906D40701070209640DF41362166E180318092CF92CFE2E3C3002A4FFA60EA6F3FE52FF0EFF61'.replace( /(\w{4})/g, '\\u$1' ),
word: new RegExp('[' + wDiff.letters + ']+([\'’_]?[' + wDiff.letters + ']+)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.', 'g'),
 
// New paragraph characters without \n and \r
// chars
'regExpNewParagraph': '\\f\\u2029',
character: /./g
};
}
 
// Exclamation marks without '!'
// regExps for sliding gaps: newlines and space/word breaks
'regExpExclamationMarks': '01C301C301C3055C055C07F919441944203C203C20482048FE15FE57FF01'.replace( /(\w{4})/g, '\\u$1' ),
if (wDiff.regExpSlideStop === undefined) { wDiff.regExpSlideStop = new RegExp('[\\n\\r' + wDiff.newLines + ']$'); }
if (wDiff.regExpSlideBorder === undefined) { wDiff.regExpSlideBorder = new RegExp('[ \\t' + wDiff.newLinesAll + wDiff.newParagraph + '\\x0C\\x0b]$'); }
 
// Question marks without '?'
// regExps for counting words
'regExpQuestionMarks': '037E055E061F13671945204720492CFA2CFB2E2EA60FA6F7FE56FF1F'.replace( /(\w{4})/g, '\\u$1' ) + '\\u11143',
if (wDiff.regExpWord === undefined) { wDiff.regExpWord = new RegExp('[' + wDiff.letters + ']+([\'’_]?[' + wDiff.letters + ']+)*', 'g'); }
if (wDiff.regExpChunk === undefined) { wDiff.regExpChunk = wDiff.regExpSplit.chunk; }
 
/** Clip settings */
// regExp detecting blank-only and single-char blocks
if (wDiff.regExpBlankBlock === undefined) { wDiff.regExpBlankBlock = /^([^\t\S]+|[^\t])$/; }
 
// Find clip position: characters from right (heading, paragraph, line break, blanks, or characters)
//
'clipHeadingLeft': 1500,
// shorten output settings
'clipParagraphLeftMax': 1500,
//
'clipParagraphLeftMin': 500,
'clipLineLeftMax': 1000,
'clipLineLeftMin': 500,
'clipBlankLeftMax': 1000,
'clipBlankLeftMin': 500,
'clipCharsLeft': 500,
 
// charactersFind beforeclip diffposition: tagcharacters tofrom search for previousright (heading, paragraph, line break, cutblanks, or characters)
'clipHeadingRight': 1500,
if (wDiff.headingBefore === undefined) { wDiff.headingBefore = 1500; }
'clipParagraphRightMax': 1500,
if (wDiff.paragraphBeforeMax === undefined) { wDiff.paragraphBeforeMax = 1500; }
'clipParagraphRightMin': 500,
if (wDiff.paragraphBeforeMin === undefined) { wDiff.paragraphBeforeMin = 500; }
'clipLineRightMax': 1000,
if (wDiff.lineBeforeMax === undefined) { wDiff.lineBeforeMax = 1000; }
'clipLineRightMin': 500,
if (wDiff.lineBeforeMin === undefined) { wDiff.lineBeforeMin = 500; }
'clipBlankRightMax': 1000,
if (wDiff.blankBeforeMax === undefined) { wDiff.blankBeforeMax = 1000; }
'clipBlankRightMin': 500,
if (wDiff.blankBeforeMin === undefined) { wDiff.blankBeforeMin = 500; }
'clipCharsRight': 500,
if (wDiff.charsBefore === undefined) { wDiff.charsBefore = 500; }
 
// charactersMaximum afternumber diffof taglines to search for nextclip heading, paragraph, line break, or charactersposition
'clipLinesRightMax': 10,
if (wDiff.headingAfter === undefined) { wDiff.headingAfter = 1500; }
'clipLinesLeftMax': 10,
if (wDiff.paragraphAfterMax === undefined) { wDiff.paragraphAfterMax = 1500; }
if (wDiff.paragraphAfterMin === undefined) { wDiff.paragraphAfterMin = 500; }
if (wDiff.lineAfterMax === undefined) { wDiff.lineAfterMax = 1000; }
if (wDiff.lineAfterMin === undefined) { wDiff.lineAfterMin = 500; }
if (wDiff.blankAfterMax === undefined) { wDiff.blankAfterMax = 1000; }
if (wDiff.blankAfterMin === undefined) { wDiff.blankAfterMin = 500; }
if (wDiff.charsAfter === undefined) { wDiff.charsAfter = 500; }
 
// Skip clipping if ranges are too close
// lines before and after diff tag to search for previous heading, paragraph, line break, cut characters
'clipSkipLines': 5,
if (wDiff.linesBeforeMax === undefined) { wDiff.linesBeforeMax = 10; }
'clipSkipChars': 1000,
if (wDiff.linesAfterMax === undefined) { wDiff.linesAfterMax = 10; }
 
// Css stylesheet
// maximal fragment distance to join close fragments
'cssMarkLeft': '◀',
if (wDiff.fragmentJoinLines === undefined) { wDiff.fragmentJoinLines = 5; }
'cssMarkRight': '▶',
if (wDiff.fragmentJoinChars === undefined) { wDiff.fragmentJoinChars = 1000; }
'stylesheet':
 
// Insert
//
'.wikEdDiffInsert { font-weight: bold; background-color: #bbddff; color: #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
// css classes
'.wikEdDiffInsertBlank { background-color: #66bbff; }' +
//
'.wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; }' +
 
// Delete
if (wDiff.symbolMarkLeft === undefined) { wDiff.symbolMarkLeft = '◀'; }
'.wikEdDiffDelete { font-weight: bold; background-color: #ffe49c; color: #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
if (wDiff.symbolMarkRight === undefined) { wDiff.symbolMarkRight = '▶'; }
'.wikEdDiffDeleteBlank { background-color: #ffd064; }' +
if (wDiff.stylesheet === undefined) {
'.wikEdDiffFragment:hover .wikEdDiffDeleteBlank { background-color: #ffe49c; }' +
wDiff.stylesheet =
 
// insertBlock
'.wDiffInsertwikEdDiffBlock { font-weight: bold; background-color: #bbddff; color: #222e8e8e8; border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; }' +
'.wikEdDiffBlock { }' +
'.wDiffInsertBlank { background-color: #66bbff; }' +
'.wDiffFragment:hover .wDiffInsertBlankwikEdDiffBlock0 { background-color: #bbddffffff80; }' +
'.wikEdDiffBlock1 { background-color: #d0ff80; }' +
'.wikEdDiffBlock2 { background-color: #ffd8f0; }' +
'.wikEdDiffBlock3 { background-color: #c0ffff; }' +
'.wikEdDiffBlock4 { background-color: #fff888; }' +
'.wikEdDiffBlock5 { background-color: #bbccff; }' +
'.wikEdDiffBlock6 { background-color: #e8c8ff; }' +
'.wikEdDiffBlock7 { background-color: #ffbbbb; }' +
'.wikEdDiffBlock8 { background-color: #a0e8a0; }' +
'.wikEdDiffBlockHighlight { background-color: #777; color: #fff; border: solid #777; border-width: 1px 0; }' +
 
// deleteMark
'.wDiffDeletewikEdDiffMarkLeft, .wikEdDiffMarkRight { font-weight: bold; background-color: #ffe49c; color: #222666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; }' +
'.wikEdDiffMarkLeft:before { content: "{cssMarkLeft}"; }' +
'.wDiffDeleteBlank { background-color: #ffd064; }' +
'.wikEdDiffMarkRight:before { content: "{cssMarkRight}"; }' +
'.wDiffFragment:hover .wDiffDeleteBlank { background-color: #ffe49c; }' +
'.wikEdDiffMarkLeft.wikEdDiffNoUnicode:before { content: "<"; }' +
'.wikEdDiffMarkRight.wikEdDiffNoUnicode:before { content: ">"; }' +
'.wikEdDiffMark { background-color: #e8e8e8; color: #666; }' +
'.wikEdDiffMark0 { background-color: #ffff60; }' +
'.wikEdDiffMark1 { background-color: #c8f880; }' +
'.wikEdDiffMark2 { background-color: #ffd0f0; }' +
'.wikEdDiffMark3 { background-color: #a0ffff; }' +
'.wikEdDiffMark4 { background-color: #fff860; }' +
'.wikEdDiffMark5 { background-color: #b0c0ff; }' +
'.wikEdDiffMark6 { background-color: #e0c0ff; }' +
'.wikEdDiffMark7 { background-color: #ffa8a8; }' +
'.wikEdDiffMark8 { background-color: #98e898; }' +
'.wikEdDiffMarkHighlight { background-color: #777; color: #fff; }' +
 
// blockWrappers
'.wikEdDiffContainer { }' +
'.wDiffBlockLeft, .wDiffBlockRight { font-weight: bold; background-color: #e8e8e8; border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; }' +
'.wikEdDiffFragment { 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; }' +
'.wDiffBlock { }' +
'.wikEdDiffNoChange { white-space: pre-wrap; background: #f0f0f0; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; text-align: center; }' +
'.wDiffBlock0 { background-color: #ffff80; }' +
'.wDiffBlock1wikEdDiffSeparator { backgroundmargin-colorbottom: #d0ff801em; }' +
'.wikEdDiffOmittedChars { }' +
'.wDiffBlock2 { background-color: #ffd8f0; }' +
'.wDiffBlock3 { background-color: #c0ffff; }' +
'.wDiffBlock4 { background-color: #fff888; }' +
'.wDiffBlock5 { background-color: #bbccff; }' +
'.wDiffBlock6 { background-color: #e8c8ff; }' +
'.wDiffBlock7 { background-color: #ffbbbb; }' +
'.wDiffBlock8 { background-color: #a0e8a0; }' +
'.wDiffBlockHighlight { background-color: #777; color: #fff; border: solid #777; border-width: 1px 0; }' +
 
// markNewline
'.wikEdDiffNewline:before { content: "¶"; color: transparent; }' +
'.wDiffMarkLeft, .wDiffMarkRight { font-weight: bold; background-color: #ffe49c; color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; }' +
'.wDiffMarkRightwikEdDiffBlock:hover .wikEdDiffNewline:before { contentcolor: "' + wDiff.symbolMarkRight + '"#aaa; }' +
'.wikEdDiffBlockHighlight .wikEdDiffNewline:before { color: transparent; }' +
'.wDiffMarkLeft:before { content: "' + wDiff.symbolMarkLeft + '"; }' +
'.wDiffMarkwikEdDiffBlockHighlight:hover { background-color.wikEdDiffNewline:before #e8e8e8;{ color: #666ccc; }' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffNewline:before, .wikEdDiffInsert:hover .wikEdDiffNewline:before { color: #999; }' +
'.wDiffMark0 { background-color: #ffff60; }' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffNewline:before, .wikEdDiffDelete:hover .wikEdDiffNewline:before { color: #aaa; }' +
'.wDiffMark1 { background-color: #c8f880; }' +
'.wDiffMark2 { background-color: #ffd0f0; }' +
'.wDiffMark3 { background-color: #a0ffff; }' +
'.wDiffMark4 { background-color: #fff860; }' +
'.wDiffMark5 { background-color: #b0c0ff; }' +
'.wDiffMark6 { background-color: #e0c0ff; }' +
'.wDiffMark7 { background-color: #ffa8a8; }' +
'.wDiffMark8 { background-color: #98e898; }' +
'.wDiffMarkHighlight { background-color: #777; color: #fff; }' +
 
// wrappersTab
'.wikEdDiffTab { position: relative; }' +
'.wDiffContainer { }' +
'.wikEdDiffTabSymbol { position: absolute; top: -0.2em; }' +
'.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; }' +
'.wikEdDiffTabSymbol:before { content: "→"; font-size: smaller; color: #ccc; }' +
'.wDiffNoChange { white-space: pre-wrap; background: #f0f0f0; border: #bbb solid; border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; }' +
'.wikEdDiffBlock .wikEdDiffTabSymbol:before { color: #aaa; }' +
'.wDiffSeparator { margin-bottom: 1em; }' +
'.wikEdDiffBlockHighlight .wikEdDiffTabSymbol:before { color: #aaa; }' +
'.wDiffOmittedChars { }' +
'.wikEdDiffInsert .wikEdDiffTabSymbol:before { color: #aaa; }' +
'.wikEdDiffDelete .wikEdDiffTabSymbol:before { color: #bbb; }' +
 
// newlineSpace
'.wikEdDiffSpace { position: relative; }' +
'.wDiffNewline:before { content: "¶"; color: transparent; }' +
'.wikEdDiffSpaceSymbol { position: absolute; top: -0.2em; left: -0.05em; }' +
'.wDiffBlockLeft:hover .wDiffNewline:before, .wDiffBlockRight:hover .wDiffNewline:before { color: #aaa; }' +
'.wDiffBlockHighlight .wDiffNewlinewikEdDiffSpaceSymbol:before { content: "·"; color: transparent; }' +
'.wDiffBlockHighlightwikEdDiffBlock:hover .wDiffNewlinewikEdDiffSpaceSymbol:before { color: #ccc999; }' +
'.wikEdDiffBlockHighlight .wikEdDiffSpaceSymbol:before { color: transparent; }' +
'.wDiffBlockHighlight:hover .wDiffInsert .wDiffNewline:before, .wDiffInsert:hover .wDiffNewline:before { color: #999; }' +
'.wDiffBlockHighlightwikEdDiffBlockHighlight:hover .wDiffDelete .wDiffNewline:before, .wDiffDelete:hover .wDiffNewlinewikEdDiffSpaceSymbol:before { color: #aaaddd; }' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffSpaceSymbol:before, .wikEdDiffInsert:hover .wikEdDiffSpaceSymbol:before { color: #888; }' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffSpaceSymbol:before, .wikEdDiffDelete:hover .wikEdDiffSpaceSymbol:before { color: #999; }' +
 
// tabError
'.wikEdDiffError .wikEdDiffFragment, .wikEdDiffError .wikEdDiffNoChange { background: #faa; }',
'.wDiffTab { position: relative; }' +
};
'.wDiffTabSymbol { position: absolute; top: -0.2em; }' +
'.wDiffTabSymbol:before { content: "→"; font-size: smaller; color: #ccc; }' +
'.wDiffBlockLeft .wDiffTabSymbol:before, .wDiffBlockRight .wDiffTabSymbol:before { color: #aaa; }' +
'.wDiffBlockHighlight .wDiffTabSymbol:before { color: #aaa; }' +
'.wDiffInsert .wDiffTabSymbol:before { color: #aaa; }' +
'.wDiffDelete .wDiffTabSymbol:before { color: #bbb; }' +
 
/** Add regular expressions to configuration settings */
// space
'.wDiffSpace { position: relative; }' +
'.wDiffSpaceSymbol { position: absolute; top: -0.2em; left: -0.05em; }' +
'.wDiffSpaceSymbol:before { content: "·"; color: transparent; }' +
'.wDiffBlockLeft:hover .wDiffSpaceSymbol:before, .wDiffBlockRight:hover .wDiffSpaceSymbol:before { color: #999; }' +
'.wDiffBlockHighlight .wDiffSpaceSymbol:before { color: transparent; }' +
'.wDiffBlockHighlight:hover .wDiffSpaceSymbol:before { color: #ddd; }' +
'.wDiffBlockHighlight:hover .wDiffInsert .wDiffSpaceSymbol:before, .wDiffInsert:hover .wDiffSpaceSymbol:before { color: #888; }' +
'.wDiffBlockHighlight:hover .wDiffDelete .wDiffSpaceSymbol:before, .wDiffDelete:hover .wDiffSpaceSymbol:before { color: #999; }' +
 
this.config.regExp = {
 
// RegExps for splitting text
// error
'split': {
'.wDiffError .wDiffContainer, .wDiffError .wDiffFragment, .wDiffError .wDiffNoChange { background: #faa; }';
}
 
// Split into paragraphs, after double newlines
//
'paragraph': new RegExp(
// css styles
'(.|\\n)*?((\\r\\n|\\n|\\r){2,}|[' +
//
this.config.regExpNewParagraph +
'])+', 'g' ),
 
// Split into sentences, after .space and newlines
if (wDiff.styleInsert === undefined) { wDiff.styleInsert = ''; }
'sentence': new RegExp(
if (wDiff.styleDelete === undefined) { wDiff.styleDelete = ''; }
'[^' + this.config.regExpNewLinesAll +
if (wDiff.styleInsertBlank === undefined) { wDiff.styleInsertBlank = ''; }
']*?([.!?;' + this.config.regExpFullStops +
if (wDiff.styleDeleteBlank === undefined) { wDiff.styleDeleteBlank = ''; }
this.config.regExpExclamationMarks + this.config.regExpQuestionMarks +
if (wDiff.styleBlock === undefined) { wDiff.styleBlock = ''; }
']+[' +
if (wDiff.styleBlockLeft === undefined) { wDiff.styleBlockLeft = ''; }
this.config.regExpBlanks +
if (wDiff.styleBlockRight === undefined) { wDiff.styleBlockRight = ''; }
']+)?([' +
if (wDiff.styleBlockHighlight === undefined) { wDiff.styleBlockHighlight = ''; }
this.config.regExpNewLines +
if (wDiff.styleBlockColor === undefined) { wDiff.styleBlockColor = []; }
']|\\r\\n|\\n|\\r)', 'g' ),
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 = ''; }
 
// Split into inline chunks
//
'chunk': new RegExp(
// output html
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
//
'\\{\\{[^\\{\\}\\n]+\\}\\}|' + // {{template}}
'\\[[^\\[\\]\\n]+\\]|' + // [ext. link]
'<\\/?[^<>\\[\\]\\{\\}\\n]+>|' + // <html>
'\\[\\[[^\\[\\]\\|\\n]+\\]\\]\\||' + // [[wiki link|
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'),
 
// Split into words, multi-char markup, and chars
// dynamic replacements: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
'word': new RegExp( '[' +
// class plus html comment are required indicators for TextDiff.shortenOutput()
this.config.regExpLetters +
']+([\'’_]?[' +
this.config.regExpLetters +
']+)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.', 'g' ),
 
// Split into chars
if (wDiff.blockEvent === undefined) { wDiff.blockEvent = ' onmouseover="wDiff.blockHandler(undefined, this, \'mouseover\');"'; }
'character': /./g
},
 
// RegExps for sliding gaps: newlines and space/word breaks
if (wDiff.htmlContainerStart === undefined) { wDiff.htmlContainerStart = '<div class="wDiffContainer" id="wDiffContainer" style="' + wDiff.styleContainer + '">'; }
'slideStop': new RegExp( '[' + this.config.regExpNewLinesAll + ']$'),
if (wDiff.htmlContainerEnd === undefined) { wDiff.htmlContainerEnd = '</div>'; }
'slideBorder': new RegExp(
'[' + this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']$' ),
 
// RegExps for counting words
if (wDiff.htmlInsertStart === undefined) { wDiff.htmlInsertStart = '<span class="wDiffInsert" style="' + wDiff.styleInsert + '" title="+">'; }
'countWords': new RegExp(
if (wDiff.htmlInsertStartBlank === undefined) { wDiff.htmlInsertStartBlank = '<span class="wDiffInsert wDiffInsertBlank" style="' + wDiff.styleInsertBlank + '" title="+">'; }
'[' + this.config.regExpLetters +
if (wDiff.htmlInsertEnd === undefined) { wDiff.htmlInsertEnd = '</span><!--wDiffInsert-->'; }
']+([\'’_]?[' + 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
if (wDiff.htmartlDeleteSt === undefined) { wDiff.htmlDeleteStart = '<span class="wDiffDelete" style="' + wDiff.styleDelete + '" title="−">'; }
'blankBlock': /^([^\t\S]+|[^\t])$/,
if (wDiff.htmlDeleteStartBlank === undefined) { wDiff.htmlDeleteStartBlank = '<span class="wDiffDelete wDiffDeleteBlank" style="' + wDiff.styleDeleteBlank + '" title="−">'; }
if (wDiff.htmlDeleteEnd === undefined) { wDiff.htmlDeleteEnd = '</span><!--wDiffDelete-->'; }
 
// RegExps for clipping
if (wDiff.htmlBlockLeftStart === undefined) { wDiff.htmlBlockLeftStart = '<span class="wDiffBlockLeft" style="' + wDiff.styleBlockLeft + '" title="' + wDiff.symbolMarkLeft + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
'clipLine': new RegExp(
if (wDiff.htmlBlockLeftColoredStart === undefined) { wDiff.htmlBlockLeftColoredStart = '<span class="wDiffBlockLeft wDiffBlock wDiffBlock{class}" style="' + wDiff.styleBlockLeft + wDiff.styleBlock + '{block}" title="' + wDiff.symbolMarkLeft + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
'[' + this.config.regExpNewLinesAll + this.config.regExpNewParagraph + ']+', 'g' ),
if (wDiff.htmlBlockLeftEnd === undefined) { wDiff.htmlBlockLeftEnd = '</span><!--wDiffBlockLeft-->'; }
'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.htmlBlockRightStart === undefined) { wDiff.htmlBlockRightStart = '<span class="wDiffBlockRight" style="' + wDiff.styleBlockRight + '" title="' + wDiff.symbolMarkRight + '" id="wDiffBlock{number}"' + wDiff.blockEvent + '>'; }
if (wDiff.htmlBlockRightColoredStart === undefined) { wDiff.htmlBlockRightColoredStart = '<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-->'; }
 
this.config.msg = {
if (wDiff.htmlMarkLeft === undefined) { wDiff.htmlMarkLeft = '<span class="wDiffMarkLeft" style="' + wDiff.styleMarkLeft + '"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->'; }
'wiked-diff-empty': '(No difference)',
if (wDiff.htmlMarkLeftColored === undefined) { wDiff.htmlMarkLeftColored = '<span class="wDiffMarkLeft wDiffMark wDiffMark{class}" style="' + wDiff.styleMarkLeft + wDiff.styleMark + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkLeft-->'; }
'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!'
};
 
/**
if (wDiff.htmlMarkRight === undefined) { wDiff.htmlMarkRight = '<span class="wDiffMarkRight" style="' + wDiff.styleMarkRight + '"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->'; }
* Add output html fragments to configuration settings
if (wDiff.htmlMarkRightColored === undefined) { wDiff.htmlMarkRightColored = '<span class="wDiffMarkRight wDiffMark wDiffMark{class}" style="' + wDiff.styleMarkRight + wDiff.styleMark + '{mark}"{title} id="wDiffMark{number}"' + wDiff.blockEvent + '></span><!--wDiffMarkRight-->'; }
* 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">',
if (wDiff.htmlNewline === undefined) { wDiff.htmlNewline = '<span class="wDiffNewline" style="' + wDiff.styleNewline + '">\n</span>'; }
'containerEnd': '</div>',
if (wDiff.htmlTab === undefined) { wDiff.htmlTab = '<span class="wDiffTab" style="' + wDiff.styleTab + '"><span class="wDiffTabSymbol" style="' + wDiff.styleTabSymbol + '"></span>\t</span>'; }
if (wDiff.htmlSpace === undefined) { wDiff.htmlSpace = '<span class="wDiffSpace" style="' + wDiff.styleSpace + '"><span class="wDiffSpaceSymbol" style="' + wDiff.styleSpaceSymbol + '"></span> </span>'; }
 
'fragmentStart': '<div class="wikEdDiffFragment" style="white-space: pre-wrap;">',
// shorten output
'fragmentEnd': '</div>',
'separator': '<div class="wikEdDiffSeparator"></div>',
 
'insertStart': '<span class="wikEdDiffInsert" title="' + this.config.msg['wiked-diff-ins'] + '">',
if (wDiff.htmlFragmentStart === undefined) { wDiff.htmlFragmentStart = '<pre class="wDiffFragment" style="' + wDiff.styleFragment + '">'; }
'insertStartBlank': '<span class="wikEdDiffInsert wikEdDiffInsertBlank" title="' + this.config.msg['wiked-diff-ins'] + '">',
if (wDiff.htmlFragmentEnd === undefined) { wDiff.htmlFragmentEnd = '</pre>'; }
'insertEnd': '</span>',
 
'deleteStart': '<span class="wikEdDiffDelete" title="' + this.config.msg['wiked-diff-del'] + '">',
if (wDiff.htmlNoChange === undefined) { wDiff.htmlNoChange = '<pre class="wDiffNoChange" style="' + wDiff.styleNoChange + '" title="="></pre>'; }
'deleteStartBlank': '<span class="wikEdDiffDelete wikEdDiffDeleteBlank" title="' + this.config.msg['wiked-diff-del'] + '">',
if (wDiff.htmlSeparator === undefined) { wDiff.htmlSeparator = '<div class="wDiffSeparator" style="' + wDiff.styleSeparator + '"></div>'; }
'deleteEnd': '</span>',
if (wDiff.htmlOmittedChars === undefined) { wDiff.htmlOmittedChars = '<span class="wDiffOmittedChars" style="' + wDiff.styleOmittedChars + '">…</span>'; }
 
'blockStart': '<span class="wikEdDiffBlock" title="{title}" id="wikEdDiffBlock{number}" onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
if (wDiff.htmlErrorStart === undefined) { wDiff.htmlErrorStart = '<div class="wDiffError" style="' + wDiff.styleError + '">'; }
'blockColoredStart': '<span class="wikEdDiffBlock wikEdDiffBlock wikEdDiffBlock{number}" title="{title}" id="wikEdDiffBlock{number}" onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
if (wDiff.htmlErrorEnd === undefined) { wDiff.htmlErrorEnd = '</div>'; }
'blockEnd': '</span>',
 
'markLeft': '<span class="wikEdDiffMarkLeft{nounicode}" title="{title}" id="wikEdDiffMark{number}" onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
//
'markLeftColored': '<span class="wikEdDiffMarkLeft{nounicode} wikEdDiffMark wikEdDiffMark{number}" title="{title}" id="wikEdDiffMark{number}" onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
// javascript handler for output code, IE 8 compatible
//
 
'markRight': '<span class="wikEdDiffMarkRight{nounicode}" title="{title}" id="wikEdDiffMark{number}" onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
// wDiff.blockHandler: event handler for block and mark elements
'markRightColored': '<span class="wikEdDiffMarkRight{nounicode} wikEdDiffMark wikEdDiffMark{number}" title="{title}" id="wikEdDiffMark{number}" onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
if (wDiff.blockHandler === undefined) { wDiff.blockHandler = function (event, element, type) {
 
'newline': '<span class="wikEdDiffNewline">\n</span>',
// IE compatibility
'tab': '<span class="wikEdDiffTab"><span class="wikEdDiffTabSymbol"></span>\t</span>',
if ( (event === undefined) && (window.event !== undefined) ) {
'space': '<span class="wikEdDiffSpace"><span class="wikEdDiffSpaceSymbol"></span> </span>',
event = window.event;
}
 
'omittedChars': '<span class="wikEdDiffOmittedChars">…</span>',
// get mark/block elements
if (wDiff.showBlockMoves === false) {
return;
}
var number = element.id.replace(/\D/g, '');
var block = document.getElementById('wDiffBlock' + number);
var mark = document.getElementById('wDiffMark' + number);
 
'errorStart': '<div class="wikEdDiffError" title="Error: diff not consistent with versions!">',
// highlight corresponding mark/block pairs
'errorEnd': '</div>'
if (type == 'mouseover') {
};
element.onmouseover = null;
element.onmouseout = function (event) { wDiff.blockHandler(event, element, 'mouseout'); };
element.onclick = function (event) { wDiff.blockHandler(event, element, 'click'); };
block.className += ' wDiffBlockHighlight';
mark.className += ' wDiffMarkHighlight';
}
 
/*
// remove mark/block highlighting
* Add JavaScript event handler function to configuration settings.
if ( (type == 'mouseout') || (type == 'click') ) {
* Highlights corresponding block and mark elements on hover and jumps between them on click.
element.onmouseout = null;
* Code for use in non-jQuery environments and legacy browsers (at least IE 8 compatible).
element.onmouseover = function (event) { wDiff.blockHandler(event, element, 'mouseover'); };
*
* @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
// reset, allow outside container (e.g. legend)
if ( ( event === undefined ) && ( window.event !== undefined ) ) {
if (type != 'click') {
event = window.event;
block.className = block.className.replace(/wDiffBlockHighlight/g, '');
}
mark.className = mark.className.replace(/wDiffMarkHighlight/g, '');
 
// Get mark/block elements
// getElementsByClassName
var containernumber = documentelement.getElementByIdid.replace( /\D/g, 'wDiffContainer' );
var block = document.getElementById( 'wikEdDiffBlock' + number );
if (container !== null) {
var mark = document.getElementById( 'wikEdDiffMark' + number );
var spans = container.getElementsByTagName('span');
forif (var imark === 0; i < spans.length; inull ++) {
return;
if ( (spans[i] != block) && (spans[i] != mark) ) {
if (spans[i].className.indexOf(' wDiffBlockHighlight') != -1) {
spans[i].className = spans[i].className.replace(/ wDiffBlockHighlight/g, '');
}
else if (spans[i].className.indexOf(' wDiffMarkHighlight') != -1) {
spans[i].className = spans[i].className.replace(/ wDiffMarkHighlight/g, '');
}
}
}
}
}
}
 
// scroll toHighlight corresponding mark/block elementpairs
if ( type == 'clickmouseover' ) {
element.onmouseover = null;
 
element.onmouseout = function ( event ) {
// get corresponding element
window.wikEdDiffBlockHandler( event, element, 'mouseout' );
var corrElement;
};
if (element == block) {
element.onclick = function ( event ) {
corrElement = mark;
window.wikEdDiffBlockHandler( event, element, 'click' );
}
else { };
block.className += ' wikEdDiffBlockHighlight';
corrElement = block;
mark.className += ' wikEdDiffMarkHighlight';
}
 
// Remove mark/block highlighting
// get element height (getOffsetTop)
if ( ( type == 'mouseout' ) || ( type == 'click' ) ) {
var corrElementPos = 0;
element.onmouseout = null;
var node = corrElement;
element.onmouseover = function ( event ) {
do {
window.wikEdDiffBlockHandler( event, element, 'mouseover' );
corrElementPos += node.offsetTop;
};
} while ( (node = node.offsetParent) !== null );
 
// Reset, allow outside container (e.g. legend)
// get scroll height
if ( type != 'click' ) {
var top;
block.className = block.className.replace( /wikEdDiffBlockHighlight/g, '' );
if (window.pageYOffset !== undefined) {
mark.className = mark.className.replace( /wikEdDiffMarkHighlight/g, '' );
top = window.pageYOffset;
}
else {
top = document.documentElement.scrollTop;
}
 
// GetElementsByClassName
// get cursor pos
var container = document.getElementById( 'wikEdDiffContainer' );
var cursor;
if (event.pageY container !== undefinednull ) {
var spans = container.getElementsByTagName( 'span' );
cursor = event.pageY;
for ( var i = 0; i < spans.length; i ++ ) {
}
else if (event.clientY ( spans[i] != block ) && ( spans[i] != undefinedmark ) ) {
if ( spans[i].className.indexOf( ' wikEdDiffBlockHighlight' ) != -1 ) {
cursor = event.clientY + top;
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
// get line height
varif line( type == 'click' ) 12;{
if (window.getComputedStyle !== undefined) {
line = parseInt(window.getComputedStyle(corrElement).getPropertyValue('line-height'));
}
 
// scrollGet corresponding element under mouse cursor
var corrElement;
window.scroll(0, corrElementPos + top - cursor + line / 2);
if ( element == block ) {
}
corrElement = mark;
return;
}; }
else {
corrElement = block;
}
 
// Get element height (getOffsetTop )
var corrElementPos = 0;
var node = corrElement;
do {
corrElementPos += node.offsetTop;
} while ( ( node = node.offsetParent ) !== null );
 
// Get scroll height
//
var top;
// end of configuration and customization settings
if ( window.pageYOffset !== undefined ) {
//
top = window.pageYOffset;
}
else {
top = document.documentElement.scrollTop;
}
 
// Get cursor pos
var cursor;
if ( event.pageY !== undefined ) {
cursor = event.pageY;
}
else if ( event.clientY !== undefined ) {
cursor = event.clientY + top;
}
 
// Get line height
// wDiff.init(): initialize wDiff
var line = 12;
// called from: on code load
if ( window.getComputedStyle !== undefined ) {
// calls: .addStyleSheet(), .addScript()
line = parseInt( window.getComputedStyle( corrElement ).getPropertyValue( 'line-height' ) );
}
 
// Scroll element under mouse cursor
wDiff.init = function () {
window.scroll( 0, corrElementPos + top - cursor + line / 2 );
 
wDiff.error = false;
 
// legacy for short time
wDiff.Diff = wDiff.diff;
 
// add styles to head
wDiff.addStyleSheet(wDiff.stylesheet);
 
// add block handler to head if running under Greasemonkey
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(), TextDiff.htmlFormat(), 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.timer === true) {
console.time('diff');
}
 
// run the diff
textDiff.diff();
 
// start timer
if (wDiff.timer === true) {
console.timeEnd('diff');
}
 
// shorten output
if ( ( (full !== undefined) && (full !== true) ) || ( (full === undefined) && (wDiff.fullDiff !== true) ) ) {
 
// start timer
if (wDiff.timer === 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.timer === true) {
console.timeEnd('shorten');
}
}
 
/** @var WikEdDiffText oldText Old text version object with text and token list */
// markup tabs, add container
this.oldText = null;
else {
textDiff.htmlFormat();
}
 
/** @var array blocks Block data (consecutive text tokens) in new text order */
// stop timer
this.blocks = [];
if (wDiff.timer === true) {
console.timeEnd('diff');
}
 
/** @var array groups Section blocks that are consecutive in old text order */
// run unit tests
this.groups = [];
if (wDiff.unitTesting === true) {
wDiff.unitTests(textDiff);
}
return textDiff.html;
};
 
/** @var array sections Block sections with no block move crosses outside a section */
this.sections = [];
 
/** @var object timer Debug timer array: string 'label' => float milliseconds. */
// wDiff.unitTests(): test diff for consistency between input and output
this.timer = {};
// input: textDiff: text diff object after calling .diff()
// called from: .diff()
 
/** @var array recursionTimer Count time spent in recursion level in milliseconds. */
wDiff.unitTests = function (textDiff) {
this.recursionTimer = [];
 
//** startOutput timerdata */
if (wDiff.timer === true) {
console.time('unit tests');
}
 
/** @var bool error Unit tests have detected a diff error */
var html = textDiff.html;
this.error = false;
 
/** @var array fragments Diff fragment list for markup, abstraction layer for customization */
// check if output is consistent with new text
this.fragments = [];
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 string html Html code of diff */
// check if output is consistent with old text
this.html = '';
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');
}
 
if (wDiff.error === true) {
html = wDiff.htmlErrorStart + html + wDiff.htmlErrorEnd;
}
textDiff.html = html;
 
// stop timer
if (wDiff.timer === true) {
console.timeEnd('unit tests');
}
return;
};
 
 
//
// wDiff.Text class: data and methods for single text version (old or new)
// called from: TextDiff.init()
//
 
wDiff.Text = function (string, parent) {
 
this.parent = parent;
this.string = null;
this.tokens = [];
this.first = null;
this.last = null;
this.words = {};
 
 
//**
// Text.init():* Constructor, initialize textsettings, load js and objectcss
*
//
* @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 712 ⟶ 722:
 
 
/**
// 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.fragmentStart +
next: null,
this.config.htmlCode.noChangeStart +
link: null,
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
number: null,
this.config.htmlCode.noChangeEnd +
unique: false
this.config.htmlCode.fragmentEnd +
};
this.config.htmlCode.containerEnd;
number ++;
return this.html;
}
 
// 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) ) {
if (prev !this.newText.text === 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;
}
 
// New symbols object
// set text first and last token index
ifvar (numbersymbols > 0)= {
token: [],
hashTable: {},
linked: false
};
 
// initialSplit new and old text splitinto paragraps
this.newText.splitText( 'paragraph' );
if (token === undefined) {
this.firstoldText.splitText( ='paragraph' 0);
this.last = prev;
}
 
// Calculate diff
// first or last token has been split
this.calculateDiff( symbols, 'paragraph' );
else {
if (token == this.first) {
this.first = first;
}
if (token == this.last) {
this.last = prev;
}
}
}
return;
};
 
// Refine different paragraphs into sentences
this.newText.splitRefine( 'sentence' );
this.oldText.splitRefine( 'sentence' );
 
// Calculate refined diff
// Text.splitRefine(): split unique unmatched tokens into smaller tokens
this.calculateDiff( symbols, 'sentence' );
// changes: text (text.newText or text.oldText) .tokens list
// called from: TextDiff.diff()
// calls: .split()
 
// Refine different paragraphs into chunks
this.splitRefine = function (regExp) {
if ( this.config.timer === true ) {
this.time( 'chunk split' );
}
this.newText.splitRefine( 'chunk' );
this.oldText.splitRefine( 'chunk' );
if ( this.config.timer === true ) {
this.timeEnd( 'chunk split' );
}
 
// cycleCalculate throughrefined tokens listdiff
this.calculateDiff( symbols, 'chunk' );
var i = this.first;
while ( (i !== null) && (this.tokens[i] !== null) ) {
 
// refineRefine uniquedifferent unmatched tokenssentences into smaller tokenswords
if ( this.tokens[i]config.linktimer === nulltrue ) {
this.splittime(regExp, i'word split' );
}
i = this.tokens[i]newText.nextsplitRefine( 'word' );
this.oldText.splitRefine( 'word' );
if ( this.config.timer === true ) {
this.timeEnd( 'word split' );
}
return;
};
 
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( symbols, 'word', false, true );
 
// Slide gaps
// Text.enumerateTokens(): enumerate text token list
if ( this.config.timer === true ) {
// called from: TextDiff.diff()
this.time( 'word slide' );
// changes: .tokens list
}
 
this.slideGaps( this.newText, this.oldText );
this.enumerateTokens = function () {
this.slideGaps( this.oldText, this.newText );
 
if ( this.config.timer === true ) {
// enumerate tokens list
this.timeEnd( 'word slide' );
var number = 0;
var i = this.first;
while ( (i !== null) && (this.tokens[i] !== null) ) {
this.tokens[i].number = number;
number ++;
i = this.tokens[i].next;
}
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( symbols, 'character', false, 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;
};
 
// Enumerate token lists
this.newText.enumerateTokens();
this.oldText.enumerateTokens();
 
// Detect moved blocks
// initialize text object
if ( this.config.timer === true ) {
this.init();
this.time( 'blocks' );
};
}
this.detectBlocks();
if ( this.config.timer === true ) {
this.timeEnd( 'blocks' );
}
 
// Assemble blocks into fragment table
this.getDiffFragments();
 
// Stop diff timer
//
if ( this.config.timer === true ) {
// wDiff.TextDiff class: main wDiff class, includes all data structures and methods required for a diff
this.timeEnd( 'diff' );
// called from: wDiff.diff()
}
//
 
// Unit tests
wDiff.TextDiff = function (oldString, newString) {
if ( this.config.unitTesting === true ) {
 
// Test diff to test consistency between input and output
this.newText = null;
if ( this.config.timer === true ) {
this.oldText = null;
this.blockstime( ='unit []tests' );
}
this.groups = [];
this.sections = []unitTests();
if ( this.config.timer === true ) {
this.html = '';
this.timeEnd( 'unit tests' );
}
}
 
// Clipping
if ( this.config.fullDiff === false ) {
 
// Clipping unchanged sections from unmoved block text
//
if ( this.config.timer === true ) {
// TextDiff.init(): initialize diff object
this.time( 'clip' );
//
}
this.clipDiffFragments();
if ( this.config.timer === true ) {
this.timeEnd( 'clip' );
}
}
 
// Create html formatted diff code from diff fragments
this.init = function () {
if ( this.config.timer === true ) {
this.time( 'html' );
}
this.getDiffHtml();
if ( this.config.timer === true ) {
this.timeEnd( 'html' );
}
 
// No change
this.newText = new wDiff.Text(newString, this);
if ( this.oldTexthtml === new'' wDiff.Text(oldString, this); {
this.html =
return;
this.config.htmlCode.containerStart +
};
this.config.htmlCode.fragmentStart +
 
this.config.htmlCode.noChangeStart +
 
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
// TextDiff.diff(): main method
this.config.htmlCode.noChangeEnd +
// input: version: 'new', 'old', show only one marked-up version, .oldString, .newString
this.config.htmlCode.fragmentEnd +
// called from: wDiff.diff()
this.config.htmlCode.containerEnd;
// calls: Text.split(), Text.splitRefine(), .calculateDiff(), .slideGaps(), .enumerateTokens(), .detectBlocks(), .assembleDiff()
// changes: .html
 
this.diff = function (version) {
 
// trap trivial changes: no change
if (this.newText.string == this.oldText.string) {
return;
}
 
// Add error indicator
// trap trivial changes: old text deleted
if ( this.error === true ) {
if ( (this.oldText.string === '') || ( (this.oldText.string == '\n') && (this.newText.string.charAt(this.newText.string.length - 1) == '\n') ) ) {
this.html = wDiffthis.config.htmlCode.htmlInsertStarterrorStart + this.htmlEscape(this.newText.string)html + wDiffthis.config.htmlCode.htmlInsertEnderrorEnd;
return;
}
 
// Stop total timer
// trap trivial changes: new text deleted
if ( this.config.timer === true ) {
if ( (this.newText.string === '') || ( (this.newText.string == '\n') && (this.oldText.string.charAt(this.oldText.string.length - 1) == '\n') ) ) {
this.timeEnd( 'total' );
this.html = wDiff.htmlDeleteStart + this.htmlEscape(this.oldText.string) + wDiff.htmlDeleteEnd;
return;
}
 
// newDebug symbols objectlog
if ( this.config.debug === true ) {
var symbols = {
console.log( 'HTML:\n', this.html );
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 trough 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,035 ⟶ 1,019:
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,056 ⟶ 1,040:
}
 
// cycleCycle trough gaps and add old text gap data
for ( var gap = 0; gap < gaps.length; gap ++ ) {
 
// cycleCycle trough old text tokens list
var j = gaps[gap].oldFirst;
while ( ( 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,071 ⟶ 1,055:
}
 
/** 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 gap = 0; gap < gaps.length; 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 ( ( 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 ( ( token.indexOf( tokenFirst ) !== 0 ) || ( token.indexOf( tokenLast ) != token.length - tokenLast.length ) ) {
continue;
}
Line 1,104 ⟶ 1,086:
}
 
// cycleCycle trough 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,124 ⟶ 1,106:
}
 
// 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,138 ⟶ 1,120:
}
 
// 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,147 ⟶ 1,129:
}
 
// noNo simple insertion or deletion of internal string
if ( left + right != shorterToken.length ) {
 
// notNot addition or deletion of flanking strings in tokens (smaller 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 this gap
charSplit = false;
break;
Line 1,164 ⟶ 1,146:
}
 
// sameSame token length
else if ( newToken != oldToken ) {
 
// tokensTokens less than 50 % identical
var ident = 0;
for ( var pos = 0; pos < shorterToken.length; 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,182 ⟶ 1,164:
}
 
// nextNext list elements
if ( i == gaps[gap].newLast ) {
break;
}
Line 1,193 ⟶ 1,175:
}
 
/** Refine words into chars in selected gaps */
//
// refine words into chars in selected gaps
//
 
for ( var gap = 0; gap < gaps.length; gap ++ ) {
if ( gaps[gap].charSplit === true ) {
 
// cycleCycle trough 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 ( ( 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,243 ⟶ 1,223:
 
 
/**
// 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 ) {
 
// Cycle through tokens list
this.slideGaps = function (text, textLinked) {
 
// cycle 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,283 ⟶ 1,264:
}
 
// testTest slide up, remember last line break or word border
var front = text.tokens[gapFront].prev;
var back = gapBack;
var gapFrontBlankTest = wDiffthis.regExpSlideBorderconfig.regExp.slideBorder.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 )
) {
 
Line 1,298 ⟶ 1,279:
back = text.tokens[back].prev;
 
// stopStop at line break
if ( front !== null ) {
if (wDiff this.config.regExp.regExpSlideStopslideStop.test( text.tokens[front].token ) === true ) {
frontStop = front;
break;
}
 
// stopStop at first word border (blank/word or word/blank )
if (wDiff this.config.regExp.regExpSlideBorderslideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
frontStop = front;
}
Line 1,313 ⟶ 1,294:
}
 
// 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,336 ⟶ 1,317:
 
 
/**
// 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 matched 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', 'sentence', 'chunk', 'word', or 'character'
*
* Optionally for recursive or repeated calls:
* @param bool repeat Repeat 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 ( symbols, level, repeat, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel ) {
 
if ( recursionLevel === undefined ) {
this.calculateDiff = function (symbols, level, repeat, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
recursionLevel = 0;
 
// start timer
if ( (wDiff.timer === true) && (repeat !== true) && (recursionLevel === undefined) ) {
console.time(level);
}
 
// setSet defaults
if ( newStart === undefined ) { newStart = this.newText.first; }
if ( newEnd === undefined ) { newEnd = this.newText.last; }
if ( oldStart === undefined ) { oldStart = this.oldText.first; }
if ( oldEnd === undefined ) { oldEnd = this.oldText.last; }
 
if (recursionLevel === undefined) { recursionLevel = 0; }
// Start timers
if ( ( this.config.timer === true ) && ( repeat !== true ) && ( recursionLevel === 0 ) ) {
this.time( level );
}
if ( ( this.config.timer === true ) && ( repeat !== true ) ) {
this.time( level + recursionLevel );
}
 
// limitLimit recursion depth
if ( recursionLevel > 10 ) {
return;
}
 
//**
// pass* Pass 1: parse new text into symbol table.
/ */
 
// cycleCycle trough new text tokens list
var i = newStart;
while ( ( i !== null ) && ( this.newText.tokens[i] !== 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 current = symbols.token.length;
symbols.hashhashTable[token] = current;
symbols.token[current] = {
newCount: 1,
Line 1,392 ⟶ 1,387:
}
 
// orOr update existing entry
else {
 
// incrementIncrement token counter for new text
var hashToArray = symbols.hashhashTable[token];
symbols.token[hashToArray].newCount ++;
}
}
 
// nextNext list element
if ( i == newEnd ) {
break;
}
Line 1,408 ⟶ 1,403:
}
 
//**
// pass* Pass 2: parse old text into symbol table.
/ */
 
// cycleCycle trough old text tokens list
var j = oldStart;
while ( ( j !== null ) && ( this.oldText.tokens[j] !== 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 current = symbols.token.length;
symbols.hashhashTable[token] = current;
symbols.token[current] = {
newCount: 0,
Line 1,430 ⟶ 1,425:
}
 
// 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;
}
}
 
// nextNext list element
if ( j === oldEnd ) {
break;
}
Line 1,449 ⟶ 1,444:
}
 
//**
// pass* Pass 3: connect unique tokens.
/ */
 
// cycleCycle trough symbol array
for ( var i = 0; i < symbols.token.length; 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;
 
// connectConnect from new to old and from old to new
if ( this.newText.tokens[newToken].link === null ) {
 
// doDo not use spaces as unique markers
if ( /^\s+$/.test(this.newText.tokens[newToken].token ) === false) {
 
this.newText.tokens[newToken].link = oldToken;
Line 1,471 ⟶ 1,466:
symbols.linked = true;
 
// checkCheck if token contains unique word
if ( recursionLevel === 0 ) {
var unique = false;
if ( level == 'character' ) {
unique = true;
}
else {
var token = this.newText.tokens[newToken].token;
var words = ( token.match(wDiff this.regExpWordconfig.regExp.countWords ) || [] ).concat( token.match(wDiff this.regExpChunkconfig.regExp.countChunks ) || [] );
 
// uniqueUnique if longer than min block length
if ( words.length >= wDiffthis.config.blockMinLength ) {
unique = true;
}
 
// uniqueUnique if it contains at least one unique word
else {
for ( var word = 0; word < words.length; word ++ ) {
if ( ( this.oldText.words[ words[word] ] == 1 ) && ( this.newText.words[ words[word] ] == 1 ) ) {
unique = true;
break;
Line 1,497 ⟶ 1,492:
}
 
// setSet unique
if ( unique === true ) {
this.newText.tokens[newToken].unique = true;
this.oldText.tokens[oldToken].unique = true;
Line 1,508 ⟶ 1,503:
}
 
// continueContinue passes only if unique tokens have been linked previously
if ( symbols.linked === true ) {
 
//**
// pass* Pass 4: connect adjacent identical tokens downwards.
/ */
 
// getGet surrounding connected tokens
var i = newStart;
if ( this.newText.tokens[i].prev !== null ) {
i = this.newText.tokens[i].prev;
}
var iStop = newEnd;
if ( this.newText.tokens[iStop].next !== null ) {
iStop = this.newText.tokens[iStop].next;
}
var j = null;
 
// cycleCycle trough new text tokens list down
do {
 
// connectedConnected pair
var link = this.newText.tokens[i].link;
if ( link !== null ) {
j = this.oldText.tokens[link].next;
}
 
// connectConnect if tokens are the same
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;
Line 1,542 ⟶ 1,537:
}
 
// notNot same
else {
j = null;
}
i = this.newText.tokens[i].next;
} while ( i !== iStop );
 
//**
// pass* Pass 5: connect adjacent identical tokens upwards.
/ */
 
// getGet surrounding connected tokens
var i = newEnd;
if ( this.newText.tokens[i].next !== null ) {
i = this.newText.tokens[i].next;
}
var iStop = newStart;
if ( this.newText.tokens[iStop].prev !== null ) {
iStop = this.newText.tokens[iStop].prev;
}
var j = null;
 
// cycleCycle trough new text tokens list up
do {
 
// connectedConnected pair
var link = this.newText.tokens[i].link;
if ( link !== null ) {
j = this.oldText.tokens[link].prev;
}
 
// connectConnect if tokens are the same
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;
Line 1,580 ⟶ 1,575:
}
 
// notNot same
else {
j = null;
}
i = this.newText.tokens[i].prev;
} while ( i !== iStop );
 
//**
// connect* Connect adjacent identical tokens downwards from text start,
* treat boundary as connected, stop after first connected token.
/ */
 
// onlyOnly for full text diff
if ( ( newStart == this.newText.first ) && ( newEnd == this.newText.last ) ) {
 
// fromFrom start
var i = this.newText.first;
var j = this.oldText.first;
 
// cycleCycle trough new text tokens list down, connect identical tokens, stop after first connected token
while ( ( i !== null ) && ( j !== null ) && ( this.newText.tokens[i].link === null ) && ( this.oldText.tokens[j].link === null ) && ( this.newText.tokens[i].token == this.oldText.tokens[j].token ) ) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
j = this.oldText.tokens[j].next;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
 
// fromFrom end
var i = this.newText.last;
var j = this.oldText.last;
 
// cycleCycle trough old text tokens list up, connect identical tokens, stop after first connected token
while ( ( i !== null ) && ( j !== null ) && ( this.newText.tokens[i].link === null ) && ( this.oldText.tokens[j].link === null ) && ( this.newText.tokens[i].token == this.oldText.tokens[j].token ) ) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
j = this.oldText.tokens[j].prev;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
}
 
//**
// repeat* 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").
/ */
 
// newNew empty symbols object
if ( repeat !== true ) {
var symbolsRepeat = {
token: [],
hashhashTable: {},
linked: false
};
this.calculateDiff( symbolsRepeat, level, true, false, newStart, newEnd, oldStart, oldEnd );
}
 
//**
// refine* Refine by recursively diffing unresolved regions causedwith byempty additionsymbol of common tokens around sequences of common tokens, onlytable at word level split
* Helps against gaps caused by addition of common tokens around sequences of common tokens
//
*/
 
if ( ( recurse === true ) && (wDiff this.config.recursiveDiff === true ) ) {
 
//**
// recursively* Recursively diff still unresolved regions downwards.
/ */
 
// cycleCycle trough new text tokens list
var i = newStart;
var j = oldStart;
 
while ( ( i !== null ) && ( this.newText.tokens[i] !== null ) ) {
 
// getGet j from previous tokens match
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;
}
}
 
// checkCheck for the start of an unresolved sequence
if ( ( j !== null ) && ( this.oldText.tokens[j] !== null ) && ( this.newText.tokens[i].link === null ) && ( this.oldText.tokens[j].link === null ) ) {
 
// determineDetermine the limits of the unresolved new sequence
var iStart = i;
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;
}
Line 1,675 ⟶ 1,673:
}
 
// determineDetermine the limits of the unresolved old sequence
var jStart = j;
var jEnd = null;
var jLength = 0;
var jNext = j;
while ( ( jNext !== null ) && ( this.oldText.tokens[jNext].link === null ) ) {
jEnd = jNext;
jLength ++;
if ( jEnd == oldEnd ) {
break;
}
Line 1,689 ⟶ 1,687:
}
 
// recursivelyRecursively diff the unresolved sequence
if ( ( iLength > 1 ) || ( jLength > 1 ) ) {
 
// newNew empty symbols object for sub-region
var symbolsRecurse = {
token: [],
hashhashTable: {},
linked: false
};
this.calculateDiff( symbolsRecurse, level, false, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1 );
}
i = iEnd;
}
 
// nextNext list element
if ( i == newEnd ) {
break;
}
Line 1,710 ⟶ 1,708:
}
 
//**
// recursively* Recursively diff still unresolved regions upwards.
/ */
 
// cycleCycle trough new text tokens list
var i = newEnd;
var j = oldEnd;
while ( ( i !== null ) && ( this.newText.tokens[i] !== null ) ) {
 
// getGet j from next matched tokens
var iPrev = this.newText.tokens[i].next;
if ( iPrev !== null ) {
var jPrev = this.newText.tokens[iPrev].link;
if ( jPrev !== null ) {
j = this.oldText.tokens[jPrev].prev;
}
}
 
// checkCheck for the start of an unresolved sequence
if ( ( j !== null ) && ( this.oldText.tokens[j] !== null ) && ( this.newText.tokens[i].link === null ) && ( this.oldText.tokens[j].link === null ) ) {
 
// determineDetermine the limits of the unresolved new sequence
var iStart = null;
var iEnd = i;
var iLength = 0;
var iNext = i;
while ( ( iNext !== null ) && ( this.newText.tokens[iNext].link === null ) ) {
iStart = iNext;
iLength ++;
if ( iStart == newStart ) {
break;
}
Line 1,745 ⟶ 1,743:
}
 
// determineDetermine 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;
}
Line 1,759 ⟶ 1,757:
}
 
// recursivelyRecursively diff the unresolved sequence
if ( ( iLength > 1 ) || ( jLength > 1 ) ) {
 
// newNew empty symbols object for sub-region
var symbolsRecurse = {
token: [],
hashhashTable: {},
linked: false
};
this.calculateDiff( symbolsRecurse, level, false, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1 );
}
i = iStart;
}
 
// nextNext list element
if ( i == newStart ) {
break;
}
Line 1,782 ⟶ 1,780:
}
 
// stopStop timertimers
if ( (wDiff this.config.timer === true ) && ( repeat !== true) && (recursionLevel === 0) ) {
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 ) && ( repeat !== true ) && ( 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)
if ( this.config.timer === true ) {
this.time( 'setFixed' );
}
this.setFixed();
if ( this.config.timer === true ) {
this.time( 'setFixed' );
}
 
// Convert groups to insertions/deletions if maximum block length is too short
// collect deletion ('del') blocks from old text
var unlinkCount = 0;
this.getDelBlocks();
if ( ( this.config.unlinkBlocks === true ) && ( this.config.blockMinLength > 0 ) ) {
if ( this.config.timer === true ) {
this.time( 'unlink' );
}
 
// 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 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 {
 
// convertConvert '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.getSameBlocks();
this.getSections();
this.getGroups();
this.setFixed();
this.getDelBlocks();
this.positionDelBlocks();
}
}
} while (unlinked === true);
if ( this.config.timer === true ) {
this.timeEnd( 'unlink' );
}
}
 
// 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,878 ⟶ 1,897:
 
 
/**
// 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 () {
 
var blocks = this.blocks;
 
// clearClear blocks array
blocks.splice( 0 );
 
// cycleCycle through old text to find matched (linked) 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 token = this.oldText.tokens[j].token;
count ++;
if ( this.newText.tokens[i].unique === true ) {
unique = true;
}
stringtext += 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,930 ⟶ 1,950:
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 block = 0; block < blocks.length; block ++ ) {
blocks[block].newBlock = block;
}
Line 1,955 ⟶ 1,975:
 
 
/**
// 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 () {
 
Line 1,964 ⟶ 1,987:
var sections = this.sections;
 
// clearClear sections array
sections.splice( 0 );
 
// cycleCycle through blocks
for ( var block = 0; block < blocks.length; block ++ ) {
 
var sectionStart = block;
Line 1,976 ⟶ 1,999:
var sectionOldMax = oldMax;
 
// checkCheck right
for ( var j = sectionStart + 1; j < blocks.length; 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,989 ⟶ 2,012:
}
 
// 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;
}
Line 2,009 ⟶ 2,032:
 
 
/**
// 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 () {
 
Line 2,019 ⟶ 2,043:
var groups = this.groups;
 
// clearClear groups array
groups.splice( 0 );
 
// cycleCycle through blocks
for ( var block = 0; block < blocks.length; 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.length; 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,055 ⟶ 2,079:
}
 
// 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,082 ⟶ 2,106:
movedFrom: null,
color: null
} );
block = groupEnd;
}
Line 2,090 ⟶ 2,114:
 
 
/**
// 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 () {
 
Line 2,101 ⟶ 2,127:
var sections = this.sections;
 
// cycleCycle through sections
for ( var section = 0; section < sections.length; section ++ ) {
var blockStart = sections[section].blockStart;
var blockEnd = sections[section].blockEnd;
Line 2,109 ⟶ 2,135:
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,123 ⟶ 2,149:
}
 
// markMark fixed groups
for ( var i = 0; i < maxPath.length; 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;
}
Line 2,138 ⟶ 2,164:
 
 
/**
// 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,177 ⟶ 2,205:
}
 
// 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,190 ⟶ 2,218:
 
 
/**
// TextDiff.getDelBlocks(): collect deletion ('del') blocks from old text
* Collect deletion ('-') blocks from old text
// called from: .detectBlocks()
*
// changes: .blocks
* @param[in] WikEdDiffText oldText Old Text object
 
* @param[out] array blocks Blocks table object
*/
this.getDelBlocks = function () {
 
var blocks = this.blocks;
 
// cycleCycle through old text to find matched (linked) 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,224 ⟶ 2,254:
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;
Line 2,247 ⟶ 2,277:
 
 
/**
// 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
* Marks '|' and deletions '-' get newNumber of reference block
// new: 1 D 2 1 D 2
* and are sorted around it by old text number
// fixed: * *
*
// new number: 1 1 2 2
* @param[in/out] array blocks Blocks table, newNumber, section, group, and fixed properties
// 'mark' and 'del' get new number of reference block and are sorted around it by old text number
*
 
*/
this.positionDelBlocks = function () {
 
Line 2,265 ⟶ 2,296:
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 block = 0; block < blocksOld.length; 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, similar to position '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 ( ( prevBlock !== null ) && ( prevBlock.type == 'same=' ) && ( prevBlockNumber != groups[ prevBlock.group ].blockEnd ) ) {
refBlock = prevBlock;
}
 
// moveMove before next block if not start of group
else if ( ( nextBlock !== null ) && ( nextBlock.type == 'same=' ) && ( 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,329 ⟶ 2,360:
}
 
// moveMove before first block
if ( refBlock === null ) {
delBlock.newNumber = -1;
}
 
// updateUpdate 'del-' block data
else {
delBlock.newNumber = refBlock.newNumber;
Line 2,343 ⟶ 2,374:
}
 
// sortSort 'del-' blocks in and update groups
this.sortBlocks();
 
Line 2,350 ⟶ 2,381:
 
 
/**
// TextDiff.unlinkBlocks(): convert 'same' blocks in groups into 'ins'/'del' pairs if too short and too common
* Convert matching '=' blocks in groups into insertion/deletion ('+'/'-') pairs
// called from: .detectBlocks()
* if too short and too common
// calls: .unlinkSingleBlock()
* Prevents fragmentated diffs for very different versions
// changes: .newText/oldText[].link
*
// returns: true if text tokens were unlinked
* @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 () {
 
Line 2,361 ⟶ 2,396:
var groups = this.groups;
 
// cycleCycle 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 < this.config.blockMinLength ) && ( groups[group].unique === false ) ) {
// unlink whole group if no block is at least blockMinLength words long and unique
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( (groups[group].maxWords < wDiff.blockMinLength) && (groups[group].unique === false) ) {
for if (var blocks[block].type == blockStart; block <'=' blockEnd; block ++) {
if this.unlinkSingleBlock( (blocks[block].type == 'same') {;
this.unlinkSingleBlock(blocks[block]);
unlinked = true;
}
Line 2,377 ⟶ 2,411:
}
 
// otherwiseOtherwise unlink block flanks
else {
 
// unlinkUnlink blocks from start
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type == 'same=' ) {
 
// stopStop 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;
Line 2,394 ⟶ 2,428:
}
 
// unlinkUnlink blocks from end
for ( var block = blockEnd; block > blockStart; block -- ) {
if ( ( blocks[block].type == 'same=' ) ) {
 
// stopStop unlinking if more than one word or a unique word
if ( ( blocks[block].words > 1 ) || ( ( blocks[block].words == 1 ) && ( blocks[block].unique === true ) ) ) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
Line 2,412 ⟶ 2,446:
 
 
/**
// TextDiff.unlinkBlock(): unlink text tokens of single block, converting them into 'ins'/'del' pair
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs
// called from: .unlinkBlocks()
*
// changes: text.newText/oldText[].link
* @param[in] array blocks Blocks table object
* @param[out] WikEdDiffText newText, oldText Text objects, link property
*/
this.unlinkSingleBlock = function ( block ) {
 
// Cycle through old text
this.unlinkSingleBlock = function (block) {
 
// cycle through old text
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
 
// unlinkUnlink tokens
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
this.oldText.tokens[j].link = null;
Line 2,431 ⟶ 2,467:
 
 
/**
// TextDiff.getInsBlocks(): collect insertion ('ins') blocks from new text
* Collect insertion ('+') blocks from new text
// called from: .detectBlocks()
*
// calls: .sortBlocks()
* @param[in] WikEdDiffText newText New Text object
// changes: .blocks
* @param[out] array blocks Blocks table object
 
*/
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,470 ⟶ 2,507:
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();
 
Line 2,488 ⟶ 2,525:
 
 
/**
// 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,497 ⟶ 2,536:
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 block = 0; block < blocks.length; block ++ ) {
var blockGroup = blocks[block].group;
if ( blockGroup !== null ) {
if ( blockGroup !== group ) {
group = blocks[block].group;
groups[group].blockStart = block;
Line 2,523 ⟶ 2,562:
 
 
/**
// 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 () {
 
Line 2,532 ⟶ 2,573:
var groups = this.groups;
 
// setSet group numbers of 'ins+' blocks inside existing groups
for ( var group = 0; group < groups.length; 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,543 ⟶ 2,584:
}
 
// addAdd remaining 'ins+' blocks to new groups
 
// cycleCycle through blocks
for ( var block = 0; block < blocks.length; 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,564 ⟶ 2,605:
movedFrom: null,
color: null
} );
}
}
Line 2,571 ⟶ 2,612:
 
 
/**
// 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)*
* Mark direction: groups.movedGroup.blockStart < groups.group.blockStart
// fixed: * *
// * Group mark directionside: $groups.$movedGroup.blockStartoldNumber < .$groups[.$group].blockStartoldNumber
* Marks '|' and deletions '-' get newNumber of reference block
// group side: .movedGroup.oldNumber < .groups[group].oldNumber
// * 'mark' and 'del' get new number 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 () {
 
Line 2,594 ⟶ 2,638:
var color = 1;
 
// makeMake shallow copy of blocks
var blocksOld = blocks.slice();
 
// enumerateEnumerate copy
for ( var i = 0; i < blocksOld.length; 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;
} );
 
// createCreate lookup table: original to sorted
var lookupSorted = [];
for ( var i = 0; i < blocksOld.length; i ++ ) {
lookupSorted[ blocksOld[i].number ] = i;
}
 
// cycleCycle through groups (moved group)
for ( var moved = 0; moved < groups.length; 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, similar to position '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,662 ⟶ 2,706:
}
 
// 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,683 ⟶ 2,727:
movedFrom: null,
color: null
} );
}
else {
Line 2,690 ⟶ 2,734:
}
 
// insertInsert 'mark|' block
blocks.push( {
oldBlock: null,
newBlock: null,
Line 2,701 ⟶ 2,745:
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,715 ⟶ 2,759:
}
 
// sortSort 'mark|' blocks in and update groups
this.sortBlocks();
 
Line 2,722 ⟶ 2,766:
 
 
/**
// 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
//
for ( var group = 0; group < groupsSort.length; group ++ ) {
// create group diffs
//
 
// 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
if (wDiff.coloredBlocks === true) {
if ( ( type == '=' ) || ( type == '-' ) || ( type == '+' ) ) {
html = this.htmlCustomize(wDiff.htmlBlockLeftColoredStart, color);
fragments.push( {
text: blocks[block].text,
type: type,
color: color
} );
}
 
// Add '<' and '>' marks
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 = '>';
html = this.htmlCustomize(wDiff.htmlBlockLeftStart, color);
}
 
// Add mark
fragments.push( {
text: markText,
type: markType,
color: movedGroup.color
} );
}
}
else if (moveDir == 'right') {
 
if (wDiff.coloredBlocks === true) {
// Add moved block end
html = this.htmlCustomize(wDiff.htmlBlockRightColoredStart, color);
if ( color !== null ) {
fragments.push( {
text: '',
type: ' )',
color: color
} );
}
}
 
// Cycle through fragments, join consecutive fragments of same type (i.e. '-' blocks)
for ( var fragment = 1; fragment < fragments.length; fragment ++ ) {
 
// Check if joinable
if (
fragments[fragment].type == fragments[fragment - 1].type &&
fragments[fragment].color == fragments[fragment - 1].color &&
fragments[fragment].text !== '' && fragments[fragment - 1].text !== ''
) {
 
// 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
for ( var fragment = 0; fragment < fragments.length; 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;
if ( ( text.length < minRight ) && ( text.length < 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 !== text.length ) {
lines.push( text.length );
}
 
// 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 !== text.length ) {
paragraphs.push( text.length );
}
 
// 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 = text.length;
if ( this.config.clipLinesLeftMax < lines.length ) {
rangeLeftMax = lines[this.config.clipLinesLeftMax];
}
 
// Find first heading from left
if ( rangeLeft === null ) {
for ( var j = 0; j < headingsEnd.length; j ++ ) {
if ( ( headingsEnd[j] > this.config.clipHeadingLeft ) || ( headingsEnd[j] > rangeLeftMax ) ) {
break;
}
rangeLeft = headingsEnd[j];
rangeLeftType = 'heading';
break;
}
else {}
 
html = this.htmlCustomize(wDiff.htmlBlockRightStart, color);
// Find first paragraph from left
if ( rangeLeft === null ) {
for ( var j = 0; j < paragraphs.length; j ++ ) {
if ( ( paragraphs[j] > this.config.clipParagraphLeftMax ) || ( paragraphs[j] > rangeLeftMax ) ) {
break;
}
if ( paragraphs[j] > this.config.clipParagraphLeftMin ) {
rangeLeft = paragraphs[j];
rangeLeftType = 'paragraph';
break;
}
}
}
 
htmlFrags.push(html);
// Find first line break from left
if ( rangeLeft === null ) {
for ( var j = 0; j < lines.length; j ++ ) {
if ( ( lines[j] > this.config.clipLineLeftMax ) || ( lines[j] > rangeLeftMax ) ) {
break;
}
if ( lines[j] > this.config.clipLineLeftMin ) {
rangeLeft = lines[j];
rangeLeftType = 'line';
break;
}
}
}
 
// Find blank from left
if ( rangeLeft === null ) {
this.config.regExp.clipBlank.lastIndex = this.config.clipBlankLeftMin;
if ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if ( ( regExpMatch.index < this.config.clipBlankLeftMax ) && ( regExpMatch.index < rangeLeftMax ) ) {
rangeLeft = regExpMatch.index;
rangeLeftType = 'blank';
}
}
}
 
// Fixed number of chars from left
if ( rangeLeft === null ) {
if ( this.config.clipCharsLeft < rangeLeftMax ) {
rangeLeft = this.config.clipCharsLeft;
rangeLeftType = 'chars';
}
}
 
// Fixed number of lines from left
if ( rangeLeft === null ) {
rangeLeft = rangeLeftMax;
rangeLeftType = 'fixed';
}
}
 
// Find clip pos from right, skip for last non-container block
// cycle through blocks
forif (var blockfragment != blockStart;fragments.length block- <= blockEnd; block3 ++) {
var html = '';
var type = blocks[block].type;
var string = blocks[block].string;
 
// htmlMaximum escapelines textto stringsearch from right
var rangeRightMin = 0;
string = this.htmlEscape(string);
if ( lines.length >= this.config.clipLinesRightMax ) {
rangeRightMin = lines[lines.length - this.config.clipLinesRightMax];
}
 
// addFind 'same'last (unchanged)heading textfrom and moved blockright
if (type rangeRight === null 'same') {
iffor (color !var j = headings.length - 1; j >= null0; j -- ) {
if ( ( headings[j] < text.length - this.config.clipHeadingRight ) || ( headings[j] < rangeRightMin ) ) {
if (version != 'old') {
break;
html = this.htmlFormatBlock(string);
}
rangeRight = headings[j];
rangeRightType = 'heading';
break;
}
else {}
 
html = string;
// Find last paragraph from right
if ( rangeRight === null ) {
for ( var j = paragraphs.length - 1; j >= 0 ; j -- ) {
if ( ( paragraphs[j] < text.length - this.config.clipParagraphRightMax ) || ( paragraphs[j] < rangeRightMin ) ) {
break;
}
if ( paragraphs[j] < text.length - this.config.clipParagraphRightMin ) {
rangeRight = paragraphs[j];
rangeRightType = 'paragraph';
break;
}
}
}
 
// addFind 'del'last textline &&break (blocks[block].fixedfrom == true)right
else if ( (typerangeRight == 'del') && (version != 'new')null ) {
for ( var j = lines.length - 1; j >= 0; j -- ) {
 
if ( ( lines[j] < text.length - this.config.clipLineRightMax ) || ( lines[j] < rangeRightMin ) ) {
// for old version skip 'del' inside moved group
break;
if ( (version != 'old') || (color === null) ) {
if (wDiff.regExpBlankBlock.test(string) === true) {
html = wDiff.htmlDeleteStartBlank;
}
if ( lines[j] < text.length - this.config.clipLineRightMin ) {
else {
htmlrangeRight = wDiff.htmlDeleteStartlines[j];
rangeRightType = 'line';
break;
}
html += this.htmlFormatBlock(string) + wDiff.htmlDeleteEnd;
}
}
 
// addFind 'ins'last textblank from right
else if ( (typerangeRight == 'ins') && (version != 'old')null ) {
var startPos = text.length - this.config.clipBlankRightMax;
if (wDiff.regExpBlankBlock.test(string) === true) {
if ( startPos < rangeRightMin ) {
html = wDiff.htmlInsertStartBlank;
startPos = rangeRightMin;
}
this.config.regExp.clipBlank.lastIndex = startPos;
var lastPos = null;
while ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if ( regExpMatch.index > text.length - this.config.clipBlankRightMin ) {
if ( lastPos !== null ) {
rangeRight = lastPos;
rangeRightType = 'blank';
}
break;
}
lastPos = regExpMatch.index;
}
}
 
// Fixed number of chars from right
if ( rangeRight === null ) {
if ( text.length - this.config.clipCharsRight > rangeRightMin ) {
rangeRight = text.length - this.config.clipCharsRight;
rangeRightType = 'chars';
}
}
 
// Fixed number of lines from right
if ( rangeRight === null ) {
rangeRight = rangeRightMin;
rangeRightType = 'fixed';
}
}
 
// Check if we skip clipping if ranges are close together
if ( ( rangeLeft !== null ) && ( rangeRight !== null ) ) {
 
// Skip if overlapping ranges
if ( rangeLeft > rangeRight ) {
continue;
}
 
// Skip if chars too close
var skipChars = rangeRight - rangeLeft;
if ( skipChars < this.config.clipSkipChars ) {
continue;
}
 
// Skip if lines too close
var skipLines = 0;
for ( var j = 0; j < lines.length; j ++ ) {
if ( ( lines[j] > rangeRight ) || ( skipLines > this.config.clipSkipLines ) ) {
break;
}
if ( lines[j] > rangeLeft ) {
skipLines ++;
}
}
if ( skipLines < this.config.clipSkipLines ) {
continue;
}
}
 
// Skip if nothing to clip
if ( ( rangeLeft === null ) && ( rangeRight === null ) ) {
continue;
}
 
// Split left text
var textLeft = null;
var omittedLeft = null;
if ( rangeLeft !== null ) {
textLeft = text.slice( 0, rangeLeft );
 
// Remove trailing empty lines
textLeft = textLeft.replace( this.config.regExp.clipTrimNewLinesLeft, '' );
 
// Get omission indicators, remove trailing blanks
if ( rangeLeftType == 'chars' ) {
omittedLeft = '~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
else if ( rangeLeftType == 'blank' ) {
omittedLeft = ' ~';
textLeft = textLeft.replace( this.config.regExp.clipTrimBlanksLeft, '' );
}
}
 
// Split right text,
var textRight = null;
var omittedRight = null;
if ( rangeRight !== null ) {
textRight = text.slice( rangeRight );
 
// Remove leading empty lines
textRight = textRight.replace( this.config.regExp.clipTrimNewLinesRight, '' );
 
// Get omission indicators, remove leading blanks
if ( rangeRightType == 'chars' ) {
omittedRight = '~';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
else if ( rangeRightType == 'blank' ) {
omittedRight = '~ ';
textRight = textRight.replace( this.config.regExp.clipTrimBlanksRight, '' );
}
}
 
// Remove split element
fragments.splice( fragment, 1 );
 
// Add left text to fragments list
if ( rangeLeft !== null ) {
fragments.splice( fragment ++, 0, { text: textLeft, type: '=', color: null } );
if ( omittedLeft !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedLeft, color: null } );
}
}
 
// 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 } );
}
 
// Add right text to fragments list
if ( rangeRight !== null ) {
if ( omittedRight !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedRight, color: null } );
}
fragments.splice( fragment ++, 0, { text: textRight, type: '=', color: null } );
}
}
 
// Debug log
if ( this.config.debug === true ) {
this.debugFragments( 'Fragments' );
}
 
return;
};
 
 
/**
* Create html formatted diff code from diff fragments
*
* @param[in] array fragments Fragments array, abstraction layer for diff code
* @param string|undefined version
* Output version: 'new' or 'old': only text from new or old version, used for unit tests
* @param[out] string html Html code of diff
*/
this.getDiffHtml = function ( version ) {
 
var fragments = this.fragments;
 
// No change, only one unchanged block in containers
if ( ( fragments.length == 5 ) && ( fragments[2].type == '=' ) ) {
this.html =
this.config.htmlCode.noChangeStart +
this.htmlEscape( this.config.msg['wiked-diff-empty'] ) +
this.config.htmlCode.noChangeEnd;
return;
}
 
// Cycle through fragments
var htmlFragments = [];
for ( var fragment = 0; fragment < fragments.length; fragment ++ ) {
var text = fragments[fragment].text;
var type = fragments[fragment].type;
var color = fragments[fragment].color;
var html = '';
 
// Test if text is blanks-only or a single character
var blank = false;
if ( text !== '' ) {
blank = this.config.regExp.blankBlock.test( text );
}
 
// Add container start markup
if ( type == '{' ) {
html = this.config.htmlCode.containerStart;
}
 
// Add container end markup
else if ( type == '}' ) {
html = this.config.htmlCode.containerEnd;
}
 
// Add fragment start markup
if ( type == '[' ) {
html = this.config.htmlCode.fragmentStart;
}
 
// Add fragment end markup
else if ( type == ']' ) {
html = this.config.htmlCode.fragmentEnd;
}
 
// Add fragment separator markup
else if ( type == ',' ) {
html = this.config.htmlCode.separator;
}
 
// Add omission markup
if ( type == '~' ) {
html = this.config.htmlCode.omittedChars;
}
 
// Add omission markup
if ( type == ' ~' ) {
html = ' ' + this.config.htmlCode.omittedChars;
}
 
// Add omission markup
if ( type == '~ ' ) {
html = this.config.htmlCode.omittedChars + ' ';
}
 
// Add colored left-pointing block start markup
else if ( type == '(<' ) {
if ( version != 'old' ) {
 
// Get title
var title;
if ( this.config.noUnicodeSymbols === true ) {
title = this.config.msg['wiked-diff-block-left-nounicode'];
}
else {
title = this.config.msg['wiked-diff-block-left'];
}
 
// Get html
if ( this.config.coloredBlocks === true ) {
html = this.config.htmlCode.blockColoredStart;
}
else {
html = wDiffthis.htmlInsertStartconfig.htmlCode.blockStart;
}
html += this.htmlFormatBlockhtmlCustomize(string) +html, wDiff.htmlInsertEndcolor, title );
}
}
 
// Add colored right-pointing block start markup
// add 'mark' code
else if ( (type == 'mark(>' ) &&{
if ( version != 'newold') ) {
var moved = blocks[block].moved;
var movedGroup = groups[moved];
var markColor = movedGroup.color;
 
// Get title
var title;
if ( this.config.noUnicodeSymbols === true ) {
title = this.config.msg['wiked-diff-block-right-nounicode'];
}
else {
title = this.config.msg['wiked-diff-block-right'];
}
 
// Get html
// get moved block text ('same' and 'del')
if ( this.config.coloredBlocks === true ) {
var string = '';
html = this.config.htmlCode.blockColoredStart;
for (var mark = movedGroup.blockStart; mark <= movedGroup.blockEnd; mark ++) {
}
if ( (blocks[mark].type == 'same') || (blocks[mark].type == 'del') ) {
else {
string += blocks[mark].string;
html = this.config.htmlCode.blockStart;
}
html = this.htmlCustomize( html, color, title );
}
}
 
// Add colored block end markup
else if ( type == ' )' ) {
if ( version != 'old' ) {
html = this.config.htmlCode.blockEnd;
}
}
 
// Add '=' (unchanged) text and moved block
if ( type == '=' ) {
text = this.htmlEscape( text );
if ( color !== null ) {
if ( version != 'old' ) {
html = this.markupBlanks( text, true );
}
}
else {
html = this.markupBlanks( text );
}
}
 
// Add '-' text
else if ( type == '-' ) {
if ( version != 'new' ) {
 
// For old version skip '-' inside moved group
if ( ( version != 'old' ) || ( color === null ) ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
if ( blank === true ) {
html = this.config.htmlCode.deleteStartBlank;
}
else {
html = this.config.htmlCode.deleteStart;
}
html += text + this.config.htmlCode.deleteEnd;
}
}
}
 
// Add '+' text
// display as deletion at original position
else if ( (wDiff.showBlockMoves === false) || (versiontype == 'old+') ) {
if ( version != 'old' ) {
string = this.htmlEscape(string);
stringtext = this.htmlFormatBlockhtmlEscape(string text );
text = this.markupBlanks( text, true );
if (version == 'old') {
if ( blank === true ) {
if (movedGroup.blockStart < groupsSort[group].blockStart) {
html = this.config.htmlCode.insertStartBlank;
if (wDiff.coloredBlocks === true) {
}
html = this.htmlCustomize(wDiff.htmlBlockLeftColoredStart, markColor) + string + wDiff.htmlBlockLeftEnd;
}else {
html = this.config.htmlCode.insertStart;
else {
}
html = this.htmlCustomize(wDiff.htmlBlockLeftStart, markColor) + string + wDiff.htmlBlockLeftEnd;
html += text + this.config.htmlCode.insertEnd;
}
}
}
 
// Add '<' and '>' code
else if ( ( type == '<' ) || ( type == '>' ) ) {
if ( version != 'new' ) {
 
// Display as deletion at original position
if ( ( this.config.showBlockMoves === false ) || ( version == 'old' ) ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
if ( version == 'old' ) {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.blockColoredStart, color ) + text + this.config.htmlCode.blockEnd;
}
else {
html = this.htmlCustomize( this.config.htmlCode.blockStart, color ) + text + this.config.htmlCode.blockEnd;
if (wDiff.coloredBlocks === true) {
html = this.htmlCustomize(wDiff.htmlBlockRightColoredStart, markColor) + string + wDiff.htmlBlockRightEnd;
}
else {
html = this.htmlCustomize(wDiff.htmlBlockRightStart, markColor) + string + wDiff.htmlBlockRightEnd;
}
}
}
else {
if (wDiff.regExpBlankBlock.test(string) blank === true ) {
html = wDiffthis.htmlDeleteStartBlankconfig.htmlCode.deleteStartBlank + stringtext + wDiffthis.config.htmlCode.htmlDeleteEnddeleteEnd;
}
else {
html = wDiffthis.htmlDeleteStartconfig.htmlCode.deleteStart + stringtext + wDiffthis.config.htmlCode.htmlDeleteEnddeleteEnd;
}
}
}
 
// displayDisplay as mark, get mark direction
else {
if (movedGroup.blockStart type == '<' groupsSort[group].blockStart) {
if (wDiff this.config.coloredBlocks === true ) {
html = this.htmlCustomize(wDiff this.htmlMarkLeftColoredconfig.htmlCode.markLeftColored, markColorcolor, stringtext );
}
else {
html = this.htmlCustomize(wDiff this.htmlMarkLeftconfig.htmlCode.markLeft, markColorcolor, stringtext );
}
}
else {
if (wDiff this.config.coloredBlocks === true ) {
html = this.htmlCustomize(wDiff this.htmlMarkRightColoredconfig.htmlCode.markRightColored, markColorcolor, stringtext );
}
else {
html = this.htmlCustomize(wDiff this.htmlMarkRightconfig.htmlCode.markRight, markColorcolor, stringtext );
}
}
}
}
htmlFrags.push(html);
}
 
// add colored block end markup
if (version != 'old') {
var html = '';
if (moveDir == 'left') {
html = wDiff.htmlBlockLeftEnd;
}
else if (moveDir == 'right') {
html = wDiff.htmlBlockRightEnd;
}
htmlFrags.push(html);
}
htmlFragments.push( html );
}
 
// joinJoin fragments
this.html = htmlFragshtmlFragments.join( '' );
 
return;
Line 2,921 ⟶ 3,500:
 
 
//**
* Customize html code fragments
// TextDiff.htmlCustomize(): customize move indicator html: {block}: block number style, {mark}: mark number style, {class}: class number, {number}: block number, {title}: title attribute (popup)
* Replaces:
// input: text (html or css code), number: block number, title: title attribute (popup) text
* {number}: class/color/block/mark/id number
// returns: customized text
* {title}: title attribute (popup)
// called from: .assembleDiff()
* {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
this.htmlCustomize = function (text, number, title) {
html = html.replace( /\{number\}/g, number);
 
// Replace {nounicode} with wikEdDiffNoUnicode class name
if (wDiff.coloredBlocks === true) {
if ( this.config.noUnicodeSymbols === true ) {
var blockStyle = wDiff.styleBlockColor[number];
html = html.replace( /\{nounicode\}/g, ' wikEdDiffNoUnicode');
if (blockStyle === undefined) {
blockStyle = '';
}
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 {
texthtml = texthtml.replace( /\{block\}|\{mark\}|\{classnounicode\}/g, '');
}
text = text.replace(/\{number\}/g, number);
 
// shortenShorten title text, replace {title}
if ( (title !== undefined) && (title !== '') ) {
var max = 512;
var end = 128;
var gapMark = ' [...] ';
if ( title.length > max ) {
title = title.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;');
texthtml = texthtml.replace( /\{title\}/, ' title="' + title + '"');
}
else {
text = text.replace(/\{title\}/, '');
}
return texthtml;
};
 
 
/**
// TextDiff.htmlEscape(): replace html-sensitive characters in output text with character entities
* Replace html-sensitive characters in output text with character entities
// input: html text
*
// returns: escaped html text
* @param string html Html code to be escaped
// called from: .diff(), .assembleDiff()
* @return string Escaped html code
*/
this.htmlEscape = function ( html ) {
 
html = html.replace( /&/g, '&amp;');
this.htmlEscape = function (html) {
html = html.replace( /</g, '&lt;');
 
html = html.replace( /&>/g, '&ampgt;');
html = html.replace( /<"/g, '&ltquot;');
html =return html.replace(/>/g, '&gt;');
html = html.replace(/"/g, '&quot;');
return (html);
};
 
 
/**
// TextDiff.htmlFormatBlock(): markup newlines and spaces in blocks
* Markup tabs, newlines, and spaces in diff fragment text
// input: string
*
// returns: formatted string
* @param bool highlight Highlight newlines and tabs in addition to spaces
// called from: .diff(), .assembleDiff()
* @param string html Text code to be marked-up
* @return string Marked-up text
*/
this.markupBlanks = function ( html, highlight ) {
 
html = html.replace( / /g, this.config.htmlCode.space);
this.htmlFormatBlock = function (string) {
if ( highlight === true ) {
 
html = html.replace( /\n/g, this.config.htmlCode.newline);
// spare blanks in tags
string html = stringhtml.replace(/(<[^>]*>)|( )/\t/g, function (p, p1, p2this.config.htmlCode.tab) {;
}
if (p2 == ' ') {
return wDiff.htmlSpacehtml;
}
return p1;
});
string = string.replace(/\n/g, wDiff.htmlNewline);
return string;
};
 
 
/**
// TextDiff.shortenOutput(): shorten diff html by removing unchanged sections
* Count real words in text
// input: diff html string from .diff()
*
// returns: shortened html with removed unchanged passages indicated by (...) or separator
* @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;
this.shortenOutput = function () {
};
 
var html = this.html;
var diff = '';
 
/**
// scan for diff html tags
* Test diff code for consistency with input versions
var regExpDiff = /<\w+\b[^>]*\bclass="[^">]*?\bwDiff(MarkLeft|MarkRight|BlockLeft|BlockRight|Delete|Insert)\b[^">]*"[^>]*>(.|\n)*?<!--wDiff\1-->/g;
* Prints results to debug console
var tagsStart = [];
*
var tagsEnd = [];
* @param[in] WikEdDiffText newText, oldText Text objects
var i = 0;
*/
this.unitTests = function () {
 
// Check if output is consistent with new text
// save tag positions
this.getDiffHtml( 'new' );
var regExpMatch;
var diff = this.html.replace( /<[^>]*>/g, '');
while ( (regExpMatch = regExpDiff.exec(html)) !== null ) {
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
// combine consecutive diff tags
this.getDiffHtml( 'old' );
if ( (i > 0) && (tagsEnd[i - 1] == regExpMatch.index) ) {
var diff = this.html.replace( /<[^>]*>/g, '');
tagsEnd[i - 1] = regExpMatch.index + regExpMatch[0].length;
var text = this.htmlEscape( this.oldText.text );
}
if ( diff != text ) {
else {
console.log( 'Error: wikEdDiff unit test failure: diff not consistent with old text version!' );
tagsStart[i] = regExpMatch.index;
this.error = true;
tagsEnd[i] = regExpMatch.index + regExpMatch[0].length;
console.log( 'old text:\n', text );
i ++;
console.log( 'old diff:\n', diff );
}
}
else {
console.log( 'OK: wikEdDiff unit test passed: diff consistent with old text.' );
}
 
return;
// no diff tags detected
};
if (tagsStart.length === 0) {
 
this.html = wDiff.htmlNoChange;
 
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';
for ( var i = 0; i < blocks.length; 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 );
};
 
// define regexps
var regExpLine = /^(\n+|.)|(\n+|.)$|\n+/g;
var regExpHeading = /(^|\n)(<[^>]+>)*(==+.+?==+|\{\||\|\}).*?\n?/g;
var regExpParagraph = /^(\n\n+|.)|(\n\n+|.)$|\n\n+/g;
var regExpBlank = /(<[^>]+>)*\s+/g;
 
/**
// get line positions
* Dump groups object to browser console
var lines = [];
*
var regExpMatch;
* @param string name Group name
while ( (regExpMatch = regExpLine.exec(html)) !== null) {
* @param[in] array groups Groups table object
lines.push(regExpMatch.index);
*/
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';
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;
// get heading positions
var headingsdump = []'\ni \ttype \tcolor \ttext\n';
for ( var i = 0; i < fragments.length; i ++ ) {
var headingsEnd = [];
dump += i + ' \t"' + fragments[i].type + '" \t' + fragments[i].color + ' \t' + this.debugShortenText( fragments[i].text, 120, 40 ) + '\n';
while ( (regExpMatch = regExpHeading.exec(html)) !== null ) {
headings.push(regExpMatch.index);
headingsEnd.push(regExpMatch.index + regExpMatch[0].length);
}
console.log( name + ':\n' + dump );
};
 
 
// get paragraph positions
/**
var paragraphs = [];
* Shorten text for dumping
while ( (regExpMatch = regExpParagraph.exec(html)) !== null ) {
*
paragraphs.push(regExpMatch.index);
* @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 + '"';
};
 
// determine fragment border positions around diff tags
var lineMaxBefore = 0;
var headingBefore = 0;
var paragraphBefore = 0;
var lineBefore = 0;
 
/**
var lineMaxAfter = 0;
* Start timer 'label', analogous to JavaScript console timer.
var headingAfter = 0;
* Usage: this.time('label');
var paragraphAfter = 0;
*
var lineAfter = 0;
* @param string label Timer label
* @param[out] array timer
*/
this.time = function ( label ) {
 
this.timer[label] = new Date().getTime();
var rangeStart = [];
return;
var rangeEnd = [];
};
var rangeStartType = [];
var rangeEndType = [];
 
// 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
* Stop timer 'label', analogous to JavaScript console timer
var rangeStartMin = 0;
* Logs time in milliseconds since start to browser console
for (var j = lineMaxBefore; j < lines.length - 1; j ++) {
* Usage: this.timeEnd('label');
if (tagStart < lines[j + 1]) {
*
if (j - wDiff.linesBeforeMax >= 0) {
* @param string label Timer label.
rangeStartMin = lines[j - wDiff.linesBeforeMax];
* @param bool noLog Do not log result.
}
* @return float Time in milliseconds, rounded to two decimal digits.
lineMaxBefore = j;
*/
break;
this.timeEnd = function ( label, noLog ) {
}
}
 
var diff = 0;
// find last heading before diff tag
if (rangeStart this.timer[ilabel] =!== undefined ) {
var start = this.timer[label];
for (var j = headingBefore; j < headings.length - 1; j ++) {
var stop = new Date().getTime();
if (headings[j] > tagStart) {
diff = stop - start;
break;
this.timer[label] = undefined;
}
if (headings[j +noLog 1]!== >true tagStart) {
console.log( label + ': ' + diff + ' ms' );
if ( (headings[j] > tagStart - wDiff.headingBefore) && (headings[j] > rangeStartMin) ) {
rangeStart[i] = headings[j];
rangeStartType[i] = 'heading';
headingBefore = j;
}
break;
}
}
}
}
return diff;
};
 
// find last paragraph before diff tag
if (rangeStart[i] === undefined) {
for (var j = paragraphBefore; j < paragraphs.length - 1; j ++) {
if (paragraphs[j] > tagStart) {
break;
}
if (paragraphs[j + 1] > tagStart - wDiff.paragraphBeforeMin) {
if ( (paragraphs[j] > tagStart - wDiff.paragraphBeforeMax) && (paragraphs[j] > rangeStartMin) ) {
rangeStart[i] = paragraphs[j];
rangeStartType[i] = 'paragraph';
paragraphBefore = j;
}
break;
}
}
}
 
/**
// find last line break before diff tag
* Log recursion timer results to browser console
if (rangeStart[i] === undefined) {
* Usage: this.timeRecursionEnd();
for (var j = lineBefore; j < lines.length - 1; j ++) {
*
if (lines[j + 1] > tagStart - wDiff.lineBeforeMin) {
* @param string text Text label for output
if ( (lines[j] > tagStart - wDiff.lineBeforeMax) && (lines[j] > rangeStartMin) ) {
* @param[in] array recursionTimer Accumulated recursion times
rangeStart[i] = lines[j];
*/
rangeStartType[i] = 'line';
this.timeRecursionEnd = function ( text ) {
lineBefore = j;
}
break;
}
}
}
 
if ( this.recursionTimer.length > 1 ) {
// find last blank before diff tag
if (rangeStart[i] === undefined) {
var lastPos = tagStart - wDiff.blankBeforeMax;
if (lastPos < rangeStartMin) {
lastPos = rangeStartMin;
}
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;
}
}
 
// fixedSubtract numbertimes ofspent charsin beforedeeper diff tagrecursions
for ( var i = 0; i < this.recursionTimer.length - 1; i ++ ) {
if (rangeStart[i] === undefined) {
this.recursionTimer[i] -= this.recursionTimer[i + 1];
if (tagStart - wDiff.charsBefore > rangeStartMin) {
rangeStart[i] = tagStart - wDiff.charsBefore;
rangeStartType[i] = 'chars';
}
}
 
// Log recursion times
// fixed number of lines before diff tag
for ( var i = 0; i < this.recursionTimer.length; i ++ ) {
if (rangeStart[i] === undefined) {
console.log( text + ' recursion ' + i + ': ' + this.recursionTimer[i] + ' ms\n' );
rangeStart[i] = rangeStartMin;
rangeStartType[i] = 'lines';
}
}
this.recursionTimer = [];
return;
};
 
// maximal lines to search after diff tag
var rangeEndMax = html.length;
for (var j = lineMaxAfter; j < lines.length; j ++) {
if (lines[j] > tagEnd) {
if (j + wDiff.linesAfterMax < lines.length) {
rangeEndMax = lines[j + wDiff.linesAfterMax];
}
lineMaxAfter = j;
break;
}
}
 
/**
// find first heading after diff tag
* Log variable values to debug console
if (rangeEnd[i] === undefined) {
* Usage: this.debug('var', var);
for (var j = headingAfter; j < headingsEnd.length; j ++) {
*
if (headingsEnd[j] > tagEnd) {
* @param string name Object identifier
if ( (headingsEnd[j] < tagEnd + wDiff.headingAfter) && (headingsEnd[j] < rangeEndMax) ) {
* @param mixed|undefined name Object to be logged
rangeEnd[i] = headingsEnd[j];
*/
rangeEndType[i] = 'heading';
this.debug = function ( name, object ) {
paragraphAfter = j;
}
break;
}
}
}
 
if ( object === undefined ) {
// find first paragraph after diff tag
console.log( name );
if (rangeEnd[i] === undefined) {
}
for (var j = paragraphAfter; j < paragraphs.length; j ++) {
else {
if (paragraphs[j] > tagEnd + wDiff.paragraphAfterMin) {
console.log( name + ': ' + object );
if ( (paragraphs[j] < tagEnd + wDiff.paragraphAfterMax) && (paragraphs[j] < rangeEndMax) ) {
}
rangeEnd[i] = paragraphs[j];
return;
rangeEndType[i] = 'paragraph';
};
paragraphAfter = j;
}
break;
}
}
}
 
// find first line break after diff tag
if (rangeEnd[i] === undefined) {
for (var j = lineAfter; j < lines.length; j ++) {
if (lines[j] > tagEnd + wDiff.lineAfterMin) {
if ( (lines[j] < tagEnd + wDiff.lineAfterMax) && (lines[j] < rangeEndMax) ) {
rangeEnd[i] = lines[j];
rangeEndType[i] = 'line';
lineAfter = j;
}
break;
}
}
}
 
/**
// find blank after diff tag
* Add script to document head
if (rangeEnd[i] === undefined) {
*
regExpBlank.lastIndex = tagEnd + wDiff.blankAfterMin;
* @param string code JavaScript code
if ( (regExpMatch = regExpBlank.exec(html)) !== null ) {
*/
if ( (regExpMatch.index < tagEnd + wDiff.blankAfterMax) && (regExpMatch.index < rangeEndMax) ) {
this.addScript = function ( code ) {
rangeEnd[i] = regExpMatch.index;
rangeEndType[i] = 'blank';
}
}
}
 
if ( document.getElementById( 'wikEdDiffBlockHandler' ) === null ) {
// fixed number of chars after diff tag
var script = document.createElement( 'script' );
if (rangeEnd[i] === undefined) {
script.id = 'wikEdDiffBlockHandler';
if (tagEnd + wDiff.charsAfter < rangeEndMax) {
if ( script.innerText !== undefined ) {
rangeEnd[i] = tagEnd + wDiff.charsAfter;
rangeEndType[i]script.innerText = 'chars'code;
}
}
else {
 
script.textContent = code;
// fixed number of lines after diff tag
if (rangeEnd[i] === undefined) {
rangeEnd[i] = rangeEndMax;
rangeEndType[i] = 'lines';
}
document.getElementsByTagName( 'head' )[0].appendChild( script );
}
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
* Add stylesheet to document head, cross-browser >= IE6
var lines = 0;
*
if (fragmentEnd[j - 1] < rangeStart[i]) {
* @param string css CSS code
var join = html.substring(fragmentEnd[j - 1], rangeStart[i]);
*/
lines = (join.match(/\n/g) || []).length;
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
this.html = diff;
*/
WikEdDiff.WikEdDiffText = function ( text, parent ) {
 
/** @var WikEdDiff parent Parent object for configuration settings and debugging methods */
// markup tabs, add container
this.htmlFormat()parent = parent;
 
/** @var string text Text of this version */
return;
this.text = null;
};
 
/** @var array tokens Tokens list */
this.tokens = [];
 
/** @var int first, last First and last index of tokens list */
// TextDiff.htmlFormat(): markup tabs, add container
this.first = null;
// changes: .diff
this.last = null;
// called from: .diff(), .shortenOutput()
 
/** @var array words Word counts for version text */
this.htmlFormat = function () {
this.words = {};
 
this.html = this.html.replace(/\t/g, wDiff.htmlTab);
this.html = wDiff.htmlContainerStart + this.html + wDiff.htmlContainerEnd;
return;
};
 
/**
* Constructor, initialize text object
*
* @param string text Text of version
* @param WikEdDiff parent Parent, for configuration settings and debugging methods
*/
this.init = function () {
 
// if wDiff.wordCount(): counttypeof wordstext in!= 'string' ) {
text = text.toString();
// called from: .getGroups(), .getSameBlocks()
}
//
 
// IE / Mac fix
this.wordCount = function (string) {
this.text = text.replace( /\r\n?/g, '\n');
 
// parse and count words and chunks for identification of unique real words
return (string.match(wDiff.regExpWord) || []).length;
if ( this.parent.config.timer === true ) {
this.parent.time( 'wordParse' );
}
this.wordParse( this.parent.config.regExp.countWords );
this.wordParse( this.parent.config.regExp.countChunks );
if ( this.parent.config.timer === true ) {
this.parent.timeEnd( 'wordParse' );
}
return;
};
 
 
/**
// TextDiff.debugBlocks(): dump blocks object for debugging
* Parse and count words and chunks for identification of unique words
// input: text: title, group: block 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.debugBlocks = function (text, blocks) {
while ( ( regExpMatch = regExp.exec( this.text ) ) !== null ) {
 
var word = regExpMatch[0];
if (blocks === undefined) {
blocksif =( this.blocks;words[word] === undefined ) {
this.words[word] = 1;
}
else {
this.words[word] ++;
}
}
return;
var dump = '\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq \twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \tstring\n';
for (var i = 0; i < blocks.length; 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.debugShortenString(blocks[i].string) + '\n';
}
console.log(text + ':\n' + dump);
};
 
 
/**
// TextDiff.debugGroups(): dump groups object for debugging
* Split text into paragraph, sentence, chunk, word, or character tokens
// input: text: title, group: group object (optional)
*
//
* @param string level Level of splitting: paragraph, 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;
this.debugGroups = function (text, groups) {
var next = null;
var current = this.tokens.length;
var first = current;
var text = '';
 
// Split full text or specified token
if (groups === undefined) {
if ( token === undefined ) {
groups = this.groups;
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;
while ( ( regExpMatch = this.parent.config.regExp.split[level].exec( text ) ) !== null ) {
if ( regExpMatch.index > lastIndex ) {
split.push( text.substring( lastIndex, regExpMatch.index ) );
}
split.push( regExpMatch[0] );
lastIndex = this.parent.config.regExp.split[level].lastIndex;
}
if ( lastIndex < text.length ) {
split.push( text.substring( lastIndex ) );
}
 
// Cycle trough new tokens
// TextDiff.debugShortenString(): shorten string for dumping
for ( var i = 0; i < split.length; i ++ ) {
// called from .debugBlocks, .debugGroups, Text.debugText
//
 
// Insert current item, link to previous
this.debugShortenString = function (string) {
this.tokens[current] = {
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: 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 ) && ( this.tokens[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] !== 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 ) && ( tokens[i] !== null ) ) {
}
dump += i + ' \t' + tokens[i].link + ' \t( ' + tokens[i].prev + ' \t' + tokens[i].next + ' ) \t' + tokens[i].unique + ' \t#' + tokens[i].number + ' \t' + parent.debugShortenText( tokens[i].token ) + '\n';
else {
i = tokens[i].next;
style.appendChild( document.createTextNode(css) );
}
console.log( name + ':\n' + dump );
document.getElementsByTagName('head')[0].appendChild(style);
return;
};
 
 
// Initialize WikEdDiffText object
// initialize wDiff
wDiff this.init();
};
 
// </syntaxhighlight>