User:Cacycle/diff.js: Difference between revisions

Content deleted Content added
1.2.1 (October 10, 2014) rm fragment container for no change message
1.2.1 (October 14, 2014) fix slideGaps(), fix sentence split, fix space highlighting, pre container, optimizations: calculateDiff() borders array, for loop pre-calc, freeing memory, 'repeatedDiff' option
Line 4:
// @name wikEd diff
// @version 1.2.1
// @date October 0914, 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 72:
* .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 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 ⟶ 137:
'use strict';
 
/** Define global objects. */
var wikEdDiffConfig;
var WED;
Line 141 ⟶ 143:
 
/**
* wikEd diff main class.
*
* @class WikEdDiff
Line 147 ⟶ 149:
var WikEdDiff = function () {
 
/** @var array config Configuration and customization settings. */
this.config = {
 
/** Core diff settings (with default values). */
 
/**
Line 160 ⟶ 162:
/**
* @var bool config.showBlockMoves
* Enable block move layout with highlighted blocks and marks at theirthe original positions (true)
*/
'showBlockMoves': true,
Line 169 ⟶ 171:
*/
'charDiff': true,
 
/**
* @var bool config.repeatedDiff
* Enable repeated diff to resolve problematic sequences (true)
*/
'repeatedDiff': true,
 
/**
Line 221 ⟶ 229:
/**
* @var bool config.debug
* Show debug infos and stats (block, group, and fragment data objects) in debug console (false)
*/
'debug': false,
Line 237 ⟶ 245:
'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 ⟶ 295:
 
// 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 ⟶ 303:
 
// 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 ⟶ 324:
'clipCharsLeft': 500,
 
// Find clip position: characters from right (heading, paragraph, line break, blanks, or characters)
'clipHeadingRight': 1500,
'clipParagraphRightMax': 1500,
Line 297 ⟶ 348:
 
// 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 { }' +
'.wikEdDiffBlock0 { background-color: #ffff80; }' +
Line 318 ⟶ 378:
'.wikEdDiffBlock7 { background-color: #ffbbbb; }' +
'.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}"; }' +
Line 340 ⟶ 406:
// Wrappers
'.wikEdDiffContainer { }' +
'.wikEdDiffFragment {' +
'white-space: pre-wrap; background: #fff; border: #bbb solid;' +
'border-width: 1px 1px 1px 0.5em; border-radius: 0.5em; font-family: sans-serif;' +
'font-size: 88%; line-height: 1.6; box-shadow: 2px 2px 2px #ddd; padding: 1em; margin: 0; }' +
'}' +
'.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 { 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;' +
'}' +
'.wikEdDiffSeparator { margin-bottom: 1em; }' +
'.wikEdDiffOmittedChars { }' +
Line 350 ⟶ 424:
'.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
Line 369 ⟶ 447:
'.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 387 ⟶ 471:
'(.|\\n)*?((\\r\\n|\\n|\\r){2,}|[' +
this.config.regExpNewParagraph +
'])+', 'g' ),
'g'
),
 
// Split into sentences, after/[^ \n][^\n]*?[.space!?;]+(?=[ and newlines\n]|$)|\r\n|\n|\r/
'sentence': new RegExp(
'[^' + this.config.regExpNewLinesAll +
']*?([.!?;' + this.config.regExpFullStops +
this.config.regExpExclamationMarks + this.config.regExpQuestionMarks +
']+[' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
']+)?([' +
'][^' +
this.config.regExpNewLinesAll +
']*?[.!?;' +
this.config.regExpFullStops +
this.config.regExpExclamationMarks +
this.config.regExpQuestionMarks +
']+(?=[' +
this.config.regExpBlanks +
this.config.regExpNewLinesAll +
']|$)|[' +
this.config.regExpNewLines +
']|\\r\\n|\\n|\\r)', 'g' ),
'g'
),
 
// Split into inline chunks
Line 409 ⟶ 504:
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'),
),
 
// Split into words, multi-char markup, and chars
'word': new RegExp(
'[' +
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(
'[' +
this.config.regExpLetters +
']+([\'’_]?[' +
this.config.regExpLetters +
']+ )*',
'g'
),
'countChunks': new RegExp(
'\\[\\[[^\\[\\]\\n]+\\]\\]|' + // [[wiki link]]
Line 443 ⟶ 560:
'\\{\\{[^\\{\\}\\|\\n]+\\||' + // {{template|
'\\b((https?:|)\\/\\/)[^\\x00-\\x20\\s"\\[\\]\\x7f]+', // link
'g'),
),
 
