UNPKG

han-css

Version:

The CSS typography framework optimised for Hanzi

1,752 lines (1,503 loc) 75.6 kB
/*! * 漢字標準格式 v3.2.7 | 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.2.7' var ROUTINE = [ // Initialise the condition with feature-detecting // classes (Modernizr-alike), binding onto the root // element, possibly `<html>`. 'initCond', // Address element normalisation 'renderElem', // Handle Biaodian /* 'jinzify', */ 'renderHanging', 'renderJiya', // Address Hanzi and Western script mixed spacing 'renderHWS', // Address Basic Biaodian correction in Firefox 'correctBasicBD', // 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 ) { try { if ( typeof method === 'string' ) { it[ method ]() } else if ( Array.isArray( method )) { it[ method.shift() ].apply( it, method ) } } catch ( e ) {} }) 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( '(' + rBdClose + '*|[…⋯]*)([、,。.])(?!' + 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 var $ = { // Simplified query selectors which return the node list // in an array id: function( selector, context ) { return ( context || document ).getElementById( selector ) }, tag: function( selector, context ) { return this.makeArray( ( context || document ).getElementsByTagName( selector ) ) }, qsa: function( selector, context ) { return this.makeArray( ( context || document ).querySelectorAll( selector ) ) }, // Create a document fragment, a text node with text // or an element with/without classes create: function( elem, clazz ) { var elem = '!' === elem ? document.createDocumentFragment() : '' === elem ? document.createTextNode( clazz || '' ) : document.createElement( elem ) try { if ( clazz ) { elem.className = clazz } } catch (e) {} return elem }, // Clone a 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 }, // Return if the current node should be ignored, // `<wbr>` or comments isIgnorable: function( node ) { return node.nodeName === 'WBR' || node.nodeType === Node.COMMENT_NODE }, // Convert array-like objects into real arrays // for the native prototype methods makeArray: function( obj ) { return Array.prototype.slice.call( obj ) }, // Extend target with an object extend: function( target, object ) { var isExtensible = typeof target === 'object' || typeof target === 'function' || typeof object === 'object' if ( !isExtensible ) return 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 }()) ); function createBdGroup( portion, match ) { var elem = $.create( 'h-char-group', 'biaodian cjk' ) if ( portion.index === 0 && portion.isEnd ) { elem.innerHTML = match[0] } else { elem.innerHTML = portion.text elem.classList.add( 'portion' ) if ( portion.index === 0 ) { elem.classList.add( 'isFirst' ) } else if ( portion.isEnd ) { elem.classList.add( 'isEnd' ) } } return elem } function createBdChar( char ) { var div = $.create( 'div' ) var unicode = char.charCodeAt( 0 ).toString( 16 ) var clazz = 'biaodian cjk ' + ( char.match( TYPESET.char.biaodian.open ) ? 'bd-open' : char.match( TYPESET.char.biaodian.close ) ? 'bd-close bd-end' : char.match( TYPESET.char.biaodian.end ) ? 'bd-end' : char.match( new RegExp( '(' + UNICODE.biaodian.liga + ')' )) ? 'bd-liga' : '' ) div.innerHTML = '<h-char unicode="' + unicode + '" class="' + clazz + '">' + char + '</h-char>' return div.firstChild } $.extend( Fibre.fn, { // 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-hangable, h-char-group, h-word' ) 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({ biaodian: false, punct: false, hanzi: false, // Includes Kana latin: false, ellinika: false, kirillica: false, kana: false, eonmun: false }, option || {}) this.avoid( 'h-char' ) if ( option.biaodian ) { this.replace( TYPESET.char.biaodian.all, function( portion, match ) { return createBdChar( match[0] ) } ).replace( TYPESET.char.biaodian.liga, function( portion, match ) { return createBdChar( match[0] ) } ) } if ( option.hanzi || option.cjk ) { this.wrap( TYPESET.char.hanzi, $.clone( $.create( 'h-char', 'hanzi cjk' )) ) } if ( option.punct ) { this.wrap( TYPESET.char.punct.all, $.clone( $.create( 'h-char', 'punct' )) ) } if ( option.latin ) { this.wrap( TYPESET.char.latin, $.clone( $.create( 'h-char', 'alphabet latin' )) ) } if ( option.ellinika || option.greek ) { this.wrap( TYPESET.char.ellinika, $.clone( $.create( 'h-char', 'alphabet ellinika greek' )) ) } if ( option.kirillica || option.cyrillic ) { this.wrap( TYPESET.char.kirillica, $.clone( $.create( 'h-char', 'alphabet kirillica cyrillic' )) ) } if ( option.kana ) { this.wrap( TYPESET.char.kana, $.clone( $.create( 'h-char', 'kana' )) ) } if ( option.eonmun || option.hangul ) { this.wrap( TYPESET.char.eonmun, $.clone( $.create( 'h-char', 'eonmun hangul' )) ) } this.endAvoid() return this } }) Han.find = Fibre 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 = [ '<style>', rule, '</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 `<rp>` via `display: none` ret = ( getStyle( rp, 'display' ) === 'none' || // but in IE, `<rp>` has `display: inline`, so