/* * 漢字標準格式 v2.1.1 * --- * Hanzi-optimised CSS Mode * * * * Lisence: MIT Lisence * Last Modified: 2013/10/21 * */ jQuery.noConflict(); (function($){ var version = '2.1.1', tests = [], rubies, unicode = [], rendered = 'han-js-rendered', classes = [rendered], fontfaces = [], han = function() { $(document).on('ready', function(){ fontfaces['songti'] = test_for_fontface( 'Han Songti' ); fontfaces['kaiti'] = test_for_fontface( 'Han Kaiti' ); fontfaces['fangsong'] = test_for_fontface( 'Han Fangsong' ); for ( var font in fontfaces ) { classes.push( ( fontfaces[font] ? '' : 'no-' ) + 'fontface-' + font ); } $('html').addClass( classes.join(' ') ); init(); }); }, init = function( range ) { if ( !range && $('html').hasClass('no-han-init') ) return; var range = range || 'body'; if ( range !== 'body' && !$(range).hasClass(rendered) ) $(range).addClass(rendered); else if ( range === 'body' && !$('html').hasClass(rendered) ) $('html').addClass(rendered); /* * 加強漢字註音功能 * --- * Enhance `` element * * **注意:**需置於``的hack前。 * * **Note:** The necessity of being * placed before the hack of * the `` element is required. */ $(range).find('ruby.pinyin').addClass('romanization'); $(range).find('ruby.zhuyin').addClass('mps'); $(range).find('ruby').each(function() { var html = $(this).html(); // 羅馬拼音(在不支援``的瀏覽器下) if ( !$(this).hasClass('mps') && !tests['ruby']() ) { var result = html .replace(//ig, '') .replace(/<\/rt>/ig, ''); $(this).html('' + result + ''); // 注音符號 } else if ( $(this).hasClass('mps') ) { var generic = $(this).css('font-family'), zhuyin_font = ( generic.match(/(sans-serif|monospace)$/) ) ? 'sans-serif' : 'serif', hanzi = unicode_set('hanzi'), shengmu = unicode['bopomofo']['mps']['shengmu'], jieyin = unicode['bopomofo']['mps']['jieyin'], yunmu = unicode['bopomofo']['mps']['yunmu'], tone = unicode['bopomofo']['tone']['five'], reg = '/(' + hanzi + ')(.*)<\\/rt>/ig'; html = html.replace(eval(reg), function(entire, character, mps){ var form, yin, diao, data, zi; form = ( mps.match(eval('/(' + shengmu + ')/')) ) ? 'shengmu' : ''; form += ( mps.match(eval('/(' + jieyin + ')/')) ) ? (( form !== '' ) ? '-' : '') + 'jieyin' : ''; form += ( mps.match(eval('/(' + yunmu + ')/')) ) ? (( form !== '' ) ? '-' : '') + 'yunmu' : ''; yin = mps.replace(eval('/(' + tone + ')/g'), ''), diao = ( mps.match(/([\u02D9])/) ) ? '0' : ( mps.match(/([\u02CA])/) ) ? '2' : ( mps.match(/([\u02C5\u02C7])/) ) ? '3' : ( mps.match(/([\u02CB])/) ) ? '4' : '1'; data = 'data-zy="' + yin + '" data-tone="' + diao + '" data-form="' + form + '"'; zi = '' + character + ''; return zi + '' + mps + ''; }); $(this).replaceWith( $('').addClass('zhuyin-' + zhuyin_font).html( html ) ); } }); /* * 漢拉間隙 * --- * Gaps between Hanzi and Latin Letter * */ if ( $('html').hasClass('han-la') ) $(range).each(function(){ var hanzi = unicode_set('hanzi'), latin = unicode_set('latin') + '|' + unicode['punc'][0], punc = unicode['punc']; patterns = [ '/(' + hanzi + ')(' + latin + '|' + punc[1] + ')/ig', '/(' + latin + '|' + punc[2] + ')(' + hanzi + ')/ig' ]; patterns.forEach(function( exp ){ findAndReplaceDOMText(this, { find: eval(exp), replace: '$1$2' }); }, this); findAndReplaceDOMText(this, { find: '', replace: function(){ return _span( 'hanla' ); } }); this.normalize(); $('* > span.hanla:first-child').parent().each(function(){ if ( this.firstChild.nodeType == 1 ) { $(this).before( $('') ); $(this).find('span.hanla:first-child').remove(); } }); }); /* * 修正相鄰註記元素``的底線相連問題 * --- * fixing the underline-adjacency issues on `` element * */ if ( $('html').hasClass('han-lab-underline') ) $(range).find('u').charize('', true, true); else $(range).each(function() { var html = $(this).html(); $(this) .html( html.replace(/<\/u>(|)*?/ig, '$1') ) .find('u[data-adjacent]').addClass('adjacent').removeAttr('data-adjacent'); }); /* 強調元素``的着重號 * --- * punctuation: CJK emphasis dots * on `` element * */ $(range).find('em').charize({ latin: ( tests['textemphasis']() ) ? 'none' : 'individual' }); /* 修正引言元素``不為WebKit引擎支援的問題 * --- * punctuation: CJK quotes on `` (WebKit) * */ if ( !tests['quotes']() ) $(range).find('q q').each(function() { if ( $(this).parents('q').length%2 != 0 ) $(this).addClass('double'); }); }, unicode_set = function( set ) { var join = ( set.match(/[hanzi|latin]/) ) ? true : false, result = ( join ) ? unicode[set].join('|') : unicode[set]; return result; }, _span = function( className ) { var span = document.createElement('span'); span.className = className; return span; }, findAndReplaceDOMText = function( a, b ) { var b = b; b.filterElements = function( el ) { var name = el.nodeName.toLowerCase(), classes = ( el.nodeType == 1 ) ? el.getAttribute('class') : '', charized = ( classes && classes.match(/han-js-charized/) != null ) ? true : false; return name !== 'style' && name !== 'script' && !charized; }; return window.findAndReplaceDOMText(a,b); }, inject_element_with_styles = function( rule, callback, nodes, testnames ) { var style, ret, node, docOverflow, docElement = document.documentElement, div = document.createElement('div'), body = document.body, fakeBody = body || document.createElement('body'); style = [''].join(''); (body ? div : fakeBody).innerHTML += style; fakeBody.appendChild(div); if ( !body ) { fakeBody.style.background = ''; fakeBody.style.overflow = 'hidden'; docOverflow = docElement.style.overflow; docElement.style.overflow = 'hidden'; docElement.appendChild(fakeBody); } ret = callback(div, rule); if ( !body ) { fakeBody.parentNode.removeChild(fakeBody); docElement.style.overflow = docOverflow; } else div.parentNode.removeChild(div); return !!ret; }, write_on_canvas = function( text, font ) { var canvasNode = document.createElement('canvas'); canvasNode.width = '50'; canvasNode.height = '20'; canvasNode.style.display = 'none'; canvasNode.className = 'han_support_tests'; document.body.appendChild(canvasNode); var ctx = canvasNode.getContext('2d'); ctx.textBaseline = 'top'; ctx.font = '15px ' + font + ', sans-serif'; ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.fillText( text, 0, 0 ); return ctx; }, test_for_fontface = function( font ) { if ( !tests['fontface']() ) return false; try { var sans = write_on_canvas( '辭Q', 'sans-serif' ), test = write_on_canvas( '辭Q', font ), support; for (var j = 1; j <= 20; j++) { for (var i = 1; i <= 50; i++) { var sansData = sans.getImageData(i, j, 1, 1).data, testData = test.getImageData(i, j, 1, 1).data, alpha = []; alpha['sans'] = sansData[3]; alpha['test'] = testData[3]; if ( support !== 'undefined' && alpha['test'] != alpha['sans'] ) support = true; else if ( support ) break; if ( i == 20 && j == 20 ) if ( !support ) support = false; } } $('canvas.han_support_tests').remove(); return support; } catch ( err ) { return false; } }; /* -------------------------------------------------------- * Unicode區域說明(6.2.0) * -------------------------------------------------------- * 或參考: * http://css.hanzi.co/manual/api/javascript_jiekou-han.unicode * -------------------------------------------------------- * ** 以下歸類為「拉丁字母」(`unicode('latin')`)** * * 基本拉丁字母:a-z * 阿拉伯數字:0-9 * 拉丁字母補充-1:[\u00C0-\u00FF] * 拉丁字母擴展-A區:[\u0100-\u017F] * 拉丁字母擴展-B區:[\u0180-\u024F] * 拉丁字母附加區:[\u1E00-\u1EFF] * ** 符號:[~!@#&;=_\$\%\^\*\-\+\,\.\/(\\)\?\:\'\"\[\]\(\)'"<>‘“”’] * * -------------------------------------------------------- * ** 以下歸類為「漢字」(`unicode('hanzi')`)** * * CJK一般:[\u4E00-\u9FFF] * CJK擴展-A區:[\u3400-\u4DB5] * CJK擴展-B區:[\u20000-\u2A6D6] * CJK Unicode 4.1:[\u9FA6-\u9FBB][\uFA70-\uFAD9] * CJK Unicode 5.1:[\u9FBC-\u9FC3] * CJK擴展-C區:[\u2A700-\u2B734] * CJK擴展-D區:[\u2B740-\u2B81D](急用漢字) * CJK擴展-E區:[\u2B820-\u2F7FF](**註**:暫未支援) * CJK擴展-F區(**註**:暫未支援) * CJK筆畫區:[\u31C0-\u31E3] * 數字「〇」:[\u3007] * 日文假名:[\u3040-\u309E][\u30A1-\u30FA][\u30FD\u30FE](**註**:排除片假名中點、長音符) * * CJK相容表意文字:[\uF900-\uFAFF](**註**:不使用) * -------------------------------------------------------- * ** 符號 * [·・︰、,。:;?!—⋯….·「『(〔【《〈“‘」』)〕】》〉’”–ー—] * ** 其他 * * 漢語注音符號、擴充:[\u3105-\u312D][\u31A0-\u31BA] * 國語五聲調(三聲有二種符號):[\u02D9\u02CA\u02C5\u02C7\u02CB] * 台灣漢語方言音擴充聲調:[\u02EA\u02EB] * * */ unicode['latin'] = [ '[A-Za-z0-9\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u1E00-\u1EFF]' ]; unicode['punc'] = [ '[@&;=_\,\.\?\!\$\%\^\*\-\+\/]', '[\(\\[\'"<‘“]', '[\)\\]\'">”’]' ]; unicode['hanzi'] = [ '[\u4E00-\u9FFF]', '[\u3400-\u4DB5\u9FA6-\u9FBB\uFA70-\uFAD9\u9FBC-\u9FC3\u3007\u3040-\u309E\u30A1-\u30FA\u30FD\u30FE]', '[\uD840-\uD868][\uDC00-\uDFFF]|\uD869[\uDC00-\uDEDF]', '\uD86D[\uDC00-\uDF3F]|[\uD86A-\uD86C][\uDC00-\uDFFF]|\uD869[\uDF00-\uDFFF]', '\uD86D[\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1F]', '[\u31C0-\u31E3]' ]; unicode['biaodian'] = [ '[·・︰、,。:;?!—ー⋯….·/]', '[「『(〔【《〈“‘]', '[」』)〕】》〉’”]' ]; unicode['bopomofo'] = []; unicode['bopomofo']['mps'] = []; unicode['bopomofo']['mps'][0] = '[\u3105-\u312D]'; unicode['bopomofo']['mps']['shengmu'] = '[\u3105-\u3119\u312A-\u312C]'; unicode['bopomofo']['mps']['jieyin'] = '[\u3127-\u3129]'; unicode['bopomofo']['mps']['yunmu'] = '[\u311A-\u3126\u312D]'; unicode['bopomofo']['extend'] = '[\u31A0-\u31BA]'; unicode['bopomofo']['tone'] = []; unicode['bopomofo']['tone']['five'] = '[\u02D9\u02CA\u02C5\u02C7\u02CB]'; unicode['bopomofo']['tone']['extend'] = '[\u02EA\u02EB]'; /* tests for HTML5/CSS3 features */ /* CSS3 property: `column-width` */ tests['columnwidth'] = function() { var cw = $('
tester
'), bool = ( /^200px$/.test( cw.css("-webkit-column-width") ) || /^200px$/.test( cw.css("-moz-column-width") ) || /^200px$/.test( cw.css("-ms-column-width") ) || /^200px$/.test( cw.css("column-width") ) ) ? true : false; return bool; }; /* -------------------------------------------------------- * test for '@font-face' * -------------------------------------------------------- * Originates from Modernizr (http://modernizr.com) */ tests['fontface'] = function() { var bool; inject_element_with_styles('@font-face {font-family:"font";src:url("https://")}', function( node, rule ) { var style = document.getElementById('han-support'), sheet = style.sheet || style.styleSheet, cssText = sheet ? (sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '') : ''; bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0; }); return bool; }; tests['ruby'] = function() { if ( rubies != null ) return rubies; var ruby = document.createElement('ruby'), rt = document.createElement('rt'), rp = document.createElement('rp'), docElement = document.documentElement, displayStyleProperty = 'display'; ruby.appendChild(rp); ruby.appendChild(rt); docElement.appendChild(ruby); // browsers that support hide the via "display:none" rubies = ( getStyle(rp, displayStyleProperty) == 'none' || // but in IE browsers has "display:inline" so, the test needs other conditions: getStyle(ruby, displayStyleProperty) == 'ruby' && getStyle(rt, displayStyleProperty) == 'ruby-text' ) ? true : false; docElement.removeChild(ruby); // the removed child node still exists in memory, so ... ruby = null; rt = null; rp = null; return rubies; function getStyle( element, styleProperty ) { var result; if ( window.getComputedStyle ) // for non-IE browsers result = document.defaultView.getComputedStyle(element,null).getPropertyValue(styleProperty); else if ( element.currentStyle ) // for IE result = element.currentStyle[styleProperty]; return result; } }; tests['textemphasis'] = function() { var em = $('tester'), bool = ( /^dot$/.test( em.css("-webkit-text-emphasis-style") ) || /^dot$/.test( em.css("text-emphasis-style") ) || /^dot$/.test( em.css("-moz-text-emphasis-style") ) || /^dot$/.test( em.css("-ms-text-emphasis-style") ) ) ? true : false; return bool; }; tests['quotes'] = function() { var q = $('tester'), bool = /^"“" "”" "‘" "’"$/.test( q.css("quotes") ); return bool; }; tests['writingmode'] = function() { var wm = $('
tester
'), bool = ( /^tb-rl$/.test( wm.css("writing-mode") ) || /^vertical-rl$/.test( wm.css("-webkit-writing-mode") ) || /^tb-rl$/.test( wm.css("-moz-writing-mode") ) || /^tb-rl$/.test( wm.css("-ms-writing-mode") ) ) ? true: false; return bool; }; $.fn.extend({ hanInit: function() { return init(this); }, bitouwei: function() { return this.each(function(){ $(this).addClass( 'han-js-bitouwei-rendered' ); var tou = unicode['biaodian'][0] + unicode['biaodian'][2], wei = unicode['biaodian'][1], start = unicode['punc'][0] + unicode['punc'][2], end = unicode['punc'][1]; tou = tou.replace(/\]\[/g, '' ); start = start.replace(/\]\[/g, '' ); // CJK characters findAndReplaceDOMText(this, { find: eval( '/(' + wei + ')(' + unicode_set('hanzi') + ')(' + tou + ')/ig' ), wrap: _span( 'bitouwei bitouweidian' ) }); findAndReplaceDOMText(this, { find: eval( '/(' + unicode_set('hanzi') + ')(' + tou + ')/ig' ), wrap: _span( 'bitouwei bitoudian' ) }); findAndReplaceDOMText(this, { find: eval( '/(' + wei + ')(' + unicode_set('hanzi') + ')/ig' ), wrap: _span( 'bitouwei biweidian' ) }); // Latin letters findAndReplaceDOMText(this, { find: eval( '/(' + end + ')(' + unicode_set('latin') + '+)(' + start + ')/ig' ), wrap: _span( 'bitouwei bitouweidian' ) }); findAndReplaceDOMText(this, { find: eval( '/(' + unicode_set('latin') + '+)(' + start + ')/ig' ), wrap: _span( 'bitouwei bitoudian' ) }); findAndReplaceDOMText(this, { find: eval( '/(' + end + ')(' + unicode_set('latin') + '+)/ig' ), wrap: _span( 'bitouwei biweidian' ) }); }); }, charize: function( glyph, charClass, innerSpan ){ var glyph = glyph || {}, charClass = (charClass == true) ? true : false; glyph = { cjk: glyph.cjk || 'individual', bitouwei: (glyph.bitouwei == false) ? false : true, latin: glyph.latin || 'group' }; return this.each(function(){ if ( glyph.bitouwei ) $(this).bitouwei(); // CJK characters if ( glyph.cjk === 'individual' ) findAndReplaceDOMText(this, { find: eval( '/(' + unicode_set('hanzi') + ')/ig' ), wrap: _span( 'char cjk' ) }); if ( glyph.cjk === 'individual' || glyph.cjk === 'biaodian' ) findAndReplaceDOMText(this, { find: eval( '/(' + unicode_set('biaodian') + ')/ig' ), wrap: _span( 'char cjk biaodian' ) }); if ( glyph.cjk === 'group' ) findAndReplaceDOMText(this, { find: eval( '/(' + unicode_set('hanzi') + '+|' + unicode_set('biaodian') + '+)/ig' ), wrap: _span( 'char cjk' ) }); var latin_regex = ( glyph.latin === 'group' ) ? '/(' + unicode_set('latin') + '+)/ig' : '/(' + unicode_set('latin') + ')/ig'; findAndReplaceDOMText(this, { find: eval( latin_regex ), wrap: _span( 'char latin' ) }); findAndReplaceDOMText(this, { find: eval( '/(' + unicode_set('punc') + '+)/ig' ), wrap: _span( 'char latin punc' ) }); findAndReplaceDOMText(this, { find: /([\s]+)/ig, wrap: _span( 'char space' ) }); if ( innerSpan ) $(this).find('.char').each(function(){ $(this).html( $('').text( $(this).text() ) ); }); if ( charClass ) $(this).addClass('han-js-charized'); }); } }); for ( var feature in tests ) { classes.push( ( tests[feature]() ? '' : 'no-' ) + feature ); if ( !tester ) var tester = ''; tester += ' ' + feature + ': tests[\'' + feature + '\'](),\n'; } !function(window) { eval("tester = ({\n" + tester.replace(/\n$/ig, '\nfont: test_for_fontface\n}') + ")"); }(); han(); window.han = { unicode: unicode_set, support: tester } })(jQuery); /** * findAndReplaceDOMText v 0.4.0 * @author James Padolsey http://james.padolsey.com * @license http://unlicense.org/UNLICENSE * * Matches the text of a DOM node against a regular expression * and replaces each match (or node-separated portions of the match) * in the specified element. */ window.findAndReplaceDOMText = (function() { var PORTION_MODE_RETAIN = 'retain'; var PORTION_MODE_FIRST = 'first'; var doc = document; var toString = {}.toString; function isArray(a) { return toString.call(a) == '[object Array]'; } function escapeRegExp(s) { return String(s).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); } function exposed() { // Try deprecated arg signature first: return deprecated.apply(null, arguments) || findAndReplaceDOMText.apply(null, arguments); } function deprecated(regex, node, replacement, captureGroup, elFilter) { if ((node && !node.nodeType) && arguments.length <= 2) { return false; } var isReplacementFunction = typeof replacement == 'function'; if (isReplacementFunction) { replacement = (function(original) { return function(portion, match) { return original(portion.text, match.startIndex); }; }(replacement)); } // Awkward support for deprecated argument signature (<0.4.0) var instance = findAndReplaceDOMText(node, { find: regex, wrap: isReplacementFunction ? null : replacement, replace: isReplacementFunction ? replacement : '$' + (captureGroup || '&'), prepMatch: function(m, mi) { // Support captureGroup (a deprecated feature) if (!m[0]) throw 'findAndReplaceDOMText cannot handle zero-length matches'; if (captureGroup > 0) { var cg = m[captureGroup]; m.index += m[0].indexOf(cg); m[0] = cg; } m.endIndex = m.index + m[0].length; m.startIndex = m.index; m.index = mi; return m; }, filterElements: elFilter }); exposed.revert = function() { return instance.revert(); }; return true; } /** * findAndReplaceDOMText * * Locates matches and replaces with replacementNode * * @param {Node} node Element or Text node to search within * @param {RegExp} options.find The regular expression to match * @param {String|Element} [options.wrap] A NodeName, or a Node to clone * @param {String|Function} [options.replace='$&'] What to replace each match with * @param {Function} [options.filterElements] A Function to be called to check whether to * process an element. (returning true = process element, * returning false = avoid element) */ function findAndReplaceDOMText(node, options) { return new Finder(node, options); } exposed.Finder = Finder; /** * Finder -- encapsulates logic to find and replace. */ function Finder(node, options) { options.portionMode = options.portionMode || PORTION_MODE_RETAIN; this.node = node; this.options = options; // ENable match-preparation method to be passed as option: this.prepMatch = options.prepMatch || this.prepMatch; this.reverts = []; this.matches = this.search(); if (this.matches.length) { this.processMatches(); } } Finder.prototype = { /** * Searches for all matches that comply with the instance's 'match' option */ search: function() { var match; var matchIndex = 0; var regex = this.options.find; var text = this.getAggregateText(); var matches = []; regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex; if (regex.global) { while (match = regex.exec(text)) { matches.push(this.prepMatch(match, matchIndex++)); } } else { if (match = text.match(regex)) { matches.push(this.prepMatch(match, 0)); } } return matches; }, /** * Prepares a single match with useful meta info: */ prepMatch: function(match, matchIndex) { if (!match[0]) { throw new Error('findAndReplaceDOMText cannot handle zero-length matches'); } match.endIndex = match.index + match[0].length; match.startIndex = match.index; match.index = matchIndex; return match; }, /** * Gets aggregate text within subject node */ getAggregateText: function() { var elementFilter = this.options.filterElements; return getText(this.node); /** * Gets aggregate text of a node without resorting * to broken innerText/textContent */ function getText(node) { if (node.nodeType === 3) { return node.data; } if (elementFilter && !elementFilter(node)) { return ''; } var txt = ''; if (node = node.firstChild) do { txt += getText(node); } while (node = node.nextSibling); return txt; } }, /** * Steps through the target node, looking for matches, and * calling replaceFn when a match is found. */ processMatches: function() { var matches = this.matches; var node = this.node; var elementFilter = this.options.filterElements; var startPortion, endPortion, innerPortions = [], curNode = node, match = matches.shift(), atIndex = 0, // i.e. nodeAtIndex matchIndex = 0, portionIndex = 0, doAvoidNode; out: while (true) { if (curNode.nodeType === 3) { if (!endPortion && curNode.length + atIndex >= match.endIndex) { // We've found the ending endPortion = { node: curNode, index: portionIndex++, text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex), indexInMatch: atIndex - match.startIndex, indexInNode: match.startIndex - atIndex, // always zero for end-portions endIndexInNode: match.endIndex - atIndex, isEnd: true }; } else if (startPortion) { // Intersecting node innerPortions.push({ node: curNode, index: portionIndex++, text: curNode.data, indexInMatch: atIndex - match.startIndex, indexInNode: 0 // always zero for inner-portions }); } if (!startPortion && curNode.length + atIndex > match.startIndex) { // We've found the match start startPortion = { node: curNode, index: portionIndex++, indexInMatch: 0, indexInNode: match.startIndex - atIndex, endIndexInNode: match.endIndex - atIndex, text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex) }; } atIndex += curNode.data.length; } doAvoidNode = curNode.nodeType === 1 && elementFilter && !elementFilter(curNode); if (startPortion && endPortion) { curNode = this.replaceMatch(match, startPortion, innerPortions, endPortion); // processMatches has to return the node that replaced the endNode // and then we step back so we can continue from the end of the // match: atIndex -= (endPortion.node.data.length - endPortion.endIndexInNode); startPortion = null; endPortion = null; innerPortions = []; match = matches.shift(); portionIndex = 0; matchIndex++; if (!match) { break; // no more matches } } else if ( !doAvoidNode && (curNode.firstChild || curNode.nextSibling) ) { // Move down or forward: curNode = curNode.firstChild || curNode.nextSibling; continue; } // Move forward or up: while (true) { if (curNode.nextSibling) { curNode = curNode.nextSibling; break; } else if (curNode.parentNode !== node) { curNode = curNode.parentNode; } else { break out; } } } }, /** * Reverts ... TODO */ revert: function() { // Reversion occurs backwards so as to avoid nodes subsequently // replaced during the matching phase (a forward process): for (var l = this.reverts.length; l--;) { this.reverts[l](); } this.reverts = []; }, prepareReplacementString: function(string, portion, match, matchIndex) { var portionMode = this.options.portionMode; if ( portionMode === PORTION_MODE_FIRST && portion.indexInMatch > 0 ) { return ''; } string = string.replace(/\$(\d+|&|`|')/g, function($0, t) { var replacement; switch(t) { case '&': replacement = match[0]; break; case '`': replacement = match.input.substring(0, match.startIndex); break; case '\'': replacement = match.input.substring(match.endIndex); break; default: replacement = match[+t]; } return replacement; }); if (portionMode === PORTION_MODE_FIRST) { return string; } if (portion.isEnd) { return string.substring(portion.indexInMatch); } return string.substring(portion.indexInMatch, portion.indexInMatch + portion.text.length); }, getPortionReplacementNode: function(portion, match, matchIndex) { var replacement = this.options.replace || '$&'; var wrapper = this.options.wrap; if (wrapper && wrapper.nodeType) { // Wrapper has been provided as a stencil-node for us to clone: var clone = doc.createElement('div'); clone.innerHTML = wrapper.outerHTML || new XMLSerializer().serializeToString(wrapper); wrapper = clone.firstChild; } if (typeof replacement == 'function') { replacement = replacement(portion, match, matchIndex); if (replacement && replacement.nodeType) { return replacement; } return doc.createTextNode(String(replacement)); } var el = typeof wrapper == 'string' ? doc.createElement(wrapper) : wrapper; replacement = doc.createTextNode( this.prepareReplacementString( replacement, portion, match, matchIndex ) ); if (!el) { return replacement; } el.appendChild(replacement); return el; }, replaceMatch: function(match, startPortion, innerPortions, endPortion) { var matchStartNode = startPortion.node; var matchEndNode = endPortion.node; var preceedingTextNode; var followingTextNode; if (matchStartNode === matchEndNode) { var node = matchStartNode; if (startPortion.indexInNode > 0) { // Add `before` text node (before the match) preceedingTextNode = doc.createTextNode(node.data.substring(0, startPortion.indexInNode)); node.parentNode.insertBefore(preceedingTextNode, node); } // Create the replacement node: var newNode = this.getPortionReplacementNode( endPortion, match ); node.parentNode.insertBefore(newNode, node); if (endPortion.endIndexInNode < node.length) { // ????? // Add `after` text node (after the match) followingTextNode = doc.createTextNode(node.data.substring(endPortion.endIndexInNode)); node.parentNode.insertBefore(followingTextNode, node); } node.parentNode.removeChild(node); this.reverts.push(function() { if (preceedingTextNode === newNode.previousSibling) { preceedingTextNode.parentNode.removeChild(preceedingTextNode); } if (followingTextNode === newNode.nextSibling) { followingTextNode.parentNode.removeChild(followingTextNode); } newNode.parentNode.replaceChild(node, newNode); }); return newNode; } else { // Replace matchStartNode -> [innerMatchNodes...] -> matchEndNode (in that order) preceedingTextNode = doc.createTextNode( matchStartNode.data.substring(0, startPortion.indexInNode) ); followingTextNode = doc.createTextNode( matchEndNode.data.substring(endPortion.endIndexInNode) ); var firstNode = this.getPortionReplacementNode( startPortion, match ); var innerNodes = []; for (var i = 0, l = innerPortions.length; i < l; ++i) { var portion = innerPortions[i]; var innerNode = this.getPortionReplacementNode( portion, match ); portion.node.parentNode.replaceChild(innerNode, portion.node); this.reverts.push((function(portion, innerNode) { return function() { innerNode.parentNode.replaceChild(portion.node, innerNode); }; }(portion, innerNode))); innerNodes.push(innerNode); } var lastNode = this.getPortionReplacementNode( endPortion, match ); matchStartNode.parentNode.insertBefore(preceedingTextNode, matchStartNode); matchStartNode.parentNode.insertBefore(firstNode, matchStartNode); matchStartNode.parentNode.removeChild(matchStartNode); matchEndNode.parentNode.insertBefore(lastNode, matchEndNode); matchEndNode.parentNode.insertBefore(followingTextNode, matchEndNode); matchEndNode.parentNode.removeChild(matchEndNode); this.reverts.push(function() { preceedingTextNode.parentNode.removeChild(preceedingTextNode); firstNode.parentNode.replaceChild(matchStartNode, firstNode); followingTextNode.parentNode.removeChild(followingTextNode); lastNode.parentNode.replaceChild(matchEndNode, lastNode); }); return lastNode; } } }; return exposed; }());