// RegExp detecting blank-only and single-char blocks
Line 450 ⟶ 568:
// 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 ⟶ 605:
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+$',
'g'
),
'clipTrimBlanksRight': new RegExp(
'^[' +
Line 472 ⟶ 613:
this.config.regExpNewLinesAll +
this.config.regExpNewParagraph +
']+',
'g' )
)
};
 
/** Add messages to configuration settings. */
 
this.config.msg = {
Line 490 ⟶ 633:
 
/**
* 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 ⟶ 647:
 
'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 ⟶ 723:
 
// IE compatibility
if ( ( event === undefined ) && ( window.event !== undefined ) ) {
event = window.event;
}
Line 558 ⟶ 731:
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 ⟶ 749:
 
// Remove mark/block highlighting
if ( ( type === 'mouseout' ) || ( type === 'click' ) ) {
element.onmouseout = null;
element.onmouseover = function ( event ) {
Line 583 ⟶ 756:
 
// 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 ⟶ 764:
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 ⟶ 780:
 
// Scroll to corresponding mark/block element
if ( type === 'click' ) {
 
// Get corresponding element
var corrElement;
if ( element === block ) {
corrElement = mark;
}
Line 617 ⟶ 791:
}
 
// Get element height (getOffsetTop )
var corrElementPos = 0;
var node = corrElement;
Line 654 ⟶ 828:
};
 
/** Internal data structures. */
 
/** @var WikEdDiffText newText New text version object with text and token list */
Line 661 ⟶ 835:
/** @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 */
Line 677 ⟶ 864:
this.recursionTimer = [];
 
/** Output data. */
 
/** @var bool error Unit tests have detected a diff error */
Line 690 ⟶ 877:
 
/**
* Constructor, initialize settings, load js and css.
*
* @param[in] object wikEdDiffConfig Custom customization settings
Line 699 ⟶ 886:
 
// Import customizations from wikEdDiffConfig{}
if ( typeof wikEdDiffConfig === 'object' ) {
this.deepCopy( wikEdDiffConfig, this.config );
}
Line 710 ⟶ 897:
 
// 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 ⟶ 910:
 
/**
* 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 ⟶ 936:
// 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 ⟶ 947:
 
// Trap trivial changes: no change
if ( this.newText.text === this.oldText.text ) {
this.html =
this.config.htmlCode.containerStart +
Line 771 ⟶ 959:
// 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 ⟶ 977:
// 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 ⟶ 992:
return this.html;
}
 
// New symbols object
var symbols = {
token: [],
hashTable: {},
linked: false
};
 
// Split new and old text into paragraps
Line 817 ⟶ 998:
 
// Calculate diff
this.calculateDiff( symbols, 'paragraph' );
 
// Refine different paragraphs into sentences
Line 824 ⟶ 1,005:
 
// Calculate refined diff
this.calculateDiff( symbols, 'sentence' );
 
// Refine different paragraphs into chunks
Line 837 ⟶ 1,018:
 
// Calculate refined diff
this.calculateDiff( symbols, 'chunk' );
 
// Refine different sentences into words
Line 850 ⟶ 1,031:
 
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( symbols, 'word', false, true );
 
// Slide gaps
Line 875 ⟶ 1,056:
 
// Calculate refined diff information with recursion for unresolved gaps
this.calculateDiff( symbols, 'character', false, true );
 
// Slide gaps
Line 887 ⟶ 1,068:
}
}
 
// free memory
this.symbols = undefined;
this.bordersDown = undefined;
this.bordersUp = undefined;
 
// Enumerate token lists
Line 900 ⟶ 1,086:
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,157:
if ( this.config.timer === true ) {
this.timeEnd( 'total' );
}
 
// Debug log
if ( this.config.debug === true ) {
console.log( 'HTML:\n', this.html );
}
 
Line 988 ⟶ 1,178:
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,195:
 
// Start of gap in new and old
if ( ( gap === null ) && ( newLink === null ) && ( oldLink === null ) ) {
gap = gaps.length;
gaps.push( {
Line 1,019 ⟶ 1,209:
 
// Count chars and tokens in gap
else if ( ( gap !== null ) && ( newLink === null ) ) {
gaps[gap].newLast = i;
gaps[gap].newTokens ++;
Line 1,025 ⟶ 1,215:
 
// Gap ended
else if ( ( gap !== null ) && ( newLink !== null ) ) {
gap = null;
}
Line 1,036 ⟶ 1,226:
}
 
// 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,246:
}
 
/** 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,284:
}
 
// Cycle troughthrough new text tokens list and set charSplit
else {
var i = gaps[gap].newFirst;
Line 1,103 ⟶ 1,305:
 
// Not same token length
if ( newToken.length !== oldToken.length ) {
 
// Test for addition or deletion of internal string in tokens
Line 1,110 ⟶ 1,312:
var left = 0;
while ( left < shorterToken.length ) {
if ( newToken.charAt( left ) !== oldToken.charAt( left ) ) {
break;
}
Line 1,119 ⟶ 1,321:
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,331:
 
// 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 this gap
Line 1,143 ⟶ 1,349:
 
// 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,368:
 
// Next list elements
if ( i === gaps[gap].newLast ) {
break;
}
Line 1,171 ⟶ 1,378:
}
 
/** 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,411:
 
// Next list elements
if ( i === gaps[gap].newLast ) {
i = null;
}
if ( j === gaps[gap].oldLast ) {
j = null;
}
Line 1,220 ⟶ 1,431:
 
/**
* 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
Line 1,229 ⟶ 1,440:
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,456:
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,265 ⟶ 1,478:
var gapFrontBlankTest = this.config.regExp.slideBorder.test( text.tokens[gapFront].token );
var frontStop = front;
if ( text.tokens[back].link === null ) {
while (
( front !== null ) && (
back !== null ) &&
( text.tokens[front].link !== null ) &&
( text.tokens[front].token === text.tokens[back].token )
) {
 
front = text.tokens[front].prev;
back = text.tokens[back].prev;
if ( front !== null ) {
 
// Stop at line break
if ( front !== null ) {
if ( this.config.regExp.slideStop.test( text.tokens[front].token ) === true ) {
frontStop = front;
Line 1,282 ⟶ 1,495:
}
 
// Stop at first word border (blank/word or word/blank )
if (
this.config.regExp.slideBorder.test( text.tokens[front].token ) !==
gapFrontBlankTest ) {
) {
frontStop = front;
}
Line 1,294 ⟶ 1,510:
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,538:
* 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,330 ⟶ 1,549:
*
* 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,555:
* @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,611:
*/
 
// 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,392 ⟶ 1,638:
}
 
