User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.2.1 (October 10, 2014) rm fragment container for no change message
another background that could use some darkmode friendly color
 
(9 intermediate revisions by 3 users not shown)
Line 3:
// ==UserScript==
// @name wikEd diff
// @version 1.2.14
// @date October 0923, 2014
// @description improved word-based diff library with block move detection
// @homepage https://en.wikipedia.org/wiki/User:Cacycle/diff
Line 12:
// ==/UserScript==
 
/**
* wikEd diff: inline-style difference engine with block move support
*
* Improved JavaScript diff library that returns html/css-formatted new text version with highlighted
* highlighted deletions, insertions, and block moves. It is compatible with all browsers and is not dependent on
* not dependent on external libraries.
*
* WikEdDiff.php and the JavaScript library wikEd diff are synced one-to-one ports. Changes and
Line 26:
* 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
* to identify matching text and moved blocks (Paul Heckel: A technique for isolating differences between
* between files. Communications of the ACM 21(4):264 (1978)).
*
* Additional features:
Line 36:
* - Resolution down to characters level
* - Unicode and multilingual support
* - Stepwise split (paragraphs, lines, sentences, words, characters)
* - Recursive diff
* - Optimized code for resolving unmatched sequences
Line 70:
* .newText new text
* .oldText old text
* .maxWords word count of longest linked block
* .html diff html
* .error flag: result has not passed unit tests
* .bordersDown[] linked region borders downwards, [new index, old index]
* .bordersUp[] linked region borders upwards, [new index, old index]
* .symbols: symbols table for whole text at all refinement levels
* .token[] hash table of parsed tokens for passes 1 - 3, points to symbol[i]
* .symbol[]: array of objects that hold token counters and pointers:
* .newCount new text token counter (NC)
* .oldCount old text token counter (OC)
* .newToken token index in text.newText.tokens
* .oldToken token index in text.oldText.tokens
* .linked flag: at least one unique token pair has been linked
*
* symbols .blocks[]: array, block objectdata for(consecutive symbolstext tabletokens) datain new text order
* .token[] .oldBlock hash tablenumber of parsed tokens for passes 1 -block 3,in pointsold totext symbol[i]order
* .symbol[]: .newBlock array of objects thatnumber of holdblock tokenin countersnew andtext pointers:order
* .newCountoldNumber newold text token counternumber (NC)of first token
* .oldCountnewNumber oldnew text token counternumber (OC)of first token
* .newTokenoldStart old text token index inof text.newText.tokensfirst token
* .oldTokencount token index in text.oldText.number of tokens
* .linked .unique flag: at least onecontains unique linked token pair has been linked
* .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
*
* .blockssections[]: array, block datasections (consecutivewith textno tokens)block move incrosses newoutside texta ordersection
* .oldBlockblockStart number offirst block in old text ordersection
* .newBlockblockEnd number of last block in new text ordersection
* .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
* .oldNumber first block oldNumber
* .blockStart first block index
* .blockEnd last block index
* .unique contains unique matchedlinked token
* .maxWords word count of longest linked block
* .words word count
* .chars char count
* .fixed not moved from original position
* .movedFrom group position this group has been moved from
* .color color number of moved group
*
* .fragments[]: diff fragment list ready for markup, abstraction layer for customization
* .text block or mark text
* .color moved block or mark color number
* .type '=', '-', '+' same, deletion, insertion
* '<', '>' mark left, mark right
* '(<', '(>', ')' block start and end
* '~', ' ~', '~ ' omission indicators
* '[', ']', ',' fragment start and end, fragment separator
* '{', '}' container start and end
*
*/
 
 
// JSHint options
/* 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 */
 
Line 135 ⟶ 138:
'use strict';
 
/** Define global objects. */
var wikEdDiffConfig;
var WED;
Line 141 ⟶ 144:
 
/**
* wikEd diff main class.
*
* @class WikEdDiff
Line 147 ⟶ 150:
var WikEdDiff = function () {
 
/** @var array config Configuration and customization settings. */
this.config = {
 
/** Core diff settings (with default values). */
 
/**
Line 160 ⟶ 163:
/**
* @var bool config.showBlockMoves
* Enable block move layout with highlighted blocks and marks at theirthe original positions (true)
*/
'showBlockMoves': true,
Line 169 ⟶ 172:
*/
'charDiff': true,
 
/**
* @var bool config.repeatedDiff
* Enable repeated diff to resolve problematic sequences (true)
*/
'repeatedDiff': true,
 
/**
Line 221 ⟶ 230:
/**
* @var bool config.debug
* Show debug infos and stats (block, group, and fragment data objects) in debug console (false)
*/
'debug': false,
Line 237 ⟶ 246:
'unitTesting': false,
 
/** RegExp character classes. */
 
// UniCode letter support for regexps,
// fromFrom http://xregexp.com/addons/unicode/unicode-base.js v1.0.0
'regExpLetters':
'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' ),
'a-zA-Z0-9' + (
'00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-' +
'037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA' +
'05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-' +
'07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D' +
'09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE' +
'09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A39' +
'0A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE0' +
'0AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B83' +
'0B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C' +
'0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-' +
'0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-' +
'0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E87' +
'0E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC4' +
'0EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066' +
'106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D' +
'1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-1315' +
'1318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-' +
'17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C' +
'1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA0' +
'1BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F15' +
'1F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC4' +
'1FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-2113' +
'21152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-' +
'2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE' +
'2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-' +
'3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCC' +
'A000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-' +
'A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3' +
'A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-' +
'AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-' +
'AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9' +
'FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3D' +
'FD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-' +
'FFCFFFD2-FFD7FFDA-FFDC'
).replace( /(\w{4})/g, '\\u$1' ),
 
// New line characters without and with \n and \r
Line 250 ⟶ 296:
 
// Full stops without '.'
'regExpFullStops':
'regExpFullStops': '058906D40701070209640DF41362166E180318092CF92CFE2E3C3002A4FFA60EA6F3FE52FF0EFF61'.replace( /(\w{4})/g, '\\u$1' ),
'\\u0589\\u06D4\\u0701\\u0702\\u0964\\u0DF4\\u1362\\u166E\\u1803\\u1809' +
'\\u2CF9\\u2CFE\\u2E3C\\u3002\\uA4FF\\uA60E\\uA6F3\\uFE52\\uFF0E\\uFF61',
 
// New paragraph characters without \n and \r
Line 256 ⟶ 304:
 
// Exclamation marks without '!'
'regExpExclamationMarks': '01C301C301C3055C055C07F919441944203C203C20482048FE15FE57FF01'.replace( /(\w{4})/g, '\\u$1' ),
'\\u01C3\\u01C3\\u01C3\\u055C\\u055C\\u07F9\\u1944\\u1944' +
'\\u203C\\u203C\\u2048\\u2048\\uFE15\\uFE57\\uFF01',
 
// Question marks without '?'
'regExpQuestionMarks':
'regExpQuestionMarks': '037E055E061F13671945204720492CFA2CFB2E2EA60FA6F7FE56FF1F'.replace( /(\w{4})/g, '\\u$1' ) + '\\u11143',
'\\u037E\\u055E\\u061F\\u1367\\u1945\\u2047\\u2049' +
'\\u2CFA\\u2CFB\\u2E2E\\uA60F\\uA6F7\\uFE56\\uFF1F',
 
/** Clip settings. */
 
// Find clip position: characters from right (heading, paragraph, line break, blanks, or characters)
'clipHeadingLeft': 1500,
'clipParagraphLeftMax': 1500,
Line 273 ⟶ 325:
'clipCharsLeft': 500,
 
// Find clip position: characters from right (heading, paragraph, line break, blanks, or characters)
'clipHeadingRight': 1500,
'clipParagraphRightMax': 1500,
Line 297 ⟶ 349:
 
// Insert
'.wikEdDiffInsert {' +
'font-weight: bold; background-color: #bbddff; ' +
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
'} ' +
'.wikEdDiffInsertBlank { background-color: #66bbff; } ' +
'.wikEdDiffFragment:hover .wikEdDiffInsertBlank { background-color: #bbddff; } ' +
 
// Delete
'.wikEdDiffDelete {' +
'font-weight: bold; background-color: #ffe49c; ' +
'color: #222; border-radius: 0.25em; padding: 0.2em 1px; }' +
'} ' +
'.wikEdDiffDeleteBlank { background-color: #ffd064; } ' +
'.wikEdDiffFragment:hover .wikEdDiffDeleteBlank { background-color: #ffe49c; } ' +
 
// Block
'.wikEdDiffBlock {' +
'font-weight: bold; background-color: #e8e8e8; ' +
'border-radius: 0.25em; padding: 0.2em 1px; margin: 0 1px; }' +
'.wikEdDiffBlock} { }' +
'.wikEdDiffBlock0wikEdDiffBlock { background-color: #ffff80000; } ' +
'.wikEdDiffBlock1wikEdDiffBlock0 { background-color: #d0ff80ffff80; } ' +
'.wikEdDiffBlock2wikEdDiffBlock1 { background-color: #ffd8f0d0ff80; } ' +
'.wikEdDiffBlock3wikEdDiffBlock2 { background-color: #c0ffffffd8f0; } ' +
'.wikEdDiffBlock4wikEdDiffBlock3 { background-color: #fff888c0ffff; } ' +
'.wikEdDiffBlock5wikEdDiffBlock4 { background-color: #bbccfffff888; } ' +
'.wikEdDiffBlock6wikEdDiffBlock5 { background-color: #e8c8ffbbccff; } ' +
'.wikEdDiffBlock7wikEdDiffBlock6 { background-color: #ffbbbbe8c8ff; } ' +
'.wikEdDiffBlock8wikEdDiffBlock7 { background-color: #a0e8a0ffbbbb; } ' +
'.wikEdDiffBlock8 { background-color: #a0e8a0; } ' +
'.wikEdDiffBlockHighlight {' +
'background-color: #777; color: #fff; ' +
'border: solid #777; border-width: 1px 0; }' +
'} ' +
 
// Mark
'.wikEdDiffMarkLeft, .wikEdDiffMarkRight {' +
'font-weight: bold; background-color: #ffe49c; ' +
'color: #666; border-radius: 0.25em; padding: 0.2em; margin: 0 1px; }' +
'} ' +
'.wikEdDiffMarkLeft:before { content: "{cssMarkLeft}"; } ' +
'.wikEdDiffMarkRight:before { content: "{cssMarkRight}"; } ' +
'.wikEdDiffMarkLeft.wikEdDiffNoUnicode:before { content: "<"; } ' +
'.wikEdDiffMarkRight.wikEdDiffNoUnicode:before { content: ">"; } ' +
'.wikEdDiffMark { background-color: #e8e8e8; color: #666; } ' +
'.wikEdDiffMark0 { background-color: #ffff60; } ' +
'.wikEdDiffMark1 { background-color: #c8f880; } ' +
'.wikEdDiffMark2 { background-color: #ffd0f0; } ' +
'.wikEdDiffMark3 { background-color: #a0ffff; } ' +
'.wikEdDiffMark4 { background-color: #fff860; } ' +
'.wikEdDiffMark5 { background-color: #b0c0ff; } ' +
'.wikEdDiffMark6 { background-color: #e0c0ff; } ' +
'.wikEdDiffMark7 { background-color: #ffa8a8; } ' +
'.wikEdDiffMark8 { background-color: #98e898; } ' +
'.wikEdDiffMarkHighlight { background-color: #777; color: #fff; } ' +
 
// Wrappers
'.wikEdDiffContainer { } ' +
'.wikEdDiffFragment {' +
'white-space: pre-wrap; background-color: var(--background-color-base, #fff); border: #bbb solid; ' +
'border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif; ' +
'font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; }' +
'} ' +
'.wikEdDiffNoChange { 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; }' +
'.wikEdDiffNoChange { background: var(--background-color-interactive, #eaecf0); border: 1px #bbb solid; border-radius: 0.5em; ' +
'.wikEdDiffSeparator { margin-bottom: 1em; }' +
'line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 0.5em; margin: 1em 0; ' +
'.wikEdDiffOmittedChars { }' +
'text-align: center; ' +
'} ' +
'.wikEdDiffSeparator { margin-bottom: 1em; } ' +
'.wikEdDiffOmittedChars { } ' +
 
