/* 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 ) );