han-css
Version:
The CSS typography framework optimised for Hanzi
2,679 lines • 75.6 kB
JavaScript
/*!
* 漢字標準格式 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 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 = '<h-test-a style="display: ruby;"></h-test-a><h-test-b style="display: ruby-text-container;"></h-test-b>'
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 = '<h-test style="-moz-ruby-position:' + IC + ';-ms-ruby-position:' + IC + ';-webkit-ruby-position:' + IC + ';ruby-position:' + IC + ';"></h-test>'
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 `<h-ru>` to append.
$ru = clazz.contains( 'zhuyin' ) ? createZhuyinRu( $rb, $rt ) : createNormalRu( $rb, $rt )
// Replace the ruby text with the new `<h-ru>`,
// 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 `<h-ruby>` 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 `<h-ru>` 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 `<h-ru>` 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 )
// - <h-ru zhuyin>
// - <rb><rb/>
// - <h-zhuyin>
// - <h-yin></h-yin>
// - <h-diao></h-diao>
// - </h-zhuyin>
// - </h-ru>
$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 ? '' : '<h-zhuyin length="' + len + '" diao="' + diao + '"><h-yin>' + yin + '</h-yin><h-diao>' + diao + '</h-diao></h-zhuyin>'
}
/**
* 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 target elements (those with text-decoration-
// line) to see if we should address spacing in
// between for semantic presentation.
renderDecoLine: function( context, target ) {
var target = target || 'u, ins'
var $target = $.qsa( target, context )
var rTarget = new RegExp( '^(' + target.replace(/\,\s?/g, '|') + ')$', 'ig' )
$target
.forEach(function( elem ) {
var next
// Ignore all `<wbr>` and comments in between
do {
next = ( next || elem ).nextSibling
if ( !next ) return
} while ( $.isIgnorable( next ))
if ( next.nodeName.match( rTarget )) {
next.classList.add( 'adjacent' )
}
})
},
// Traverse target elements to render Hanzi emphasis marks
// and skip that in punctuation
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, h-char-group' )
.charify({ biaodian: true, punct: true })
} else {
$elem
.avoid( 'rt, h-char, h-char-group' )
.jinzify()
.groupify({ western: true, biaodian: 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"' )
})
var QUERY_HWS_AS_FIRST_CHILD = '* > h-hws:first-child, * > wbr:first-child + h-hws, wbr:first-child + wbr + h-hws'
//// 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 isNodeNormalizeNormal = (function() {
var div = $.create( 'div' )
div.appendChild( $.create( '', '0-' ))
div.appendChild( $.create( '', '2' ))
div.normalize()
return div.firstChild.length !== 2
})()
var hws = $.create( 'h-hws' )
hws.setAttribute( 'hidden', '' )
hws.innerHTML = ' '
$.extend( Han, {
isNodeNormalizeNormal: isNodeNormalizeNormal,
renderHWS: function( context, strict ) {
var context = context || document
var mode = strict ? 'strict' : 'base'
var finder = Han.find( context )
// Elements to be filtered according to the
// HWS rendering mode
if ( strict ) {
finder.avoid( 'textarea, code, kbd, samp, pre' )
} else {
finder.avoid( 'textarea' )
}
finder
.replace( Han.TYPESET.hws[ mode ][0], '$1<hws/>$2' )
.replace( Han.TYPESET.hws[ mode ][1], '$1<hws/>$2' )
// Deal with [' 字'], [" 字"] => ['字'], ["字"]
.replace( /(['"]+)<hws\/>(.+?)<hws\/>\1/ig, '$1$2$1' )
// Remove all `<hws/>` pre/post [“字”] and [‘字’]
// See: https://github.com/ethantw/Han/issues/59
.replace( /<hws\/>([‘“]+)/ig, '$1' )
.replace( /([’”]+)<hws\/>/ig, '$1' )
// Convert text nodes `<hws/>` into real element nodes
.replace( '<hws/>', function() {
return $.clone( hws )
})
// Deal with:
// `漢<u><hws/>zi</u>` => `漢<hws/><u>zi</u>`
$
.qsa( QUERY_HWS_AS_FIRST_CHILD, context )
.forEach(function( firstChild ) {
var parent = firstChild.parentNode
var target = parent.firstChild
// Skip all `<wbr>` and comments
while ( $.isIgnorable( target )) {
target = target.nextSibling
if ( !target ) return
}
// The ‘first-child’ of DOM is different from
// the ones of QSA, could be either an element
// or a text fragment, but the latter one is
// not what we want. We don't want comments,
// either.
while ( target.nodeName === 'H-HWS' ) {
$.remove( target, parent )
target = parent.parentNode.insertBefore( $.clone( hws ), parent )
parent = parent.parentNode
if ( isNodeNormalizeNormal ) {
parent.normalize()
}
// This is for extreme circumstances, i.e.,
// `漢<a><b><c><h-hws/>zi</c></b></a>` =>
// `漢<h-hws/><a><b><c>zi</c></b></a>`
if ( target !== parent.firstChild ) {
break
}
}
})
// Normalise nodes we messed up with
if ( isNodeNormalizeNormal ) {
context.normalize()
}
// Return the finder instance for future usage
return finder
}
})
$.extend( Han.fn, {
HWS: null,
renderHWS: function( strict ) {
Han.renderHWS( this.context, strict )
this.HWS = $.tag( 'h-hws', this.context )
return this
},
revertHWS: function() {
this.HWS.forEach(function( hws ) {
$.remove( hws )
})
return this
}
})
function detectSpaceFont() {
var div = $.create( 'div' )
var ret
div.innerHTML = '<span>a b</span><span style="font-family: \'Han Space\'">a b</span>'
body.appendChild( div )
ret = div.firstChild.offsetWidth !== div.lastChild.offsetWidth
$.remove( div, body )
return ret
}
$.extend( Han, {
detectSpaceFont: detectSpaceFont,
isSpaceFontLoaded: detectSpaceFont()
})
Han.support['han-space'] = detectSpaceFont()
Han.renderHanging = function( context ) {
var context = context || document
var finder = Han.find( context )
finder
.avoid( 'textarea, code, kbd, samp, pre, h-hangable' )
.replace(
TYPESET.jinze.hanging,
function( portion, match ) {
var elem = $.create( 'h-hangable' )
elem.innerHTML = match[1] + '<h-cs><h-inner hidden> </h-inner><h-char class="biaodian bd-close bd-end cjk">' + match[2] + '</h-char></h-cs>'
return portion.index === 0 ? elem : ''
}
)
return finder
}
$.extend( Han.fn, {
hanging: null,
renderHanging: function() {
var condClazz = this.condition.classList
var isSpaceFontLoaded = detectSpaceFont()
if ( isSpaceFontLoaded && condClazz.contains( 'no-han-space' )) {
condClazz.remove( 'no-han-space' )
condClazz.add( 'han-space' )
}
this.hanging = Han.renderHanging( this.context )
return this
},
revertHanging: function() {
try {
this.hanging.revert( 'all' )
} catch ( e ) {}
return this
}
})
Han.renderJiya = function( context ) {
var context = context || document
var finder = Han.find( context )
finder
.avoid( 'textarea, code, kbd, samp, pre, h-char-group' )
.replace(
// This is a safeguard against hanging rendering
new RegExp( '(' + UNICODE.biaodian.end + '+)(' + UNICODE.biaodian.open + '+)', 'g' ),
function( portion, match ) {
if ( portion.index === 0 ) return portion.isEnd ? match[0] : match[1]
var elem = $.create( 'h-char-group', 'biaodian cjk portion' )
elem.innerHTML = match[2]
return elem
}
)
.endAvoid()
finder
.avoid( 'textarea, code, kbd, samp, pre, h-char' )
.groupify({ biaodian: true })
.charify({ biaodian: true })
// The reason we're doing this instead of using pseudo elements in CSS
// is because WebKit has problem rendering pseudo elements containing only
// space.
$.qsa( 'h-char.biaodian.bd-open, h-char.biaodian.bd-end', context )
.forEach(function( elem ) {
if ( Han.find.matches( elem, 'h-cs *' )) return
var html = '<h-inner>' + elem.innerHTML + '</h-inner>'
var hcs = '<h-cs hidden> </h-cs>'
var isOpen = elem.classList.contains( 'bd-open' )
elem.innerHTML = isOpen ? hcs + html : html + hcs
})
return finder
}
$.extend( Han.fn, {
jiya: null,
renderJiya: function() {
this.jiya = Han.renderJiya( this.context )
return this
},
revertJiya: function() {
try {
this.jiya.revert( 'all' )
} catch ( e ) {}
return this
}
})
var mdot
mdot = $.create( 'h-char', 'biaodian cjk bd-middle' )
mdot.setAttribute( 'unicode', 'b7' )
Han.correctBasicBD = function( context, all ) {
if ( Han.support.unicoderange && !all ) return
var context = context || document
var finder
finder = Han.find( context )
finder
.wrap( /\u00B7/g, $.clone( mdot ))
.charify({ biaodian: true })
}
$.extend( Han.fn, {
basicBD: null,
correctBasicBD: function( all ) {
this.basicBD = Han.correctBasicBD( this.context, all )
return this
},
revertBasicBD: function() {
try {
this.basicBD.revert( 'all' )
} catch (e) {}
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 = '<h-inner>' + match[0] + '</h-inner>'
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
});