// Newline
'.wikEdDiffNewline:before { content: "¶"; color: transparent; } ' +
'.wikEdDiffBlock:hover .wikEdDiffNewline:before { color: #aaa; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffNewline:before { color: transparent; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffNewline:before { color: #ccc; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffNewline:before, ' +
'.wikEdDiffInsert:hover .wikEdDiffNewline:before' +
'{ color: #999; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffNewline:before, ' +
'.wikEdDiffDelete:hover .wikEdDiffNewline:before' +
'{ color: #aaa; } ' +
 
// Tab
'.wikEdDiffTab { position: relative; } ' +
'.wikEdDiffTabSymbol { position: absolute; top: -0.2em; } ' +
'.wikEdDiffTabSymbol:before { content: "→"; font-size: smaller; color: #ccc; } ' +
'.wikEdDiffBlock .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffInsert .wikEdDiffTabSymbol:before { color: #aaa; } ' +
'.wikEdDiffDelete .wikEdDiffTabSymbol:before { color: #bbb; } ' +
 
// Space
'.wikEdDiffSpace { position: relative; } ' +
'.wikEdDiffSpaceSymbol { position: absolute; top: -0.2em; left: -0.05em; } ' +
'.wikEdDiffSpaceSymbol:before { content: "·"; color: transparent; } ' +
'.wikEdDiffBlock:hover .wikEdDiffSpaceSymbol:before { color: #999; } ' +
'.wikEdDiffBlockHighlight .wikEdDiffSpaceSymbol:before { color: transparent; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffSpaceSymbol:before { color: #ddd; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffInsert .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffInsert:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #888; } ' +
'.wikEdDiffBlockHighlight:hover .wikEdDiffDelete .wikEdDiffSpaceSymbol:before,' +
'.wikEdDiffDelete:hover .wikEdDiffSpaceSymbol:before ' +
'{ color: #999; } ' +
 
// Error
'.wikEdDiffError .wikEdDiffFragment,' +
'.wikEdDiffError .wikEdDiffNoChange' +
'{ background: #faa; }',
};
 
/** Add regular expressions to configuration settings. */
 
this.config.regExp = {
Line 385 ⟶ 469:
// Split into paragraphs, after double newlines
'paragraph': new RegExp(
'(.|\\n)*?((\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
'])+', 'g' ),
'g'
),
 
// Split into sentences, after .space and newlineslines
'line': new RegExp(
'\\r\\n|\\n|\\r|[' +
this.config.regExpNewLinesAll +
']',
'g'
),
 
// Split into sentences /[^ ].*?[.!?:;]+(?= |$)/
'sentence': new RegExp(
'[^' + this.config.regExpNewLinesAll +
']*?([.!?;' + this.config.regExpFullStopsregExpBlanks +
'].*?[.!?:;' +
this.config.regExpExclamationMarks + this.config.regExpQuestionMarks +
this.config.regExpFullStops +
']+[' +
this.config.regExpExclamationMarks +
this.config.regExpQuestionMarks +
']+(?=[' +
this.config.regExpBlanks +
']+|$)?([' +,
'g'
this.config.regExpNewLines +
),
']|\\r\\n|\\n|\\r)', 'g' ),
 
// Split into inline chunks
Line 409 ⟶ 506:
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'),
),
 
// Split into words, multi-char markup, and chars
// regExpLetters speed-up: \\w+
'word': new RegExp( '[' +
'word': new RegExp(
'(\\w+|[_' +
this.config.regExpLetters +
'])+([\'’_]?[_' +
this.config.regExpLetters +
']+*)*|\\[\\[|\\]\\]|\\{\\{|\\}\\}|&\\w+;|\'\'\'|\'\'|==+|\\{\\||\\|\\}|\\|-|.', 'g' ),
'g'
),
 
// Split into chars
'character': /./g
},
 
// RegExp to detect blank tokens
'blankOnlyToken': new RegExp(
'[^' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']'
),
 
// RegExps for sliding gaps: newlines and space/word breaks
'slideStop': new RegExp( '[' + this.config.regExpNewLinesAll + ']$'),
'[' +
'slideBorder': new RegExp(
'[' + this.config.regExpBlanks +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']$' ),
),
'slideBorder': new RegExp(
'[' +
this.config.regExpBlanks +
']$'
),
 
// RegExps for counting words
'countWords': new RegExp(
'(\\w+|[_' +
this.config.regExpLetters +
'])+([\'’_]?[_' +
this.config.regExpLetters +
']+ *)*', 'g' ),
'g'
),
'countChunks': new RegExp(
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
Line 443 ⟶ 563:
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'),
),
 
// RegExp detecting blank-only and single-char blocks
Line 450 ⟶ 571:
// RegExps for clipping
'clipLine': new RegExp(
'[' + this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+', 'g' ),
'g'
),
'clipHeading': new RegExp(
'( ^|\\n)(==+.+?==+|\\{\\||\\|\\}).*?(?=\\n|$)', 'g' ),
'clipParagraph': new RegExp(
'( (\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
'])+', 'g' ),
'g'
),
'clipBlank': new RegExp(
'[' +
this.config.regExpBlanks + ']+', 'g'),
'g'
),
'clipTrimNewLinesLeft': new RegExp(
'[' +
'[' + this.config.regExpNewLinesAll + this.config.regExpNewParagraph + ']+$', 'g' ),
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimNewLinesRight': new RegExp(
'^[' +
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+', 'g' ),
'g'
),
'clipTrimBlanksLeft': new RegExp(
'[' +
Line 466 ⟶ 608:
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimBlanksRight': new RegExp(
'^[' +
Line 472 ⟶ 616:
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g' )
)
};
 
/** Add messages to configuration settings. */
 
this.config.msg = {
Line 490 ⟶ 636:
 
/**
* Add output html fragments to configuration settings.
* Dynamic replacements:
* {number}: class/color/block/mark/id number
* {title}: title attribute (popup)
* {nounicode}: noUnicodeSymbols fallback
*/
this.config.htmlCode = {
Line 504 ⟶ 650:
 
'containerStart': '<div class="wikEdDiffContainer" id="wikEdDiffContainer">',
'containerEnd': '</div>',
 
'fragmentStart': '<divpre class="wikEdDiffFragment" style="white-space: pre-wrap;">',
'fragmentEnd': '</divpre>',
'separator': '<div class="wikEdDiffSeparator"></div>',
 
'insertStart':
'<span class="wikEdDiffInsert" title="' +
this.config.msg['wiked-diff-ins'] +
'">',
'insertStartBlank':
'<span class="wikEdDiffInsert wikEdDiffInsertBlank" title="' +
this.config.msg['wiked-diff-ins'] +
'">',
'insertEnd': '</span>',
 
'deleteStart':
'<span class="wikEdDiffDelete" title="' +
this.config.msg['wiked-diff-del'] +
'">',
'deleteStartBlank':
'<span class="wikEdDiffDelete wikEdDiffDeleteBlank" title="' +
this.config.msg['wiked-diff-del'] +
'">',
'deleteEnd': '</span>',
 
'blockStart':
'blockStart': '<span class="wikEdDiffBlock" title="{title}" id="wikEdDiffBlock{number}" onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
'blockColoredStart': '<span class="wikEdDiffBlock wikEdDiffBlock wikEdDiffBlock{number}"' +
'title="{title}" id="wikEdDiffBlock{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
'blockColoredStart':
'blockEnd': '</span>',
'<span class="wikEdDiffBlock wikEdDiffBlock wikEdDiffBlock{number}"' +
'title="{title}" id="wikEdDiffBlock{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');">',
'blockEnd': '</span>',
 
'markLeft':
'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>',
'markLeftColored':
'<span class="wikEdDiffMarkLeft{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
 
'markRight':
'markRight': '<span class="wikEdDiffMarkRight{nounicode}" title="{title}" id="wikEdDiffMark{number}" onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
'markRightColored': '<span class="wikEdDiffMarkRight{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
'markRightColored':
'<span class="wikEdDiffMarkRight{nounicode} wikEdDiffMark wikEdDiffMark{number}"' +
'title="{title}" id="wikEdDiffMark{number}"' +
'onmouseover="wikEdDiffBlockHandler(undefined, this, \'mouseover\');"></span>',
 
'newline': '<span class="wikEdDiffNewline">\n</span>',
'tab': '<span class="wikEdDiffTab"><span class="wikEdDiffTabSymbol"></span>\t</span>',
'space': '<span class="wikEdDiffSpace"><span class="wikEdDiffSpaceSymbol"></span> </span>',
 
'omittedChars': '<span class="wikEdDiffOmittedChars">…</span>',
 
'errorStart': '<div class="wikEdDiffError" title="Error: diff not consistent with versions!">',
'errorEnd': '</div>'
};
 
/*
* Add JavaScript event handler function to configuration settings.
* Highlights corresponding block and mark elements on hover and jumps between them on click.
* Code for use in non-jQuery environments and legacy browsers (at least IE 8 compatible).
*
* @option Event|undefined event Browser event if available
Line 550 ⟶ 726:
 
// IE compatibility
if ( ( event === undefined ) && ( window.event !== undefined ) ) {
event = window.event;
}
Line 558 ⟶ 734:
var block = document.getElementById( 'wikEdDiffBlock' + number );
var mark = document.getElementById( 'wikEdDiffMark' + number );
if ( block === null || mark === null ) {
return;
}
 
// Highlight corresponding mark/block pairs
if ( type === 'mouseover' ) {
element.onmouseover = null;
element.onmouseout = function ( event ) {
Line 576 ⟶ 752:
 
// Remove mark/block highlighting
if ( ( type === 'mouseout' ) || ( type === 'click' ) ) {
element.onmouseout = null;
element.onmouseover = function ( event ) {
Line 583 ⟶ 759:
 
// Reset, allow outside container (e.g. legend)
if ( type !== 'click' ) {
block.className = block.className.replace( / wikEdDiffBlockHighlight/g, '' );
mark.className = mark.className.replace( / wikEdDiffMarkHighlight/g, '' );
 
// GetElementsByClassName
Line 591 ⟶ 767:
if ( container !== null ) {
var spans = container.getElementsByTagName( 'span' );
for ( var ispansLength = 0; i < spans.length; i ++ ) {
iffor ( (var spans[i] != block0; )i &&< (spansLength; spans[i] != mark )++ ) {
if ( spans[i].className.indexOf( '!== wikEdDiffBlockHighlight'block )&& spans[i] !== -1mark ) {
if ( spans[i].className.indexOf( ' wikEdDiffBlockHighlight' ) !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffBlockHighlight/g, '' );
}
else if ( spans[i].className.indexOf( ' wikEdDiffMarkHighlight' ) !== -1 ) {
spans[i].className = spans[i].className.replace( / wikEdDiffMarkHighlight/g, '' );
}
Line 606 ⟶ 783:
 
// Scroll to corresponding mark/block element
if ( type === 'click' ) {
 
// Get corresponding element
var corrElement;
if ( element === block ) {
corrElement = mark;
}
Line 617 ⟶ 794:
}
 
// Get element height (getOffsetTop )
var corrElementPos = 0;
var node = corrElement;
Line 654 ⟶ 831:
};
 
/** Internal data structures. */
 
/** @var WikEdDiffText newText New text version object with text and token list */
Line 661 ⟶ 838:
/** @var WikEdDiffText oldText Old text version object with text and token list */
this.oldText = null;
 
/** @var object symbols Symbols table for whole text at all refinement levels */
this.symbols = {
token: [],
hashTable: {},
linked: false
};
 
/** @var array bordersDown Matched region borders downwards */
this.bordersDown = [];
 
/** @var array bordersUp Matched region borders upwards */
this.bordersUp = [];
 
/** @var array blocks Block data (consecutive text tokens) in new text order */
this.blocks = [];
 
/** @var int maxWords Maximal detected word count of all linked blocks */
this.maxWords = 0;
 
/** @var array groups Section blocks that are consecutive in old text order */
Line 677 ⟶ 870:
this.recursionTimer = [];
 
/** Output data. */
 
/** @var bool error Unit tests have detected a diff error */
Line 690 ⟶ 883:
 
