jquery.chili-2.2.js 20.38 KiB
/*
===============================================================================
Chili is the jQuery code highlighter plugin
...............................................................................
LICENSE: http://www.opensource.org/licenses/mit-license.php
WEBSITE: http://noteslog.com/chili/
											   Copyright 2008 / Andrea Ercolino
===============================================================================
( function($) {
ChiliBook = { //implied global
	  version:            "2.2" // 2008-07-06
// options --------------------------------------------------------------------
	, automatic:          true
	, automaticSelector:  "pre"
	, lineNumbers:        !true
	, codeLanguage:       function( el ) {
		var recipeName = $( el ).attr( "class" );
		return recipeName ? recipeName : '';
	, recipeLoading:      true
	, recipeFolder:       "" // used like: recipeFolder + recipeName + '.js'
	// IE and FF convert   to " ", Safari and Opera do not
	, replaceSpace:       " "
	, replaceTab:         "    "
	, replaceNewLine:     "&#160;<br/>"
	, selectionStyle:     [ "position:absolute; z-index:3000; overflow:scroll;"
						  , "width:16em;"
						  , "height:9em;"
						  , "border:1px solid gray;"
						  , "padding:15px;"
						  , "background-color:yellow;"
						  ].join( ' ' )
// ------------------------------------------------------------- end of options
	, defaultReplacement: '<span class="$0">$$</span>' // TODO: make this an option again
	, recipes:            {} //repository
	, queue:              {} //registry
	, unique:             function() {
		return (new Date()).valueOf();
$.fn.chili = function( options ) {
	var book = $.extend( {}, ChiliBook, options || {} );
	function cook( ingredients, recipe, blockName ) {
		function prepareBlock( recipe, blockName ) {
			var steps = [];
			for( var stepName in recipe[ blockName ] ) {
				steps.push( prepareStep( recipe, blockName, stepName ) );
			return steps;
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
} // prepareBlock function prepareStep( recipe, blockName, stepName ) { var step = recipe[ blockName ][ stepName ]; var exp = ( typeof step._match == "string" ) ? step._match : step._match.source; return { recipe: recipe , blockName: blockName , stepName: stepName , exp: "(" + exp + ")" , length: 1 // add 1 to account for the newly added parentheses + (exp // count number of submatches in here .replace( /\\./g, "%" ) // disable any escaped character .replace( /\[.*?\]/g, "%" ) // disable any character class .match( /\((?!\?)/g ) // match any open parenthesis, not followed by a ? || [] // make sure it is an empty array if there are no matches ).length // get the number of matches , replacement: step._replace ? step._replace : book.defaultReplacement }; } // prepareStep function knowHow( steps ) { var prevLength = 1; var exps = []; for (var i = 0; i < steps.length; i++) { var exp = steps[ i ].exp; // adjust backreferences exp = exp.replace( /\\\\|\\(\d+)/g, function( m, aNum ) { return !aNum ? m : "\\" + ( prevLength + 1 + parseInt( aNum, 10 ) ); } ); exps.push( exp ); prevLength += steps[ i ].length; } var prolog = '((?:\\s|\\S)*?)'; var epilog = '((?:\\s|\\S)+)'; var source = '(?:' + exps.join( "|" ) + ')'; source = prolog + source + '|' + epilog; return new RegExp( source, recipe._case ? "g" : "gi" ); } // knowHow function escapeHTML( str ) { return str.replace( /&/g, "&amp;" ).replace( /</g, "&lt;" ); } // escapeHTML function replaceSpaces( str ) { return str.replace( / +/g, function( spaces ) { return spaces.replace( / /g, replaceSpace ); } ); } // replaceSpaces function filter( str ) { str = escapeHTML( str ); if( replaceSpace ) { str = replaceSpaces( str ); } return str; } // filter function applyRecipe( subject, recipe ) { return cook( subject, recipe ); } // applyRecipe function applyBlock( subject, recipe, blockName ) { return cook( subject, recipe, blockName ); } // applyBlock function applyStep( subject, recipe, blockName, stepName ) { var replaceSpace = book.replaceSpace; var step = prepareStep( recipe, blockName, stepName );
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
var steps = [step]; var perfect = subject.replace( knowHow( steps ), function() { return chef.apply( { steps: steps }, arguments ); } ); return perfect; } // applyStep function applyModule( subject, module, context ) { if( ! module ) { return filter( subject ); } var sub = module.split( '/' ); var recipeName = ''; var blockName = ''; var stepName = ''; switch( sub.length ) { case 1: recipeName = sub[0]; break; case 2: recipeName = sub[0]; blockName = sub[1]; break; case 3: recipeName = sub[0]; blockName = sub[1]; stepName = sub[2]; break; default: return filter( subject ); } function getRecipe( recipeName ) { var path = getPath( recipeName ); var recipe = book.recipes[ path ]; if( ! recipe ) { throw {msg:"recipe not available"}; } return recipe; } try { var recipe; if ( '' == stepName ) { if ( '' == blockName ) { if ( '' == recipeName ) { //nothing to do } else { // ( '' != recipeName ) recipe = getRecipe( recipeName ); return applyRecipe( subject, recipe ); } } else { // ( '' != blockName ) if( '' == recipeName ) { recipe = context.recipe; } else { recipe = getRecipe( recipeName ); } if( ! (blockName in recipe) ) { return filter( subject ); } return applyBlock( subject, recipe, blockName ); } } else { // ( '' != stepName ) if( '' == recipeName ) { recipe = context.recipe; } else {
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
recipe = getRecipe( recipeName ); } if( '' == blockName ) { blockName = context.blockName; } if( ! (blockName in recipe) ) { return filter( subject ); } if( ! (stepName in recipe[blockName]) ) { return filter( subject ); } return applyStep( subject, recipe, blockName, stepName ); } } catch( e ) { if (e.msg && e.msg == "recipe not available") { var cue = 'chili_' + book.unique(); if( book.recipeLoading ) { var path = getPath( recipeName ); if( ! book.queue[ path ] ) { /* this is a new recipe to download */ try { book.queue[ path ] = [ {cue: cue, subject: subject, module: module, context: context} ]; $.getJSON( path, function( recipeLoaded ) { book.recipes[ path ] = recipeLoaded; var q = book.queue[ path ]; for( var i = 0, iTop = q.length; i < iTop; i++ ) { var replacement = applyModule( q[ i ].subject, q[ i ].module, q[ i ].context ); if( book.replaceTab ) { replacement = replacement.replace( /\t/g, book.replaceTab ); } if( book.replaceNewLine ) { replacement = replacement.replace( /\n/g, book.replaceNewLine ); } $( '#' + q[ i ].cue ).replaceWith( replacement ); } } ); } catch( recipeNotAvailable ) { alert( "the recipe for '" + recipeName + "' was not found in '" + path + "'" ); } } else { /* not a new recipe, so just enqueue this element */ book.queue[ path ].push( {cue: cue, subject: subject, module: module, context: context} ); } return '<span id="' + cue + '">' + filter( subject ) + '</span>'; } return filter( subject ); } else { return filter( subject ); } } } // applyModule function addPrefix( prefix, replacement ) { var aux = replacement.replace( /(<span\s+class\s*=\s*(["']))((?:(?!__)\w)+\2\s*>)/ig, "$1" + prefix + "__$3" ); return aux; } // addPrefix function chef() { if (! arguments[ 0 ]) { return ''; } var steps = this.steps; var i = 0; // iterate steps var j = 2; // iterate chef's arguments var prolog = arguments[ 1 ]; var epilog = arguments[ arguments.length - 3 ];
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
if (! epilog) { var step; while( step = steps[ i++ ] ) { var aux = arguments; // this unmasks chef's arguments inside the next function if( aux[ j ] ) { var replacement = ''; if( $.isFunction( step.replacement ) ) { var matches = []; //Array.slice.call( aux, j, step.length ); for (var k = 0, kTop = step.length; k < kTop; k++) { matches.push( aux[ j + k ] ); } matches.push( aux[ aux.length - 2 ] ); matches.push( aux[ aux.length - 1 ] ); replacement = step.replacement .apply( { x: function() { var subject = arguments[0]; var module = arguments[1]; var context = { recipe: step.recipe , blockName: step.blockName }; return applyModule( subject, module, context ); } }, matches ); } else { //we expect step.replacement to be a string replacement = step.replacement .replace( /(\\\$)|(?:\$\$)|(?:\$(\d+))/g, function( m, escaped, K ) { if( escaped ) { /* \$ */ return "$"; } else if( !K ) { /* $$ */ return filter( aux[ j ] ); } else if( K == "0" ) { /* $0 */ return step.stepName; } else { /* $K */ return filter( aux[ j + parseInt( K, 10 ) ] ); } } ); } replacement = addPrefix( step.recipe._name, replacement ); return filter( prolog ) + replacement; } else { j+= step.length; } } } else { return filter( epilog ); } } // chef if( ! blockName ) { blockName = '_main'; checkSpices( recipe ); } if( ! (blockName in recipe) ) { return filter( ingredients ); } var replaceSpace = book.replaceSpace; var steps = prepareBlock( recipe, blockName ); var kh = knowHow( steps ); var perfect = ingredients.replace( kh, function() { return chef.apply( { steps: steps }, arguments ); } ); return perfect;
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
} // cook function loadStylesheetInline( sourceCode ) { if( document.createElement ) { var e = document.createElement( "style" ); e.type = "text/css"; if( e.styleSheet ) { // IE e.styleSheet.cssText = sourceCode; } else { var t = document.createTextNode( sourceCode ); e.appendChild( t ); } document.getElementsByTagName( "head" )[0].appendChild( e ); } } // loadStylesheetInline function checkSpices( recipe ) { var name = recipe._name; if( ! book.queue[ name ] ) { var content = ['/* Chili -- ' + name + ' */']; for (var blockName in recipe) { if( blockName.search( /^_(?!main\b)/ ) < 0 ) { for (var stepName in recipe[ blockName ]) { var step = recipe[ blockName ][ stepName ]; if( '_style' in step ) { if( step[ '_style' ].constructor == String ) { content.push( '.' + name + '__' + stepName + ' { ' + step[ '_style' ] + ' }' ); } else { for (var className in step[ '_style' ]) { content.push( '.' + name + '__' + className + ' { ' + step[ '_style' ][ className ] + ' }' ); } } } } } } content = content.join('\n'); loadStylesheetInline( content ); book.queue[ name ] = true; } } // checkSpices function askDish( el ) { var recipeName = book.codeLanguage( el ); if( '' != recipeName ) { var path = getPath( recipeName ); if( book.recipeLoading ) { /* dynamic setups come here */ if( ! book.queue[ path ] ) { /* this is a new recipe to download */ try { book.queue[ path ] = [ el ]; $.getJSON( path, function( recipeLoaded ) { book.recipes[ path ] = recipeLoaded; var q = book.queue[ path ]; for( var i = 0, iTop = q.length; i < iTop; i++ ) { makeDish( q[ i ], path ); } } ); } catch( recipeNotAvailable ) { alert( "the recipe for '" + recipeName + "' was not found in '" + path + "'" ); } }
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
else { /* not a new recipe, so just enqueue this element */ book.queue[ path ].push( el ); } /* a recipe could have been already downloaded */ makeDish( el, path ); } else { /* static setups come here */ makeDish( el, path ); } } } // askDish function makeDish( el, recipePath ) { var recipe = book.recipes[ recipePath ]; if( ! recipe ) { return; } var $el = $( el ); var ingredients = $el.text(); if( ! ingredients ) { return; } //fix for msie: \r (13) is used instead of \n (10) //fix for opera: \r\n is used instead of \n ingredients = ingredients.replace(/\r\n?/g, "\n"); //reverse fix for safari: msie, mozilla and opera render the initial \n if( $el.parent().is('pre') ) { if( ! $.browser.safari ) { ingredients = ingredients.replace(/^\n/g, ""); } } var dish = cook( ingredients, recipe ); // all happens here if( book.replaceTab ) { dish = dish.replace( /\t/g, book.replaceTab ); } if( book.replaceNewLine ) { dish = dish.replace( /\n/g, book.replaceNewLine ); } el.innerHTML = dish; //much faster than $el.html( dish ); //tried also the function replaceHtml from http://blog.stevenlevithan.com/archives/faster-than-innerhtml //but it was not faster nor without sideffects (it was not possible to count spans into el) //opera and safari select PRE text correctly if( $.browser.msie || $.browser.mozilla ) { enableSelectionHelper( el ); } var $that = $el.parent(); var classes = $that.attr( 'class' ); var ln = /ln-(\d+)-([\w][\w\-]*)|ln-(\d+)|ln-/.exec( classes ); if( ln ) { addLineNumbers( el ); var start = 0; if( ln[1] ) { start = parseInt( ln[1], 10 ); var $pieces = $( '.ln-' + ln[1] + '-' + ln[2] ); var pos = $pieces.index( $that[0] ); $pieces.slice( 0, pos ).each( function() { start += $( this ).find( 'li' ).length; } ); } else if( ln[3] ) { start = parseInt( ln[3], 10 );
491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
} else { start = 1; } $el.find( 'ol' )[0].start = start; $('body').width( $('body').width() - 1 ).width( $('body').width() + 1 ); } else if( book.lineNumbers ) { addLineNumbers( el ); } } // makeDish function enableSelectionHelper( el ) { var element = null; $( el ) .parents() .filter( "pre" ) .bind( "mousedown", function() { element = this; if( $.browser.msie ) { document.selection.empty(); } else { window.getSelection().removeAllRanges(); } } ) .bind( "mouseup", function( event ) { if( element && (element == this) ) { element = null; var selected = ''; if( $.browser.msie ) { selected = document.selection.createRange().htmlText; if( '' == selected ) { return; } selected = preserveNewLines( selected ); var container_tag = '<textarea style="STYLE">'; } else { selected = window.getSelection().toString(); //opera doesn't select new lines if( '' == selected ) { return; } selected = selected .replace( /\r/g, '' ) .replace( /^# ?/g, '' ) .replace( /\n# ?/g, '\n' ) ; var container_tag = '<pre style="STYLE">'; } var $container = $( container_tag.replace( /\bSTYLE\b/, ChiliBook.selectionStyle ) ) .appendTo( 'body' ) .text( selected ) .attr( 'id', 'chili_selection' ) .click( function() { $(this).remove(); } ) ; var top = event.pageY - Math.round( $container.height() / 2 ) + "px"; var left = event.pageX - Math.round( $container.width() / 2 ) + "px"; $container.css( { top: top, left: left } ); if( $.browser.msie ) { // window.clipboardData.setData( 'Text', selected ); //I couldn't find anything similar for Mozilla $container[0].focus(); $container[0].select(); } else { var s = window.getSelection(); s.removeAllRanges(); var r = document.createRange(); r.selectNodeContents( $container[0] );
561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
s.addRange( r ); } } } ) ; } // enableSelectionHelper function getPath( recipeName ) { return book.recipeFolder + recipeName + ".js"; } // getPath function getSelectedText() { var text = ''; if( $.browser.msie ) { text = document.selection.createRange().htmlText; } else { text = window.getSelection().toString(); } return text; } // getSelectedText function preserveNewLines( html ) { do { var newline_flag = ChiliBook.unique(); } while( html.indexOf( newline_flag ) > -1 ); var text = ''; if (/<br/i.test(html) || /<li/i.test(html)) { if (/<br/i.test(html)) { html = html.replace( /\<br[^>]*?\>/ig, newline_flag ); } else if (/<li/i.test(html)) { html = html.replace( /<ol[^>]*?>|<\/ol>|<li[^>]*?>/ig, '' ).replace( /<\/li>/ig, newline_flag ); } var el = $( '<pre>' ).appendTo( 'body' ).hide()[0]; el.innerHTML = html; text = $( el ).text().replace( new RegExp( newline_flag, "g" ), '\r\n' ); $( el ).remove(); } return text; } // preserveNewLines function addLineNumbers( el ) { function makeListItem1( not_last_line, not_last, last, open ) { var close = open ? '</span>' : ''; var aux = ''; if( not_last_line ) { aux = '<li>' + open + not_last + close + '</li>'; } else if( last ) { aux = '<li>' + open + last + close + '</li>'; } return aux; } // makeListItem1 function makeListItem2( not_last_line, not_last, last, prev_li ) { var aux = ''; if( prev_li ) { aux = prev_li; } else { aux = makeListItem1( not_last_line, not_last, last, '' ) } return aux; } // makeListItem2 var html = $( el ).html(); var br = /<br>/.test(html) ? '<br>' : '<BR>';
631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
var empty_line = '<li>' + book.replaceSpace + '</li>'; var list_items = html //extract newlines at the beginning of a span .replace( /(<span [^>]+>)((?:(?:&nbsp;|\xA0)<br>)+)(.*?)(<\/span>)/ig, '$2$1$3$4' ) // I don't know why <span .*?> does not work here //transform newlines inside of a span .replace( /(.*?)(<span .*?>)(.*?)(?:<\/span>(?:&nbsp;|\xA0)<br>|<\/span>)/ig, // but here it does function( all, before, open, content ) { if (/<br>/i.test(content)) { var pieces = before.split( br ); var lastPiece = pieces.pop(); before = pieces.join( br ); var aux = (before ? before + br : '') //+ replace1( lastPiece + content, open ); + (lastPiece + content).replace( /((.*?)(?:&nbsp;|\xA0)<br>)|(.*)/ig, function( tmp, not_last_line, not_last, last ) { var aux2 = makeListItem1( not_last_line, not_last, last, open ); return aux2; } ); return aux; } else { return all; } } ) //transform newlines outside of a span .replace( /(<li>.*?<\/li>)|((.*?)(?:&nbsp;|\xA0)<br>)|(.+)/ig, function( tmp, prev_li, not_last_line, not_last, last ) { var aux2 = makeListItem2( not_last_line, not_last, last, prev_li ); return aux2; } ) //fix empty lines for Opera .replace( /<li><\/li>/ig, empty_line ) ; el.innerHTML = '<ol>' + list_items + '</ol>'; } // addLineNumbers function revealChars( tmp ) { return $ .map( tmp.split(''), function(n, i) { return ' ' + n + ' ' + n.charCodeAt( 0 ) + ' '; } ) .join(' '); } // revealChars //----------------------------------------------------------------------------- // the coloring starts here this .each( function() { var $this = $( this ); $this.trigger( 'chili.before_coloring' ); askDish( this ); $this.trigger( 'chili.after_coloring' ); } ); return this; //----------------------------------------------------------------------------- }; //main $( function() { if( ChiliBook.automatic ) { $( ChiliBook.automaticSelector ).chili(); }
701702703704705
} ); } ) ( jQuery );