/* Highlight module for wikiEditor */ ( function ( $ ) { $.wikiEditor.modules.highlight = { /** * Core Requirements */ req: [ 'iframe' ], /** * Configuration */ cfg: { styleVersion: 3 }, /** * Internally used event handlers */ evt: { delayedChange: function ( context, event ) { if ( event.data.scope == 'realchange' ) { $.wikiEditor.modules.highlight.fn.scan( context ); $.wikiEditor.modules.highlight.fn.mark( context, event.data.scope ); } }, ready: function ( context, event ) { $.wikiEditor.modules.highlight.fn.scan( context ); $.wikiEditor.modules.highlight.fn.mark( context, 'ready' ); } }, /** * Internally used functions */ fn: { /** * Creates a highlight module within a wikiEditor * * @param config Configuration object to create module from */ create: function ( context, config ) { context.modules.highlight.markersStr = ''; }, /** * Scans text division for tokens * * @param division */ scan: function ( context, division ) { var tokenArray, text, module, exp, left, right, match; /*jshint eqnull: true */ // Remove all existing tokens tokenArray = context.modules.highlight.tokenArray = []; // Scan text for new tokens text = context.fn.getContents(); // Perform a scan for each module which provides any expressions to scan for // FIXME: This traverses the entire string once for every regex. Investigate // whether |-concatenating regexes then traversing once is faster. for ( module in context.modules ) { if ( module in $.wikiEditor.modules && 'exp' in $.wikiEditor.modules[module] ) { for ( exp in $.wikiEditor.modules[module].exp ) { // Prepare configuration var regex = $.wikiEditor.modules[module].exp[exp].regex; var label = $.wikiEditor.modules[module].exp[exp].label; var markAfter = $.wikiEditor.modules[module].exp[exp].markAfter || false; // Search for tokens var offset = 0; while ( ( match = text.substr( offset ).match( regex ) ) != null ) { right = ( left = offset + match.index ) + match[0].length; tokenArray[tokenArray.length] = { offset: markAfter ? right : left, label: label, tokenStart: left, match: match }; // Move to the right of this match offset = right; } } } } // Sort by start tokenArray.sort( function ( a, b ) { return a.tokenStart - b.tokenStart; } ); // Let the world know, a scan just happened! context.fn.trigger( 'scan' ); }, /** * Marks up text with HTML * * @param division * @param tokens */ // FIXME: What do division and tokens do? // TODO: Document the scan() and mark() APIs somewhere mark: function ( context, division, tokens ) { var i, subtracted, oldLength, j, o; // Reset markers var markers = []; // Recycle markers that will be skipped in this run if ( context.modules.highlight.markers && division !== '' ) { for ( i = 0; i < context.modules.highlight.markers.length; i++ ) { if ( context.modules.highlight.markers[i].skipDivision == division ) { markers.push( context.modules.highlight.markers[i] ); } } } context.modules.highlight.markers = markers; // Get all markers context.fn.trigger( 'mark' ); markers.sort( function ( a, b ) { return a.start - b.start || a.end - b.end; } ); // Serialize the markers array to a string and compare it with the one stored in the previous run - if they're // equal, there's no markers to change var markersStr = ''; for ( i = 0; i < markers.length; i++ ) { markersStr += markers[i].start + ',' + markers[i].end + ',' + markers[i].type + ','; } if ( context.modules.highlight.markersStr == markersStr ) { // No change, bail out return; } context.modules.highlight.markersStr = markersStr; // Traverse the iframe DOM, inserting markers where they're needed - store visited markers here so we know which // markers should be removed var visited = [], v = 0; for ( i = 0; i < markers.length; i++ ) { if ( typeof markers[i].skipDivision !== 'undefined' && ( division == markers[i].skipDivision ) ) { continue; } // We want to isolate each marker, so we may need to split textNodes if a marker starts or ends halfway one. var start = markers[i].start; var s = context.fn.getOffset( start ); if ( !s ) { // This shouldn't happen continue; } var startNode = s.node; // Don't wrap leading BRs, produces undesirable results // FIXME: It's also possible that the offset is a bit high because getOffset() has incremented .length to // fake the newline caused by startNode being in a P. In this case, prevent the textnode splitting below // from making startNode an empty textnode, IE barfs on that while ( startNode.nodeName === 'BR' || s.offset === startNode.nodeValue.length ) { start++; s = context.fn.getOffset( start ); startNode = s.node; } // The next marker starts somewhere in this textNode or at this BR if ( s.offset > 0 && s.node.nodeName == '#text' ) { // Split off the prefix - this leaves the prefix in the current node and puts the rest in a new node // which is our start node var newStartNode = startNode.splitText( s.offset < s.node.nodeValue.length ? s.offset : s.node.nodeValue.length - 1 ); var oldStartNode = startNode; startNode = newStartNode; // Update offset objects. We don't need purgeOffsets(), simply manipulating the existing offset objects // will suffice // FIXME: This manipulates context.offsets directly, which is ugly, but the performance improvement vs. // purgeOffsets() is worth it - this code doesn't set lastTextNode to newStartNode for offset objects // with lastTextNode == oldStartNode, but that doesn't really matter subtracted = s.offset; oldLength = s.length; // Update offset objects referring to oldStartNode for ( j = start - subtracted; j < start; j++ ) { if ( j in context.offsets ) { o = context.offsets[j]; o.node = oldStartNode; o.length = subtracted; } } // Update offset objects referring to newStartNode for ( j = start; j < start - subtracted + oldLength; j++ ) { if ( j in context.offsets ) { o = context.offsets[j]; o.node = newStartNode; o.offset -= subtracted; o.length -= subtracted; o.lastTextNode = oldStartNode; } } } var end = markers[i].end; // To avoid ending up at the first char of the next node, we grab the offset for end - 1 and add one to the // offset var e = context.fn.getOffset( end - 1 ); if ( !e ) { // This shouldn't happen continue; } var endNode = e.node; if ( e.offset + 1 < e.length - 1 && endNode.nodeName == '#text' ) { // Split off the suffix. This puts the suffix in a new node and leaves the rest in endNode var oldEndNode = endNode; var newEndNode = endNode.splitText( e.offset + 1 ); // Update offset objects subtracted = e.offset + 1; oldLength = e.length; // Update offset objects referring to oldEndNode for ( j = end - subtracted; j < end; j++ ) { if ( j in context.offsets ) { o = context.offsets[j]; o.node = oldEndNode; o.length = subtracted; } } // We have to insert this one, as it might not exist: we didn't call getOffset( end ) context.offsets[end] = { 'node': newEndNode, 'offset': 0, 'length': oldLength - subtracted, 'lastTextNode': oldEndNode }; // Update offset objects referring to newEndNode for ( j = end + 1; j < end - subtracted + oldLength; j++ ) { if ( j in context.offsets ) { o = context.offsets[j]; o.node = newEndNode; o.offset -= subtracted; o.length -= subtracted; o.lastTextNode = oldEndNode; } } } // Don't wrap trailing BRs, doing that causes weird issues if ( endNode.nodeName == 'BR' ) { endNode = e.lastTextNode; } // If startNode and endNode have different parents, we need to pull endNode and all textnodes in between // into startNode's parent and replace
with
if ( startNode.parentNode !== endNode.parentNode ) {
var startP = $( startNode ).closest( 'p' ).get( 0 );
var t = new context.fn.rawTraverser( startNode, startP, context.$content.get( 0 ), false );
var afterStart = startNode.nextSibling;
var lastP = startP;
var nextT = t.next();
while ( nextT && t.node !== endNode ) {
t = nextT;
nextT = t.next();
// If t.node has a different parent, merge t.node.parentNode with startNode.parentNode
if ( t.node.parentNode !== startNode.parentNode ) {
var oldParent = t.node.parentNode;
if ( afterStart ) {
if ( lastP !== t.inP ) {
// We're entering a new
, insert a
startNode.parentNode.insertBefore(
startNode.ownerDocument.createElement( 'br' ),
afterStart
);
}
// A
with just a
in it is an empty line, so let's not bother with unwrapping it
if ( !( oldParent.childNodes.length == 1 && oldParent.firstChild.nodeName == 'BR' ) ) {
// Move all children of oldParent into startNode's parent
while ( oldParent.firstChild ) {
startNode.parentNode.insertBefore( oldParent.firstChild, afterStart );
}
}
} else {
if ( lastP !== t.inP ) {
// We're entering a new
, insert a
startNode.parentNode.appendChild(
startNode.ownerDocument.createElement( 'br' )
);
}
// A
with just a
in it is an empty line, so let's not bother with unwrapping it
if ( !( oldParent.childNodes.length == 1 && oldParent.firstChild.nodeName == 'BR' ) ) {
// Move all children of oldParent into startNode's parent
while ( oldParent.firstChild ) {
startNode.parentNode.appendChild( oldParent.firstChild );
}
}
}
// Remove oldParent, which is now empty
oldParent.parentNode.removeChild( oldParent );
}
lastP = t.inP;
}
// Moving nodes around like this invalidates offset objects
// TODO: Update offset objects ourselves for performance. Requires rewriting this code block to be
// offset-based rather than traverser-based
}
// Now wrap everything between startNode and endNode (may be equal).
var ca1 = startNode, ca2 = endNode;
if ( ca1 && ca2 && ca1.parentNode ) {
var anchor = markers[i].getAnchor( ca1, ca2 );
if ( !anchor ) {
var commonAncestor = ca1.parentNode;
if ( markers[i].anchor == 'wrap') {
// We have to store things like .parentNode and .nextSibling because appendChild() changes these
var newNode = ca1.ownerDocument.createElement( 'span' );
var nextNode = ca2.nextSibling;
// Append all nodes between ca1 and ca2 (inclusive) to newNode
var n = ca1;
while ( n !== nextNode ) {
var ns = n.nextSibling;
newNode.appendChild( n );
n = ns;
}
// Insert newNode in the right place
if ( nextNode ) {
commonAncestor.insertBefore( newNode, nextNode );
} else {
commonAncestor.appendChild( newNode );
}
anchor = newNode;
} else if ( markers[i].anchor == 'tag' ) {
anchor = commonAncestor;
}
$( anchor ).data( 'marker', markers[i] ).addClass( 'wikiEditor-highlight' );
// Allow the module adding this marker to manipulate it
markers[i].afterWrap( anchor, markers[i] );
} else {
// Update the marker object
$( anchor ).data( 'marker', markers[i] );
if ( typeof markers[i].onSkip == 'function' ) {
markers[i].onSkip( anchor );
}
}
visited[v++] = anchor;
}
}
// Remove markers that were previously inserted but weren't passed to this function - visited[] contains the
// visited elements in order and find() and each() preserve order
j = 0;
context.$content.find( '.wikiEditor-highlight' ).each( function () {
if ( visited[j] == this ) {
// This marker is legit, leave it in
j++;
return true;
}
// Remove this marker
var marker = $(this).data( 'marker' );
if ( marker && typeof marker.skipDivision !== 'undefined' && ( division === marker.skipDivision ) ) {
// Don't remove these either
return true;
}
if ( marker && typeof marker.beforeUnwrap === 'function' )
marker.beforeUnwrap( this );
if ( ( marker && marker.anchor === 'tag' ) || $(this).is( 'p' ) ) {
// Remove all classes
$(this).removeAttr( 'class' );
} else {
// Assume anchor == 'wrap'
$(this).replaceWith( this.childNodes );
}
context.fn.purgeOffsets();
});
}
}
};
}( jQuery ) );