/**
* Constructor, initialize settings, load js and css.
*
* @param[in] object wikEdDiffConfig Custom customization settings
Line 699 ⟶ 892:
 
// Import customizations from wikEdDiffConfig{}
if ( typeof wikEdDiffConfig === 'object' ) {
this.deepCopy( wikEdDiffConfig, this.config );
}
Line 710 ⟶ 903:
 
// Add block handler to head if running under Greasemonkey
if ( typeof GM_info === 'object' ) {
var script = 'var wikEdDiffBlockHandler = ' + this.config.blockHandler.toString() + ';';
this.addScript( script );
Line 723 ⟶ 916:
 
/**
* Main diff method.
*
* @param string oldString Old text version
* @param string newString New text version
* @param[out] array fragment
* Diff fragment list ready for markup, abstraction layer for customized diffs
* @param[out] string html Html code of diff
* @return string Html code of diff
Line 748 ⟶ 942:
// Strip trailing newline (.js only)
if ( this.config.stripTrailingNewline === true ) {
if ( ( newString.substr( -1 ) === '\n' ) && ( oldString.substr( -1 ) === '\n' ) ) {
newString = newString.substr( 0, newString.length - 1 );
oldString = oldString.substr( 0, oldString.length - 1 );
Line 759 ⟶ 953:
 
// Trap trivial changes: no change
if ( this.newText.text === this.oldText.text ) {
this.html =
this.config.htmlCode.containerStart +
Line 771 ⟶ 965:
// Trap trivial changes: old text deleted
if (
( this.oldText.text === '' ) || (
( this.oldText.text === '\n' ) &&
( this.newText.text.charAt( this.newText.text.length - 1 ) === '\n' )
)
) {
Line 789 ⟶ 983:
// Trap trivial changes: new text deleted
if (
( this.newText.text === '' ) || (
( this.newText.text === '\n' ) &&
( this.oldText.text.charAt( this.oldText.text.length - 1 ) === '\n' )
)
) {
Line 804 ⟶ 998:
return this.html;
}
 
// New symbols object
var symbols = {
token: [],
hashTable: {},
linked: false
};
 
// Split new and old text into paragraps
if ( this.config.timer === true ) {
this.time( 'paragraph split' );
}
this.newText.splitText( 'paragraph' );
this.oldText.splitText( 'paragraph' );
if ( this.config.timer === true ) {
this.timeEnd( 'paragraph split' );
}
 
// Calculate diff
this.calculateDiff( symbols, 'paragraphline' );
 
// Refine different paragraphs into sentenceslines
if ( this.config.timer === true ) {
this.time( 'line split' );
}
this.newText.splitRefine( 'line' );
this.oldText.splitRefine( 'line' );
if ( this.config.timer === true ) {
this.timeEnd( 'line split' );
}
 
// Calculate refined diff
this.calculateDiff( 'line' );
 
// Refine different lines into sentences
if ( this.config.timer === true ) {
this.time( 'sentence split' );
}
this.newText.splitRefine( 'sentence' );
this.oldText.splitRefine( 'sentence' );
if ( this.config.timer === true ) {
this.timeEnd( 'sentence split' );
}
 
// Calculate refined diff
this.calculateDiff( symbols, 'sentence' );
 
// Refine different paragraphssentences into chunks
if ( this.config.timer === true ) {
this.time( 'chunk split' );
Line 837 ⟶ 1,049:
 
// Calculate refined diff
this.calculateDiff( symbols, 'chunk' );
 
// Refine different sentenceschunks into words
if ( this.config.timer === true ) {
this.time( 'word split' );
Line 850 ⟶ 1,062:
 
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( symbols, 'word', false, true );
 
// Slide gaps
Line 875 ⟶ 1,087:
 
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( symbols, 'character', false, true );
 
// Slide gaps
Line 887 ⟶ 1,099:
}
}
 
// Free memory
this.symbols = undefined;
this.bordersDown = undefined;
this.bordersUp = undefined;
this.newText.words = undefined;
this.oldText.words = undefined;
 
// Enumerate token lists
Line 900 ⟶ 1,119:
this.timeEnd( 'blocks' );
}
 
// Free memory
this.newText.tokens = undefined;
this.oldText.tokens = undefined;
 
// Assemble blocks into fragment table
this.getDiffFragments();
 
// Free memory
this.blocks = undefined;
this.groups = undefined;
this.sections = undefined;
 
// Stop diff timer
Line 962 ⟶ 1,190:
if ( this.config.timer === true ) {
this.timeEnd( 'total' );
}
 
// Debug log
if ( this.config.debug === true ) {
console.log( 'HTML:\n', this.html );
}
 
Line 988 ⟶ 1,211:
this.splitRefineChars = function () {
 
/** Find corresponding gaps. */
 