// 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,656:
*/
 
// 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,433 ⟶ 1,686:
}
 
// 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,704:
*/
 
// 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;
Line 1,456 ⟶ 1,717:
 
// Do not use spaces as unique markers
if (
if ( /^\s+$/.test(this.newText.tokens[newToken].token ) === false) {
this.config.regExp.blankOnlyToken.test( this.newText.tokens[newToken].token ) === true
) {
 
// Link new an old tokens
this.newText.tokens[newToken].link = oldToken;
this.oldText.tokens[oldToken].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].token;
var words =
( token.match( this.config.regExp.countWords ) || [] ).concatlength +
( token.match( this.config.regExp.countChunks ) || [] ).length;
 
// Unique if longer than min block length
if ( words.length >= this.config.blockMinLength ) {
unique = true;
}
Line 1,479 ⟶ 1,749:
// Unique if it contains at least one unique word
else {
for ( var wordwordsLength = 0; word < words.length; word ++ ) {
iffor ( ( this.oldText.words[var words[word] ] == 10; )word &&< (wordsLength; this.newText.words[ words[word] ] == 1 )++ ) {
if (
this.oldText.words[ words[word] ] === 1 &&
this.newText.words[ words[word] ] === 1
) {
unique = true;
break;
Line 1,506 ⟶ 1,780:
*/
 
// 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,824:
*/
 
// 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,017:
 
// Stop timers
if ( ( this.config.timer === true ) && (repeating repeat !=== true )false ) {
if ( this.recursionTimer[recursionLevel] === undefined ) {
this.recursionTimer[recursionLevel] = 0;
Line 1,783 ⟶ 2,023:
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,033:
 
/**
* Main method for processing raw diff data, extracting deleted, inserted, and moved blocks.
*
* Scheme of blocks, sections, and groups (old block numbers):
Line 1,836 ⟶ 2,076:
// Convert groups to insertions/deletions if maximum block length is too short
var unlinkCount = 0;
if ( ( this.config.unlinkBlocks === true ) && ( this.config.blockMinLength > 0 ) ) {
if ( this.config.timer === true ) {
this.time( 'unlink' );
Line 1,843 ⟶ 2,083:
// Repeat as long as unlinking is possible
var unlinked = true;
while ( ( unlinked === true ) && ( unlinkCount < this.config.unlinkMax ) ) {
 
// Convert '=' to '+'/'-' pairs
Line 1,882 ⟶ 2,122:
 
// Debug log
if ( ( this.config.timer === true ) || ( this.config.debug === true ) ) {
console.log( 'Unlink count: ', unlinkCount );
}
Line 1,894 ⟶ 2,134:
 
/**
* Collect identical corresponding matching ('=') blocks from old text and sort by new text.
*
* @param[in] WikEdDiffText newText, oldText Text objects
Line 1,906 ⟶ 2,146:
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,152:
 
// Skip '-' blocks
while ( ( j !== null ) && ( this.oldText.tokens[j].link === null ) ) {
j = this.oldText.tokens[j].next;
}
Line 1,926 ⟶ 2,166:
var unique = false;
var text = '';
while ( ( i !== null ) && ( j !== null ) && ( this.oldText.tokens[j].link === i ) ) {
var token = this.oldText.tokens[j].token;
count ++;
Line 1,964 ⟶ 2,204:
 
// 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;
}
Line 1,973 ⟶ 2,214:
/**
* 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,987 ⟶ 2,228:
 
// 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,238:
 
// Check right
for ( var j = sectionStart + 1; j < blocks.lengthblocksLength; j ++ ) {
 
// Check for crossing over to the left
Line 2,029 ⟶ 2,271:
 
/**
* Find groups of continuous old text blocks.
*
* @param[out] array groups Groups table object
Line 2,043 ⟶ 2,285:
 
// 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,298:
 
// 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,111 ⟶ 2,354:
 
/**
* Set longest sequence of increasing groups in sections as fixed (not moved).
*
* @param[in] array sections Sections table object
Line 2,124 ⟶ 2,367:
 
// 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,390:
 
// 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,161 ⟶ 2,406:
 
/**
* Recusively find path of groups in increasing old group order with longest char length.
*
* @param int start Path start group
Line 2,215 ⟶ 2,460:
 
/**
* Collect deletion ('-') blocks from old text.
*
* @param[in] WikEdDiffText oldText Old Text object
Line 2,224 ⟶ 2,469:
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,478:
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,508:
if ( j !== null ) {
i = this.oldText.tokens[j].link;
while ( ( i !== null ) && ( j !== null ) && ( this.oldText.tokens[j].link === i ) ) {
i = this.newText.tokens[i].next;
j = this.oldText.tokens[j].next;
Line 2,274 ⟶ 2,519:
 
/**
* Position deletion '-' blocks into new text order.
* Deletion blocks move with fixed reference:
* Old: 1 D 2 1 D 2
Line 2,281 ⟶ 2,526:
* 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,299 ⟶ 2,545:
 
// 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,575:
// 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,605:
else {
for ( var fixed = block; fixed >= 0; fixed -- ) {
if ( ( blocksOld[fixed].type === '=' ) && ( blocksOld[fixed].fixed === true ) ) {
refBlock = blocksOld[fixed];
break;
Line 2,379 ⟶ 2,635:
/**
* 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
Line 2,394 ⟶ 2,650:
// Cycle through groups
var unlinked = false;
for ( var groupgroupsLength = 0; group < groups.length; group ++ ) {
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;
Line 2,412 ⟶ 2,669:
// 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;
}
Line 2,426 ⟶ 2,683:
// 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 (
if ( ( blocks[block].words > 1 ) || ( ( blocks[block].words == 1 ) && ( blocks[block].unique === true ) ) ) {
blocks[block].words > 1 ||
( blocks[block].words === 1 && blocks[block].unique === true )
) {
break;
}
Line 2,443 ⟶ 2,703:
 
/**
* Unlink text tokens of single block, convert them into into insertion/deletion ('+'/'-') pairs.
*
* @param[in] array blocks Blocks table object
Line 2,464 ⟶ 2,724:
 
/**
* Collect insertion ('+') blocks from new text.
*
* @param[in] WikEdDiffText newText New Text object
Line 2,478 ⟶ 2,738:
 
// Jump over linked (matched) block
while ( ( i !== null ) && ( this.newText.tokens[i].link !== null ) ) {
i = this.newText.tokens[i].next;
}
Line 2,487 ⟶ 2,747:
var count = 0;
var text = '';
while ( ( i !== null ) && ( this.newText.tokens[i].link === null ) ) {
count ++;
text += this.newText.tokens[i].token;
Line 2,522 ⟶ 2,782:
 
/**
* Sort blocks by new text token number and update groups.
*
* @param[in/out] array groups Groups table object
Line 2,543 ⟶ 2,803:
// 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,820:
 
/**
* Set group numbers of insertion '+' blocks.
*
* @param[in/out] array groups Groups table object
Line 2,570 ⟶ 2,831:
 
// 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,845:
 
// Cycle through blocks
for ( var blockblocksLength = 0; block < blocks.length; block ++ ) {
for ( var block = 0; block < blocksLength; block ++ ) {
 
// Skip existing groups
Line 2,609 ⟶ 2,872:
 
/**
* Mark original positions of moved groups.
* Scheme: moved block marks at original positions relative to fixed groups:
* Groups: 3 7
Line 2,619 ⟶ 2,882:
* | |> 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,638 ⟶ 2,903:
 
// Enumerate copy
for ( var iblocksOldLength = 0; i < blocksOld.length; i ++ ) {
for ( var i = 0; i < blocksOldLength; i ++ ) {
blocksOld[i].number = i;
}
Line 2,653 ⟶ 2,919:
// 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 ⟶ 2,932:
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 ⟶ 2,951:
// 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 ⟶ 2,963:
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,763 ⟶ 3,031:
 
/**
* Collect diff fragment list for markup, create abstraction layer for customized diffs.
* Adds the following fagment types:
* '=', '-', '+' same, deletion, insertion
Line 2,788 ⟶ 3,056:
 
// 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,083:
 
// Add '=' unchanged text and moved block
if ( ( type === '=' ) || ( type === '-' ) || ( type === '+' ) ) {
fragments.push( {
text: blocks[block].text,
Line 2,823 ⟶ 3,092:
 
// 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,136:
 
// 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,162:
 
/**
* Clip unchanged sections from unmoved block text.
* Adds the following fagment types:
* '~', ' ~', '~ ' omission indicators
Line 2,900 ⟶ 3,174:
 
// Skip if only one fragment in containers, no change
if ( ( fragments.length === 5 ) ) {
return;
}
Line 2,935 ⟶ 3,209:
 
// 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,221:
// 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,237:
lines.unshift( 0 );
}
if ( lastIndex !== text.lengthtextLength ) {
lines.push( text.lengthtextLength );
}
 
Line 2,983 ⟶ 3,259:
paragraphs.unshift( 0 );
}
if ( lastIndex !== text.lengthtextLength ) {
paragraphs.push( text.lengthtextLength );
}
 
Line 2,994 ⟶ 3,270:
 
// 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,280:
// 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,293:
// 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,311:
// 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,324:
}
 
// 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,354:
 
// 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,365:
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,380:
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,397:
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,413:
// Find last blank from right
if ( rangeRight === null ) {
var startPos = text.lengthtextLength - this.config.clipBlankRightMax;
if ( startPos < rangeRightMin ) {
startPos = rangeRightMin;
Line 3,126 ⟶ 3,420:
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,433:
// 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,447:
 
// Check if we skip clipping if ranges are close together
if ( ( rangeLeft !== null ) && ( rangeRight !== null ) ) {
 
// Skip if overlapping ranges
Line 3,168 ⟶ 3,462:
// 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,477:
 
// Skip if nothing to clip
if ( ( rangeLeft === null ) && ( rangeRight === null ) ) {
continue;
}
Line 3,196 ⟶ 3,491:
 
// 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,501:
}
 
// Split right text,
var textRight = null;
var omittedRight = null;
Line 3,216 ⟶ 3,511:
 
// 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,238 ⟶ 3,533:
 
// 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 } );
Line 3,263 ⟶ 3,558:
 
/**
* Create html formatted diff code from diff fragments.
*
* @param[in] array fragments Fragments array, abstraction layer for diff code
Line 3,275 ⟶ 3,570:
 
// No change, only one unchanged block in containers
if ( ( fragments.length === 5 ) && ( fragments[2].type === '=' ) ) {
this.html = '';
return;
Line 3,282 ⟶ 3,577:
// 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,591:
 
// 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,655:
 
// Add colored right-pointing block start markup
else if ( type === '(>' ) {
if ( version !== 'old' ) {
 
// Get title
Line 3,383 ⟶ 3,679:
 
// Add colored block end markup
else if ( type === ' )' ) {
if ( version !== 'old' ) {
html = this.config.htmlCode.blockEnd;
}
Line 3,390 ⟶ 3,686:
 
// 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,699:
 
// 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,718:
 
// Add '+' text
else if ( type === '+' ) {
if ( version !== 'old' ) {
text = this.htmlEscape( text );
text = this.markupBlanks( text, true );
Line 3,437 ⟶ 3,733:
 
// 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,769:
// Display as mark
else {
if ( type === '<' ) {
if ( this.config.coloredBlocks === true ) {
html = this.htmlCustomize( this.config.htmlCode.markLeftColored, color, text );
Line 3,494 ⟶ 3,799:
 
/**
* Customize html code fragments.
* Replaces:
* {number}: class/color/block/mark/id number
Line 3,523 ⟶ 3,828:
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,843:
 
/**
* Replace html-sensitive characters in output text with character entities.
*
* @param string html Html code to be escaped
Line 3,551 ⟶ 3,859:
 
/**
* 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,867:
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,877:
 
/**
* Count real words in text.
*
* @param string text Text for word counting
Line 3,581 ⟶ 3,889:
 
/**
* Test diff code for consistency with input versions.
* Prints results to debug console.
*
* @param[in] WikEdDiffText newText, oldText Text objects
Line 3,592 ⟶ 3,900:
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 ⟶ 3,916:
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 ⟶ 3,933:
 
/**
* Dump blocks object to browser console.
*
* @param string name Block name
Line 3,631 ⟶ 3,943:
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 ⟶ 3,961:
 
/**
* Dump groups object to browser console.
*
* @param string name Group name
Line 3,650 ⟶ 3,971:
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 ⟶ 3,987:
 
/**
* Dump fragments array to browser console.
*
* @param string name Fragments name
Line 3,668 ⟶ 3,996:
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,007:
 
/**
* 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,033:
this.debugShortenText = function ( text, max, end ) {
 
if ( typeof text !== 'string' ) {
text = text.toString();
}
Line 3,705 ⟶ 4,053:
/**
* Start timer 'label', analogous to JavaScript console timer.
* Usage: this.time( 'label' );
*
* @param string label Timer label
Line 3,718 ⟶ 4,066:
 
/**
* 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,743 ⟶ 4,091:
 
/**
* Log recursion timer results to browser console.
* Usage: this.timeRecursionEnd();
*
Line 3,754 ⟶ 4,102:
 
// 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' );
}
Line 3,769 ⟶ 4,119:
 
/**
* Log variable values to debug console.
* Usage: this.debug( 'var', var );
*
* @param string name Object identifier
Line 3,788 ⟶ 4,138:
 
/**
* Add script to document head.
*
* @param string code JavaScript code
Line 3,810 ⟶ 4,160:
 
/**
* Add stylesheet to document head, cross-browser >= IE6.
*
* @param string css CSS code
Line 3,838 ⟶ 4,188:
 
/**
* Recursive deep copy from target over source for customization import.
*
* @param object source Source object
Line 3,864 ⟶ 4,214:
 
/**
* Data and methods for single text version (old or new one).
*
* @class WikEdDiffText
Line 3,888 ⟶ 4,238:
 
/**
* Constructor, initialize text object.
*
* @param string text Text of version
Line 3,895 ⟶ 4,245:
this.init = function () {
 
if ( typeof text !== 'string' ) {
text = text.toString();
}
Line 3,916 ⟶ 4,266:
 
/**
* Parse and count words and chunks for identification of unique words.
*
* @param string regExp Regular expression for counting words
Line 3,939 ⟶ 4,289:
 
/**
* Split text into paragraph, sentence, chunk, word, or character tokens.
*
* @param string level Level of splitting: paragraph, sentence, chunk, word, or character
Line 3,981 ⟶ 4,331:
}
 
// 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
Line 4,004 ⟶ 4,355:
 
// 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,375:
// 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,388:
 
/**
* Split unique unmatched tokens into smaller tokens.
*
* @param string level Level of splitting: sentence, chunk, or word
Line 4,046 ⟶ 4,397:
// 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,410:
 
/**
* Enumerate text token list before detecting blocks.
*
* @param[out] array tokens Tokens list
Line 4,068 ⟶ 4,419:
var number = 0;
var i = this.first;
while ( ( i !== null ) && ( this.tokens[i] !== null ) ) {
this.tokens[i].number = number;
number ++;
Line 4,078 ⟶ 4,429:
 
/**
* Dump tokens object to browser console.
*
* @param string name Text name
Line 4,088 ⟶ 4,439:
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;
}