Markdown 語法說明中文版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1269 lines
31 KiB

/*
* 漢字標準格式 v2.2.0
* ---
* Hanzi-optimised CSS Mode
*
*
*
* Lisence: MIT Lisence
* Last Modified: 2013/11/27
*
*/
jQuery.noConflict();
(function($){
var version = '2.2.0',
tests = [],
rubies,
unicode = [],
rendered = 'han-js-rendered',
classes = [rendered],
fontfaces = [],
han = function() {
$(document).on('ready', function(){
// `unicode-range`
classes.push( ( test_for_unicoderange() ? '' : 'no-' ) + 'unicoderange' );
// 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' );
for ( var font in fontfaces ) {
classes.push( ( fontfaces[font] ? '' : 'no-' ) + 'fontface-' + font );
}
// altogether
$('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 `<ruby>` element
*
* **注意:**需置於`<em>`的hack前。
*
* **Note:** The necessity of being
* placed before the hack of
* the `<em>` element is required.
*/
$(range).find('ruby.pinyin').addClass('romanization');
$(range).find('ruby.zhuyin').addClass('mps');
$(range).find('ruby').each(function() {
var html = $(this).html();
// 羅馬拼音(在不支援`<ruby>`的瀏覽器下)
if ( !$(this).hasClass('mps') && !tests['ruby']() ) {
var result = html
.replace(/<rt>/ig, '</span><span class="rt"><span class="rt inner">')
.replace(/<\/rt>/ig, '</span></span></span><span class="rr"><span class="rb">');
$(this).html('<span class="rr"><span class="rb">' + result + '</span>');
// 注音符號
} 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>(.*)<\\/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 = '<span class="zi" ' + data + '>' + character + '</span>';
return zi + '<span class="zy">' + mps + '</span>';
});
$(this).replaceWith(
$('<span class="han-js-zhuyin-rendered"></span>').addClass('zhuyin-' + zhuyin_font).html( html )
);
}
});
/*
* 漢拉間隙
* ---
* 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 = [
'/(' + hanzi + ')(' + latin + '|' + punc[1] + ')/ig',
'/(' + latin + '|' + punc[2] + ')(' + hanzi + ')/ig'
];
patterns.forEach(function( exp ){
findAndReplaceDOMText(this, {
find: eval(exp),
replace: '$1<hanla>$2'
});
}, this);
findAndReplaceDOMText(this, {
find: '<hanla>',
replace: function(){
return _span( 'hanla' );
}
});
this.normalize();
$('* > span.hanla:first-child').parent().each(function(){
if ( this.firstChild.nodeType == 1 ) {
$(this).before( $('<span class="hanla"></span>') );
$(this).find('span.hanla:first-child').remove();
}
});
});
/*
* 修正相鄰註記元素`<u>`的底線相連問題
* ---
* fixing the underline-adjacency issue on `<u>` element
*
*/
if ( $('html').hasClass('han-lab-underline') )
$(range).find('u').charize('', true, true);
else
$(range).find('u').each(function(){
var next = this.nextSibling;
_ignore(next);
_adj(next);
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);
}
}
});
/* 強調元素`<em>`的着重號
* ---
* punctuation: CJK emphasis dots on `<em>` element
*
*/
$(range).find('em').charize({
latin: ( tests['textemphasis']() ) ? 'none' : 'individual'
});
/* 修正引言元素`<q>`不為WebKit引擎支援的問題
* ---
* punctuation: Quote issue on `<q>` 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;
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 = ['<style id="han-support">', rule, '</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 = $('<div style="display: none; column-width: 200px; -webkit-column-width: 200px;">tester</div>'),
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 <ruby> hide the <rp> via "display:none"
rubies = ( getStyle(rp, displayStyleProperty) == 'none' ||
// but in IE browsers <rp> 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 = $('<span style="display: none; text-emphasis: dot; -moz-text-emphasis: dot; -ms-text-emphasis: dot; -webkit-text-emphasis: dot;">tester</span>'),
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 = $('<q style="display: none; quotes: \'“\' \'”\' \'‘\' \'’\'">tester</q>'),
bool = /^"“" "”" "‘" "’"$/.test( q.css("quotes") );
return bool;
};
tests['writingmode'] = function() {
var wm = $('<div style="display: none; writing-mode: tb-rl; -moz-writing-mode: tb-rl; -ms-writing-mode: tb-rl; -webkit-writing-mode: vertical-rl;">tester</div>'),
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(
$('<span>').text( $(this).text() )
);
});
if ( charClass )
$(this).addClass('han-js-charized');
});
}
});
// tests
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,
'\nunicoderange: test_for_unicoderange, \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;
}());