// Cycle troughthrough new text tokens list
var gaps = [];
var gap = null;
var i = this.newText.first;
var j = this.oldText.first;
while ( ( i !== null ) && ( this.newText.tokens[i] !== null ) ) {
 
// Get token links
Line 1,005 ⟶ 1,228:
 
// Start of gap in new and old
if ( ( gap === null ) && ( newLink === null ) && ( oldLink === null ) ) {
gap = gaps.length;
gaps.push( {
Line 1,019 ⟶ 1,242:
 
// Count chars and tokens in gap
else if ( ( gap !== null ) && ( newLink === null ) ) {
gaps[gap].newLast = i;
gaps[gap].newTokens ++;
Line 1,025 ⟶ 1,248:
 
// Gap ended
else if ( ( gap !== null ) && ( newLink !== null ) ) {
gap = null;
}
Line 1,036 ⟶ 1,259:
}
 
// Cycle troughthrough gaps and add old text gap data
for ( var gapgapsLength = 0; gap < gaps.length; gap ++ ) {
for ( var gap = 0; gap < gapsLength; gap ++ ) {
 
// Cycle troughthrough old text tokens list
var j = gaps[gap].oldFirst;
while (
while ( ( j !== null ) && ( this.oldText.tokens[j] !== null ) && ( this.oldText.tokens[j].link === null ) ) {
j !== null &&
this.oldText.tokens[j] !== null &&
this.oldText.tokens[j].link === null
) {
 
// Count old chars and tokens in gap
Line 1,051 ⟶ 1,279:
}
 
/** Select gaps of identical token number and strong similarity of all tokens. */
 
for ( var gapgapsLength = 0; gap < gaps.length; gap ++ ) {
for ( var gap = 0; gap < gapsLength; gap ++ ) {
var charSplit = true;
 
// Not same gap length
if ( gaps[gap].newTokens !== gaps[gap].oldTokens ) {
 
// One word became separated by space, dash, or any string
if ( ( gaps[gap].newTokens === 1 ) && ( gaps[gap].oldTokens === 3 ) ) {
var token = this.newText.tokens[ gaps[gap].newFirst ].token;
var tokenFirst = this.oldText.tokens[ gaps[gap].oldFirst ].token;
var tokenLast = this.oldText.tokens[ gaps[gap].oldLast ].token;
if (
if ( ( token.indexOf( tokenFirst ) !== 0 ) || ( token.indexOf( tokenLast ) != token.length - tokenLast.length ) ) {
token.indexOf( tokenFirst ) !== 0 ||
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
}
else if ( ( gaps[gap].oldTokens === 1 ) && ( gaps[gap].newTokens === 3 ) ) {
var token = this.oldText.tokens[ gaps[gap].oldFirst ].token;
var tokenFirst = this.newText.tokens[ gaps[gap].newFirst ].token;
var tokenLast = this.newText.tokens[ gaps[gap].newLast ].token;
if (
if ( ( token.indexOf( tokenFirst ) !== 0 ) || ( token.indexOf( tokenLast ) != token.length - tokenLast.length ) ) {
token.indexOf( tokenFirst ) !== 0 ||
token.indexOf( tokenLast ) !== token.length - tokenLast.length
) {
continue;
}
Line 1,082 ⟶ 1,317:
}
 
// Cycle troughthrough new text tokens list and set charSplit
else {
var i = gaps[gap].newFirst;
Line 1,103 ⟶ 1,338:
 
// Not same token length
if ( newToken.length !== oldToken.length ) {
 
// Test for addition or deletion of internal string in tokens
Line 1,110 ⟶ 1,345:
var left = 0;
while ( left < shorterToken.length ) {
if ( newToken.charAt( left ) !== oldToken.charAt( left ) ) {
break;
}
Line 1,119 ⟶ 1,354:
var right = 0;
while ( right < shorterToken.length ) {
if (
if ( newToken.charAt( newToken.length - 1 - right ) != oldToken.charAt( oldToken.length - 1 - right ) ) {
newToken.charAt( newToken.length - 1 - right ) !==
oldToken.charAt( oldToken.length - 1 - right )
) {
break;
}
Line 1,126 ⟶ 1,364:
 
// No simple insertion or deletion of internal string
if ( left + right !== shorterToken.length ) {
 
// Not addition or deletion of flanking strings in tokens
// (smallerSmaller token not part of larger token )
if ( longerToken.indexOf( shorterToken ) === -1 ) {
 
// Same text at start or end shorter than different text
if ( ( left < shorterToken.length / 2 ) && (right < shorterToken.length / 2) ) {
 
// Do not split into chars in this gap
charSplit = false;
break;
Line 1,143 ⟶ 1,382:
 
// Same token length
else if ( newToken !== oldToken ) {
 
// Tokens less than 50 % identical
var ident = 0;
for ( var postokenLength = 0; pos < shorterToken.length; pos ++ ) {
iffor ( shorterToken.charAt(var pos ) == longerToken.charAt(0; pos < tokenLength; pos )++ ) {
if ( shorterToken.charAt( pos ) === longerToken.charAt( pos ) ) {
ident ++;
}
Line 1,161 ⟶ 1,401:
 
// Next list elements
if ( i === gaps[gap].newLast ) {
break;
}
Line 1,171 ⟶ 1,411:
}
 
/** Refine words into chars in selected gaps. */
 
for ( var gapgapsLength = 0; gap < gaps.length; gap ++ ) {
for ( var gap = 0; gap < gapsLength; gap ++ ) {
if ( gaps[gap].charSplit === true ) {
 
// Cycle troughthrough new text tokens list, link spaces, and split into chars
var i = gaps[gap].newFirst;
var j = gaps[gap].oldFirst;
var newGapLength = i - gaps[gap].newLast;
var oldGapLength = j - gaps[gap].oldLast;
while ( ( i !== null ) || ( j !== null ) ) {
 
// Link identical tokens (spaces) to keep char refinement to words
if (
if ( ( newGapLength == oldGapLength ) && ( this.newText.tokens[i].token == this.oldText.tokens[j].token ) ) {
newGapLength === oldGapLength &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
Line 1,200 ⟶ 1,444:
 
// Next list elements
if ( i === gaps[gap].newLast ) {
i = null;
}
if ( j === gaps[gap].oldLast ) {
j = null;
}
Line 1,220 ⟶ 1,464:
 
/**
* Move gaps with ambiguous identical fronts to last newline border or otherwise last word border.
*
* @param[in/out] wikEdDiffText text, textLinked These two are newText and oldText
*/
this.slideGaps = function ( text, textLinked ) {
 
var regExpSlideBorder = this.config.regExp.slideBorder;
var regExpSlideStop = this.config.regExp.slideStop;
 
// Cycle through tokens list
var i = text.first;
var gapStart = null;
while ( ( i !== null ) && ( text.tokens[i] !== null ) ) {
 
// Remember gap start
if ( ( gapStart === null ) && ( text.tokens[i].link === null ) ) {
gapStart = i;
}
 
// Find gap end
else if ( ( gapStart !== null ) && ( text.tokens[i].link !== null ) ) {
var gapFront = gapStart;
var gapBack = text.tokens[i].prev;
Line 1,245 ⟶ 1,492:
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,263 ⟶ 1,512:
var front = text.tokens[gapFront].prev;
var back = gapBack;
var gapFrontBlankTest = this.config.regExp.slideBorderregExpSlideBorder.test( text.tokens[gapFront].token );
var frontStop = front;
if ( text.tokens[back].link === null ) {
while (
( front !== null ) && (
back !== null ) &&
( text.tokens[front].link !== null ) &&
( text.tokens[front].token === text.tokens[back].token )
) {
if ( front !== null ) {
 
// Stop at line break
front = text.tokens[front].prev;
back if =( regExpSlideStop.test( text.tokens[backfront].prev;token ) === true ) {
 
// Stop at line break
if ( front !== null ) {
if ( this.config.regExp.slideStop.test( text.tokens[front].token ) === true ) {
frontStop = front;
break;
}
 
// Stop at first word border (blank/word or word/blank )
if (
if ( this.config.regExp.slideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
regExpSlideBorder.test( text.tokens[front].token ) !== gapFrontBlankTest ) {
frontStop = front;
}
}
front = text.tokens[front].prev;
back = text.tokens[back].prev;
}
}
Line 1,294 ⟶ 1,544:
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,319 ⟶ 1,572:
* Pass 1: parse new text into symbol table
* Pass 2: parse old text into symbol table
* Pass 3: connect unique matchedmatching tokens
* Pass 4: connect adjacent identical tokens downwards
* Pass 5: connect adjacent identical tokens upwards
Line 1,327 ⟶ 1,580:
*
* @param array symbols Symbol table object
* @param string level Split level: 'paragraph', 'line', 'sentence', 'chunk', 'word', or 'character'
*
* Optionally for recursive or repeated calls:
* @param bool repeatrepeating RepeatCurrently repeating with empty symbol table
* @param bool recurse Enable recursion
* @param int newStart, newEnd, oldStart, oldEnd Text object tokens indices
Line 1,336 ⟶ 1,589:
* @param[in/out] WikEdDiffText newText, oldText Text object, tokens list link property
*/
this.calculateDiff = function ( symbols, level, repeat, recurse, newStart, newEnd, oldStart, oldEnd, recursionLevel ) {
level,
 
recurse,
if ( recursionLevel === undefined ) {
repeating,
recursionLevel = 0;
newStart,
}
oldStart,
up,
recursionLevel
) {
 
// Set defaults
if ( repeating === undefined ) { repeating = false; }
if ( recurse === undefined ) { recurse = false; }
if ( newStart === undefined ) { newStart = this.newText.first; }
if ( newEnd === undefined ) { newEnd = this.newText.last; }
if ( oldStart === undefined ) { oldStart = this.oldText.first; }
if ( oldEndup === undefined ) { oldEndup = this.oldText.lastfalse; }
if ( recursionLevel === undefined ) { recursionLevel = 0; }
 
// Start timers
if ( ( this.config.timer === true ) && (repeating repeat !=== true )false && ( recursionLevel === 0 ) ) {
this.time( level );
}
if ( ( this.config.timer === true ) && (repeating repeat !=== true )false ) {
this.time( level + recursionLevel );
}
 
// Get object symbols table and linked region borders
// Limit recursion depth
var symbols;
if ( recursionLevel > 10 ) {
var bordersDown;
return;
var bordersUp;
if ( recursionLevel === 0 && repeating === false ) {
symbols = this.symbols;
bordersDown = this.bordersDown;
bordersUp = this.bordersUp;
}
 
// Create empty local symbols table and linked region borders arrays
else {
symbols = {
token: [],
hashTable: {},
linked: false
};
bordersDown = [];
bordersUp = [];
}
 
 
// Updated versions of linked region borders
var bordersUpNext = [];
var bordersDownNext = [];
 
/**
Line 1,365 ⟶ 1,645:
*/
 
// Cycle troughthrough new text tokens list
var i = newStart;
while ( ( i !== null ) && ( this.newText.tokens[i] !== null ) ) {
if ( this.newText.tokens[i].link === null ) {
 
Line 1,373 ⟶ 1,653:
var token = this.newText.tokens[i].token;
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
var currentsymbols.hashTable[token] = symbols.token.length;
symbols.hashTable[token].push( = current;{
symbols.token[current] = {
newCount: 1,
oldCount: 0,
newToken: i,
oldToken: null
} );
}
 
Line 1,392 ⟶ 1,671:
}
 
// NextStop listafter elementgap if recursing
else if ( irecursionLevel ==> newEnd0 ) {
break;
}
 
i = this.newText.tokens[i].next;
// Get next token
if ( up === false ) {
i = this.newText.tokens[i].next;
}
else {
i = this.newText.tokens[i].prev;
}
}
 
Line 1,403 ⟶ 1,689:
*/
 
// Cycle troughthrough old text tokens list
var j = oldStart;
while ( ( j !== null ) && ( this.oldText.tokens[j] !== null ) ) {
if ( this.oldText.tokens[j].link === null ) {
 
Line 1,411 ⟶ 1,697:
var token = this.oldText.tokens[j].token;
if ( Object.prototype.hasOwnProperty.call( symbols.hashTable, token ) === false ) {
var currentsymbols.hashTable[token] = symbols.token.length;
symbols.hashTable[token].push( = current;{
symbols.token[current] = {
newCount: 0,
oldCount: 1,
newToken: null,
oldToken: j
} );
}
 
Line 1,433 ⟶ 1,718:
}
 
// NextStop listafter elementgap if recursing
else if ( jrecursionLevel ===> oldEnd0 ) {
break;
}
 
j = this.oldText.tokens[j].next;
// Get next token
if ( up === false ) {
j = this.oldText.tokens[j].next;
}
else {
j = this.oldText.tokens[j].prev;
}
}
 
Line 1,444 ⟶ 1,736:
*/
 
// Cycle troughthrough symbol array
for ( var isymbolsLength = 0; i < symbols.token.length; i ++ ) {
for ( var i = 0; i < symbolsLength; i ++ ) {
 
// Find tokens in the symbol table that occur only once in both versions
if ( ( symbols.token[i].newCount === 1 ) && ( symbols.token[i].oldCount === 1 ) ) {
var newToken = symbols.token[i].newToken;
var oldToken = symbols.token[i].oldToken;
var newTokenObj = this.newText.tokens[newToken];
var oldTokenObj = this.oldText.tokens[oldToken];
 
// Connect from new to old and from old to new
if ( this.newText.tokens[newToken]newTokenObj.link === null ) {
 
// Do not use spaces as unique markers
if (
if ( /^\s+$/.test(this.newText.tokens[newToken].token ) === false) {
this.config.regExp.blankOnlyToken.test( newTokenObj.token ) === true
) {
 
// Link new and old tokens
this.newText.tokens[newToken].link = oldToken;
this.oldText.tokens[oldToken]newTokenObj.link = newTokenoldToken;
oldTokenObj.link = newToken;
symbols.linked = true;
 
// Save linked region borders
bordersDown.push( [newToken, oldToken] );
bordersUp.push( [newToken, oldToken] );
 
// Check if token contains unique word
if ( recursionLevel === 0 ) {
var unique = false;
if ( level === 'character' ) {
unique = true;
}
else {
var token = this.newText.tokens[newToken]newTokenObj.token;
var words =
( token.match( this.config.regExp.countWords ) || [] ).concat(
( token.match( this.config.regExp.countChunks ) || [] );
);
 
// Unique if longer than min block length
ifvar (wordsLength = words.length >= this.config.blockMinLength ) {;
if ( wordsLength >= this.config.blockMinLength ) {
unique = true;
}
Line 1,479 ⟶ 1,785:
// Unique if it contains at least one unique word
else {
for ( var wordi = 0; wordi < words.lengthwordsLength; wordi ++ ) {
ifvar ( ( this.oldText.words[ words[word] ] == 1 ) && ( this.newText.words[ words[wordi] ] == 1 ) ) {;
if (
this.oldText.words[word] === 1 &&
this.newText.words[word] === 1 &&
Object.prototype.hasOwnProperty.call( this.oldText.words, word ) === true &&
Object.prototype.hasOwnProperty.call( this.newText.words, word ) === true
) {
unique = true;
break;
Line 1,490 ⟶ 1,802:
// Set unique
if ( unique === true ) {
this.newText.tokens[newToken]newTokenObj.unique = true;
this.oldText.tokens[oldToken]oldTokenObj.unique = true;
}
}
Line 1,506 ⟶ 1,818:
*/
 
// GetCycle surroundingthrough connectedlist of linked new text tokens
var ibordersLength = newStartbordersDown.length;
for ( var match = 0; match < bordersLength; match ++ ) {
if ( this.newText.tokens[i].prev !== null ) {
var i = this.newText.tokensbordersDown[imatch][0].prev;
var j = bordersDown[match][1];
}
var iStop = newEnd;
if ( this.newText.tokens[iStop].next !== null ) {
iStop = this.newText.tokens[iStop].next;
}
var j = null;
 
// Cycle trough new text tokens listNext down
do var {iMatch = i;
var jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
 
// Cycle through new text list gap region downwards
// Connected pair
while (
var link = this.newText.tokens[i].link;
if ( link i !== null ) {&&
j !== null &&
j = this.oldText.tokens[link].next;
this.newText.tokens[i].link === null &&
}
this.oldText.tokens[j].link === null
) {
 
// Connect if tokenssame are the sametoken
else if ( ( j !== null ) && ( this.oldText.tokens[j].link === null ) && ( this.newText.tokens[i].token === this.oldText.tokens[j].token ) ) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
 
// Not a match yet, maybe in next refinement level
else {
bordersDownNext.push( [iMatch, jMatch] );
break;
}
 
// Next token down
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
 
// Not same
else {
j = null;
}
i = this.newText.tokens[i].next;
} while ( i !== iStop );
 
/**
Line 1,544 ⟶ 1,862:
*/
 
// GetCycle surroundingthrough list of connected new text tokens
var ibordersLength = newEndbordersUp.length;
for ( var match = 0; match < bordersLength; match ++ ) {
if ( this.newText.tokens[i].next !== null ) {
var i = this.newText.tokensbordersUp[imatch][0].next;
var j = bordersUp[match][1];
}
var iStop = newStart;
if ( this.newText.tokens[iStop].prev !== null ) {
iStop = this.newText.tokens[iStop].prev;
}
var j = null;
 
// Cycle trough new text tokens listNext up
do var {iMatch = i;
var jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
 
// Cycle through new text gap region upwards
// Connected pair
while (
var link = this.newText.tokens[i].link;
if ( link i !== null ) {&&
j !== null &&
j = this.oldText.tokens[link].prev;
this.newText.tokens[i].link === null &&
}
this.oldText.tokens[j].link === null
) {
 
// Connect if tokenssame are the sametoken
else if ( ( j !== null ) && ( this.oldText.tokens[j].link === null ) && ( this.newText.tokens[i].token === this.oldText.tokens[j].token ) ) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
}
 
// Not a match yet, maybe in next refinement level
else {
bordersUpNext.push( [iMatch, jMatch] );
break;
}
 
// Next token up
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
}
 
// Not same
else {
j = null;
}
i = this.newText.tokens[i].prev;
} while ( i !== iStop );
 
/**
* Connect adjacent identical tokens downwards from text start,.
* treatTreat boundary as connected, stop after first connected token.
*/
 
// Only for full text diff
if ( ( newStartrecursionLevel === this.newText.first )0 && ( newEndrepeating === this.newText.last )false ) {
 
// From start
var i = this.newText.first;
var j = this.oldText.first;
var iMatch = null;
var jMatch = null;
 
// Cycle troughthrough newold text tokens list down,
// connectConnect identical tokens, stop after first connected token
while (
while ( ( i !== null ) && ( j !== null ) && ( this.newText.tokens[i].link === null ) && ( this.oldText.tokens[j].link === null ) && ( this.newText.tokens[i].token == this.oldText.tokens[j].token ) ) {
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
if ( iMatch !== null ) {
bordersDownNext.push( [iMatch, jMatch] );
}
 
// From end
var i = this.newText.last;
var j = this.oldText.last;
iMatch = null;
jMatch = null;
 
// Cycle troughthrough old text tokens list up,
// connectConnect identical tokens, stop after first connected token
while (
while ( ( i !== null ) && ( j !== null ) && ( this.newText.tokens[i].link === null ) && ( this.oldText.tokens[j].link === null ) && ( this.newText.tokens[i].token == this.oldText.tokens[j].token ) ) {
i !== null &&
j !== null &&
this.newText.tokens[i].link === null &&
this.oldText.tokens[j].link === null &&
this.newText.tokens[i].token === this.oldText.tokens[j].token
) {
this.newText.tokens[i].link = j;
this.oldText.tokens[j].link = i;
iMatch = i;
jMatch = j;
i = this.newText.tokens[i].prev;
j = this.oldText.tokens[j].prev;
}
if ( iMatch !== null ) {
bordersUpNext.push( [iMatch, jMatch] );
}
}
 
// Save updated linked region borders to object
if ( recursionLevel === 0 && repeating === false ) {
this.bordersDown = bordersDownNext;
this.bordersUp = bordersUpNext;
}
 
// Merge local updated linked region borders into object
else {
this.bordersDown = this.bordersDown.concat( bordersDownNext );
this.bordersUp = this.bordersUp.concat( bordersUpNext );
}
 
 
/**
* Repeat once with empty symbol table to link hidden unresolved common tokens in cross-overs.
* ("and" in "and this a and b that" -> "and this a and b that").
*/
 
if ( repeating === false && this.config.repeatedDiff === true ) {
// New empty symbols object
if ( var repeat !== true ) {;
this.calculateDiff( level, recurse, repeat, newStart, oldStart, up, recursionLevel );
var symbolsRepeat = {
token: [],
hashTable: {},
linked: false
};
this.calculateDiff( symbolsRepeat, level, true, false, newStart, newEnd, oldStart, oldEnd );
}
 
/**
* Refine by recursively diffing unresolvednot linked regions with emptynew symbol table at word level.
* At word and character level only.
* Helps against gaps caused by addition of common tokens around sequences of common tokens.
*/
 
if (
if ( ( recurse === true ) && ( this.config.recursiveDiff === true ) ) {
recurse === true &&
this.config['recursiveDiff'] === true &&
recursionLevel < this.config.recursionMax
) {
 
/**
* Recursively diff still unresolved regionsgap downwards.
*/
 
// Cycle troughthrough newlist textof tokenslinked listregion borders
var ibordersLength = newStartbordersDownNext.length;
for ( match = 0; match < bordersLength; match ++ ) {
var j = oldStart;
var i = bordersDownNext[match][0];
var j = bordersDownNext[match][1];
 
// Next token down
while ( ( i !== null ) && ( this.newText.tokens[i] !== null ) ) {
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
 
// GetStart jrecursion fromat previousfirst tokensgap matchtoken pair
if (
var iPrev = this.newText.tokens[i].prev;
if ( iPrev i !== null ) {&&
j !== null &&
var jPrev = this.newText.tokens[iPrev].link;
ifthis.newText.tokens[i].link ( jPrev !=== null ) {&&
j = this.oldText.tokens[jPrevj].next;link === null
}) {
var repeat = false;
var dirUp = false;
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
}
 
// Check 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 ) ) {
 
// Determine 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;
}
iNext = this.newText.tokens[iNext].next;
}
 
// Determine 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;
}
jNext = this.oldText.tokens[jNext].next;
}
 
// Recursively diff the unresolved sequence
if ( ( iLength > 1 ) || ( jLength > 1 ) ) {
 
// New empty symbols object for sub-region
var symbolsRecurse = {
token: [],
hashTable: {},
linked: false
};
this.calculateDiff( symbolsRecurse, level, false, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1 );
}
i = iEnd;
}
 
// Next list element
if ( i == newEnd ) {
break;
}
i = this.newText.tokens[i].next;
}
 
/**
* Recursively diff still unresolved regionsgap upwards.
*/
 
// Cycle troughthrough newlist textof tokenslinked listregion borders
var ibordersLength = newEndbordersUpNext.length;
for ( match = 0; match < bordersLength; match ++ ) {
var j = oldEnd;
var i = bordersUpNext[match][0];
while ( ( i !== null ) && ( this.newText.tokens[i] !== null ) ) {
var j = bordersUpNext[match][1];
 
// GetNext jtoken from next matched tokensup
var iPrevi = this.newText.tokens[i].nextprev;
j = this.oldText.tokens[j].prev;
if ( iPrev !== null ) {
var jPrev = this.newText.tokens[iPrev].link;
if ( jPrev !== null ) {
j = this.oldText.tokens[jPrev].prev;
}
}
 
// CheckStart forrecursion theat startfirst ofgap antoken unresolved sequencepair
if (
if ( ( j !== null ) && ( this.oldText.tokens[j] !== null ) && ( this.newText.tokens[i].link === null ) && ( this.oldText.tokens[j].link === null ) ) {
i !== null &&
 
j !== null &&
// Determine the limits of the unresolved new sequence
var iStartthis.newText.tokens[i].link === null; &&
var iEndthis.oldText.tokens[j].link === i;null
var) iLength = 0;{
var iNextrepeat = ifalse;
var dirUp = true;
while ( ( iNext !== null ) && ( this.newText.tokens[iNext].link === null ) ) {
this.calculateDiff( level, recurse, repeat, i, j, dirUp, recursionLevel + 1 );
iStart = iNext;
iLength ++;
if ( iStart == newStart ) {
break;
}
iNext = this.newText.tokens[iNext].prev;
}
 
// Determine the limits of the unresolved old sequence
var jStart = null;
var jEnd = j;
var jLength = 0;
var jNext = j;
while ( ( jNext !== null ) && ( this.oldText.tokens[jNext].link === null ) ) {
jStart = jNext;
jLength ++;
if ( jStart == oldStart ) {
break;
}
jNext = this.oldText.tokens[jNext].prev;
}
 
// Recursively diff the unresolved sequence
if ( ( iLength > 1 ) || ( jLength > 1 ) ) {
 
// New empty symbols object for sub-region
var symbolsRecurse = {
token: [],
hashTable: {},
linked: false
};
this.calculateDiff( symbolsRecurse, level, false, true, iStart, iEnd, jStart, jEnd, recursionLevel + 1 );
}
i = iStart;
}
 
// Next list element
if ( i == newStart ) {
break;
}
i = this.newText.tokens[i].prev;
}
}
Line 1,777 ⟶ 2,055:
 
// Stop timers
if ( ( this.config.timer === true ) && (repeating repeat !=== true )false ) {
if ( this.recursionTimer[recursionLevel] === undefined ) {
this.recursionTimer[recursionLevel] = 0;
Line 1,783 ⟶ 2,061:
this.recursionTimer[recursionLevel] += this.timeEnd( level + recursionLevel, true );
}
if ( ( this.config.timer === true ) && (repeating repeat !=== true )false && ( recursionLevel === 0 ) ) {
this.timeRecursionEnd( level );
this.timeEnd( level );
Line 1,793 ⟶ 2,071:
 
/**
* Main method for processing raw diff data, extracting deleted, inserted, and moved blocks.
*
* Scheme of blocks, sections, and groups (old block numbers):
Line 1,826 ⟶ 2,104:
 
// Set 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
// Only for more complex texts that actually have blocks of minimum block length
var unlinkCount = 0;
if (
if ( ( this.config.unlinkBlocks === true ) && ( this.config.blockMinLength > 0 ) ) {
this.config.unlinkBlocks === true &&
this.config.blockMinLength > 0 &&
this.maxWords >= this.config.blockMinLength
) {
if ( this.config.timer === true ) {
this.time( 'unlinktotal unlinking' );
}
 
// Repeat as long as unlinking is possible
var unlinked = true;
while ( ( unlinked === true ) && ( unlinkCount < this.config.unlinkMax ) ) {
 
// Convert '=' to '+'/'-' pairs
Line 1,855 ⟶ 2,132:
 
// Repeat block detection from start
this.maxWords = 0;
this.getSameBlocks();
this.getSections();
Line 1,862 ⟶ 2,140:
}
if ( this.config.timer === true ) {
this.timeEnd( 'unlinktotal unlinking' );
}
}
Line 1,882 ⟶ 2,160:
 
// Debug log
if ( ( this.config.timer === true ) || ( this.config.debug === true ) ) {
console.log( 'Unlink count: ', unlinkCount );
}
Line 1,894 ⟶ 2,172:
 
/**
* Collect identical corresponding matching ('=') blocks from old text and sort by new text.
*
* @param[in] WikEdDiffText newText, oldText Text objects
Line 1,900 ⟶ 2,178:
*/
this.getSameBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'getSameBlocks' );
}
 
var blocks = this.blocks;
Line 1,906 ⟶ 2,188:
blocks.splice( 0 );
 
// Cycle through old text to find matchedconnected (linked, matched) blocks
var j = this.oldText.first;
var i = null;
Line 1,912 ⟶ 2,194:
 
// Skip '-' blocks
while ( ( j !== null ) && ( this.oldText.tokens[j].link === null ) ) {
j = this.oldText.tokens[j].next;
}
Line 1,926 ⟶ 2,208:
var unique = false;
var text = '';
while ( ( i !== null ) && ( j !== null ) && ( this.oldText.tokens[j].link === i ) ) {
var tokentext += this.oldText.tokens[j].token;
count ++;
if ( this.newText.tokens[i].unique === true ) {
unique = true;
}
text += token;
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
Line 1,964 ⟶ 2,245:
 
// Number blocks in new text order
for ( var blockblocksLength = 0; block < blocks.length; block ++ ) {
for ( var block = 0; block < blocksLength; block ++ ) {
blocks[block].newBlock = block;
}
 
if ( this.config.timer === true ) {
this.timeEnd( 'getSameBlocks' );
}
return;
Line 1,973 ⟶ 2,259:
/**
* Collect independent block sections with no block move crosses
* outside a section for per-section determination of non-moving fixed groups.
*
* @param[out] array sections Sections table object
Line 1,979 ⟶ 2,265:
*/
this.getSections = function () {
 
if ( this.config.timer === true ) {
this.time( 'getSections' );
}
 
var blocks = this.blocks;
Line 1,987 ⟶ 2,277:
 
// Cycle through blocks
for ( var blockblocksLength = 0; block < blocks.length; block ++ ) {
for ( var block = 0; block < blocksLength; block ++ ) {
 
var sectionStart = block;
Line 1,996 ⟶ 2,287:
 
// Check right
for ( var j = sectionStart + 1; j < blocks.lengthblocksLength; j ++ ) {
 
// Check for crossing over to the left
Line 2,023 ⟶ 2,314:
block = sectionEnd;
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getSections' );
}
return;
Line 2,029 ⟶ 2,323:
 
/**
* Find groups of continuous old text blocks.
*
* @param[out] array groups Groups table object
Line 2,035 ⟶ 2,329:
*/
this.getGroups = function () {
 
if ( this.config.timer === true ) {
this.time( 'getGroups' );
}
 
var blocks = this.blocks;
Line 2,043 ⟶ 2,341:
 
// Cycle through blocks
for ( var blockblocksLength = 0; block < blocks.length; block ++ ) {
for ( var block = 0; block < blocksLength; block ++ ) {
var groupStart = block;
var groupEnd = block;
Line 2,055 ⟶ 2,354:
 
// Check right
for ( var i = groupEnd + 1; i < blocks.lengthblocksLength; i ++ ) {
 
// Check for crossing over to the left
if ( blocks[i].oldBlock !== oldBlock + 1 ) {
break;
}
Line 2,104 ⟶ 2,403:
} );
block = groupEnd;
 
// Set global word count of longest linked block
if ( maxWords > this.maxWords ) {
this.maxWords = maxWords;
}
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getGroups' );
}
return;
Line 2,111 ⟶ 2,418:
 
/**
* Set longest sequence of increasing groups in sections as fixed (not moved).
*
* @param[in] array sections Sections table object
Line 2,118 ⟶ 2,425:
*/
this.setFixed = function () {
 
if ( this.config.timer === true ) {
this.time( 'setFixed' );
}
 
var blocks = this.blocks;
Line 2,124 ⟶ 2,435:
 
// Cycle through sections
for ( var sectionsectionsLength = 0; section < sections.length; section ++ ) {
for ( var section = 0; section < sectionsLength; section ++ ) {
var blockStart = sections[section].blockStart;
var blockEnd = sections[section].blockEnd;
Line 2,146 ⟶ 2,458:
 
// Mark fixed groups
for ( var imaxPathLength = 0; i < maxPath.length; i ++ ) {
for ( var i = 0; i < maxPathLength; i ++ ) {
var group = maxPath[i];
groups[group].fixed = true;
Line 2,155 ⟶ 2,468:
}
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'setFixed' );
}
return;
Line 2,161 ⟶ 2,477:
 
/**
* Recusively find path of groups in increasing old group order with longest char length.
*
* @param int start Path start group
Line 2,215 ⟶ 2,531:
 
/**
* CollectConvert deletionmatching ('-=') blocks fromin oldgroups into insertion/deletion ('+'/'-') textpairs
* if too short and too common.
* Prevents fragmentated diffs for very different versions.
*
* @param[in] array blocks Blocks table object
* @param[in/out] WikEdDiffText newText, oldText Text object, linked property
* @param[in/out] array groups Groups table object
* @return bool True if text tokens were unlinked
*/
this.unlinkBlocks = function () {
 
var blocks = this.blocks;
var groups = this.groups;
 
// Cycle through groups
var unlinked = false;
var groupsLength = groups.length;
for ( var group = 0; group < groupsLength; group ++ ) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
 
// Unlink whole group if no block is at least blockMinLength words long and unique
if ( groups[group].maxWords < this.config.blockMinLength && groups[group].unique === false ) {
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === '=' ) {
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
 
// Otherwise unlink block flanks
else {
 
// Unlink blocks from start
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type === '=' ) {
 
// Stop unlinking if more than one word or a unique word
if ( blocks[block].words > 1 || blocks[block].unique === true ) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
blockStart = block;
}
}
 
// Unlink blocks from end
for ( var block = blockEnd; block > blockStart; block -- ) {
if ( blocks[block].type === '=' ) {
 
// Stop unlinking if more than one word or a unique word
if (
blocks[block].words > 1 ||
( blocks[block].words === 1 && blocks[block].unique === true )
) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
}
return unlinked;
};
 
 
/**
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs.
*
* @param[in] array blocks Blocks table object
* @param[out] WikEdDiffText newText, oldText Text objects, link property
*/
this.unlinkSingleBlock = function ( block ) {
 
// Cycle through old text
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
 
// Unlink tokens
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
this.oldText.tokens[j].link = null;
j = this.oldText.tokens[j].next;
}
return;
};
 
 
/**
* Collect deletion ('-') blocks from old text.
*
* @param[in] WikEdDiffText oldText Old Text object
Line 2,221 ⟶ 2,628:
*/
this.getDelBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'getDelBlocks' );
}
 
var blocks = this.blocks;
 
// Cycle through old text to find matchedconnected (linked, matched) blocks
var j = this.oldText.first;
var i = null;
Line 2,233 ⟶ 2,644:
var count = 0;
var text = '';
while ( ( j !== null ) && ( this.oldText.tokens[j].link === null ) ) {
count ++;
text += this.oldText.tokens[j].token;
Line 2,263 ⟶ 2,674:
if ( j !== null ) {
i = this.oldText.tokens[j].link;
while ( ( i !== null ) && ( j !== null ) && ( this.oldText.tokens[j].link === i ) ) {
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
}
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'getDelBlocks' );
}
return;
Line 2,274 ⟶ 2,688:
 
/**
* Position deletion '-' blocks into new text order.
* Deletion blocks move with fixed reference:
* Old: 1 D 2 1 D 2
Line 2,281 ⟶ 2,695:
* Fixed: * *
* newNumber: 1 1 2 2
*
* Marks '|' and deletions '-' get newNumber of reference block
* and are sorted around it by old text number.
*
* @param[in/out] array blocks Blocks table, newNumber, section, group, and fixed properties
Line 2,288 ⟶ 2,703:
*/
this.positionDelBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'positionDelBlocks' );
}
 
var blocks = this.blocks;
Line 2,299 ⟶ 2,718:
 
// Cycle through blocks in old text order
for ( var blockblocksOldLength = 0; block < blocksOld.length; block ++ ) {
for ( var block = 0; block < blocksOldLength; block ++ ) {
var delBlock = blocksOld[block];
 
// '-' block only
if ( delBlock.type !== '-' ) {
continue;
}
 
// Find fixed '=' reference block from original block position to position '-' block,
// similarSimilar to position marks '|' code
 
// Get old text prev block
Line 2,327 ⟶ 2,748:
// Move after prev block if fixed
var refBlock = null;
if ( ( prevBlock !== null ) && ( prevBlock.type === '=' ) && ( prevBlock.fixed === true ) ) {
refBlock = prevBlock;
}
 
// Move before next block if fixed
else if ( ( nextBlock !== null ) && ( nextBlock.type === '=' ) && ( nextBlock.fixed === true ) ) {
refBlock = nextBlock;
}
 
// Move after prev block if not start of group
else if (
else if ( ( prevBlock !== null ) && ( prevBlock.type == '=' ) && ( prevBlockNumber != groups[ prevBlock.group ].blockEnd ) ) {
prevBlock !== null &&
prevBlock.type === '=' &&
prevBlockNumber !== groups[ prevBlock.group ].blockEnd
) {
refBlock = prevBlock;
}
 
// Move before next block if not start of group
else if (
else if ( ( nextBlock !== null ) && ( nextBlock.type == '=' ) && ( nextBlockNumber != groups[ nextBlock.group ].blockStart ) ) {
nextBlock !== null &&
nextBlock.type === '=' &&
nextBlockNumber !== groups[ nextBlock.group ].blockStart
) {
refBlock = nextBlock;
}
Line 2,349 ⟶ 2,778:
else {
for ( var fixed = block; fixed >= 0; fixed -- ) {
if ( ( blocksOld[fixed].type === '=' ) && ( blocksOld[fixed].fixed === true ) ) {
refBlock = blocksOld[fixed];
break;
Line 2,373 ⟶ 2,802:
this.sortBlocks();
 
if ( this.config.timer === true ) {
return;
this.timeEnd( 'positionDelBlocks' );
};
 
 
/**
* Convert matching '=' blocks in groups into insertion/deletion ('+'/'-') pairs
* if too short and too common
* Prevents fragmentated diffs for very different versions
*
* @param[in] array blocks Blocks table object
* @param[in/out] WikEdDiffText newText, oldText Text object, linked property
* @param[in/out] array groups Groups table object
* @return bool True if text tokens were unlinked
*/
this.unlinkBlocks = function () {
 
var blocks = this.blocks;
var groups = this.groups;
 
// Cycle through groups
var unlinked = false;
for ( var group = 0; group < groups.length; group ++ ) {
var blockStart = groups[group].blockStart;
var blockEnd = groups[group].blockEnd;
// Unlink whole group if no block is at least blockMinLength words long and unique
if ( ( groups[group].maxWords < this.config.blockMinLength ) && ( groups[group].unique === false ) ) {
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type == '=' ) {
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
 
// Otherwise unlink block flanks
else {
 
// Unlink blocks from start
for ( var block = blockStart; block <= blockEnd; block ++ ) {
if ( blocks[block].type == '=' ) {
 
// Stop unlinking if more than one word or a unique word
if ( ( blocks[block].words > 1 ) || ( blocks[block].unique === true ) ) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
blockStart = block;
}
}
 
// Unlink blocks from end
for ( var block = blockEnd; block > blockStart; block -- ) {
if ( ( blocks[block].type == '=' ) ) {
 
// Stop unlinking if more than one word or a unique word
if ( ( blocks[block].words > 1 ) || ( ( blocks[block].words == 1 ) && ( blocks[block].unique === true ) ) ) {
break;
}
this.unlinkSingleBlock( blocks[block] );
unlinked = true;
}
}
}
}
return unlinked;
};
 
 
/**
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs
*
* @param[in] array blocks Blocks table object
* @param[out] WikEdDiffText newText, oldText Text objects, link property
*/
this.unlinkSingleBlock = function ( block ) {
 
// Cycle through old text
var j = block.oldStart;
for ( var count = 0; count < block.count; count ++ ) {
 
// Unlink tokens
this.newText.tokens[ this.oldText.tokens[j].link ].link = null;
this.oldText.tokens[j].link = null;
j = this.oldText.tokens[j].next;
}
return;
Line 2,464 ⟶ 2,810:
 
/**
* Collect insertion ('+') blocks from new text.
*
* @param[in] WikEdDiffText newText New Text object
Line 2,470 ⟶ 2,816:
*/
this.getInsBlocks = function () {
 
if ( this.config.timer === true ) {
this.time( 'getInsBlocks' );
}
 
var blocks = this.blocks;
Line 2,478 ⟶ 2,828:
 
// Jump over linked (matched) block
while ( ( i !== null ) && ( this.newText.tokens[i].link !== null ) ) {
i = this.newText.tokens[i].next;
}
Line 2,487 ⟶ 2,837:
var count = 0;
var text = '';
while ( ( i !== null ) && ( this.newText.tokens[i].link === null ) ) {
count ++;
text += this.newText.tokens[i].token;
Line 2,517 ⟶ 2,867:
this.sortBlocks();
 
if ( this.config.timer === true ) {
this.timeEnd( 'getInsBlocks' );
}
return;
};
Line 2,522 ⟶ 2,875:
 
/**
* Sort blocks by new text token number and update groups.
*
* @param[in/out] array groups Groups table object
Line 2,543 ⟶ 2,896:
// Cycle through blocks and update groups with new block numbers
var group = null;
for ( var blockblocksLength = 0; block < blocks.length; block ++ ) {
for ( var block = 0; block < blocksLength; block ++ ) {
var blockGroup = blocks[block].group;
if ( blockGroup !== null ) {
Line 2,559 ⟶ 2,913:
 
/**
* Set group numbers of insertion '+' blocks.
*
* @param[in/out] array groups Groups table object
Line 2,565 ⟶ 2,919:
*/
this.setInsGroups = function () {
 
if ( this.config.timer === true ) {
this.time( 'setInsGroups' );
}
 
var blocks = this.blocks;
Line 2,570 ⟶ 2,928:
 
// Set group numbers of '+' blocks inside existing groups
for ( var groupgroupsLength = 0; group < groups.length; group ++ ) {
for ( var group = 0; group < groupsLength; group ++ ) {
var fixed = groups[group].fixed;
for ( var block = groups[group].blockStart; block <= groups[group].blockEnd; block ++ ) {
Line 2,583 ⟶ 2,942:
 
// Cycle through blocks
for ( var blockblocksLength = 0; block < blocks.length; block ++ ) {
for ( var block = 0; block < blocksLength; block ++ ) {
 
// Skip existing groups
Line 2,603 ⟶ 2,963:
} );
}
}
if ( this.config.timer === true ) {
this.timeEnd( 'setInsGroups' );
}
return;
Line 2,609 ⟶ 2,972:
 
/**
* Mark original positions of moved groups.
* Scheme: moved block marks at original positions relative to fixed groups:
* Groups: 3 7
Line 2,619 ⟶ 2,982:
* | |> 9 (no next larger fixed)
* Fixed: * *
*
* Mark direction: groups.movedGroup.blockStart < groups.group.blockStart
* Group side: $groups.$movedGroup.oldNumber < $groups.$group.oldNumber
*
* Marks '|' and deletions '-' get newNumber of reference block
* and are sorted around it by old text number.
*
* @param[in/out] array groups Groups table object, movedFrom property
Line 2,628 ⟶ 2,993:
*/
this.insertMarks = function () {
 
if ( this.config.timer === true ) {
this.time( 'insertMarks' );
}
 
var blocks = this.blocks;
Line 2,638 ⟶ 3,007:
 
// Enumerate copy
for ( var iblocksOldLength = 0; i < blocksOld.length; i ++ ) {
for ( var i = 0; i < blocksOldLength; i ++ ) {
blocksOld[i].number = i;
}
Line 2,653 ⟶ 3,023:
// Create lookup table: original to sorted
var lookupSorted = [];
for ( var i = 0; i < blocksOld.lengthblocksOldLength; i ++ ) {
lookupSorted[ blocksOld[i].number ] = i;
}
 
// Cycle through groups (moved group)
for ( var movedgroupsLength = 0; moved < groups.length; moved ++ ) {
for ( var moved = 0; moved < groupsLength; moved ++ ) {
var movedGroup = groups[moved];
if ( movedGroup.fixed !== false ) {
Line 2,665 ⟶ 3,036:
var movedOldNumber = movedGroup.oldNumber;
 
// Find fixed '=' reference block from original block position to position '|' block,
// similarSimilar to position deletions '-' code
 
// Get old text prev block
Line 2,683 ⟶ 3,055:
// Move after prev block if fixed
var refBlock = null;
if ( ( prevBlock !== null ) && ( prevBlock.type === '=' ) && ( prevBlock.fixed === true ) ) {
refBlock = prevBlock;
}
 
// Move before next block if fixed
else if ( ( nextBlock !== null ) && ( nextBlock.type === '=' ) && ( nextBlock.fixed === true ) ) {
refBlock = nextBlock;
}
Line 2,695 ⟶ 3,067:
else {
for ( var fixed = lookupSorted[ movedGroup.blockStart ] - 1; fixed >= 0; fixed -- ) {
if ( ( blocksOld[fixed].type === '=' ) && ( blocksOld[fixed].fixed === true ) ) {
refBlock = blocksOld[fixed];
break;
Line 2,758 ⟶ 3,130:
this.sortBlocks();
 
if ( this.config.timer === true ) {
this.timeEnd( 'insertMarks' );
}
return;
};
Line 2,763 ⟶ 3,138:
 
/**
* Collect diff fragment list for markup, create abstraction layer for customized diffs.
* Adds the following fagment types:
* '=', '-', '+' same, deletion, insertion
Line 2,788 ⟶ 3,163:
 
// Cycle through groups
for ( var groupgroupsSortLength = 0; group < groupsSort.length; group ++ ) {
for ( var group = 0; group < groupsSortLength; group ++ ) {
var blockStart = groupsSort[group].blockStart;
var blockEnd = groupsSort[group].blockEnd;
Line 2,814 ⟶ 3,190:
 
// Add '=' unchanged text and moved block
if ( ( type === '=' ) || ( type === '-' ) || ( type === '+' ) ) {
fragments.push( {
text: blocks[block].text,
Line 2,823 ⟶ 3,199:
 
// Add '<' and '>' marks
else if ( type === '|' ) {
var movedGroup = groups[ blocks[block].moved ];
 
// Get mark text
var markText = '';
for (
for ( var movedBlock = movedGroup.blockStart; movedBlock <= movedGroup.blockEnd; movedBlock ++ ) {
ifvar ( ( blocks[movedBlock].type == '=' ) || ( blocks[movedBlock]movedGroup.type == '-' ) ) {blockStart;
movedBlock <= movedGroup.blockEnd;
movedBlock ++
) {
if ( blocks[movedBlock].type === '=' || blocks[movedBlock].type === '-' ) {
markText += blocks[movedBlock].text;
}
Line 2,863 ⟶ 3,243:
 
// Cycle through fragments, join consecutive fragments of same type (i.e. '-' blocks)
for ( var fragmentfragmentsLength = 1; fragment < fragments.length; fragment ++ ) {
for ( var fragment = 1; fragment < fragmentsLength; 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 !== ''
) {
Line 2,888 ⟶ 3,269:
 
/**
* Clip unchanged sections from unmoved block text.
* Adds the following fagment types:
* '~', ' ~', '~ ' omission indicators
Line 2,900 ⟶ 3,281:
 
// Skip if only one fragment in containers, no change
if ( ( fragments.length === 5 ) ) {
return;
}
Line 2,935 ⟶ 3,316:
 
// Cycle through fragments
for ( var fragmentfragmentsLength = 0; fragment < fragments.length; fragment ++ ) {
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
 
// Skip if not an unmoved and unchanged block
var type = fragments[fragment].type;
var color = fragments[fragment].color;
if ( ( type !== '=' ) || ( color !== null ) ) {
continue;
}
Line 2,946 ⟶ 3,328:
// Skip if too short for clipping
var text = fragments[fragment].text;
ifvar (textLength (= text.length < minRight ) && ( text.length < minLeft ) ) {;
if ( textLength < minRight && textLength < minLeft ) {
continue;
}
Line 2,961 ⟶ 3,344:
lines.unshift( 0 );
}
if ( lastIndex !== text.lengthtextLength ) {
lines.push( text.lengthtextLength );
}
 
Line 2,983 ⟶ 3,366:
paragraphs.unshift( 0 );
}
if ( lastIndex !== text.lengthtextLength ) {
paragraphs.push( text.lengthtextLength );
}
 
Line 2,994 ⟶ 3,377:
 
// Find clip pos from left, skip for first non-container block
if ( fragment !== 2 ) {
 
// Maximum lines to search from left
var rangeLeftMax = text.lengthtextLength;
if ( this.config.clipLinesLeftMax < lines.length ) {
rangeLeftMax = lines[this.config.clipLinesLeftMax];
Line 3,004 ⟶ 3,387:
// Find first heading from left
if ( rangeLeft === null ) {
for ( var jheadingsLength = 0; j < headingsEnd.length; j ++ ) {
for ( var j = 0; j < headingsLength; j ++ ) {
if ( ( headingsEnd[j] > this.config.clipHeadingLeft ) || ( headingsEnd[j] > rangeLeftMax ) ) {
break;
}
Line 3,016 ⟶ 3,400:
// Find first paragraph from left
if ( rangeLeft === null ) {
for ( var jparagraphsLength = 0; j < paragraphs.length; j ++ ) {
for ( var j = 0; j < paragraphsLength; j ++ ) {
if ( ( paragraphs[j] > this.config.clipParagraphLeftMax ) || ( paragraphs[j] > rangeLeftMax ) ) {
if (
paragraphs[j] > this.config.clipParagraphLeftMax ||
paragraphs[j] > rangeLeftMax
) {
break;
}
Line 3,030 ⟶ 3,418:
// Find first line break from left
if ( rangeLeft === null ) {
for ( var jlinesLength = 0; j < lines.length; j ++ ) {
iffor ( (var lines[j] >= this.config.clipLineLeftMax0; )j ||< (linesLength; lines[j] > rangeLeftMax )++ ) {
if ( lines[j] > this.config.clipLineLeftMax || lines[j] > rangeLeftMax ) {
break;
}
Line 3,042 ⟶ 3,431:
}
 
// Find first blank from left
if ( rangeLeft === null ) {
this.config.regExp.clipBlank.lastIndex = this.config.clipBlankLeftMin;
if ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if (
if ( ( regExpMatch.index < this.config.clipBlankLeftMax ) && ( regExpMatch.index < rangeLeftMax ) ) {
regExpMatch.index < this.config.clipBlankLeftMax &&
regExpMatch.index < rangeLeftMax
) {
rangeLeft = regExpMatch.index;
rangeLeftType = 'blank';
Line 3,069 ⟶ 3,461:
 
// Find clip pos from right, skip for last non-container block
if ( fragment !== fragments.length - 3 ) {
 
// Maximum lines to search from right
Line 3,080 ⟶ 3,472:
if ( rangeRight === null ) {
for ( var j = headings.length - 1; j >= 0; j -- ) {
if (
if ( ( headings[j] < text.length - this.config.clipHeadingRight ) || ( headings[j] < rangeRightMin ) ) {
headings[j] < textLength - this.config.clipHeadingRight ||
headings[j] < rangeRightMin
) {
break;
}
Line 3,092 ⟶ 3,487:
if ( rangeRight === null ) {
for ( var j = paragraphs.length - 1; j >= 0 ; j -- ) {
if (
if ( ( paragraphs[j] < text.length - this.config.clipParagraphRightMax ) || ( paragraphs[j] < rangeRightMin ) ) {
paragraphs[j] < textLength - this.config.clipParagraphRightMax ||
paragraphs[j] < rangeRightMin
) {
break;
}
if ( paragraphs[j] < text.lengthtextLength - this.config.clipParagraphRightMin ) {
rangeRight = paragraphs[j];
rangeRightType = 'paragraph';
Line 3,106 ⟶ 3,504:
if ( rangeRight === null ) {
for ( var j = lines.length - 1; j >= 0; j -- ) {
if (
if ( ( lines[j] < text.length - this.config.clipLineRightMax ) || ( lines[j] < rangeRightMin ) ) {
lines[j] < textLength - this.config.clipLineRightMax ||
lines[j] < rangeRightMin
) {
break;
}
if ( lines[j] < text.lengthtextLength - this.config.clipLineRightMin ) {
rangeRight = lines[j];
rangeRightType = 'line';
Line 3,119 ⟶ 3,520:
// Find last blank from right
if ( rangeRight === null ) {
var startPos = text.lengthtextLength - this.config.clipBlankRightMax;
if ( startPos < rangeRightMin ) {
startPos = rangeRightMin;
Line 3,126 ⟶ 3,527:
var lastPos = null;
while ( ( regExpMatch = this.config.regExp.clipBlank.exec( text ) ) !== null ) {
if ( regExpMatch.index > text.lengthtextLength - this.config.clipBlankRightMin ) {
if ( lastPos !== null ) {
rangeRight = lastPos;
Line 3,139 ⟶ 3,540:
// Fixed number of chars from right
if ( rangeRight === null ) {
if ( text.lengthtextLength - this.config.clipCharsRight > rangeRightMin ) {
rangeRight = text.lengthtextLength - this.config.clipCharsRight;
rangeRightType = 'chars';
}
Line 3,153 ⟶ 3,554:
 
// Check if we skip clipping if ranges are close together
if ( ( rangeLeft !== null ) && ( rangeRight !== null ) ) {
 
// Skip if overlapping ranges
Line 3,168 ⟶ 3,569:
// Skip if lines too close
var skipLines = 0;
for ( var jlinesLength = 0; j < lines.length; j ++ ) {
iffor ( (var lines[j] >= rangeRight0; )j ||< (linesLength; skipLines > this.config.clipSkipLinesj )++ ) {
if ( lines[j] > rangeRight || skipLines > this.config.clipSkipLines ) {
break;
}
Line 3,182 ⟶ 3,584:
 
// Skip if nothing to clip
if ( ( rangeLeft === null ) && ( rangeRight === null ) ) {
continue;
}
Line 3,196 ⟶ 3,598:
 
// 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, '' );
Line 3,206 ⟶ 3,608:
}
 
// Split right text,
var textRight = null;
var omittedRight = null;
Line 3,216 ⟶ 3,618:
 
// 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, '' );
Line 3,228 ⟶ 3,630:
// Remove split element
fragments.splice( fragment, 1 );
fragmentsLength --;
 
// Add left text to fragments list
if ( rangeLeft !== null ) {
fragments.splice( fragment ++, 0, { text: textLeft, type: '=', color: null } );
fragmentsLength ++;
if ( omittedLeft !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedLeft, color: null } );
fragmentsLength ++;
}
}
 
// Add fragment container and separator to list
if ( ( rangeLeft !== null ) && ( rangeRight !== null ) ) {
fragments.splice( fragment ++, 0, { text: '', type: ']', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: ',', color: null } );
fragments.splice( fragment ++, 0, { text: '', type: '[', color: null } );
fragmentsLength += 3;
}
 
Line 3,248 ⟶ 3,654:
if ( omittedRight !== null ) {
fragments.splice( fragment ++, 0, { text: '', type: omittedRight, color: null } );
fragmentsLength ++;
}
fragments.splice( fragment ++, 0, { text: textRight, type: '=', color: null } );
fragmentsLength ++;
}
}
Line 3,263 ⟶ 3,671:
 
/**
* Create html formatted diff code from diff fragments.
*
* @param[in] array fragments Fragments array, abstraction layer for diff code
Line 3,275 ⟶ 3,683:
 
// No change, only one unchanged block in containers
if ( ( fragments.length === 5 ) && ( fragments[2].type === '=' ) ) {
this.html = '';
return;
Line 3,282 ⟶ 3,690:
// Cycle through fragments
var htmlFragments = [];
for ( var fragmentfragmentsLength = 0; fragment < fragments.length; fragment ++ ) {
for ( var fragment = 0; fragment < fragmentsLength; fragment ++ ) {
var text = fragments[fragment].text;
var type = fragments[fragment].type;
Line 3,295 ⟶ 3,704:
 
// 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
Line 3,359 ⟶ 3,768:
 
// Add colored right-pointing block start markup
else if ( type === '(>' ) {
if ( version !== 'old' ) {
 
// Get title
Line 3,383 ⟶ 3,792:
 
// Add colored block end markup
else if ( type === ' )' ) {
if ( version !== 'old' ) {
html = this.config.htmlCode.blockEnd;
}
Line 3,390 ⟶ 3,799:
 
// Add '=' (unchanged) text and moved block
if ( type === '=' ) {
text = this.htmlEscape( text );
if ( color !== null ) {
if ( version !== 'old' ) {
html = this.markupBlanks( text, true );
}
Line 3,403 ⟶ 3,812:
 
// 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 );
Line 3,422 ⟶ 3,831:
 
// Add '+' text
else if ( type === '+' ) {
if ( version !== 'old' ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
Line 3,437 ⟶ 3,846:
 
// Add '<' and '>' code
else if ( ( type === '<' ) || ( type === '>' ) ) {
if ( version !== 'new' ) {
 
// Display as deletion at original position
if ( ( this.config.showBlockMoves === false ) || ( version === 'old' ) ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
if ( version === 'old' ) {
if ( this.config.coloredBlocks === true ) {
html =
this.htmlCustomize( this.config.htmlCode.blockColoredStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
else {
html =
this.htmlCustomize( this.config.htmlCode.blockStart, color ) +
text +
this.config.htmlCode.blockEnd;
}
}
else {
if ( blank === true ) {
html =
this.config.htmlCode.deleteStartBlank +
text +
this.config.htmlCode.deleteEnd;
}
else {
Line 3,464 ⟶ 3,882:
// Display as mark
else {
if ( type === '<' ) {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markLeftColored, color, text );
Line 3,494 ⟶ 3,912:
 
/**
* Customize html code fragments.
* Replaces:
* {number}: class/color/block/mark/id number
Line 3,523 ⟶ 3,941:
var gapMark = ' [...] ';
if ( title.length > max ) {
title =
title.substr( 0, max - gapMark.length - end ) +
gapMark +
title.substr( title.length - end );
}
title = this.htmlEscape( title );
Line 3,535 ⟶ 3,956:
 
/**
* Replace html-sensitive characters in output text with character entities.
*
* @param string html Html code to be escaped
Line 3,551 ⟶ 3,972:
 
/**
* Markup tabs, newlines, and spaces in diff fragment text.
*
* @param bool highlight Highlight newlines and tabsspaces in addition to spacestabs
* @param string html Text code to be marked-up
* @return string Marked-up text
Line 3,559 ⟶ 3,980:
this.markupBlanks = function ( html, highlight ) {
 
html = html.replace( / /g, this.config.htmlCode.space);
if ( highlight === true ) {
html = html.replace( / /g, this.config.htmlCode.space);
html = html.replace( /\n/g, this.config.htmlCode.newline);
html = html.replace( /\t/g, this.config.htmlCode.tab);
}
html = html.replace( /\t/g, this.config.htmlCode.tab);
return html;
};
Line 3,569 ⟶ 3,990:
 
/**
* Count real words in text.
*
* @param string text Text for word counting
Line 3,581 ⟶ 4,002:
 
/**
* Test diff code for consistency with input versions.
* Prints results to debug console.
*
* @param[in] WikEdDiffText newText, oldText Text objects
Line 3,592 ⟶ 4,013:
var diff = this.html.replace( /<[^>]*>/g, '');
var text = this.htmlEscape( this.newText.text );
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with new text version!' );
);
this.error = true;
console.log( 'new text:\n', text );
Line 3,606 ⟶ 4,029:
var diff = this.html.replace( /<[^>]*>/g, '');
var text = this.htmlEscape( this.oldText.text );
if ( diff !== text ) {
console.log(
'Error: wikEdDiff unit test failure: diff not consistent with old text version!' );
);
this.error = true;
console.log( 'old text:\n', text );
Line 3,621 ⟶ 4,046:
 
/**
* Dump blocks object to browser console.
*
* @param string name Block name
Line 3,631 ⟶ 4,056:
blocks = this.blocks;
}
var dump =
var dump = '\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq \twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \ttext\n';
'\ni \toldBl \tnewBl \toldNm \tnewNm \toldSt \tcount \tuniq' +
for ( var i = 0; i < blocks.length; i ++ ) {
'\twords \tchars \ttype \tsect \tgroup \tfixed \tmoved \ttext\n';
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';
var blocksLength = blocks.length;
for ( var i = 0; i < blocksLength; i ++ ) {
dump +=
i + ' \t' + blocks[i].oldBlock + ' \t' + blocks[i].newBlock + ' \t' +
blocks[i].oldNumber + ' \t' + blocks[i].newNumber + ' \t' + blocks[i].oldStart + ' \t' +
blocks[i].count + ' \t' + blocks[i].unique + ' \t' + blocks[i].words + ' \t' +
blocks[i].chars + ' \t' + blocks[i].type + ' \t' + blocks[i].section + ' \t' +
blocks[i].group + ' \t' + blocks[i].fixed + ' \t' + blocks[i].moved + ' \t' +
this.debugShortenText( blocks[i].text ) + '\n';
}
console.log( name + ':\n' + dump );
Line 3,640 ⟶ 4,074:
 
/**
* Dump groups object to browser console.
*
* @param string name Group name
Line 3,650 ⟶ 4,084:
groups = this.groups;
}
var dump =
'\ni \toldNm \tblSta \tblEnd \tuniq \tmaxWo' +
'\twords \tchars \tfixed \toldNm \tmFrom \tcolor\n';
var groupsLength = groupsLength;
for ( var i = 0; i < groups.length; i ++ ) {
dump +=
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';
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 );
Line 3,659 ⟶ 4,100:
 
/**
* Dump fragments array to browser console.
*
* @param string name Fragments name
Line 3,668 ⟶ 4,109:
var fragments = this.fragments;
var dump = '\ni \ttype \tcolor \ttext\n';
for ( var ifragmentsLength = 0; i < fragments.length; i ++ ) {
for ( var i = 0; i < fragmentsLength; i ++ ) {
dump +=
i + ' \t"' + fragments[i].type + '" \t' + fragments[i].color + ' \t' +
this.debugShortenText( fragments[i].text, 120, 40 ) + '\n';
}
console.log( name + ':\n' + dump );
Line 3,676 ⟶ 4,120:
 
/**
* Dump borders array to browser console.
* Shorten text for dumping
*
* @param string name Arrays name
* @param[in] array border Match border array
*/
this.debugBorders = function ( name, borders ) {
 
var dump = '\ni \t[ new \told ]\n';
var bordersLength = borders.length;
for ( var i = 0; i < bordersLength; i ++ ) {
dump += i + ' \t[ ' + borders[i][0] + ' \t' + borders[i][1] + ' ]\n';
}
console.log( name, dump );
};
 
 
/**
* Shorten text for dumping.
*
* @param string text Text to be shortened
Line 3,685 ⟶ 4,146:
this.debugShortenText = function ( text, max, end ) {
 
if ( typeof text !== 'string' ) {
text = text.toString();
}
Line 3,705 ⟶ 4,166:
/**
* Start timer 'label', analogous to JavaScript console timer.
* Usage: this.time( 'label' );
*
* @param string label Timer label
* @param[out] array timer Current time in milliseconds (float)
*/
this.time = function ( label ) {
Line 3,718 ⟶ 4,179:
 
/**
* Stop timer 'label', analogous to JavaScript console timer.
* Logs time in milliseconds since start to browser console.
* Usage: this.timeEnd( 'label' );
*
* @param string label Timer label.
* @param bool noLog Do not log result.
* @return float Time in milliseconds, rounded to two decimal digits.
*/
this.timeEnd = function ( label, noLog ) {
Line 3,735 ⟶ 4,196:
this.timer[label] = undefined;
if ( noLog !== true ) {
console.log( label + ': ' + diff.toFixed( 2 ) + ' ms' );
}
}
Line 3,743 ⟶ 4,204:
 
/**
* Log recursion timer results to browser console.
* Usage: this.timeRecursionEnd();
*
Line 3,754 ⟶ 4,215:
 
// Subtract times spent in deeper recursions
for ( var itimerEnd = 0; i < this.recursionTimer.length - 1; i ++ ) {
for ( var i = 0; i < timerEnd; i ++ ) {
this.recursionTimer[i] -= this.recursionTimer[i + 1];
}
 
// Log recursion times
for ( var itimerLength = 0; i < this.recursionTimer.length; i ++ ) {
for ( var i = 0; i < timerLength; i ++ ) {
console.log( text + ' recursion ' + i + ': ' + this.recursionTimer[i] + ' ms\n' );
console.log( text + ' recursion ' + i + ': ' + this.recursionTimer[i].toFixed( 2 ) + ' ms' );
}
}
Line 3,769 ⟶ 4,232:
 
/**
* Log variable values to debug console.
* Usage: this.debug( 'var', var );
*
* @param string name Object identifier
Line 3,788 ⟶ 4,251:
 
/**
* Add script to document head.
*
* @param string code JavaScript code
Line 3,810 ⟶ 4,273:
 
/**
* Add stylesheet to document head, cross-browser >= IE6.
*
* @param string css CSS code
Line 3,838 ⟶ 4,301:
 
/**
* Recursive deep copy from target over source for customization import.
*
* @param object source Source object
Line 3,864 ⟶ 4,327:
 
/**
* Data and methods for single text version (old or new one).
*
* @class WikEdDiffText
Line 3,888 ⟶ 4,351:
 
/**
* Constructor, initialize text object.
*
* @param string text Text of version
Line 3,895 ⟶ 4,358:
this.init = function () {
 
if ( typeof text !== 'string' ) {
text = text.toString();
}
Line 3,902 ⟶ 4,365:
this.text = text.replace( /\r\n?/g, '\n');
 
// parseParse and count words and chunks for identification of unique real words
if ( this.parent.config.timer === true ) {
this.parent.time( 'wordParse' );
Line 3,916 ⟶ 4,379:
 
/**
* Parse and count words and chunks for identification of unique words.
*
* @param string regExp Regular expression for counting words
Line 3,924 ⟶ 4,387:
this.wordParse = function ( regExp ) {
 
var regExpMatch = this.text.match( regExp );
while (if ( regExpMatch = regExp.exec( this.text ) ) !== null ) {
var wordmatchLength = regExpMatch[0].length;
iffor (var this.words[word]i === undefined0; i < matchLength; i ++) {
this.words[var word] = 1regExpMatch[i];
if ( Object.prototype.hasOwnProperty.call( this.words, word ) === false ) {
}
this.words[word] = 1;
else {
}
this.words[word] ++;
else {
this.words[word] ++;
}
}
}
Line 3,939 ⟶ 4,405:
 
/**
* Split text into paragraph, line, sentence, chunk, word, or character tokens.
*
* @param string level Level of splitting: paragraph, line, sentence, chunk, word, or character
* @param int|null token Index of token to be split, otherwise uses full text
* @param[in] string text Full text to be split
Line 3,970 ⟶ 4,436:
var regExpMatch;
var lastIndex = 0;
whilevar ( ( regExpMatchregExp = this.parent.config.regExp.split[level].exec( text ) ) !== null ) {;
while ( ( regExpMatch = regExp.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 ) {
Line 3,981 ⟶ 4,448:
}
 
// Cycle troughthrough new tokens
for ( var isplitLength = 0; i < split.length; i ++ ) {
for ( var i = 0; i < splitLength; i ++ ) {
 
// Insert current item, link to previous
this.tokens[current] =.push( {
token: split[i],
prev: prev,
Line 3,992 ⟶ 4,460:
number: null,
unique: false
} );
number ++;
 
Line 4,004 ⟶ 4,472:
 
// Connect last new item and existing next item
if ( ( number > 0 ) && ( token !== undefined ) ) {
if ( prev !== null ) {
this.tokens[prev].next = next;
Line 4,024 ⟶ 4,492:
// First or last token has been split
else {
if ( token === this.first ) {
this.first = first;
}
if ( token === this.last ) {
this.last = prev;
}
Line 4,037 ⟶ 4,505:
 
/**
* Split unique unmatched tokens into smaller tokens.
*
* @param string level Level of splitting: line, sentence, chunk, or word
* @param[in] array tokens Tokens list
*/
Line 4,046 ⟶ 4,514:
// Cycle through tokens list
var i = this.first;
while ( ( i !== null ) && ( this.tokens[i] !== null ) ) {
 
// Refine unique unmatched tokens into smaller tokens
Line 4,059 ⟶ 4,527:
 
/**
* Enumerate text token list before detecting blocks.
*
* @param[out] array tokens Tokens list
Line 4,068 ⟶ 4,536:
var number = 0;
var i = this.first;
while ( ( i !== null ) && ( this.tokens[i] !== null ) ) {
this.tokens[i].number = number;
number ++;
Line 4,078 ⟶ 4,546:
 
/**
* Dump tokens object to browser console.
*
* @param string name Text name
Line 4,088 ⟶ 4,556:
var tokens = this.tokens;
var dump = 'first: ' + this.first + '\tlast: ' + this.last + '\n';
dump += '\ni \tlink \t( prev \tnext ) \tuniq \t#num \t"token"\n';
var i = this.first;
while ( ( i !== null ) && ( tokens[i] !== null ) ) {
dump +=
dump += i + ' \t' + tokens[i].link + ' \t( ' + tokens[i].prev + ' \t' + tokens[i].next + ' ) \t' + tokens[i].unique + ' \t#' + tokens[i].number + ' \t' + parent.debugShortenText( tokens[i].token ) + '\n';
i + ' \t' + tokens[i].link + ' \t(' + tokens[i].prev + ' \t' + tokens[i].next + ') \t' +
tokens[i].unique + ' \t#' + tokens[i].number + ' \t' +
parent.debugShortenText( tokens[i].token ) + '\n';
i = tokens[i].next;
}