From d1349672e9933b95802e039cc7bbeaed1798c4e5 Mon Sep 17 00:00:00 2001 From: othree_kao Date: Tue, 3 Dec 2013 15:51:31 +0800 Subject: [PATCH] Upgrade han.js --- js/han.js | 2118 +++++++++++++++++++++++++++-------------------------- 1 file changed, 1081 insertions(+), 1037 deletions(-) diff --git a/js/han.js b/js/han.js index 40e20bf..7599314 100644 --- a/js/han.js +++ b/js/han.js @@ -1,13 +1,13 @@ /* - * 漢字標準格式 v2.1.1 + * 漢字標準格式 v2.2.0 * --- * Hanzi-optimised CSS Mode * * * * Lisence: MIT Lisence - * Last Modified: 2013/10/21 + * Last Modified: 2013/11/27 * */ @@ -16,700 +16,725 @@ jQuery.noConflict(); (function($){ - var version = '2.1.1', + var version = '2.2.0', - tests = [], - rubies, + tests = [], + rubies, - unicode = [], + unicode = [], - rendered = 'han-js-rendered', - classes = [rendered], - fontfaces = [], + 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' ); + han = function() { + $(document).on('ready', function(){ - for ( var font in fontfaces ) { - classes.push( ( fontfaces[font] ? '' : 'no-' ) + 'fontface-' + font ); - } + // `unicode-range` + classes.push( ( test_for_unicoderange() ? '' : 'no-' ) + 'unicoderange' ); - $('html').addClass( classes.join(' ') ); + // The 4(-1) Typefaces + fontfaces['songti'] = test_for_fontface( 'Han Songti' ); + fontfaces['kaiti'] = test_for_fontface( 'Han Kaiti' ); + fontfaces['fangsong'] = test_for_fontface( 'Han Fangsong' ); - init(); - }); - }, + for ( var font in fontfaces ) { + classes.push( ( fontfaces[font] ? '' : 'no-' ) + 'fontface-' + font ); + } + // altogether + $('html').addClass( classes.join(' ') ); - init = function( range ) { - if ( !range && $('html').hasClass('no-han-init') ) - return; + init(); + }); + }, - var range = range || 'body'; - if ( range !== 'body' && !$(range).hasClass(rendered) ) - $(range).addClass(rendered); - else if ( range === 'body' && !$('html').hasClass(rendered) ) - $('html').addClass(rendered); + 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(); + /* + * 加強漢字註音功能 + * --- + * Enhance `` element + * + * **注意:**需置於``的hack前。 + * + * **Note:** The necessity of being + * placed before the hack of + * the `` element is required. + */ - // 羅馬拼音(在不支援``的瀏覽器下) - if ( !$(this).hasClass('mps') && !tests['ruby']() ) { - var result = html - .replace(//ig, '') - .replace(/<\/rt>/ig, ''); + $(range).find('ruby.pinyin').addClass('romanization'); + $(range).find('ruby.zhuyin').addClass('mps'); - $(this).html('' + result + ''); + $(range).find('ruby').each(function() { + var html = $(this).html(); - // 注音符號 - } else if ( $(this).hasClass('mps') ) { - var generic = $(this).css('font-family'), - zhuyin_font = ( generic.match(/(sans-serif|monospace)$/) ) ? 'sans-serif' : 'serif', + // 羅馬拼音(在不支援``的瀏覽器下) + if ( !$(this).hasClass('mps') && !tests['ruby']() ) { + var result = html + .replace(//ig, '') + .replace(/<\/rt>/ig, ''); - hanzi = unicode_set('hanzi'), + $(this).html('' + result + ''); - shengmu = unicode['bopomofo']['mps']['shengmu'], - jieyin = unicode['bopomofo']['mps']['jieyin'], - yunmu = unicode['bopomofo']['mps']['yunmu'], - tone = unicode['bopomofo']['tone']['five'], + // 注音符號 + } else if ( $(this).hasClass('mps') ) { + var generic = $(this).css('font-family'), + zhuyin_font = ( generic.match(/(sans-serif|monospace)$/) ) ? 'sans-serif' : 'serif', - reg = '/(' + hanzi + ')(.*)<\\/rt>/ig'; + hanzi = unicode_set('hanzi'), + shengmu = unicode['bopomofo']['mps']['shengmu'], + jieyin = unicode['bopomofo']['mps']['jieyin'], + yunmu = unicode['bopomofo']['mps']['yunmu'], + tone = unicode['bopomofo']['tone']['five'], - html = html.replace(eval(reg), function(entire, character, mps){ - var form, yin, diao, data, zi; + reg = '/(' + hanzi + ')(.*)<\\/rt>/ig'; - 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'; + html = html.replace(eval(reg), function(entire, character, mps){ + var form, yin, diao, data, zi; - data = 'data-zy="' + yin + '" data-tone="' + diao + '" data-form="' + form + '"'; - zi = '' + character + ''; + form = ( mps.match(eval('/(' + shengmu + ')/')) ) ? 'shengmu' : ''; + form += ( mps.match(eval('/(' + jieyin + ')/')) ) ? (( form !== '' ) ? '-' : '') + 'jieyin' : ''; + form += ( mps.match(eval('/(' + yunmu + ')/')) ) ? (( form !== '' ) ? '-' : '') + 'yunmu' : ''; - return zi + '' + mps + ''; - }); + 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 + ''; - $(this).replaceWith( - $('').addClass('zhuyin-' + zhuyin_font).html( html ) - ); - } - }); + 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' - ]; + /* + * 漢拉間隙 + * --- + * Kerning 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.forEach(function( exp ){ - findAndReplaceDOMText(this, { - find: eval(exp), - replace: '$1$2' - }); - }, this); + patterns = [ + '/(' + hanzi + ')(' + latin + '|' + punc[1] + ')/ig', + '/(' + latin + '|' + punc[2] + ')(' + hanzi + ')/ig' + ]; - findAndReplaceDOMText(this, { - find: '', - replace: function(){ - return _span( 'hanla' ); - } - }); - this.normalize(); + patterns.forEach(function( exp ){ + findAndReplaceDOMText(this, { + find: eval(exp), + replace: '$1$2' + }); + }, this); - $('* > span.hanla:first-child').parent().each(function(){ - if ( this.firstChild.nodeType == 1 ) { - $(this).before( $('') ); - $(this).find('span.hanla:first-child').remove(); - } - }); - }); + 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; + /* + * 修正相鄰註記元素``的底線相連問題 + * --- + * fixing the underline-adjacency issue on `` element + * + */ - 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; + if ( $('html').hasClass('han-lab-underline') ) + $(range).find('u').charize('', true, true); + else + $(range).find('u').each(function(){ + var next = this.nextSibling; - return name !== 'style' && name !== 'script' && !charized; - }; + _ignore(next); + _adj(next); - return window.findAndReplaceDOMText(a,b); - }, + function _adj( next ) { + if ( next.nodeName === "U" ) + $(next).addClass('adjacent'); + } + + function _ignore( next ) { + if ( next.nodeName === "WBR" || next.nodeType == 8 ) { + var next = next.nextSibling; + _ignore(next); + _adj(next); + } + } + }); + + + + /* 強調元素``的着重號 + * --- + * punctuation: CJK emphasis dots on `` element + * + */ + + $(range).find('em').charize({ + latin: ( tests['textemphasis']() ) ? 'none' : 'individual' + }); + + + /* 修正引言元素``不為WebKit引擎支援的問題 + * --- + * punctuation: Quote issue on `` element (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; - 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; - }; + return span; + }, - /* -------------------------------------------------------- - * test for '@font-face' - * -------------------------------------------------------- - * Originates from Modernizr (http://modernizr.com) - */ + 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; - tests['fontface'] = function() { - var bool; + return name !== 'style' && name !== 'script' && !charized; + }; - 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 || '') : ''; + return window.findAndReplaceDOMText(a,b); + }, - bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0; - }); - return bool; - }; + 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( test, compare, zi ) { + if ( !tests['fontface']() ) + return false; + + var test = test, + compare = compare || 'sans-serif', + zi = zi || '辭Q'; + + try { + var sans = write_on_canvas( zi, compare ), + test = write_on_canvas( zi, test ), + 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; + } + }; + + + test_for_unicoderange = function() { + return !test_for_fontface( 'han-unicode-range', 'Arial, "Droid Sans"', 'a' ); + }; + + + /* -------------------------------------------------------- + * 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](**註**:不使用) + * [\uFA0E-\uFA0F\uFA11\uFA13-\uFA14\uFA1F\uFA21\uFA23-\uFA24\uFA27-\uFA29](**註**:12個例外) + * -------------------------------------------------------- + * + ** 符號 + * [·・︰、,。:;?!—⋯….·「『(〔【《〈“‘」』)〕】》〉’”–ー—] + * + ** 其他 + * + * 漢語注音符號、擴充:[\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\uFA0E-\uFA0F\uFA11\uFA13-\uFA14\uFA1F\uFA21\uFA23-\uFA24\uFA27-\uFA29]', + '[\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; + }; - tests['ruby'] = function() { - if ( rubies != null ) - return rubies; + /* -------------------------------------------------------- + * test for '@font-face' + * -------------------------------------------------------- + * Originates from Modernizr (http://modernizr.com) + */ + tests['fontface'] = function() { + var bool; - var ruby = document.createElement('ruby'), - rt = document.createElement('rt'), - rp = document.createElement('rp'), - docElement = document.documentElement, - displayStyleProperty = 'display'; + 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 || '') : ''; - ruby.appendChild(rp); - ruby.appendChild(rt); - docElement.appendChild(ruby); + bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0; + }); - // 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; + return bool; + }; + + tests['ruby'] = function() { + if ( rubies != null ) + return rubies; - docElement.removeChild(ruby); - // the removed child node still exists in memory, so ... - ruby = null; - rt = null; - rp = 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); - function getStyle( element, styleProperty ) { - var result; + // 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; - 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; - } - }; + docElement.removeChild(ruby); + // the removed child node still exists in memory, so ... + ruby = null; + rt = null; + rp = null; + return rubies; - 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; + function getStyle( element, styleProperty ) { + var result; - return bool; - }; + 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['quotes'] = function() { - var q = $('tester'), - bool = /^"“" "”" "‘" "’"$/.test( q.css("quotes") ); + tests['textemphasis'] = function() { + var em = $('tester'), - return bool; - }; + 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['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; + tests['quotes'] = function() { + var q = $('tester'), - return bool; - }; + 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; - $.fn.extend({ - hanInit: function() { - return init(this); - }, + return bool; + }; - 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, '' ); + $.fn.extend({ + hanInit: function() { + return init(this); + }, - // 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' ) - }); + bitouwei: function() { + return this.each(function(){ + $(this).addClass( 'han-js-bitouwei-rendered' ); - findAndReplaceDOMText(this, { - find: eval( '/(' + wei + ')(' + unicode_set('hanzi') + ')/ig' ), - wrap: _span( 'bitouwei biweidian' ) - }); + 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, '' ); - // 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' ) - }); + // CJK characters + findAndReplaceDOMText(this, { + find: eval( '/(' + wei + ')(' + unicode_set('hanzi') + ')(' + tou + ')/ig' ), + wrap: _span( 'bitouwei bitouweidian' ) + }); - findAndReplaceDOMText(this, { - find: eval( '/(' + end + ')(' + unicode_set('latin') + '+)/ig' ), - wrap: _span( 'bitouwei biweidian' ) - }); - }); - }, + 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' ) + }); - 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' - }; + // Latin letters + findAndReplaceDOMText(this, { + find: eval( '/(' + end + ')(' + unicode_set('latin') + '+)(' + start + ')/ig' ), + wrap: _span( 'bitouwei bitouweidian' ) + }); - return this.each(function(){ - if ( glyph.bitouwei ) - $(this).bitouwei(); + 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' ) + }); + }); + }, - // CJK characters - if ( glyph.cjk === 'individual' ) - findAndReplaceDOMText(this, { - find: eval( '/(' + unicode_set('hanzi') + ')/ig' ), - wrap: _span( 'char cjk' ) - }); + charize: function( glyph, charClass, innerSpan ){ + var glyph = glyph || {}, + charClass = (charClass == true) ? true : false; - if ( glyph.cjk === 'individual' || glyph.cjk === 'biaodian' ) - findAndReplaceDOMText(this, { - find: eval( '/(' + unicode_set('biaodian') + ')/ig' ), - wrap: _span( 'char cjk biaodian' ) - }); + glyph = { + cjk: glyph.cjk || 'individual', + bitouwei: (glyph.bitouwei == false) ? false : true, + latin: glyph.latin || 'group' + }; + return this.each(function(){ + if ( glyph.bitouwei ) + $(this).bitouwei(); - if ( glyph.cjk === 'group' ) - findAndReplaceDOMText(this, { - find: eval( '/(' + unicode_set('hanzi') + '+|' + unicode_set('biaodian') + '+)/ig' ), - wrap: _span( 'char cjk' ) - }); + // CJK characters + if ( glyph.cjk === 'individual' ) + findAndReplaceDOMText(this, { + find: eval( '/(' + unicode_set('hanzi') + ')/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' ) - }); + if ( glyph.cjk === 'individual' || glyph.cjk === 'biaodian' ) + findAndReplaceDOMText(this, { + find: eval( '/(' + unicode_set('biaodian') + ')/ig' ), + wrap: _span( 'char cjk biaodian' ) + }); - findAndReplaceDOMText(this, { - find: eval( '/(' + unicode_set('punc') + '+)/ig' ), - wrap: _span( 'char latin punc' ) - }); + if ( glyph.cjk === 'group' ) + findAndReplaceDOMText(this, { + find: eval( '/(' + unicode_set('hanzi') + '+|' + unicode_set('biaodian') + '+)/ig' ), + wrap: _span( 'char cjk' ) + }); - findAndReplaceDOMText(this, { - find: /([\s]+)/ig, - wrap: _span( 'char space' ) - }); + var latin_regex = ( glyph.latin === 'group' ) ? + '/(' + unicode_set('latin') + '+)/ig' : + '/(' + unicode_set('latin') + ')/ig'; - if ( innerSpan ) - $(this).find('.char').each(function(){ - $(this).html( - $('').text( $(this).text() ) - ); - }); + findAndReplaceDOMText(this, { + find: eval( latin_regex ), + wrap: _span( 'char latin' ) + }); - if ( charClass ) - $(this).addClass('han-js-charized'); - }); - } - }); + 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 ); + // tests + for ( var feature in tests ) { + classes.push( ( tests[feature]() ? '' : 'no-' ) + feature ); - if ( !tester ) - var tester = ''; + if ( !tester ) + var tester = ''; - tester += ' ' + feature + ': tests[\'' + feature + '\'](),\n'; - } + tester += ' ' + feature + ': tests[\'' + feature + '\'](),\n'; + } - !function(window) { - eval("tester = ({\n" + tester.replace(/\n$/ig, '\nfont: test_for_fontface\n}') + ")"); - }(); + !function(window) { + eval("tester = ({\n" + tester.replace(/\n$/ig, + '\nunicoderange: test_for_unicoderange, \nfont: test_for_fontface\n}') + ")"); + }(); - han(); + han(); - window.han = { - unicode: unicode_set, - support: tester - } + window.han = { + unicode: unicode_set, + support: tester + } })(jQuery); @@ -725,501 +750,520 @@ jQuery.noConflict(); */ window.findAndReplaceDOMText = (function() { - var PORTION_MODE_RETAIN = 'retain'; - var PORTION_MODE_FIRST = 'first'; - - var doc = document; - var toString = {}.toString; + 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); + }); - function isArray(a) { - return toString.call(a) == '[object Array]'; - } + return lastNode; + } + } - 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); - } + return exposed; - 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; - -}()); \ No newline at end of file