/*! * 漢字標準格式 v3.3.0 | MIT License | css.hanzi.co * Han.css: the CSS typography framework optimised for Hanzi */ void function( global, factory ) { // CommonJS if ( typeof module === 'object' && typeof module.exports === 'object' ) { module.exports = factory( global, true ) // AMD } else if ( typeof define === 'function' && define.amd ) { define(function() { return factory( global, true ) }) // Global namespace } else { factory( global ) } }( typeof window !== 'undefined' ? window : this, function( window, noGlobalNS ) { 'use strict' var document = window.document var root = document.documentElement var body = document.body var VERSION = '3.3.0' var ROUTINE = [ // Initialise the condition with feature-detecting // classes (Modernizr-alike), binding onto the root // element, possibly ``. 'initCond', // Address element normalisation 'renderElem', // Handle Biaodian /* 'jinzify', */ 'renderJiya', 'renderHanging', // Address Biaodian correction 'correctBiaodian', // Address Hanzi and Western script mixed spacing 'renderHWS', // Address presentational correction to combining ligatures 'substCombLigaWithPUA' // Address semantic correction to inaccurate characters // **Note:** inactivated by default /* 'substInaccurateChar', */ ] // Define Han var Han = function( context, condition ) { return new Han.fn.init( context, condition ) } var init = function() { if ( arguments[ 0 ] ) { this.context = arguments[ 0 ] } if ( arguments[ 1 ] ) { this.condition = arguments[ 1 ] } return this } Han.version = VERSION Han.fn = Han.prototype = { version: VERSION, constructor: Han, // Body as the default target context context: body, // Root element as the default condition condition: root, // Default rendering routine routine: ROUTINE, init: init, setRoutine: function( routine ) { if ( Array.isArray( routine )) { this.routine = routine } return this }, // Note that the routine set up here will execute // only once. The method won't alter the routine in // the instance or in the prototype chain. render: function( routine ) { var it = this var routine = Array.isArray( routine ) ? routine : this.routine routine .forEach(function( method ) { if ( typeof method === 'string' && typeof it[ method ] === 'function' ) { it[ method ]() } else if ( Array.isArray( method ) && typeof it[ method[0] ] === 'function' ) { it[ method.shift() ].apply( it, method ) } }) return this } } Han.fn.init.prototype = Han.fn /** * Shortcut for `render()` under the default * situation. * * Once initialised, replace `Han.init` with the * instance for future usage. */ Han.init = function() { return Han.init = Han().render() } var UNICODE = { /** * Western punctuation (西文標點符號) */ punct: { base: '[\u2026,.;:!?\u203D_]', sing: '[\u2010-\u2014\u2026]', middle: '[\\\/~\\-&\u2010-\u2014_]', open: '[\'"‘“\\(\\[\u00A1\u00BF\u2E18\u00AB\u2039\u201A\u201C\u201E]', close: '[\'"”’\\)\\]\u00BB\u203A\u201B\u201D\u201F]', end: '[\'"”’\\)\\]\u00BB\u203A\u201B\u201D\u201F\u203C\u203D\u2047-\u2049,.;:!?]', }, /** * CJK biaodian (CJK標點符號) */ biaodian: { base: '[︰.、,。:;?!ー]', liga: '[—…⋯]', middle: '[·\/-゠\uFF06\u30FB\uFF3F]', open: '[「『《〈(〔[{【〖]', close: '[」』》〉)〕]}】〗]', end: '[」』》〉)〕]}】〗︰.、,。:;?!ー]' }, /** * CJK-related blocks (CJK相關字符區段) * * 1. 中日韓統一意音文字:[\u4E00-\u9FFF] Basic CJK unified ideographs * 2. 擴展-A區:[\u3400-\u4DB5] Extended-A * 3. 擴展-B區:[\u20000-\u2A6D6]([\uD840-\uD869][\uDC00-\uDED6]) Extended-B * 4. 擴展-C區:[\u2A700-\u2B734](\uD86D[\uDC00-\uDF3F]|[\uD86A-\uD86C][\uDC00-\uDFFF]|\uD869[\uDF00-\uDFFF]) Extended-C * 5. 擴展-D區:[\u2B740-\u2B81D](急用漢字,\uD86D[\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1F]) Extended-D * 6. 擴展-E區:[\u2B820-\u2F7FF](暫未支援) Extended-E (not supported yet) * 7. 擴展-F區(暫未支援) Extended-F (not supported yet) * 8. 筆畫區:[\u31C0-\u31E3] Strokes * 9. 意音數字「〇」:[\u3007] Ideographic number zero * 10. 相容意音文字及補充:[\uF900-\uFAFF][\u2F800-\u2FA1D](不使用) Compatibility ideograph and supplement (not supported) 12 exceptions: [\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29] https://zh.wikipedia.org/wiki/中日韓統一表意文字#cite_note-1 * 11. 康熙字典及簡化字部首:[\u2F00-\u2FD5\u2E80-\u2EF3] Kangxi and supplement radicals * 12. 意音文字描述字元:[\u2FF0-\u2FFA] Ideographic description characters */ hanzi: { base: '[\u4E00-\u9FFF\u3400-\u4DB5\u31C0-\u31E3\u3007\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD800-\uDBFF][\uDC00-\uDFFF]', desc: '[\u2FF0-\u2FFA]', radical: '[\u2F00-\u2FD5\u2E80-\u2EF3]' }, /** * Latin script blocks (拉丁字母區段) * * 1. 基本拉丁字母:A-Za-z Basic Latin * 2. 阿拉伯數字:0-9 Digits * 3. 補充-1:[\u00C0-\u00FF] Latin-1 supplement * 4. 擴展-A區:[\u0100-\u017F] Extended-A * 5. 擴展-B區:[\u0180-\u024F] Extended-B * 5. 擴展-C區:[\u2C60-\u2C7F] Extended-C * 5. 擴展-D區:[\uA720-\uA7FF] Extended-D * 6. 附加區:[\u1E00-\u1EFF] Extended additional * 7. 變音組字符:[\u0300-\u0341\u1DC0-\u1DFF] Combining diacritical marks */ latin: { base: '[A-Za-z0-9\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u2C60-\u2C7F\uA720-\uA7FF\u1E00-\u1EFF]', combine: '[\u0300-\u0341\u1DC0-\u1DFF]' }, /** * Elli̱niká (Greek) script blocks (希臘字母區段) * * 1. 希臘字母及擴展:[\u0370–\u03FF\u1F00-\u1FFF] Basic Greek & Greek Extended * 2. 阿拉伯數字:0-9 Digits * 3. 希臘字母變音組字符:[\u0300-\u0345\u1DC0-\u1DFF] Combining diacritical marks */ ellinika: { base: '[0-9\u0370-\u03FF\u1F00-\u1FFF]', combine: '[\u0300-\u0345\u1DC0-\u1DFF]' }, /** * Kirillica (Cyrillic) script blocks (西里爾字母區段) * * 1. 西里爾字母及補充:[\u0400-\u0482\u048A-\u04FF\u0500-\u052F] Basic Cyrillic and supplement * 2. 擴展B區:[\uA640-\uA66E\uA67E-\uA697] Extended-B * 3. 阿拉伯數字:0-9 Digits * 4. 西里爾字母組字符:[\u0483-\u0489\u2DE0-\u2DFF\uA66F-\uA67D\uA69F](位擴展A、B區) Cyrillic combining diacritical marks (in extended-A, B) */ kirillica: { base: '[0-9\u0400-\u0482\u048A-\u04FF\u0500-\u052F\uA640-\uA66E\uA67E-\uA697]', combine: '[\u0483-\u0489\u2DE0-\u2DFF\uA66F-\uA67D\uA69F]' }, /** * Kana (假名) * * 1. 日文假名:[\u30A2\u30A4\u30A6\u30A8\u30AA-\u30FA\u3042\u3044\u3046\u3048\u304A-\u3094\u309F\u30FF] Japanese Kana * 2. 假名補充[\u1B000\u1B001](\uD82C[\uDC00-\uDC01]) Kana supplement * 3. 日文假名小寫:[\u3041\u3043\u3045\u3047\u3049\u30A1\u30A3\u30A5\u30A7\u30A9\u3063\u3083\u3085\u3087\u308E\u3095\u3096\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6\u31F0-\u31FF] Japanese small Kana * 4. 假名組字符:[\u3099-\u309C] Kana combining characters * 5. 半形假名:[\uFF66-\uFF9F] Halfwidth Kana * 6. 符號:[\u309D\u309E\u30FB-\u30FE] Marks */ kana: { base: '[\u30A2\u30A4\u30A6\u30A8\u30AA-\u30FA\u3042\u3044\u3046\u3048\u304A-\u3094\u309F\u30FF]|\uD82C[\uDC00-\uDC01]', small: '[\u3041\u3043\u3045\u3047\u3049\u30A1\u30A3\u30A5\u30A7\u30A9\u3063\u3083\u3085\u3087\u308E\u3095\u3096\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6\u31F0-\u31FF]', combine: '[\u3099-\u309C]', half: '[\uFF66-\uFF9F]', mark: '[\u30A0\u309D\u309E\u30FB-\u30FE]' }, /** * Eonmun (Hangul, 諺文) * * 1. 諺文音節:[\uAC00-\uD7A3] Eonmun (Hangul) syllables * 2. 諺文字母:[\u1100-\u11FF\u314F-\u3163\u3131-\u318E\uA960-\uA97C\uD7B0-\uD7FB] Eonmun (Hangul) letters * 3. 半形諺文字母:[\uFFA1-\uFFDC] Halfwidth Eonmun (Hangul) letters */ eonmun: { base: '[\uAC00-\uD7A3]', letter: '[\u1100-\u11FF\u314F-\u3163\u3131-\u318E\uA960-\uA97C\uD7B0-\uD7FB]', half: '[\uFFA1-\uFFDC]' }, /** * Zhuyin (注音符號, Mandarin & Dialect Phonetic Symbols) * * 1. 國語注音、方言音符號:[\u3105-\u312D][\u31A0-\u31BA] Bopomofo phonetic symbols * 2. 平上去聲調號:[\u02D9\u02CA\u02C5\u02C7\u02EA\u02EB\u02CB] (**註:**國語三聲包含乙個不合規範的符號) Level, rising, departing tones * 3. 入聲調號:[\u31B4-\u31B7][\u0358\u030d]? Checked (entering) tones */ zhuyin: { base: '[\u3105-\u312D\u31A0-\u31BA]', initial: '[\u3105-\u3119\u312A-\u312C\u31A0-\u31A3]', medial: '[\u3127-\u3129]', final: '[\u311A-\u3129\u312D\u31A4-\u31B3\u31B8-\u31BA]', tone: '[\u02D9\u02CA\u02C5\u02C7\u02CB\u02EA\u02EB]', checked: '[\u31B4-\u31B7][\u0358\u030d]?' } } var TYPESET = (function() { var rWhite = '[\\x20\\t\\r\\n\\f]' // Whitespace characters // http://www.w3.org/TR/css3-selectors/#whitespace var rPtOpen = UNICODE.punct.open var rPtClose = UNICODE.punct.close var rPtEnd = UNICODE.punct.end var rPtMid = UNICODE.punct.middle var rPtSing = UNICODE.punct.sing var rPt = rPtOpen + '|' + rPtEnd + '|' + rPtMid var rBDOpen = UNICODE.biaodian.open var rBDClose = UNICODE.biaodian.close var rBDEnd = UNICODE.biaodian.end var rBDMid = UNICODE.biaodian.middle var rBDLiga = UNICODE.biaodian.liga + '{2}' var rBD = rBDOpen + '|' + rBDEnd + '|' + rBDMid var rKana = UNICODE.kana.base + UNICODE.kana.combine + '?' var rKanaS = UNICODE.kana.small + UNICODE.kana.combine + '?' var rKanaH = UNICODE.kana.half var rEon = UNICODE.eonmun.base + '|' + UNICODE.eonmun.letter var rEonH = UNICODE.eonmun.half var rHan = UNICODE.hanzi.base + '|' + UNICODE.hanzi.desc + '|' + UNICODE.hanzi.radical + '|' + rKana var rCbn = UNICODE.ellinika.combine var rLatn = UNICODE.latin.base + rCbn + '*' var rGk = UNICODE.ellinika.base + rCbn + '*' var rCyCbn = UNICODE.kirillica.combine var rCy = UNICODE.kirillica.base + rCyCbn + '*' var rAlph = rLatn + '|' + rGk + '|' + rCy // For words like `it's`, `Jones’s` or `'99` var rApo = '[\u0027\u2019]' var rChar = rHan + '|(?:' + rAlph + '|' + rApo + ')+' var rZyS = UNICODE.zhuyin.initial var rZyJ = UNICODE.zhuyin.medial var rZyY = UNICODE.zhuyin.final var rZyD = UNICODE.zhuyin.tone + '|' + UNICODE.zhuyin.checked return { /* Character-level selector (字級選擇器) */ char: { punct: { all: new RegExp( '(' + rPt + ')', 'g' ), open: new RegExp( '(' + rPtOpen + ')', 'g' ), end: new RegExp( '(' + rPtEnd + ')', 'g' ), sing: new RegExp( '(' + rPtSing + ')', 'g' ) }, biaodian: { all: new RegExp( '(' + rBD + ')', 'g' ), open: new RegExp( '(' + rBDOpen + ')', 'g' ), close: new RegExp( '(' + rBDClose + ')', 'g' ), end: new RegExp( '(' + rBDEnd + ')', 'g' ), liga: new RegExp( '(' + rBDLiga + ')', 'g' ) }, hanzi: new RegExp( '(' + rHan + ')', 'g' ), latin: new RegExp( '(' + rLatn + ')', 'ig' ), ellinika: new RegExp( '(' + rGk + ')', 'ig' ), kirillica: new RegExp( '(' + rCy + ')', 'ig' ), kana: new RegExp( '(' + rKana + '|' + rKanaS + '|' + rKanaH + ')', 'g' ), eonmun: new RegExp( '(' + rEon + '|' + rEonH + ')', 'g' ) }, /* Word-level selectors (詞級選擇器) */ group: { biaodian: [ new RegExp( '((' + rBD + '){2,})', 'g' ), new RegExp( '(' + rBDLiga + rBDOpen + ')', 'g' ) ], punct: null, hanzi: new RegExp( '(' + rHan + ')+', 'g' ), western: new RegExp( '(' + rLatn + '|' + rGk + '|' + rCy + '|' + rPt + ')+', 'ig' ), kana: new RegExp( '(' + rKana + '|' + rKanaS + '|' + rKanaH + ')+', 'g' ), eonmun: new RegExp( '(' + rEon + '|' + rEonH + '|' + rPt + ')+', 'g' ) }, /* Punctuation Rules (禁則) */ jinze: { hanging: new RegExp( rWhite + '*([、,。.])(?!' + rBDEnd + ')', 'ig' ), touwei: new RegExp( '(' + rBDOpen + '+)(' + rChar + ')(' + rBDEnd + '+)', 'ig' ), tou: new RegExp( '(' + rBDOpen + '+)(' + rChar + ')', 'ig' ), wei: new RegExp( '(' + rChar + ')(' + rBDEnd + '+)', 'ig' ), middle: new RegExp( '(' + rChar + ')(' + rBDMid + ')(' + rChar + ')', 'ig' ) }, zhuyin: { form: new RegExp( '^\u02D9?(' + rZyS + ')?(' + rZyJ + ')?(' + rZyY + ')?(' + rZyD + ')?$' ), diao: new RegExp( '(' + rZyD + ')', 'g' ) }, /* Hanzi and Western mixed spacing (漢字西文混排間隙) * - Basic mode * - Strict mode */ hws: { base: [ new RegExp( '('+ rHan + ')(' + rAlph + '|' + rPtOpen + ')', 'ig' ), new RegExp( '('+ rAlph + '|' + rPtEnd + ')(' + rHan + ')', 'ig' ) ], strict: [ new RegExp( '('+ rHan + ')' + rWhite + '?(' + rAlph + '|' + rPtOpen + ')', 'ig' ), new RegExp( '('+ rAlph + '|' + rPtEnd + ')' + rWhite + '?(' + rHan + ')', 'ig' ) ] }, // The feature displays the following characters // in its variant form for font consistency and // presentational reason. Meanwhile, this won't // alter the original character in the DOM. 'display-as': { 'ja-font-for-hant': [ // '夠 够', '查 査', '啟 啓', '鄉 鄕', '值 値', '污 汚' ], 'comb-liga-pua': [ [ '\u0061[\u030d\u0358]', '\uDB80\uDC61' ], [ '\u0065[\u030d\u0358]', '\uDB80\uDC65' ], [ '\u0069[\u030d\u0358]', '\uDB80\uDC69' ], [ '\u006F[\u030d\u0358]', '\uDB80\uDC6F' ], [ '\u0075[\u030d\u0358]', '\uDB80\uDC75' ], [ '\u31B4[\u030d\u0358]', '\uDB8C\uDDB4' ], [ '\u31B5[\u030d\u0358]', '\uDB8C\uDDB5' ], [ '\u31B6[\u030d\u0358]', '\uDB8C\uDDB6' ], [ '\u31B7[\u030d\u0358]', '\uDB8C\uDDB7' ] ], 'comb-liga-vowel': [ [ '\u0061[\u030d\u0358]', '\uDB80\uDC61' ], [ '\u0065[\u030d\u0358]', '\uDB80\uDC65' ], [ '\u0069[\u030d\u0358]', '\uDB80\uDC69' ], [ '\u006F[\u030d\u0358]', '\uDB80\uDC6F' ], [ '\u0075[\u030d\u0358]', '\uDB80\uDC75' ] ], 'comb-liga-zhuyin': [ [ '\u31B4[\u030d\u0358]', '\uDB8C\uDDB4' ], [ '\u31B5[\u030d\u0358]', '\uDB8C\uDDB5' ], [ '\u31B6[\u030d\u0358]', '\uDB8C\uDDB6' ], [ '\u31B7[\u030d\u0358]', '\uDB8C\uDDB7' ] ] }, // The feature actually *converts* the character // in the DOM for semantic reason. // // Note that this could be aggressive. 'inaccurate-char': [ [ '[\u2022\u2027]', '\u00B7' ], [ '\u22EF\u22EF', '\u2026\u2026' ], [ '\u2500\u2500', '\u2014\u2014' ], [ '\u2035', '\u2018' ], [ '\u2032', '\u2019' ], [ '\u2036', '\u201C' ], [ '\u2033', '\u201D' ] ] } })() Han.UNICODE = UNICODE Han.TYPESET = TYPESET // Aliases Han.UNICODE.cjk = Han.UNICODE.hanzi Han.UNICODE.greek = Han.UNICODE.ellinika Han.UNICODE.cyrillic = Han.UNICODE.kirillica Han.UNICODE.hangul = Han.UNICODE.eonmun Han.UNICODE.zhuyin.ruyun = Han.UNICODE.zhuyin.checked Han.TYPESET.char.cjk = Han.TYPESET.char.hanzi Han.TYPESET.char.greek = Han.TYPESET.char.ellinika Han.TYPESET.char.cyrillic = Han.TYPESET.char.kirillica Han.TYPESET.char.hangul = Han.TYPESET.char.eonmun Han.TYPESET.group.hangul = Han.TYPESET.group.eonmun Han.TYPESET.group.cjk = Han.TYPESET.group.hanzi var $ = { /** * Query selectors which return arrays of the resulted * node lists. */ id: function( selector, $context ) { return ( $context || document ).getElementById( selector ) }, tag: function( selector, $context ) { return this.makeArray( ( $context || document ).getElementsByTagName( selector ) ) }, qs: function( selector, $context ) { return ( $context || document ).querySelector( selector ) }, qsa: function( selector, $context ) { return this.makeArray( ( $context || document ).querySelectorAll( selector ) ) }, parent: function( $node, selector ) { return selector ? (function() { if ( typeof $.matches !== 'function' ) return while (!$.matches( $node, selector )) { if ( !$node || $node === document.documentElement ) { $node = undefined break } $node = $node.parentNode } return $node })() : $node ? $node.parentNode : undefined }, /** * Create a document fragment, a text node with text * or an element with/without classes. */ create: function( name, clazz ) { var $elmt = '!' === name ? document.createDocumentFragment() : '' === name ? document.createTextNode( clazz || '' ) : document.createElement( name ) try { if ( clazz ) { $elmt.className = clazz } } catch (e) {} return $elmt }, /** * Clone a DOM node (text, element or fragment) deeply * or childlessly. */ clone: function( $node, deep ) { return $node.cloneNode( typeof deep === 'boolean' ? deep : true ) }, /** * Remove a node (text, element or fragment). */ remove: function( $node ) { return $node.parentNode.removeChild( $node ) }, /** * Set attributes all in once with an object. */ setAttr: function( target, attr ) { if ( typeof attr !== 'object' ) return var len = attr.length // Native `NamedNodeMap``: if ( typeof attr[0] === 'object' && 'name' in attr[0] ) { for ( var i = 0; i < len; i++ ) { if ( attr[ i ].value !== undefined ) { target.setAttribute( attr[ i ].name, attr[ i ].value ) } } // Plain object: } else { for ( var name in attr ) { if ( attr.hasOwnProperty( name ) && attr[ name ] !== undefined ) { target.setAttribute( name, attr[ name ] ) } } } return target }, /** * Indicate whether or not the given node is an * element. */ isElmt: function( $node ) { return $node && $node.nodeType === Node.ELEMENT_NODE }, /** * Indicate whether or not the given node should * be ignored (`` or comments). */ isIgnorable: function( $node ) { if ( !$node ) return false return ( $node.nodeName === 'WBR' || $node.nodeType === Node.COMMENT_NODE ) }, /** * Convert array-like objects into real arrays. */ makeArray: function( object ) { return Array.prototype.slice.call( object ) }, /** * Extend target with an object. */ extend: function( target, object ) { if (( typeof target === 'object' || typeof target === 'function' ) && typeof object === 'object' ) { for ( var name in object ) { if (object.hasOwnProperty( name )) { target[ name ] = object[ name ] } } } return target } } var Fibre = /*! * Fibre.js v0.2.1 | MIT License | github.com/ethantw/fibre.js * Based on findAndReplaceDOMText */ function( Finder ) { 'use strict' var VERSION = '0.2.1' var NON_INLINE_PROSE = Finder.NON_INLINE_PROSE var AVOID_NON_PROSE = Finder.PRESETS.prose.filterElements var global = window || {} var document = global.document || undefined function matches( node, selector, bypassNodeType39 ) { var Efn = Element.prototype var matches = Efn.matches || Efn.mozMatchesSelector || Efn.msMatchesSelector || Efn.webkitMatchesSelector if ( node instanceof Element ) { return matches.call( node, selector ) } else if ( bypassNodeType39 ) { if ( /^[39]$/.test( node.nodeType )) return true } return false } if ( typeof document === 'undefined' ) throw new Error( 'Fibre requires a DOM-supported environment.' ) var Fibre = function( context, preset ) { return new Fibre.fn.init( context, preset ) } Fibre.version = VERSION Fibre.matches = matches Fibre.fn = Fibre.prototype = { constructor: Fibre, version: VERSION, finder: [], context: undefined, portionMode: 'retain', selector: {}, preset: 'prose', init: function( context, noPreset ) { if ( !!noPreset ) this.preset = null this.selector = { context: null, filter: [], avoid: [], boundary: [] } if ( !context ) { throw new Error( 'A context is required for Fibre to initialise.' ) } else if ( context instanceof Node ) { if ( context instanceof Document ) this.context = context.body || context else this.context = context } else if ( typeof context === 'string' ) { this.context = document.querySelector( context ) this.selector.context = context } return this }, filterFn: function( node ) { var filter = this.selector.filter.join( ', ' ) || '*' var avoid = this.selector.avoid.join( ', ' ) || null var result = matches( node, filter, true ) && !matches( node, avoid ) return ( this.preset === 'prose' ) ? AVOID_NON_PROSE( node ) && result : result }, boundaryFn: function( node ) { var boundary = this.selector.boundary.join( ', ' ) || null var result = matches( node, boundary ) return ( this.preset === 'prose' ) ? NON_INLINE_PROSE( node ) || result : result }, filter: function( selector ) { if ( typeof selector === 'string' ) { this.selector.filter.push( selector ) } return this }, endFilter: function( all ) { if ( all ) { this.selector.filter = [] } else { this.selector.filter.pop() } return this }, avoid: function( selector ) { if ( typeof selector === 'string' ) { this.selector.avoid.push( selector ) } return this }, endAvoid: function( all ) { if ( all ) { this.selector.avoid = [] } else { this.selector.avoid.pop() } return this }, addBoundary: function( selector ) { if ( typeof selector === 'string' ) { this.selector.boundary.push( selector ) } return this }, removeBoundary: function() { this.selector.boundary = [] return this }, setMode: function( portionMode ) { this.portionMode = portionMode === 'first' ? 'first' : 'retain' return this }, replace: function( regexp, newSubStr ) { var it = this it.finder.push(Finder( it.context, { find: regexp, replace: newSubStr, filterElements: function( currentNode ) { return it.filterFn( currentNode ) }, forceContext: function( currentNode ) { return it.boundaryFn( currentNode ) }, portionMode: it.portionMode })) return it }, wrap: function( regexp, strElemName ) { var it = this it.finder.push(Finder( it.context, { find: regexp, wrap: strElemName, filterElements: function( currentNode ) { return it.filterFn( currentNode ) }, forceContext: function( currentNode ) { return it.boundaryFn( currentNode ) }, portionMode: it.portionMode })) return it }, revert: function( level ) { var max = this.finder.length var level = Number( level ) || ( level === 0 ? Number(0) : ( level === 'all' ? max : 1 )) if ( typeof max === 'undefined' || max === 0 ) return this else if ( level > max ) level = max for ( var i = level; i > 0; i-- ) { this.finder.pop().revert() } return this } } // Deprecated API(s) Fibre.fn.filterOut = Fibre.fn.avoid // Make sure init() inherit from Fibre() Fibre.fn.init.prototype = Fibre.fn return Fibre }( /** * findAndReplaceDOMText v 0.4.3 * @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. */ (function() { var PORTION_MODE_RETAIN = 'retain' var PORTION_MODE_FIRST = 'first' var doc = document var toString = {}.toString var hasOwn = {}.hasOwnProperty 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.NON_PROSE_ELEMENTS = { br:1, hr:1, // Media / Source elements: script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1, // Input elements input:1, textarea:1, select:1, option:1, optgroup: 1, button:1 } exposed.NON_CONTIGUOUS_PROSE_ELEMENTS = { // Elements that will not contain prose or block elements where we don't // want prose to be matches across element borders: // Block Elements address:1, article:1, aside:1, blockquote:1, dd:1, div:1, dl:1, fieldset:1, figcaption:1, figure:1, footer:1, form:1, h1:1, h2:1, h3:1, h4:1, h5:1, h6:1, header:1, hgroup:1, hr:1, main:1, nav:1, noscript:1, ol:1, output:1, p:1, pre:1, section:1, ul:1, // Other misc. elements that are not part of continuous inline prose: br:1, li: 1, summary: 1, dt:1, details:1, rp:1, rt:1, rtc:1, // Media / Source elements: script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1, // Input elements input:1, textarea:1, select:1, option:1, optgroup: 1, button:1, // Table related elements: table:1, tbody:1, thead:1, th:1, tr:1, td:1, caption:1, col:1, tfoot:1, colgroup:1 } exposed.NON_INLINE_PROSE = function(el) { return hasOwn.call(exposed.NON_CONTIGUOUS_PROSE_ELEMENTS, el.nodeName.toLowerCase()) } // Presets accessed via `options.preset` when calling findAndReplaceDOMText(): exposed.PRESETS = { prose: { forceContext: exposed.NON_INLINE_PROSE, filterElements: function(el) { return !hasOwn.call(exposed.NON_PROSE_ELEMENTS, el.nodeName.toLowerCase()) } } } exposed.Finder = Finder /** * Finder -- encapsulates logic to find and replace. */ function Finder(node, options) { var preset = options.preset && exposed.PRESETS[options.preset] options.portionMode = options.portionMode || PORTION_MODE_RETAIN if (preset) { for (var i in preset) { if (hasOwn.call(preset, i) && !hasOwn.call(options, i)) { options[i] = preset[i] } } } 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 offset = 0 var regex = this.options.find var textAggregation = this.getAggregateText() var matches = [] var self = this regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex matchAggregation(textAggregation) function matchAggregation(textAggregation) { for (var i = 0, l = textAggregation.length; i < l; ++i) { var text = textAggregation[i] if (typeof text !== 'string') { // Deal with nested contexts: (recursive) matchAggregation(text) continue } if (regex.global) { while (match = regex.exec(text)) { matches.push(self.prepMatch(match, matchIndex++, offset)) } } else { if (match = text.match(regex)) { matches.push(self.prepMatch(match, 0, offset)) } } offset += text.length } } return matches }, /** * Prepares a single match with useful meta info: */ prepMatch: function(match, matchIndex, characterOffset) { if (!match[0]) { throw new Error('findAndReplaceDOMText cannot handle zero-length matches') } match.endIndex = characterOffset + match.index + match[0].length match.startIndex = characterOffset + match.index match.index = matchIndex return match }, /** * Gets aggregate text within subject node */ getAggregateText: function() { var elementFilter = this.options.filterElements var forceContext = this.options.forceContext return getText(this.node) /** * Gets aggregate text of a node without resorting * to broken innerText/textContent */ function getText(node, txt) { if (node.nodeType === 3) { return [node.data] } if (elementFilter && !elementFilter(node)) { return [] } var txt = [''] var i = 0 if (node = node.firstChild) do { if (node.nodeType === 3) { txt[i] += node.data continue } var innerText = getText(node) if ( forceContext && node.nodeType === 1 && (forceContext === true || forceContext(node)) ) { txt[++i] = innerText txt[++i] = '' } else { if (typeof innerText[0] === 'string') { // Bridge nested text-node data so that they're // not considered their own contexts: // I.e. ['some', ['thing']] -> ['something'] txt[i] += innerText.shift() } if (innerText.length) { txt[++i] = innerText txt[++i] = '' } } } 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, nodeStack = [node] 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: if (curNode.firstChild) { nodeStack.push(curNode) curNode = curNode.firstChild } else { curNode = curNode.nextSibling } continue } // Move forward or up: while (true) { if (curNode.nextSibling) { curNode = curNode.nextSibling break } curNode = nodeStack.pop() if (curNode === node) { 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 (!replacement.data) { return replacement } 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 }()) ); var isNodeNormalizeNormal = (function() { //// Disabled `Node.normalize()` for temp due to //// issue below in IE11. //// See: http://stackoverflow.com/questions/22337498/why-does-ie11-handle-node-normalize-incorrectly-for-the-minus-symbol var div = $.create( 'div' ) div.appendChild($.create( '', '0-' )) div.appendChild($.create( '', '2' )) div.normalize() return div.firstChild.length !== 2 })() function getFuncOrElmt( obj ) { return ( typeof obj === 'function' || obj instanceof Element ) ? obj : undefined } function createBDGroup( portion ) { var clazz = portion.index === 0 && portion.isEnd ? 'biaodian cjk' : 'biaodian cjk portion ' + ( portion.index === 0 ? 'is-first' : portion.isEnd ? 'is-end' : 'is-inner' ) var $elmt = $.create( 'h-char-group', clazz ) $elmt.innerHTML = portion.text return $elmt } function createBDChar( char ) { var div = $.create( 'div' ) var unicode = char.charCodeAt( 0 ).toString( 16 ) div.innerHTML = ( '' + char + '' ) return div.firstChild } function getBDType( char ) { return char.match( TYPESET.char.biaodian.open ) ? 'bd-open' : char.match( TYPESET.char.biaodian.close ) ? 'bd-close bd-end' : char.match( TYPESET.char.biaodian.end ) ? ( /(?:\u3001|\u3002|\uff0c)/i.test( char ) ? 'bd-end bd-cop' : 'bd-end' ) : char.match(new RegExp( UNICODE.biaodian.liga )) ? 'bd-liga' : char.match(new RegExp( UNICODE.biaodian.middle )) ? 'bd-middle' : '' } $.extend( Fibre.fn, { normalize: function() { if ( isNodeNormalizeNormal ) { this.context.normalize() } return this }, // Force punctuation & biaodian typesetting rules to be applied. jinzify: function( selector ) { return ( this .filter( selector || null ) .avoid( 'h-jinze' ) .replace( TYPESET.jinze.touwei, function( portion, match ) { var elem = $.create( 'h-jinze', 'touwei' ) elem.innerHTML = match[0] return (( portion.index === 0 && portion.isEnd ) || portion.index === 1 ) ? elem : '' } ) .replace( TYPESET.jinze.wei, function( portion, match ) { var elem = $.create( 'h-jinze', 'wei' ) elem.innerHTML = match[0] return portion.index === 0 ? elem : '' } ) .replace( TYPESET.jinze.tou, function( portion, match ) { var elem = $.create( 'h-jinze', 'tou' ) elem.innerHTML = match[0] return (( portion.index === 0 && portion.isEnd ) || portion.index === 1 ) ? elem : '' } ) .replace( TYPESET.jinze.middle, function( portion, match ) { var elem = $.create( 'h-jinze', 'middle' ) elem.innerHTML = match[0] return (( portion.index === 0 && portion.isEnd ) || portion.index === 1 ) ? elem : '' } ) .endAvoid() .endFilter() ) }, groupify: function( option ) { var option = $.extend({ biaodian: false, //punct: false, hanzi: false, // Includes Kana kana: false, eonmun: false, western: false // Includes Latin, Greek and Cyrillic }, option || {}) this.avoid( 'h-word, h-char-group' ) if ( option.biaodian ) { this.replace( TYPESET.group.biaodian[0], createBDGroup ).replace( TYPESET.group.biaodian[1], createBDGroup ) } if ( option.hanzi || option.cjk ) { this.wrap( TYPESET.group.hanzi, $.clone($.create( 'h-char-group', 'hanzi cjk' )) ) } if ( option.western ) { this.wrap( TYPESET.group.western, $.clone($.create( 'h-word', 'western' )) ) } if ( option.kana ) { this.wrap( TYPESET.group.kana, $.clone($.create( 'h-char-group', 'kana' )) ) } if ( option.eonmun || option.hangul ) { this.wrap( TYPESET.group.eonmun, $.clone($.create( 'h-word', 'eonmun hangul' )) ) } this.endAvoid() return this }, charify: function( option ) { var option = $.extend({ avoid: true, biaodian: false, punct: false, hanzi: false, // Includes Kana latin: false, ellinika: false, kirillica: false, kana: false, eonmun: false }, option || {}) if ( option.avoid ) { this.avoid( 'h-char' ) } if ( option.biaodian ) { this.replace( TYPESET.char.biaodian.all, getFuncOrElmt( option.biaodian ) || function( portion ) { return createBDChar( portion.text ) } ).replace( TYPESET.char.biaodian.liga, getFuncOrElmt( option.biaodian ) || function( portion ) { return createBDChar( portion.text ) } ) } if ( option.hanzi || option.cjk ) { this.wrap( TYPESET.char.hanzi, getFuncOrElmt( option.hanzi || option.cjk ) || $.clone($.create( 'h-char', 'hanzi cjk' )) ) } if ( option.punct ) { this.wrap( TYPESET.char.punct.all, getFuncOrElmt( option.punct ) || $.clone($.create( 'h-char', 'punct' )) ) } if ( option.latin ) { this.wrap( TYPESET.char.latin, getFuncOrElmt( option.latin ) || $.clone($.create( 'h-char', 'alphabet latin' )) ) } if ( option.ellinika || option.greek ) { this.wrap( TYPESET.char.ellinika, getFuncOrElmt( option.ellinika || option.greek ) || $.clone($.create( 'h-char', 'alphabet ellinika greek' )) ) } if ( option.kirillica || option.cyrillic ) { this.wrap( TYPESET.char.kirillica, getFuncOrElmt( option.kirillica || option.cyrillic ) || $.clone($.create( 'h-char', 'alphabet kirillica cyrillic' )) ) } if ( option.kana ) { this.wrap( TYPESET.char.kana, getFuncOrElmt( option.kana ) || $.clone($.create( 'h-char', 'kana' )) ) } if ( option.eonmun || option.hangul ) { this.wrap( TYPESET.char.eonmun, getFuncOrElmt( option.eonmun || option.hangul ) || $.clone($.create( 'h-char', 'eonmun hangul' )) ) } this.endAvoid() return this } }) $.extend( Han, { isNodeNormalizeNormal: isNodeNormalizeNormal, find: Fibre, createBDGroup: createBDGroup, createBDChar: createBDChar }) $.matches = Han.find.matches void [ 'setMode', 'wrap', 'replace', 'revert', 'addBoundary', 'removeBoundary', 'avoid', 'endAvoid', 'filter', 'endFilter', 'jinzify', 'groupify', 'charify' ].forEach(function( method ) { Han.fn[ method ] = function() { if ( !this.finder ) { // Share the same selector this.finder = Han.find( this.context ) } this.finder[ method ]( arguments[ 0 ], arguments[ 1 ] ) return this } }) var Locale = {} function writeOnCanvas( text, font ) { var canvas = $.create( 'canvas' ) var context canvas.width = '50' canvas.height = '20' canvas.style.display = 'none' body.appendChild( canvas ) context = canvas.getContext( '2d' ) context.textBaseline = 'top' context.font = '15px ' + font + ', sans-serif' context.fillStyle = 'black' context.strokeStyle = 'black' context.fillText( text, 0, 0 ) return { node: canvas, context: context, remove: function() { $.remove( canvas, body ) } } } function compareCanvases( treat, control ) { var ret var a = treat.context var b = control.context try { for ( var j = 1; j <= 20; j++ ) { for ( var i = 1; i <= 50; i++ ) { if ( typeof ret === 'undefined' && a.getImageData(i, j, 1, 1).data[3] !== b.getImageData(i, j, 1, 1).data[3] ) { ret = false break } else if ( typeof ret === 'boolean' ) { break } if ( i === 50 && j === 20 && typeof ret === 'undefined' ) { ret = true } } } // Remove and clean from memory treat.remove() control.remove() treat = null control = null return ret } catch (e) {} return false } function detectFont( treat, control, text ) { var treat = treat var control = control || 'sans-serif' var text = text || '辭Q' var ret control = writeOnCanvas( text, control ) treat = writeOnCanvas( text, treat ) return !compareCanvases( treat, control ) } Locale.writeOnCanvas = writeOnCanvas Locale.compareCanvases = compareCanvases Locale.detectFont = detectFont Locale.support = (function() { var PREFIX = 'Webkit Moz ms'.split(' ') // Create an element for feature detecting // (in `testCSSProp`) var elem = $.create( 'h-test' ) function testCSSProp( prop ) { var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1) var allProp = ( prop + ' ' + PREFIX.join( ucProp + ' ' ) + ucProp ).split(' ') var ret allProp.forEach(function( prop ) { if ( typeof elem.style[ prop ] === 'string' ) { ret = true } }) return ret || false } function injectElementWithStyle( rule, callback ) { var fakeBody = body || $.create( 'body' ) var div = $.create( 'div' ) var container = body ? div : fakeBody var callback = typeof callback === 'function' ? callback : function() {} var style, ret, docOverflow style = [ '' ].join('') container.innerHTML += style fakeBody.appendChild( div ) if ( !body ) { fakeBody.style.background = '' fakeBody.style.overflow = 'hidden' docOverflow = root.style.overflow root.style.overflow = 'hidden' root.appendChild( fakeBody ) } // Callback ret = callback( container, rule ) // Remove the injected scope $.remove( container ) if ( !body ) { root.style.overflow = docOverflow } return !!ret } function getStyle( elem, prop ) { var ret if ( window.getComputedStyle ) { ret = document.defaultView.getComputedStyle( elem, null ).getPropertyValue( prop ) } else if ( elem.currentStyle ) { // for IE ret = elem.currentStyle[ prop ] } return ret } return { columnwidth: testCSSProp( 'columnWidth' ), fontface: (function() { var ret injectElementWithStyle( '@font-face { font-family: font; src: url("//"); }', function( node, rule ) { var style = $.qsa( 'style', node )[0] var sheet = style.sheet || style.styleSheet var cssText = sheet ? ( sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '' ) : '' ret = /src/i.test( cssText ) && cssText.indexOf( rule.split(' ')[0] ) === 0 } ) return ret })(), ruby: (function() { var ruby = $.create( 'ruby' ) var rt = $.create( 'rt' ) var rp = $.create( 'rp' ) var ret ruby.appendChild( rp ) ruby.appendChild( rt ) root.appendChild( ruby ) // Browsers that support ruby hide the `` via `display: none` ret = ( getStyle( rp, 'display' ) === 'none' || // but in IE, `` has `display: inline`, so the test needs other conditions: getStyle( ruby, 'display' ) === 'ruby' && getStyle( rt, 'display' ) === 'ruby-text' ) ? true : false // Remove and clean from memory root.removeChild( ruby ) ruby = null rt = null rp = null return ret })(), 'ruby-display': (function() { var div = $.create( 'div' ) div.innerHTML = '' return div.querySelector( 'h-test-a' ).style.display === 'ruby' && div.querySelector( 'h-test-b' ).style.display === 'ruby-text-container' })(), 'ruby-interchar': (function() { var IC = 'inter-character' var div = $.create( 'div' ) var css div.innerHTML = '' css = div.querySelector( 'h-test' ).style return css.rubyPosition === IC || css.WebkitRubyPosition === IC || css.MozRubyPosition === IC || css.msRubyPosition === IC })(), textemphasis: testCSSProp( 'textEmphasis' ), // Address feature support test for `unicode-range` via // detecting whether it's Arial (supported) or // Times New Roman (not supported). unicoderange: (function() { var ret injectElementWithStyle( '@font-face{font-family:test-for-unicode-range;src:local(Arial),local("Droid Sans")}@font-face{font-family:test-for-unicode-range;src:local("Times New Roman"),local(Times),local("Droid Serif");unicode-range:U+270C}', function() { ret = !Locale.detectFont( 'test-for-unicode-range', // treatment group 'Arial, "Droid Sans"', // control group 'Q' // ASCII characters only ) } ) return ret })(), writingmode: testCSSProp( 'writingMode' ) } })() Locale.initCond = function( target ) { var target = target || root var ret = '' var clazz for ( var feature in Locale.support ) { clazz = ( Locale.support[ feature ] ? '' : 'no-' ) + feature target.classList.add( clazz ) ret += clazz + ' ' } return ret } var SUPPORT_IC = Locale.support[ 'ruby-interchar' ] // 1. Simple ruby polyfill; // 2. Inter-character polyfill for Zhuyin function renderSimpleRuby( $ruby ) { var frag = $.create( '!' ) var clazz = $ruby.classList var $rb, $ru frag.appendChild( $.clone( $ruby )) $ .tag( 'rt', frag.firstChild ) .forEach(function( $rt ) { var $rb = $.create( '!' ) var airb = [] var irb // Consider the previous nodes the implied // ruby base do { irb = ( irb || $rt ).previousSibling if ( !irb || irb.nodeName.match( /((?:h\-)?r[ubt])/i )) break $rb.insertBefore( $.clone( irb ), $rb.firstChild ) airb.push( irb ) } while ( !irb.nodeName.match( /((?:h\-)?r[ubt])/i )) // Create a real `` to append. $ru = clazz.contains( 'zhuyin' ) ? createZhuyinRu( $rb, $rt ) : createNormalRu( $rb, $rt ) // Replace the ruby text with the new ``, // and remove the original implied ruby base(s) try { $rt.parentNode.replaceChild( $ru, $rt ) airb.map( $.remove ) } catch ( e ) {} }) return createCustomRuby( frag ) } function renderInterCharRuby( $ruby ) { var frag = $.create( '!' ) frag.appendChild( $.clone( $ruby )) $ .tag( 'rt', frag.firstChild ) .forEach(function( $rt ) { var $rb = $.create( '!' ) var airb = [] var irb, $zhuyin // Consider the previous nodes the implied // ruby base do { irb = ( irb || $rt ).previousSibling if ( !irb || irb.nodeName.match( /((?:h\-)?r[ubt])/i )) break $rb.insertBefore( $.clone( irb ), $rb.firstChild ) airb.push( irb ) } while ( !irb.nodeName.match( /((?:h\-)?r[ubt])/i )) $zhuyin = $.create( 'rt' ) $zhuyin.innerHTML = getZhuyinHTML( $rt ) $rt.parentNode.replaceChild( $zhuyin, $rt ) }) return frag.firstChild } // 3. Complex ruby polyfill // - Double-lined annotation; // - Right-angled annotation. function renderComplexRuby( $ruby ) { var frag = $.create( '!' ) var clazz = $ruby.classList var $cloned, $rb, $ru, maxspan frag.appendChild( $.clone( $ruby )) $cloned = frag.firstChild $rb = $ru = $.tag( 'rb', $cloned ) maxspan = $rb.length // First of all, deal with Zhuyin containers // individually // // Note that we only support one single Zhuyin // container in each complex ruby void function( $rtc ) { if ( !$rtc ) return $ru = $ .tag( 'rt', $rtc ) .map(function( $rt, i ) { if ( !$rb[ i ] ) return var ret = createZhuyinRu( $rb[ i ], $rt ) try { $rb[ i ].parentNode.replaceChild( ret, $rb[ i ] ) } catch ( e ) {} return ret }) // Remove the container once it's useless $.remove( $rtc ) $cloned.setAttribute( 'rightangle', 'true' ) }( $cloned.querySelector( 'rtc.zhuyin' )) // Then, normal annotations other than Zhuyin $ .qsa( 'rtc:not(.zhuyin)', $cloned ) .forEach(function( $rtc, order ) { var ret ret = $ .tag( 'rt', $rtc ) .map(function( $rt, i ) { var rbspan = Number( $rt.getAttribute( 'rbspan' ) || 1 ) var span = 0 var aRb = [] var $rb, ret if ( rbspan > maxspan ) rbspan = maxspan do { try { $rb = $ru.shift() aRb.push( $rb ) } catch (e) {} if ( typeof $rb === 'undefined' ) break span += Number( $rb.getAttribute( 'span' ) || 1 ) } while ( rbspan > span ) if ( rbspan < span ) { if ( aRb.length > 1 ) { console.error( 'An impossible `rbspan` value detected.', ruby ) return } aRb = $.tag( 'rb', aRb[0] ) $ru = aRb.slice( rbspan ).concat( $ru ) aRb = aRb.slice( 0, rbspan ) span = rbspan } ret = createNormalRu( aRb, $rt, { 'class': clazz, span: span, order: order }) try { aRb[0].parentNode.replaceChild( ret, aRb.shift() ) aRb.map( $.remove ) } catch (e) {} return ret }) $ru = ret if ( order === 1 ) $cloned.setAttribute( 'doubleline', 'true' ) // Remove the container once it's useless $.remove( $rtc ) }) return createCustomRuby( frag ) } // Create a new fake `` element so the // style sheets will render it as a polyfill, // which also helps to avoid the UA style. function createCustomRuby( frag ) { var $ruby = frag.firstChild var hruby = $.create( 'h-ruby' ) hruby.innerHTML = $ruby.innerHTML $.setAttr( hruby, $ruby.attributes ) hruby.normalize() return hruby } function simplifyRubyClass( elem ) { if ( !elem instanceof Element ) return elem var clazz = elem.classList if ( clazz.contains( 'pinyin' )) clazz.add( 'romanization' ) else if ( clazz.contains( 'romanization' )) clazz.add( 'annotation' ) else if ( clazz.contains( 'mps' )) clazz.add( 'zhuyin' ) else if ( clazz.contains( 'rightangle' )) clazz.add( 'complex' ) return elem } /** * Create and return a new `` element * according to the given contents */ function createNormalRu( $rb, $rt, attr ) { var $ru = $.create( 'h-ru' ) var $rt = $.clone( $rt ) var attr = attr || {} attr.annotation = 'true' if ( Array.isArray( $rb )) { $ru.innerHTML = $rb.map(function( rb ) { if ( typeof rb === 'undefined' ) return '' return rb.outerHTML }).join('') + $rt.outerHTML } else { $ru.appendChild( $.clone( $rb )) $ru.appendChild( $rt ) } $.setAttr( $ru, attr ) return $ru } /** * Create and return a new `` element * in Zhuyin form */ function createZhuyinRu( $rb, $rt ) { var $rb = $.clone( $rb ) // Create an element to return var $ru = $.create( 'h-ru' ) $ru.setAttribute( 'zhuyin', true ) // - // - // - // - // - // - // - $ru.appendChild( $rb ) $ru.innerHTML += getZhuyinHTML( $rt ) return $ru } /** * Create a Zhuyin-form HTML string */ function getZhuyinHTML( rt ) { // #### Explanation #### // * `zhuyin`: the entire phonetic annotation // * `yin`: the plain pronunciation (w/out tone) // * `diao`: the tone // * `len`: the length of the plain pronunciation (`yin`) var zhuyin = typeof rt === 'string' ? rt : rt.textContent var yin, diao, len yin = zhuyin.replace( TYPESET.zhuyin.diao, '' ) len = yin ? yin.length : 0 diao = zhuyin .replace( yin, '' ) .replace( /[\u02C5]/g, '\u02C7' ) .replace( /[\u030D]/g, '\u0358' ) return len === 0 ? '' : '' + yin + '' + diao + '' } /** * Normalize `ruby` elements */ $.extend( Locale, { // Address normalisation for both simple and complex // rubies (interlinear annotations) renderRuby: function( context, target ) { var target = target || 'ruby' var $target = $.qsa( target, context ) $.qsa( 'rtc', context ) .concat( $target ).map( simplifyRubyClass ) $target .forEach(function( $ruby ) { var clazz = $ruby.classList var $new if ( clazz.contains( 'complex' )) $new = renderComplexRuby( $ruby ) else if ( clazz.contains( 'zhuyin' )) $new = SUPPORT_IC ? renderInterCharRuby( $ruby ) : renderSimpleRuby( $ruby ) // Finally, replace it if ( $new ) $ruby.parentNode.replaceChild( $new, $ruby ) }) }, simplifyRubyClass: simplifyRubyClass, getZhuyinHTML: getZhuyinHTML, renderComplexRuby: renderComplexRuby, renderSimpleRuby: renderSimpleRuby, renderInterCharRuby: renderInterCharRuby // ### TODO list ### // // * Debug mode // * Better error-tolerance }) /** * Normalisation rendering mechanism */ $.extend( Locale, { // Render and normalise the given context by routine: // // ruby -> u, ins -> s, del -> em // renderElem: function( context ) { this.renderRuby( context ) this.renderDecoLine( context ) this.renderDecoLine( context, 's, del' ) this.renderEm( context ) }, // Traverse all target elements and address // presentational corrections if any two of // them are adjacent to each other. renderDecoLine: function( context, target ) { var $$target = $.qsa( target || 'u, ins', context ) var i = $$target.length traverse: while ( i-- ) { var $this = $$target[ i ] var $prev = null // Ignore all `` and comments in between, // and add class `.adjacent` once two targets // are next to each other. ignore: do { $prev = ( $prev || $this ).previousSibling if ( !$prev ) { continue traverse } else if ( $$target[ i-1 ] === $prev ) { $this.classList.add( 'adjacent' ) } } while ( $.isIgnorable( $prev )) } }, // Traverse all target elements to render // emphasis marks. renderEm: function( context, target ) { var method = target ? 'qsa' : 'tag' var target = target || 'em' var $target = $[ method ]( target, context ) $target .forEach(function( elem ) { var $elem = Han( elem ) if ( Locale.support.textemphasis ) { $elem .avoid( 'rt, h-char' ) .charify({ biaodian: true, punct: true }) } else { $elem .avoid( 'rt, h-char, h-char-group' ) .jinzify() .groupify({ western: true }) .charify({ hanzi: true, biaodian: true, punct: true, latin: true, ellinika: true, kirillica: true }) } }) } }) Han.normalize = Locale Han.localize = Locale Han.support = Locale.support Han.detectFont = Locale.detectFont Han.fn.initCond = function() { this.condition.classList.add( 'han-js-rendered' ) Han.normalize.initCond( this.condition ) return this } void [ 'Elem', 'DecoLine', 'Em', 'Ruby' ].forEach(function( elem ) { var method = 'render' + elem Han.fn[ method ] = function( target ) { Han.normalize[ method ]( this.context, target ) return this } }) $.extend( Han.support, { // Assume that all devices support Heiti for we // use `sans-serif` to do the comparison. heiti: true, // 'heiti-gb': true, songti: Han.detectFont( '"Han Songti"' ), 'songti-gb': Han.detectFont( '"Han Songti GB"' ), kaiti: Han.detectFont( '"Han Kaiti"' ), // 'kaiti-gb': Han.detectFont( '"Han Kaiti GB"' ), fangsong: Han.detectFont( '"Han Fangsong"' ) // 'fangsong-gb': Han.detectFont( '"Han Fangsong GB"' ) }) Han.correctBiaodian = function( context ) { var context = context || document var finder = Han.find( context ) finder .avoid( 'h-char' ) .replace( /([‘“])/g, function( portion ) { var $char = Han.createBDChar( portion.text ) $char.classList.add( 'bd-open', 'punct' ) return $char }) .replace( /([’”])/g, function( portion ) { var $char = Han.createBDChar( portion.text ) $char.classList.add( 'bd-close', 'bd-end', 'punct' ) return $char }) return Han.support.unicoderange ? finder : finder.charify({ biaodian: true }) } Han.correctBasicBD = Han.correctBiaodian Han.correctBD = Han.correctBiaodian $.extend( Han.fn, { biaodian: null, correctBiaodian: function() { this.biaodian = Han.correctBiaodian( this.context ) return this }, revertCorrectedBiaodian: function() { try { this.biaodian.revert( 'all' ) } catch (e) {} return this } }) // Legacy support (deprecated): Han.fn.correctBasicBD = Han.fn.correctBiaodian Han.fn.revertBasicBD = Han.fn.revertCorrectedBiaodian var hws = '<>' var $hws = $.create( 'h-hws' ) $hws.setAttribute( 'hidden', '' ) $hws.innerHTML = ' ' function sharingSameParent( $a, $b ) { return $a && $b && $a.parentNode === $b.parentNode } function properlyPlaceHWSBehind( $node, text ) { var $elmt = $node var text = text || '' if ( $.isElmt( $node.nextSibling ) || sharingSameParent( $node, $node.nextSibling ) ) { return text + hws } else { // One of the parental elements of the current text // node would definitely have a next sibling, since // it is of the first portion and not `isEnd`. while ( !$elmt.nextSibling ) { $elmt = $elmt.parentNode } if ( $node !== $elmt ) { $elmt.insertAdjacentHTML( 'afterEnd', '' ) } } return text } function firstStepLabel( portion, mat ) { return portion.isEnd && portion.index === 0 ? mat[1] + hws + mat[2] : portion.index === 0 ? properlyPlaceHWSBehind( portion.node, portion.text ) : portion.text } function real$hwsElmt( portion ) { return portion.index === 0 ? $.clone( $hws ) : '' } var last$hwsIdx function apostrophe( portion ) { var $elmt = portion.node.parentNode if ( portion.index === 0 ) { last$hwsIdx = portion.endIndexInNode-2 } if ( $elmt.nodeName.toLowerCase() === 'h-hws' && ( portion.index === 1 || portion.indexInMatch === last$hwsIdx )) { $elmt.classList.add( 'quote-inner' ) } return portion.text } function curveQuote( portion ) { var $elmt = portion.node.parentNode if ( $elmt.nodeName.toLowerCase() === 'h-hws' ) { $elmt.classList.add( 'quote-outer' ) } return portion.text } $.extend( Han, { renderHWS: function( context, strict ) { // Elements to be filtered according to the // HWS rendering mode. var AVOID = strict ? 'textarea, code, kbd, samp, pre' : 'textarea' var mode = strict ? 'strict' : 'base' var context = context || document var finder = Han.find( context ) finder .avoid( AVOID ) // Basic situations: // - 字a => 字a // - A字 => A字 .replace( Han.TYPESET.hws[ mode ][0], firstStepLabel ) .replace( Han.TYPESET.hws[ mode ][1], firstStepLabel ) // Convert text nodes `` into real element nodes: .replace( new RegExp( '(' + hws + ')+', 'g' ), real$hwsElmt ) // Deal with: // - '' => '字' // - "" => "字" .replace( /([\'"])\s(.+?)\s\1/g, apostrophe ) // Deal with: // - “字” // - ‘字’ .replace( /\s[‘“]/g, curveQuote ) .replace( /[’”]\s/g, curveQuote ) .normalize() // Return the finder instance for future usage return finder } }) $.extend( Han.fn, { renderHWS: function( strict ) { Han.renderHWS( this.context, strict ) return this }, revertHWS: function() { $.tag( 'h-hws', this.context ) .forEach(function( hws ) { $.remove( hws ) }) this.HWS = [] return this } }) var HANGABLE_CLASS = 'bd-hangable' var HANGABLE_AVOID = 'h-char.bd-hangable' var HANGABLE_CS_HTML = '' var matches = Han.find.matches function detectSpaceFont() { var div = $.create( 'div' ) var ret div.innerHTML = 'a ba b' body.appendChild( div ) ret = div.firstChild.offsetWidth !== div.lastChild.offsetWidth $.remove( div ) return ret } function insertHangableCS( $jinze ) { var $cs = $jinze.nextSibling if ( $cs && matches( $cs, 'h-cs.jinze-outer' )) { $cs.classList.add( 'hangable-outer' ) } else { $jinze.insertAdjacentHTML( 'afterend', HANGABLE_CS_HTML ) } } Han.support['han-space'] = detectSpaceFont() $.extend( Han, { detectSpaceFont: detectSpaceFont, isSpaceFontLoaded: detectSpaceFont(), renderHanging: function( context ) { var context = context || document var finder = Han.find( context ) finder .avoid( 'textarea, code, kbd, samp, pre' ) .avoid( HANGABLE_AVOID ) .replace( TYPESET.jinze.hanging, function( portion ) { if ( /^[\x20\t\r\n\f]+$/.test( portion.text )) { return '' } var $elmt = portion.node.parentNode var $jinze, $new, $bd, biaodian if ( $jinze = $.parent( $elmt, 'h-jinze' )) { insertHangableCS( $jinze ) } biaodian = portion.text.trim() $new = Han.createBDChar( biaodian ) $new.innerHTML = '' + biaodian + '' $new.classList.add( HANGABLE_CLASS ) $bd = $.parent( $elmt, 'h-char.biaodian' ) return !$bd ? $new : (function() { $bd.classList.add( HANGABLE_CLASS ) return matches( $elmt, 'h-inner, h-inner *' ) ? biaodian : $new.firstChild })() } ) return finder } }) $.extend( Han.fn, { renderHanging: function() { var classList = this.condition.classList Han.isSpaceFontLoaded = detectSpaceFont() if ( Han.isSpaceFontLoaded && classList.contains( 'no-han-space' ) ) { classList.remove( 'no-han-space' ) classList.add( 'han-space' ) } Han.renderHanging( this.context ) return this }, revertHanging: function() { $.qsa( 'h-char.bd-hangable, h-cs.hangable-outer', this.context ).forEach(function( $elmt ) { var classList = $elmt.classList classList.remove( 'bd-hangable' ) classList.remove( 'hangable-outer' ) }) return this } }) var JIYA_CLASS = 'bd-jiya' var JIYA_AVOID = 'h-char.bd-jiya' var CONSECUTIVE_CLASS = 'bd-consecutive' var JIYA_CS_HTML = '' var matches = Han.find.matches function trimBDClass( clazz ) { return clazz.replace( /(biaodian|cjk|bd-jiya|bd-consecutive|bd-hangable)/gi, '' ).trim() } function charifyBiaodian( portion ) { var biaodian = portion.text var $elmt = portion.node.parentNode var $bd = $.parent( $elmt, 'h-char.biaodian' ) var $new = Han.createBDChar( biaodian ) var $jinze $new.innerHTML = '' + biaodian + '' $new.classList.add( JIYA_CLASS ) if ( $jinze = $.parent( $elmt, 'h-jinze' )) { insertJiyaCS( $jinze ) } return !$bd ? $new : (function() { $bd.classList.add( JIYA_CLASS ) return matches( $elmt, 'h-inner, h-inner *' ) ? biaodian : $new.firstChild })() } var prevBDType, $$prevCS function locateConsecutiveBD( portion ) { var prev = prevBDType var $elmt = portion.node.parentNode var $bd = $.parent( $elmt, 'h-char.biaodian' ) var $jinze = $.parent( $bd, 'h-jinze' ) var classList classList = $bd.classList if ( prev ) { $bd.setAttribute( 'prev', prev ) } if ( $$prevCS && classList.contains( 'bd-open' )) { $$prevCS.pop().setAttribute( 'next', 'bd-open' ) } $$prevCS = undefined if ( portion.isEnd ) { prevBDType = undefined classList.add( CONSECUTIVE_CLASS, 'end-portion' ) } else { prevBDType = trimBDClass($bd.getAttribute( 'class' )) classList.add( CONSECUTIVE_CLASS ) } if ( $jinze ) { $$prevCS = locateCS( $jinze, { prev: prev, 'class': trimBDClass($bd.getAttribute( 'class' )) }) } return portion.text } function insertJiyaCS( $jinze ) { if ( matches( $jinze, '.tou, .touwei' ) && !matches( $jinze.previousSibling, 'h-cs.jiya-outer' ) ) { $jinze.insertAdjacentHTML( 'beforebegin', JIYA_CS_HTML ) } if ( matches( $jinze, '.wei, .touwei' ) && !matches( $jinze.nextSibling, 'h-cs.jiya-outer' ) ) { $jinze.insertAdjacentHTML( 'afterend', JIYA_CS_HTML ) } } function locateCS( $jinze, attr ) { var $prev, $next if (matches( $jinze, '.tou, .touwei' )) { $prev = $jinze.previousSibling if (matches( $prev, 'h-cs' )) { $prev.className = 'jinze-outer jiya-outer' $prev.setAttribute( 'prev', attr.prev ) } } if (matches( $jinze, '.wei, .touwei' )) { $next = $jinze.nextSibling if (matches( $next, 'h-cs' )) { $next.className = 'jinze-outer jiya-outer ' + attr[ 'class' ] $next.removeAttribute( 'prev' ) } } return [ $prev, $next ] } Han.renderJiya = function( context ) { var context = context || document var finder = Han.find( context ) finder .avoid( 'textarea, code, kbd, samp, pre, h-cs' ) .avoid( JIYA_AVOID ) .charify({ avoid: false, biaodian: charifyBiaodian }) // End avoiding `JIYA_AVOID`: .endAvoid() .avoid( 'textarea, code, kbd, samp, pre, h-cs' ) .replace( TYPESET.group.biaodian[0], locateConsecutiveBD ) .replace( TYPESET.group.biaodian[1], locateConsecutiveBD ) return finder } $.extend( Han.fn, { renderJiya: function() { Han.renderJiya( this.context ) return this }, revertJiya: function() { $.qsa( 'h-char.bd-jiya, h-cs.jiya-outer', this.context ).forEach(function( $elmt ) { var classList = $elmt.classList classList.remove( 'bd-jiya' ) classList.remove( 'jiya-outer' ) }) return this } }) var QUERY_RU_W_ANNO = 'h-ru[annotation]' var SELECTOR_TO_IGNORE = 'textarea, code, kbd, samp, pre' function createCompareFactory( font, treat, control ) { return function() { var a = Han.localize.writeOnCanvas( treat, font ) var b = Han.localize.writeOnCanvas( control, font ) return Han.localize.compareCanvases( a, b ) } } function isVowelCombLigaNormal() { return createCompareFactory( '"Romanization Sans"', '\u0061\u030D', '\uDB80\uDC61' ) } function isVowelICombLigaNormal() { return createCompareFactory( '"Romanization Sans"', '\u0069\u030D', '\uDB80\uDC69' ) } function isZhuyinCombLigaNormal() { return createCompareFactory( '"Zhuyin Kaiti"', '\u31B4\u0358', '\uDB8C\uDDB4' ) } function createSubstFactory( regexToSubst ) { return function( context ) { var context = context || document var finder = Han.find( context ).avoid( SELECTOR_TO_IGNORE ) regexToSubst .forEach(function( pattern ) { finder .replace( new RegExp( pattern[ 0 ], 'ig' ), function( portion, match ) { var ret = $.clone( charCombLiga ) // Put the original content in an inner container // for better presentational effect of hidden text ret.innerHTML = '' + match[0] + '' ret.setAttribute( 'display-as', pattern[ 1 ] ) return portion.index === 0 ? ret : '' } ) }) return finder } } var charCombLiga = $.create( 'h-char', 'comb-liga' ) $.extend( Han, { isVowelCombLigaNormal: isVowelCombLigaNormal(), isVowelICombLigaNormal: isVowelICombLigaNormal(), isZhuyinCombLigaNormal: isZhuyinCombLigaNormal(), isCombLigaNormal: isVowelICombLigaNormal()(), // ### Deprecated substVowelCombLiga: createSubstFactory( Han.TYPESET[ 'display-as' ][ 'comb-liga-vowel' ] ), substZhuyinCombLiga: createSubstFactory( Han.TYPESET[ 'display-as' ][ 'comb-liga-zhuyin' ] ), substCombLigaWithPUA: createSubstFactory( Han.TYPESET[ 'display-as' ][ 'comb-liga-pua' ] ), substInaccurateChar: function( context ) { var context = context || document var finder = Han.find( context ) finder.avoid( SELECTOR_TO_IGNORE ) Han.TYPESET[ 'inaccurate-char' ] .forEach(function( pattern ) { finder .replace( new RegExp( pattern[ 0 ], 'ig' ), pattern[ 1 ] ) }) } }) $.extend( Han.fn, { 'comb-liga-vowel': null, 'comb-liga-vowel-i': null, 'comb-liga-zhuyin': null, 'inaccurate-char': null, substVowelCombLiga: function() { this['comb-liga-vowel'] = Han.substVowelCombLiga( this.context ) return this }, substVowelICombLiga: function() { this['comb-liga-vowel-i'] = Han.substVowelICombLiga( this.context ) return this }, substZhuyinCombLiga: function() { this['comb-liga-zhuyin'] = Han.substZhuyinCombLiga( this.context ) return this }, substCombLigaWithPUA: function() { if ( !Han.isVowelCombLigaNormal()) { this['comb-liga-vowel'] = Han.substVowelCombLiga( this.context ) } else if ( !Han.isVowelICombLigaNormal()) { this['comb-liga-vowel-i'] = Han.substVowelICombLiga( this.context ) } if ( !Han.isZhuyinCombLigaNormal()) { this['comb-liga-zhuyin'] = Han.substZhuyinCombLiga( this.context ) } return this }, revertVowelCombLiga: function() { try { this['comb-liga-vowel'].revert( 'all' ) } catch (e) {} return this }, revertVowelICombLiga: function() { try { this['comb-liga-vowel-i'].revert( 'all' ) } catch (e) {} return this }, revertZhuyinCombLiga: function() { try { this['comb-liga-zhuyin'].revert( 'all' ) } catch (e) {} return this }, revertCombLigaWithPUA: function() { try { this['comb-liga-vowel'].revert( 'all' ) this['comb-liga-vowel-i'].revert( 'all' ) this['comb-liga-zhuyin'].revert( 'all' ) } catch (e) {} return this }, substInaccurateChar: function() { this['inaccurate-char'] = Han.substInaccurateChar( this.context ) return this }, revertInaccurateChar: function() { try { this['inaccurate-char'].revert( 'all' ) } catch (e) {} return this } }) window.addEventListener( 'DOMContentLoaded', function() { var initContext // Use the shortcut under the default situation if ( root.classList.contains( 'han-init' )) { Han.init() // Consider ‘a configured context’ the special // case of the default situation. Will have to // replace the `Han.init` with the instance as // well (for future usage). } else if ( initContext = document.querySelector( '.han-init-context' )) { Han.init = Han( initContext ).render() } }) // Expose to global namespace if ( typeof noGlobalNS === 'undefined' || noGlobalNS === false ) { window.Han = Han } return Han });