Cool-Y.github.io/lib/Han/dist/han.js

3006 lines
80 KiB
JavaScript
Raw Normal View History

2019-04-15 07:42:42 +00:00
/*!
* 漢字標準格式 v3.3.0 | MIT License | css.hanzi.co
* Han.css: the CSS typography framework optimised for Hanzi
*/
void function( global, factory ) {
// CommonJS
if ( typeof module === 'object' && typeof module.exports === 'object' ) {
module.exports = factory( global, true )
// AMD
} else if ( typeof define === 'function' && define.amd ) {
define(function() { return factory( global, true ) })
// Global namespace
} else {
factory( global )
}
}( typeof window !== 'undefined' ? window : this, function( window, noGlobalNS ) {
'use strict'
var document = window.document
var root = document.documentElement
var body = document.body
var VERSION = '3.3.0'
var ROUTINE = [
// Initialise the condition with feature-detecting
// classes (Modernizr-alike), binding onto the root
// element, possibly `<html>`.
'initCond',
// Address element normalisation
'renderElem',
// Handle Biaodian
/* 'jinzify', */
'renderJiya',
'renderHanging',
// Address Biaodian correction
'correctBiaodian',
// Address Hanzi and Western script mixed spacing
'renderHWS',
// Address presentational correction to combining ligatures
'substCombLigaWithPUA'
// Address semantic correction to inaccurate characters
// **Note:** inactivated by default
/* 'substInaccurateChar', */
]
// Define Han
var Han = function( context, condition ) {
return new Han.fn.init( context, condition )
}
var init = function() {
if ( arguments[ 0 ] ) {
this.context = arguments[ 0 ]
}
if ( arguments[ 1 ] ) {
this.condition = arguments[ 1 ]
}
return this
}
Han.version = VERSION
Han.fn = Han.prototype = {
version: VERSION,
constructor: Han,
// Body as the default target context
context: body,
// Root element as the default condition
condition: root,
// Default rendering routine
routine: ROUTINE,
init: init,
setRoutine: function( routine ) {
if ( Array.isArray( routine )) {
this.routine = routine
}
return this
},
// Note that the routine set up here will execute
// only once. The method won't alter the routine in
// the instance or in the prototype chain.
render: function( routine ) {
var it = this
var routine = Array.isArray( routine )
? routine
: this.routine
routine
.forEach(function( method ) {
if (
typeof method === 'string' &&
typeof it[ method ] === 'function'
) {
it[ method ]()
} else if (
Array.isArray( method ) &&
typeof it[ method[0] ] === 'function'
) {
it[ method.shift() ].apply( it, method )
}
})
return this
}
}
Han.fn.init.prototype = Han.fn
/**
* Shortcut for `render()` under the default
* situation.
*
* Once initialised, replace `Han.init` with the
* instance for future usage.
*/
Han.init = function() {
return Han.init = Han().render()
}
var UNICODE = {
/**
* Western punctuation (西文標點符號)
*/
punct: {
base: '[\u2026,.;:!?\u203D_]',
sing: '[\u2010-\u2014\u2026]',
middle: '[\\\/~\\-&\u2010-\u2014_]',
open: '[\'"‘“\\(\\[\u00A1\u00BF\u2E18\u00AB\u2039\u201A\u201C\u201E]',
close: '[\'"”’\\)\\]\u00BB\u203A\u201B\u201D\u201F]',
end: '[\'"”’\\)\\]\u00BB\u203A\u201B\u201D\u201F\u203C\u203D\u2047-\u2049,.;:!?]',
},
/**
* CJK biaodian (CJK標點符號)
*/
biaodian: {
base: '[︰.、,。:;?!ー]',
liga: '[—…⋯]',
middle: '[·\/-゠\uFF06\u30FB\uFF3F]',
open: '[「『《〈(〔[{【〖]',
close: '[」』》〉)〕]}】〗]',
end: '[」』》〉)〕]}】〗︰.、,。:;?!ー]'
},
/**
* CJK-related blocks (CJK相關字符區段)
*
* 1. 中日韓統一意音文字[\u4E00-\u9FFF]
Basic CJK unified ideographs
* 2. 擴展-A區[\u3400-\u4DB5]
Extended-A
* 3. 擴展-B區[\u20000-\u2A6D6][\uD840-\uD869][\uDC00-\uDED6]
Extended-B
* 4. 擴展-C區[\u2A700-\u2B734]\uD86D[\uDC00-\uDF3F]|[\uD86A-\uD86C][\uDC00-\uDFFF]|\uD869[\uDF00-\uDFFF]
Extended-C
* 5. 擴展-D區[\u2B740-\u2B81D]急用漢字\uD86D[\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1F]
Extended-D
* 6. 擴展-E區[\u2B820-\u2F7FF]暫未支援
Extended-E (not supported yet)
* 7. 擴展-F區暫未支援
Extended-F (not supported yet)
* 8. 筆畫區[\u31C0-\u31E3]
Strokes
* 9. 意音數字[\u3007]
Ideographic number zero
* 10. 相容意音文字及補充[\uF900-\uFAFF][\u2F800-\u2FA1D]不使用
Compatibility ideograph and supplement (not supported)
12 exceptions:
[\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]
https://zh.wikipedia.org/wiki/中日韓統一表意文字#cite_note-1
* 11. 康熙字典及簡化字部首[\u2F00-\u2FD5\u2E80-\u2EF3]
Kangxi and supplement radicals
* 12. 意音文字描述字元[\u2FF0-\u2FFA]
Ideographic description characters
*/
hanzi: {
base: '[\u4E00-\u9FFF\u3400-\u4DB5\u31C0-\u31E3\u3007\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD800-\uDBFF][\uDC00-\uDFFF]',
desc: '[\u2FF0-\u2FFA]',
radical: '[\u2F00-\u2FD5\u2E80-\u2EF3]'
},
/**
* Latin script blocks (拉丁字母區段)
*
* 1. 基本拉丁字母A-Za-z
Basic Latin
* 2. 阿拉伯數字0-9
Digits
* 3. 補充-1[\u00C0-\u00FF]
Latin-1 supplement
* 4. 擴展-A區[\u0100-\u017F]
Extended-A
* 5. 擴展-B區[\u0180-\u024F]
Extended-B
* 5. 擴展-C區[\u2C60-\u2C7F]
Extended-C
* 5. 擴展-D區[\uA720-\uA7FF]
Extended-D
* 6. 附加區[\u1E00-\u1EFF]
Extended additional
* 7. 變音組字符[\u0300-\u0341\u1DC0-\u1DFF]
Combining diacritical marks
*/
latin: {
base: '[A-Za-z0-9\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u2C60-\u2C7F\uA720-\uA7FF\u1E00-\u1EFF]',
combine: '[\u0300-\u0341\u1DC0-\u1DFF]'
},
/**
* Elli̱niká (Greek) script blocks (希臘字母區段)
*
* 1. 希臘字母及擴展[\u0370\u03FF\u1F00-\u1FFF]
Basic Greek & Greek Extended
* 2. 阿拉伯數字0-9
Digits
* 3. 希臘字母變音組字符[\u0300-\u0345\u1DC0-\u1DFF]
Combining diacritical marks
*/
ellinika: {
base: '[0-9\u0370-\u03FF\u1F00-\u1FFF]',
combine: '[\u0300-\u0345\u1DC0-\u1DFF]'
},
/**
* Kirillica (Cyrillic) script blocks (西里爾字母區段)
*
* 1. 西里爾字母及補充[\u0400-\u0482\u048A-\u04FF\u0500-\u052F]
Basic Cyrillic and supplement
* 2. 擴展B區[\uA640-\uA66E\uA67E-\uA697]
Extended-B
* 3. 阿拉伯數字0-9
Digits
* 4. 西里爾字母組字符[\u0483-\u0489\u2DE0-\u2DFF\uA66F-\uA67D\uA69F]位擴展AB區
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`, `Joness` or `'99`
var rApo = '[\u0027\u2019]'
var rChar = rHan + '|(?:' + rAlph + '|' + rApo + ')+'
var rZyS = UNICODE.zhuyin.initial
var rZyJ = UNICODE.zhuyin.medial
var rZyY = UNICODE.zhuyin.final
var rZyD = UNICODE.zhuyin.tone + '|' + UNICODE.zhuyin.checked
return {
/* Character-level selector ()
*/
char: {
punct: {
all: new RegExp( '(' + rPt + ')', 'g' ),
open: new RegExp( '(' + rPtOpen + ')', 'g' ),
end: new RegExp( '(' + rPtEnd + ')', 'g' ),
sing: new RegExp( '(' + rPtSing + ')', 'g' )
},
biaodian: {
all: new RegExp( '(' + rBD + ')', 'g' ),
open: new RegExp( '(' + rBDOpen + ')', 'g' ),
close: new RegExp( '(' + rBDClose + ')', 'g' ),
end: new RegExp( '(' + rBDEnd + ')', 'g' ),
liga: new RegExp( '(' + rBDLiga + ')', 'g' )
},
hanzi: new RegExp( '(' + rHan + ')', 'g' ),
latin: new RegExp( '(' + rLatn + ')', 'ig' ),
ellinika: new RegExp( '(' + rGk + ')', 'ig' ),
kirillica: new RegExp( '(' + rCy + ')', 'ig' ),
kana: new RegExp( '(' + rKana + '|' + rKanaS + '|' + rKanaH + ')', 'g' ),
eonmun: new RegExp( '(' + rEon + '|' + rEonH + ')', 'g' )
},
/* Word-level selectors ()
*/
group: {
biaodian: [
new RegExp( '((' + rBD + '){2,})', 'g' ),
new RegExp( '(' + rBDLiga + rBDOpen + ')', 'g' )
],
punct: null,
hanzi: new RegExp( '(' + rHan + ')+', 'g' ),
western: new RegExp( '(' + rLatn + '|' + rGk + '|' + rCy + '|' + rPt + ')+', 'ig' ),
kana: new RegExp( '(' + rKana + '|' + rKanaS + '|' + rKanaH + ')+', 'g' ),
eonmun: new RegExp( '(' + rEon + '|' + rEonH + '|' + rPt + ')+', 'g' )
},
/* Punctuation Rules ()
*/
jinze: {
hanging: new RegExp( rWhite + '*([、,。.])(?!' + rBDEnd + ')', 'ig' ),
touwei: new RegExp( '(' + rBDOpen + '+)(' + rChar + ')(' + rBDEnd + '+)', 'ig' ),
tou: new RegExp( '(' + rBDOpen + '+)(' + rChar + ')', 'ig' ),
wei: new RegExp( '(' + rChar + ')(' + rBDEnd + '+)', 'ig' ),
middle: new RegExp( '(' + rChar + ')(' + rBDMid + ')(' + rChar + ')', 'ig' )
},
zhuyin: {
form: new RegExp( '^\u02D9?(' + rZyS + ')?(' + rZyJ + ')?(' + rZyY + ')?(' + rZyD + ')?$' ),
diao: new RegExp( '(' + rZyD + ')', 'g' )
},
/* Hanzi and Western mixed spacing (西)
* - Basic mode
* - Strict mode
*/
hws: {
base: [
new RegExp( '('+ rHan + ')(' + rAlph + '|' + rPtOpen + ')', 'ig' ),
new RegExp( '('+ rAlph + '|' + rPtEnd + ')(' + rHan + ')', 'ig' )
],
strict: [
new RegExp( '('+ rHan + ')' + rWhite + '?(' + rAlph + '|' + rPtOpen + ')', 'ig' ),
new RegExp( '('+ rAlph + '|' + rPtEnd + ')' + rWhite + '?(' + rHan + ')', 'ig' )
]
},
// The feature displays the following characters
// in its variant form for font consistency and
// presentational reason. Meanwhile, this won't
// alter the original character in the DOM.
'display-as': {
'ja-font-for-hant': [
// '夠 够',
'查 査',
'啟 啓',
'鄉 鄕',
'值 値',
'污 汚'
],
'comb-liga-pua': [
[ '\u0061[\u030d\u0358]', '\uDB80\uDC61' ],
[ '\u0065[\u030d\u0358]', '\uDB80\uDC65' ],
[ '\u0069[\u030d\u0358]', '\uDB80\uDC69' ],
[ '\u006F[\u030d\u0358]', '\uDB80\uDC6F' ],
[ '\u0075[\u030d\u0358]', '\uDB80\uDC75' ],
[ '\u31B4[\u030d\u0358]', '\uDB8C\uDDB4' ],
[ '\u31B5[\u030d\u0358]', '\uDB8C\uDDB5' ],
[ '\u31B6[\u030d\u0358]', '\uDB8C\uDDB6' ],
[ '\u31B7[\u030d\u0358]', '\uDB8C\uDDB7' ]
],
'comb-liga-vowel': [
[ '\u0061[\u030d\u0358]', '\uDB80\uDC61' ],
[ '\u0065[\u030d\u0358]', '\uDB80\uDC65' ],
[ '\u0069[\u030d\u0358]', '\uDB80\uDC69' ],
[ '\u006F[\u030d\u0358]', '\uDB80\uDC6F' ],
[ '\u0075[\u030d\u0358]', '\uDB80\uDC75' ]
],
'comb-liga-zhuyin': [
[ '\u31B4[\u030d\u0358]', '\uDB8C\uDDB4' ],
[ '\u31B5[\u030d\u0358]', '\uDB8C\uDDB5' ],
[ '\u31B6[\u030d\u0358]', '\uDB8C\uDDB6' ],
[ '\u31B7[\u030d\u0358]', '\uDB8C\uDDB7' ]
]
},
// The feature actually *converts* the character
// in the DOM for semantic reason.
//
// Note that this could be aggressive.
'inaccurate-char': [
[ '[\u2022\u2027]', '\u00B7' ],
[ '\u22EF\u22EF', '\u2026\u2026' ],
[ '\u2500\u2500', '\u2014\u2014' ],
[ '\u2035', '\u2018' ],
[ '\u2032', '\u2019' ],
[ '\u2036', '\u201C' ],
[ '\u2033', '\u201D' ]
]
}
})()
Han.UNICODE = UNICODE
Han.TYPESET = TYPESET
// Aliases
Han.UNICODE.cjk = Han.UNICODE.hanzi
Han.UNICODE.greek = Han.UNICODE.ellinika
Han.UNICODE.cyrillic = Han.UNICODE.kirillica
Han.UNICODE.hangul = Han.UNICODE.eonmun
Han.UNICODE.zhuyin.ruyun = Han.UNICODE.zhuyin.checked
Han.TYPESET.char.cjk = Han.TYPESET.char.hanzi
Han.TYPESET.char.greek = Han.TYPESET.char.ellinika
Han.TYPESET.char.cyrillic = Han.TYPESET.char.kirillica
Han.TYPESET.char.hangul = Han.TYPESET.char.eonmun
Han.TYPESET.group.hangul = Han.TYPESET.group.eonmun
Han.TYPESET.group.cjk = Han.TYPESET.group.hanzi
var $ = {
/**
* Query selectors which return arrays of the resulted
* node lists.
*/
id: function( selector, $context ) {
return ( $context || document ).getElementById( selector )
},
tag: function( selector, $context ) {
return this.makeArray(
( $context || document ).getElementsByTagName( selector )
)
},
qs: function( selector, $context ) {
return ( $context || document ).querySelector( selector )
},
qsa: function( selector, $context ) {
return this.makeArray(
( $context || document ).querySelectorAll( selector )
)
},
parent: function( $node, selector ) {
return selector
? (function() {
if ( typeof $.matches !== 'function' ) return
while (!$.matches( $node, selector )) {
if (
!$node ||
$node === document.documentElement
) {
$node = undefined
break
}
$node = $node.parentNode
}
return $node
})()
: $node
? $node.parentNode : undefined
},
/**
* Create a document fragment, a text node with text
* or an element with/without classes.
*/
create: function( name, clazz ) {
var $elmt = '!' === name
? document.createDocumentFragment()
: '' === name
? document.createTextNode( clazz || '' )
: document.createElement( name )
try {
if ( clazz ) {
$elmt.className = clazz
}
} catch (e) {}
return $elmt
},
/**
* Clone a DOM node (text, element or fragment) deeply
* or childlessly.
*/
clone: function( $node, deep ) {
return $node.cloneNode(
typeof deep === 'boolean'
? deep
: true
)
},
/**
* Remove a node (text, element or fragment).
*/
remove: function( $node ) {
return $node.parentNode.removeChild( $node )
},
/**
* Set attributes all in once with an object.
*/
setAttr: function( target, attr ) {
if ( typeof attr !== 'object' ) return
var len = attr.length
// Native `NamedNodeMap``:
if (
typeof attr[0] === 'object' &&
'name' in attr[0]
) {
for ( var i = 0; i < len; i++ ) {
if ( attr[ i ].value !== undefined ) {
target.setAttribute( attr[ i ].name, attr[ i ].value )
}
}
// Plain object:
} else {
for ( var name in attr ) {
if (
attr.hasOwnProperty( name ) &&
attr[ name ] !== undefined
) {
target.setAttribute( name, attr[ name ] )
}
}
}
return target
},
/**
* Indicate whether or not the given node is an
* element.
*/
isElmt: function( $node ) {
return $node && $node.nodeType === Node.ELEMENT_NODE
},
/**
* Indicate whether or not the given node should
* be ignored (`<wbr>` or comments).
*/
isIgnorable: function( $node ) {
if ( !$node ) return false
return (
$node.nodeName === 'WBR' ||
$node.nodeType === Node.COMMENT_NODE
)
},
/**
* Convert array-like objects into real arrays.
*/
makeArray: function( object ) {
return Array.prototype.slice.call( object )
},
/**
* Extend target with an object.
*/
extend: function( target, object ) {
if ((
typeof target === 'object' ||
typeof target === 'function' ) &&
typeof object === 'object'
) {
for ( var name in object ) {
if (object.hasOwnProperty( name )) {
target[ name ] = object[ name ]
}
}
}
return target
}
}
var Fibre =
/*!
* Fibre.js v0.2.1 | MIT License | github.com/ethantw/fibre.js
* Based on findAndReplaceDOMText
*/
function( Finder ) {
'use strict'
var VERSION = '0.2.1'
var NON_INLINE_PROSE = Finder.NON_INLINE_PROSE
var AVOID_NON_PROSE = Finder.PRESETS.prose.filterElements
var global = window || {}
var document = global.document || undefined
function matches( node, selector, bypassNodeType39 ) {
var Efn = Element.prototype
var matches = Efn.matches || Efn.mozMatchesSelector || Efn.msMatchesSelector || Efn.webkitMatchesSelector
if ( node instanceof Element ) {
return matches.call( node, selector )
} else if ( bypassNodeType39 ) {
if ( /^[39]$/.test( node.nodeType )) return true
}
return false
}
if ( typeof document === 'undefined' ) throw new Error( 'Fibre requires a DOM-supported environment.' )
var Fibre = function( context, preset ) {
return new Fibre.fn.init( context, preset )
}
Fibre.version = VERSION
Fibre.matches = matches
Fibre.fn = Fibre.prototype = {
constructor: Fibre,
version: VERSION,
finder: [],
context: undefined,
portionMode: 'retain',
selector: {},
preset: 'prose',
init: function( context, noPreset ) {
if ( !!noPreset ) this.preset = null
this.selector = {
context: null,
filter: [],
avoid: [],
boundary: []
}
if ( !context ) {
throw new Error( 'A context is required for Fibre to initialise.' )
} else if ( context instanceof Node ) {
if ( context instanceof Document ) this.context = context.body || context
else this.context = context
} else if ( typeof context === 'string' ) {
this.context = document.querySelector( context )
this.selector.context = context
}
return this
},
filterFn: function( node ) {
var filter = this.selector.filter.join( ', ' ) || '*'
var avoid = this.selector.avoid.join( ', ' ) || null
var result = matches( node, filter, true ) && !matches( node, avoid )
return ( this.preset === 'prose' ) ? AVOID_NON_PROSE( node ) && result : result
},
boundaryFn: function( node ) {
var boundary = this.selector.boundary.join( ', ' ) || null
var result = matches( node, boundary )
return ( this.preset === 'prose' ) ? NON_INLINE_PROSE( node ) || result : result
},
filter: function( selector ) {
if ( typeof selector === 'string' ) {
this.selector.filter.push( selector )
}
return this
},
endFilter: function( all ) {
if ( all ) {
this.selector.filter = []
} else {
this.selector.filter.pop()
}
return this
},
avoid: function( selector ) {
if ( typeof selector === 'string' ) {
this.selector.avoid.push( selector )
}
return this
},
endAvoid: function( all ) {
if ( all ) {
this.selector.avoid = []
} else {
this.selector.avoid.pop()
}
return this
},
addBoundary: function( selector ) {
if ( typeof selector === 'string' ) {
this.selector.boundary.push( selector )
}
return this
},
removeBoundary: function() {
this.selector.boundary = []
return this
},
setMode: function( portionMode ) {
this.portionMode = portionMode === 'first' ? 'first' : 'retain'
return this
},
replace: function( regexp, newSubStr ) {
var it = this
it.finder.push(Finder( it.context, {
find: regexp,
replace: newSubStr,
filterElements: function( currentNode ) {
return it.filterFn( currentNode )
},
forceContext: function( currentNode ) {
return it.boundaryFn( currentNode )
},
portionMode: it.portionMode
}))
return it
},
wrap: function( regexp, strElemName ) {
var it = this
it.finder.push(Finder( it.context, {
find: regexp,
wrap: strElemName,
filterElements: function( currentNode ) {
return it.filterFn( currentNode )
},
forceContext: function( currentNode ) {
return it.boundaryFn( currentNode )
},
portionMode: it.portionMode
}))
return it
},
revert: function( level ) {
var max = this.finder.length
var level = Number( level ) || ( level === 0 ? Number(0) :
( level === 'all' ? max : 1 ))
if ( typeof max === 'undefined' || max === 0 ) return this
else if ( level > max ) level = max
for ( var i = level; i > 0; i-- ) {
this.finder.pop().revert()
}
return this
}
}
// Deprecated API(s)
Fibre.fn.filterOut = Fibre.fn.avoid
// Make sure init() inherit from Fibre()
Fibre.fn.init.prototype = Fibre.fn
return Fibre
}(
/**
* findAndReplaceDOMText v 0.4.3
* @author James Padolsey http://james.padolsey.com
* @license http://unlicense.org/UNLICENSE
*
* Matches the text of a DOM node against a regular expression
* and replaces each match (or node-separated portions of the match)
* in the specified element.
*/
(function() {
var PORTION_MODE_RETAIN = 'retain'
var PORTION_MODE_FIRST = 'first'
var doc = document
var toString = {}.toString
var hasOwn = {}.hasOwnProperty
function isArray(a) {
return toString.call(a) == '[object Array]'
}
function escapeRegExp(s) {
return String(s).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1')
}
function exposed() {
// Try deprecated arg signature first:
return deprecated.apply(null, arguments) || findAndReplaceDOMText.apply(null, arguments)
}
function deprecated(regex, node, replacement, captureGroup, elFilter) {
if ((node && !node.nodeType) && arguments.length <= 2) {
return false
}
var isReplacementFunction = typeof replacement == 'function'
if (isReplacementFunction) {
replacement = (function(original) {
return function(portion, match) {
return original(portion.text, match.startIndex)
}
}(replacement))
}
// Awkward support for deprecated argument signature (<0.4.0)
var instance = findAndReplaceDOMText(node, {
find: regex,
wrap: isReplacementFunction ? null : replacement,
replace: isReplacementFunction ? replacement : '$' + (captureGroup || '&'),
prepMatch: function(m, mi) {
// Support captureGroup (a deprecated feature)
if (!m[0]) throw 'findAndReplaceDOMText cannot handle zero-length matches'
if (captureGroup > 0) {
var cg = m[captureGroup]
m.index += m[0].indexOf(cg)
m[0] = cg
}
m.endIndex = m.index + m[0].length
m.startIndex = m.index
m.index = mi
return m
},
filterElements: elFilter
})
exposed.revert = function() {
return instance.revert()
}
return true
}
/**
* findAndReplaceDOMText
*
* Locates matches and replaces with replacementNode
*
* @param {Node} node Element or Text node to search within
* @param {RegExp} options.find The regular expression to match
* @param {String|Element} [options.wrap] A NodeName, or a Node to clone
* @param {String|Function} [options.replace='$&'] What to replace each match with
* @param {Function} [options.filterElements] A Function to be called to check whether to
* process an element. (returning true = process element,
* returning false = avoid element)
*/
function findAndReplaceDOMText(node, options) {
return new Finder(node, options)
}
exposed.NON_PROSE_ELEMENTS = {
br:1, hr:1,
// Media / Source elements:
script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
// Input elements
input:1, textarea:1, select:1, option:1, optgroup: 1, button:1
}
exposed.NON_CONTIGUOUS_PROSE_ELEMENTS = {
// Elements that will not contain prose or block elements where we don't
// want prose to be matches across element borders:
// Block Elements
address:1, article:1, aside:1, blockquote:1, dd:1, div:1,
dl:1, fieldset:1, figcaption:1, figure:1, footer:1, form:1, h1:1, h2:1, h3:1,
h4:1, h5:1, h6:1, header:1, hgroup:1, hr:1, main:1, nav:1, noscript:1, ol:1,
output:1, p:1, pre:1, section:1, ul:1,
// Other misc. elements that are not part of continuous inline prose:
br:1, li: 1, summary: 1, dt:1, details:1, rp:1, rt:1, rtc:1,
// Media / Source elements:
script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
// Input elements
input:1, textarea:1, select:1, option:1, optgroup: 1, button:1,
// Table related elements:
table:1, tbody:1, thead:1, th:1, tr:1, td:1, caption:1, col:1, tfoot:1, colgroup:1
}
exposed.NON_INLINE_PROSE = function(el) {
return hasOwn.call(exposed.NON_CONTIGUOUS_PROSE_ELEMENTS, el.nodeName.toLowerCase())
}
// Presets accessed via `options.preset` when calling findAndReplaceDOMText():
exposed.PRESETS = {
prose: {
forceContext: exposed.NON_INLINE_PROSE,
filterElements: function(el) {
return !hasOwn.call(exposed.NON_PROSE_ELEMENTS, el.nodeName.toLowerCase())
}
}
}
exposed.Finder = Finder
/**
* Finder -- encapsulates logic to find and replace.
*/
function Finder(node, options) {
var preset = options.preset && exposed.PRESETS[options.preset]
options.portionMode = options.portionMode || PORTION_MODE_RETAIN
if (preset) {
for (var i in preset) {
if (hasOwn.call(preset, i) && !hasOwn.call(options, i)) {
options[i] = preset[i]
}
}
}
this.node = node
this.options = options
// ENable match-preparation method to be passed as option:
this.prepMatch = options.prepMatch || this.prepMatch
this.reverts = []
this.matches = this.search()
if (this.matches.length) {
this.processMatches()
}
}
Finder.prototype = {
/**
* Searches for all matches that comply with the instance's 'match' option
*/
search: function() {
var match
var matchIndex = 0
var offset = 0
var regex = this.options.find
var textAggregation = this.getAggregateText()
var matches = []
var self = this
regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex
matchAggregation(textAggregation)
function matchAggregation(textAggregation) {
for (var i = 0, l = textAggregation.length; i < l; ++i) {
var text = textAggregation[i]
if (typeof text !== 'string') {
// Deal with nested contexts: (recursive)
matchAggregation(text)
continue
}
if (regex.global) {
while (match = regex.exec(text)) {
matches.push(self.prepMatch(match, matchIndex++, offset))
}
} else {
if (match = text.match(regex)) {
matches.push(self.prepMatch(match, 0, offset))
}
}
offset += text.length
}
}
return matches
},
/**
* Prepares a single match with useful meta info:
*/
prepMatch: function(match, matchIndex, characterOffset) {
if (!match[0]) {
throw new Error('findAndReplaceDOMText cannot handle zero-length matches')
}
match.endIndex = characterOffset + match.index + match[0].length
match.startIndex = characterOffset + match.index
match.index = matchIndex
return match
},
/**
* Gets aggregate text within subject node
*/
getAggregateText: function() {
var elementFilter = this.options.filterElements
var forceContext = this.options.forceContext
return getText(this.node)
/**
* Gets aggregate text of a node without resorting
* to broken innerText/textContent
*/
function getText(node, txt) {
if (node.nodeType === 3) {
return [node.data]
}
if (elementFilter && !elementFilter(node)) {
return []
}
var txt = ['']
var i = 0
if (node = node.firstChild) do {
if (node.nodeType === 3) {
txt[i] += node.data
continue
}
var innerText = getText(node)
if (
forceContext &&
node.nodeType === 1 &&
(forceContext === true || forceContext(node))
) {
txt[++i] = innerText
txt[++i] = ''
} else {
if (typeof innerText[0] === 'string') {
// Bridge nested text-node data so that they're
// not considered their own contexts:
// I.e. ['some', ['thing']] -> ['something']
txt[i] += innerText.shift()
}
if (innerText.length) {
txt[++i] = innerText
txt[++i] = ''
}
}
} while (node = node.nextSibling)
return txt
}
},
/**
* Steps through the target node, looking for matches, and
* calling replaceFn when a match is found.
*/
processMatches: function() {
var matches = this.matches
var node = this.node
var elementFilter = this.options.filterElements
var startPortion,
endPortion,
innerPortions = [],
curNode = node,
match = matches.shift(),
atIndex = 0, // i.e. nodeAtIndex
matchIndex = 0,
portionIndex = 0,
doAvoidNode,
nodeStack = [node]
out: while (true) {
if (curNode.nodeType === 3) {
if (!endPortion && curNode.length + atIndex >= match.endIndex) {
// We've found the ending
endPortion = {
node: curNode,
index: portionIndex++,
text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex),
indexInMatch: atIndex - match.startIndex,
indexInNode: match.startIndex - atIndex, // always zero for end-portions
endIndexInNode: match.endIndex - atIndex,
isEnd: true
}
} else if (startPortion) {
// Intersecting node
innerPortions.push({
node: curNode,
index: portionIndex++,
text: curNode.data,
indexInMatch: atIndex - match.startIndex,
indexInNode: 0 // always zero for inner-portions
})
}
if (!startPortion && curNode.length + atIndex > match.startIndex) {
// We've found the match start
startPortion = {
node: curNode,
index: portionIndex++,
indexInMatch: 0,
indexInNode: match.startIndex - atIndex,
endIndexInNode: match.endIndex - atIndex,
text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex)
}
}
atIndex += curNode.data.length
}
doAvoidNode = curNode.nodeType === 1 && elementFilter && !elementFilter(curNode)
if (startPortion && endPortion) {
curNode = this.replaceMatch(match, startPortion, innerPortions, endPortion)
// processMatches has to return the node that replaced the endNode
// and then we step back so we can continue from the end of the
// match:
atIndex -= (endPortion.node.data.length - endPortion.endIndexInNode)
startPortion = null
endPortion = null
innerPortions = []
match = matches.shift()
portionIndex = 0
matchIndex++
if (!match) {
break; // no more matches
}
} else if (
!doAvoidNode &&
(curNode.firstChild || curNode.nextSibling)
) {
// Move down or forward:
if (curNode.firstChild) {
nodeStack.push(curNode)
curNode = curNode.firstChild
} else {
curNode = curNode.nextSibling
}
continue
}
// Move forward or up:
while (true) {
if (curNode.nextSibling) {
curNode = curNode.nextSibling
break
}
curNode = nodeStack.pop()
if (curNode === node) {
break out
}
}
}
},
/**
* Reverts ... TODO
*/
revert: function() {
// Reversion occurs backwards so as to avoid nodes subsequently
// replaced during the matching phase (a forward process):
for (var l = this.reverts.length; l--;) {
this.reverts[l]()
}
this.reverts = []
},
prepareReplacementString: function(string, portion, match, matchIndex) {
var portionMode = this.options.portionMode
if (
portionMode === PORTION_MODE_FIRST &&
portion.indexInMatch > 0
) {
return ''
}
string = string.replace(/\$(\d+|&|`|')/g, function($0, t) {
var replacement
switch(t) {
case '&':
replacement = match[0]
break
case '`':
replacement = match.input.substring(0, match.startIndex)
break
case '\'':
replacement = match.input.substring(match.endIndex)
break
default:
replacement = match[+t]
}
return replacement
})
if (portionMode === PORTION_MODE_FIRST) {
return string
}
if (portion.isEnd) {
return string.substring(portion.indexInMatch)
}
return string.substring(portion.indexInMatch, portion.indexInMatch + portion.text.length)
},
getPortionReplacementNode: function(portion, match, matchIndex) {
var replacement = this.options.replace || '$&'
var wrapper = this.options.wrap
if (wrapper && wrapper.nodeType) {
// Wrapper has been provided as a stencil-node for us to clone:
var clone = doc.createElement('div')
clone.innerHTML = wrapper.outerHTML || new XMLSerializer().serializeToString(wrapper)
wrapper = clone.firstChild
}
if (typeof replacement == 'function') {
replacement = replacement(portion, match, matchIndex)
if (replacement && replacement.nodeType) {
return replacement
}
return doc.createTextNode(String(replacement))
}
var el = typeof wrapper == 'string' ? doc.createElement(wrapper) : wrapper
replacement = doc.createTextNode(
this.prepareReplacementString(
replacement, portion, match, matchIndex
)
)
if (!replacement.data) {
return replacement
}
if (!el) {
return replacement
}
el.appendChild(replacement)
return el
},
replaceMatch: function(match, startPortion, innerPortions, endPortion) {
var matchStartNode = startPortion.node
var matchEndNode = endPortion.node
var preceedingTextNode
var followingTextNode
if (matchStartNode === matchEndNode) {
var node = matchStartNode
if (startPortion.indexInNode > 0) {
// Add `before` text node (before the match)
preceedingTextNode = doc.createTextNode(node.data.substring(0, startPortion.indexInNode))
node.parentNode.insertBefore(preceedingTextNode, node)
}
// Create the replacement node:
var newNode = this.getPortionReplacementNode(
endPortion,
match
)
node.parentNode.insertBefore(newNode, node)
if (endPortion.endIndexInNode < node.length) { // ?????
// Add `after` text node (after the match)
followingTextNode = doc.createTextNode(node.data.substring(endPortion.endIndexInNode))
node.parentNode.insertBefore(followingTextNode, node)
}
node.parentNode.removeChild(node)
this.reverts.push(function() {
if (preceedingTextNode === newNode.previousSibling) {
preceedingTextNode.parentNode.removeChild(preceedingTextNode)
}
if (followingTextNode === newNode.nextSibling) {
followingTextNode.parentNode.removeChild(followingTextNode)
}
newNode.parentNode.replaceChild(node, newNode)
})
return newNode
} else {
// Replace matchStartNode -> [innerMatchNodes...] -> matchEndNode (in that order)
preceedingTextNode = doc.createTextNode(
matchStartNode.data.substring(0, startPortion.indexInNode)
)
followingTextNode = doc.createTextNode(
matchEndNode.data.substring(endPortion.endIndexInNode)
)
var firstNode = this.getPortionReplacementNode(
startPortion,
match
)
var innerNodes = []
for (var i = 0, l = innerPortions.length; i < l; ++i) {
var portion = innerPortions[i]
var innerNode = this.getPortionReplacementNode(
portion,
match
)
portion.node.parentNode.replaceChild(innerNode, portion.node)
this.reverts.push((function(portion, innerNode) {
return function() {
innerNode.parentNode.replaceChild(portion.node, innerNode)
}
}(portion, innerNode)))
innerNodes.push(innerNode)
}
var lastNode = this.getPortionReplacementNode(
endPortion,
match
)
matchStartNode.parentNode.insertBefore(preceedingTextNode, matchStartNode)
matchStartNode.parentNode.insertBefore(firstNode, matchStartNode)
matchStartNode.parentNode.removeChild(matchStartNode)
matchEndNode.parentNode.insertBefore(lastNode, matchEndNode)
matchEndNode.parentNode.insertBefore(followingTextNode, matchEndNode)
matchEndNode.parentNode.removeChild(matchEndNode)
this.reverts.push(function() {
preceedingTextNode.parentNode.removeChild(preceedingTextNode)
firstNode.parentNode.replaceChild(matchStartNode, firstNode)
followingTextNode.parentNode.removeChild(followingTextNode)
lastNode.parentNode.replaceChild(matchEndNode, lastNode)
})
return lastNode
}
}
}
return exposed
}())
);
var isNodeNormalizeNormal = (function() {
//// Disabled `Node.normalize()` for temp due to
//// issue below in IE11.
//// See: http://stackoverflow.com/questions/22337498/why-does-ie11-handle-node-normalize-incorrectly-for-the-minus-symbol
var div = $.create( 'div' )
div.appendChild($.create( '', '0-' ))
div.appendChild($.create( '', '2' ))
div.normalize()
return div.firstChild.length !== 2
})()
function getFuncOrElmt( obj ) {
return (
typeof obj === 'function' ||
obj instanceof Element
)
? obj
: undefined
}
function createBDGroup( portion ) {
var clazz = portion.index === 0 && portion.isEnd
? 'biaodian cjk'
: 'biaodian cjk portion ' + (
portion.index === 0
? 'is-first'
: portion.isEnd
? 'is-end'
: 'is-inner'
)
var $elmt = $.create( 'h-char-group', clazz )
$elmt.innerHTML = portion.text
return $elmt
}
function createBDChar( char ) {
var div = $.create( 'div' )
var unicode = char.charCodeAt( 0 ).toString( 16 )
div.innerHTML = (
'<h-char unicode="' + unicode +
'" class="biaodian cjk ' + getBDType( char ) +
'">' + char + '</h-char>'
)
return div.firstChild
}
function getBDType( char ) {
return char.match( TYPESET.char.biaodian.open )
? 'bd-open'
: char.match( TYPESET.char.biaodian.close )
? 'bd-close bd-end'
: char.match( TYPESET.char.biaodian.end )
? (
/(?:\u3001|\u3002|\uff0c)/i.test( char )
? 'bd-end bd-cop'
: 'bd-end'
)
: char.match(new RegExp( UNICODE.biaodian.liga ))
? 'bd-liga'
: char.match(new RegExp( UNICODE.biaodian.middle ))
? 'bd-middle'
: ''
}
$.extend( Fibre.fn, {
normalize: function() {
if ( isNodeNormalizeNormal ) {
this.context.normalize()
}
return this
},
// Force punctuation & biaodian typesetting rules to be applied.
jinzify: function( selector ) {
return (
this
.filter( selector || null )
.avoid( 'h-jinze' )
.replace(
TYPESET.jinze.touwei,
function( portion, match ) {
var elem = $.create( 'h-jinze', 'touwei' )
elem.innerHTML = match[0]
return (( portion.index === 0 && portion.isEnd ) || portion.index === 1 ) ? elem : ''
}
)
.replace(
TYPESET.jinze.wei,
function( portion, match ) {
var elem = $.create( 'h-jinze', 'wei' )
elem.innerHTML = match[0]
return portion.index === 0 ? elem : ''
}
)
.replace(
TYPESET.jinze.tou,
function( portion, match ) {
var elem = $.create( 'h-jinze', 'tou' )
elem.innerHTML = match[0]
return (( portion.index === 0 && portion.isEnd ) || portion.index === 1 )
? elem : ''
}
)
.replace(
TYPESET.jinze.middle,
function( portion, match ) {
var elem = $.create( 'h-jinze', 'middle' )
elem.innerHTML = match[0]
return (( portion.index === 0 && portion.isEnd ) || portion.index === 1 )
? elem : ''
}
)
.endAvoid()
.endFilter()
)
},
groupify: function( option ) {
var option = $.extend({
biaodian: false,
//punct: false,
hanzi: false, // Includes Kana
kana: false,
eonmun: false,
western: false // Includes Latin, Greek and Cyrillic
}, option || {})
this.avoid( 'h-word, h-char-group' )
if ( option.biaodian ) {
this.replace(
TYPESET.group.biaodian[0], createBDGroup
).replace(
TYPESET.group.biaodian[1], createBDGroup
)
}
if ( option.hanzi || option.cjk ) {
this.wrap(
TYPESET.group.hanzi, $.clone($.create( 'h-char-group', 'hanzi cjk' ))
)
}
if ( option.western ) {
this.wrap(
TYPESET.group.western, $.clone($.create( 'h-word', 'western' ))
)
}
if ( option.kana ) {
this.wrap(
TYPESET.group.kana, $.clone($.create( 'h-char-group', 'kana' ))
)
}
if ( option.eonmun || option.hangul ) {
this.wrap(
TYPESET.group.eonmun, $.clone($.create( 'h-word', 'eonmun hangul' ))
)
}
this.endAvoid()
return this
},
charify: function( option ) {
var option = $.extend({
avoid: true,
biaodian: false,
punct: false,
hanzi: false, // Includes Kana
latin: false,
ellinika: false,
kirillica: false,
kana: false,
eonmun: false
}, option || {})
if ( option.avoid ) {
this.avoid( 'h-char' )
}
if ( option.biaodian ) {
this.replace(
TYPESET.char.biaodian.all,
getFuncOrElmt( option.biaodian )
||
function( portion ) { return createBDChar( portion.text ) }
).replace(
TYPESET.char.biaodian.liga,
getFuncOrElmt( option.biaodian )
||
function( portion ) { return createBDChar( portion.text ) }
)
}
if ( option.hanzi || option.cjk ) {
this.wrap(
TYPESET.char.hanzi,
getFuncOrElmt( option.hanzi || option.cjk )
||
$.clone($.create( 'h-char', 'hanzi cjk' ))
)
}
if ( option.punct ) {
this.wrap(
TYPESET.char.punct.all,
getFuncOrElmt( option.punct )
||
$.clone($.create( 'h-char', 'punct' ))
)
}
if ( option.latin ) {
this.wrap(
TYPESET.char.latin,
getFuncOrElmt( option.latin )
||
$.clone($.create( 'h-char', 'alphabet latin' ))
)
}
if ( option.ellinika || option.greek ) {
this.wrap(
TYPESET.char.ellinika,
getFuncOrElmt( option.ellinika || option.greek )
||
$.clone($.create( 'h-char', 'alphabet ellinika greek' ))
)
}
if ( option.kirillica || option.cyrillic ) {
this.wrap(
TYPESET.char.kirillica,
getFuncOrElmt( option.kirillica || option.cyrillic )
||
$.clone($.create( 'h-char', 'alphabet kirillica cyrillic' ))
)
}
if ( option.kana ) {
this.wrap(
TYPESET.char.kana,
getFuncOrElmt( option.kana )
||
$.clone($.create( 'h-char', 'kana' ))
)
}
if ( option.eonmun || option.hangul ) {
this.wrap(
TYPESET.char.eonmun,
getFuncOrElmt( option.eonmun || option.hangul )
||
$.clone($.create( 'h-char', 'eonmun hangul' ))
)
}
this.endAvoid()
return this
}
})
$.extend( Han, {
isNodeNormalizeNormal: isNodeNormalizeNormal,
find: Fibre,
createBDGroup: createBDGroup,
createBDChar: createBDChar
})
$.matches = Han.find.matches
void [
'setMode',
'wrap', 'replace', 'revert',
'addBoundary', 'removeBoundary',
'avoid', 'endAvoid',
'filter', 'endFilter',
'jinzify', 'groupify', 'charify'
].forEach(function( method ) {
Han.fn[ method ] = function() {
if ( !this.finder ) {
// Share the same selector
this.finder = Han.find( this.context )
}
this.finder[ method ]( arguments[ 0 ], arguments[ 1 ] )
return this
}
})
var Locale = {}
function writeOnCanvas( text, font ) {
var canvas = $.create( 'canvas' )
var context
canvas.width = '50'
canvas.height = '20'
canvas.style.display = 'none'
body.appendChild( canvas )
context = canvas.getContext( '2d' )
context.textBaseline = 'top'
context.font = '15px ' + font + ', sans-serif'
context.fillStyle = 'black'
context.strokeStyle = 'black'
context.fillText( text, 0, 0 )
return {
node: canvas,
context: context,
remove: function() {
$.remove( canvas, body )
}
}
}
function compareCanvases( treat, control ) {
var ret
var a = treat.context
var b = control.context
try {
for ( var j = 1; j <= 20; j++ ) {
for ( var i = 1; i <= 50; i++ ) {
if (
typeof ret === 'undefined' &&
a.getImageData(i, j, 1, 1).data[3] !== b.getImageData(i, j, 1, 1).data[3]
) {
ret = false
break
} else if ( typeof ret === 'boolean' ) {
break
}
if ( i === 50 && j === 20 && typeof ret === 'undefined' ) {
ret = true
}
}
}
// Remove and clean from memory
treat.remove()
control.remove()
treat = null
control = null
return ret
} catch (e) {}
return false
}
function detectFont( treat, control, text ) {
var treat = treat
var control = control || 'sans-serif'
var text = text || '辭Q'
var ret
control = writeOnCanvas( text, control )
treat = writeOnCanvas( text, treat )
return !compareCanvases( treat, control )
}
Locale.writeOnCanvas = writeOnCanvas
Locale.compareCanvases = compareCanvases
Locale.detectFont = detectFont
Locale.support = (function() {
var PREFIX = 'Webkit Moz ms'.split(' ')
// Create an element for feature detecting
// (in `testCSSProp`)
var elem = $.create( 'h-test' )
function testCSSProp( prop ) {
var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1)
var allProp = ( prop + ' ' + PREFIX.join( ucProp + ' ' ) + ucProp ).split(' ')
var ret
allProp.forEach(function( prop ) {
if ( typeof elem.style[ prop ] === 'string' ) {
ret = true
}
})
return ret || false
}
function injectElementWithStyle( rule, callback ) {
var fakeBody = body || $.create( 'body' )
var div = $.create( 'div' )
var container = body ? div : fakeBody
var callback = typeof callback === 'function' ? callback : function() {}
var style, ret, docOverflow
style = [ '<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 all target elements and address
// presentational corrections if any two of
// them are adjacent to each other.
renderDecoLine: function( context, target ) {
var $$target = $.qsa( target || 'u, ins', context )
var i = $$target.length
traverse: while ( i-- ) {
var $this = $$target[ i ]
var $prev = null
// Ignore all `<wbr>` and comments in between,
// and add class `.adjacent` once two targets
// are next to each other.
ignore: do {
$prev = ( $prev || $this ).previousSibling
if ( !$prev ) {
continue traverse
} else if ( $$target[ i-1 ] === $prev ) {
$this.classList.add( 'adjacent' )
}
} while ( $.isIgnorable( $prev ))
}
},
// Traverse all target elements to render
// emphasis marks.
renderEm: function( context, target ) {
var method = target ? 'qsa' : 'tag'
var target = target || 'em'
var $target = $[ method ]( target, context )
$target
.forEach(function( elem ) {
var $elem = Han( elem )
if ( Locale.support.textemphasis ) {
$elem
.avoid( 'rt, h-char' )
.charify({ biaodian: true, punct: true })
} else {
$elem
.avoid( 'rt, h-char, h-char-group' )
.jinzify()
.groupify({ western: true })
.charify({
hanzi: true,
biaodian: true,
punct: true,
latin: true,
ellinika: true,
kirillica: true
})
}
})
}
})
Han.normalize = Locale
Han.localize = Locale
Han.support = Locale.support
Han.detectFont = Locale.detectFont
Han.fn.initCond = function() {
this.condition.classList.add( 'han-js-rendered' )
Han.normalize.initCond( this.condition )
return this
}
void [
'Elem',
'DecoLine',
'Em',
'Ruby'
].forEach(function( elem ) {
var method = 'render' + elem
Han.fn[ method ] = function( target ) {
Han.normalize[ method ]( this.context, target )
return this
}
})
$.extend( Han.support, {
// Assume that all devices support Heiti for we
// use `sans-serif` to do the comparison.
heiti: true,
// 'heiti-gb': true,
songti: Han.detectFont( '"Han Songti"' ),
'songti-gb': Han.detectFont( '"Han Songti GB"' ),
kaiti: Han.detectFont( '"Han Kaiti"' ),
// 'kaiti-gb': Han.detectFont( '"Han Kaiti GB"' ),
fangsong: Han.detectFont( '"Han Fangsong"' )
// 'fangsong-gb': Han.detectFont( '"Han Fangsong GB"' )
})
Han.correctBiaodian = function( context ) {
var context = context || document
var finder = Han.find( context )
finder
.avoid( 'h-char' )
.replace( /([‘“])/g, function( portion ) {
var $char = Han.createBDChar( portion.text )
$char.classList.add( 'bd-open', 'punct' )
return $char
})
.replace( /([’”])/g, function( portion ) {
var $char = Han.createBDChar( portion.text )
$char.classList.add( 'bd-close', 'bd-end', 'punct' )
return $char
})
return Han.support.unicoderange
? finder
: finder.charify({ biaodian: true })
}
Han.correctBasicBD = Han.correctBiaodian
Han.correctBD = Han.correctBiaodian
$.extend( Han.fn, {
biaodian: null,
correctBiaodian: function() {
this.biaodian = Han.correctBiaodian( this.context )
return this
},
revertCorrectedBiaodian: function() {
try {
this.biaodian.revert( 'all' )
} catch (e) {}
return this
}
})
// Legacy support (deprecated):
Han.fn.correctBasicBD = Han.fn.correctBiaodian
Han.fn.revertBasicBD = Han.fn.revertCorrectedBiaodian
var hws = '<<hws>>'
var $hws = $.create( 'h-hws' )
$hws.setAttribute( 'hidden', '' )
$hws.innerHTML = ' '
function sharingSameParent( $a, $b ) {
return $a && $b && $a.parentNode === $b.parentNode
}
function properlyPlaceHWSBehind( $node, text ) {
var $elmt = $node
var text = text || ''
if (
$.isElmt( $node.nextSibling ) ||
sharingSameParent( $node, $node.nextSibling )
) {
return text + hws
} else {
// One of the parental elements of the current text
// node would definitely have a next sibling, since
// it is of the first portion and not `isEnd`.
while ( !$elmt.nextSibling ) {
$elmt = $elmt.parentNode
}
if ( $node !== $elmt ) {
$elmt.insertAdjacentHTML( 'afterEnd', '<h-hws hidden> </h-hws>' )
}
}
return text
}
function firstStepLabel( portion, mat ) {
return portion.isEnd && portion.index === 0
? mat[1] + hws + mat[2]
: portion.index === 0
? properlyPlaceHWSBehind( portion.node, portion.text )
: portion.text
}
function real$hwsElmt( portion ) {
return portion.index === 0
? $.clone( $hws )
: ''
}
var last$hwsIdx
function apostrophe( portion ) {
var $elmt = portion.node.parentNode
if ( portion.index === 0 ) {
last$hwsIdx = portion.endIndexInNode-2
}
if (
$elmt.nodeName.toLowerCase() === 'h-hws' && (
portion.index === 1 || portion.indexInMatch === last$hwsIdx
)) {
$elmt.classList.add( 'quote-inner' )
}
return portion.text
}
function curveQuote( portion ) {
var $elmt = portion.node.parentNode
if ( $elmt.nodeName.toLowerCase() === 'h-hws' ) {
$elmt.classList.add( 'quote-outer' )
}
return portion.text
}
$.extend( Han, {
renderHWS: function( context, strict ) {
// Elements to be filtered according to the
// HWS rendering mode.
var AVOID = strict
? 'textarea, code, kbd, samp, pre'
: 'textarea'
var mode = strict ? 'strict' : 'base'
var context = context || document
var finder = Han.find( context )
finder
.avoid( AVOID )
// Basic situations:
// - 字a => 字<hws/>a
// - A字 => A<hws/>字
.replace( Han.TYPESET.hws[ mode ][0], firstStepLabel )
.replace( Han.TYPESET.hws[ mode ][1], firstStepLabel )
// Convert text nodes `<hws/>` into real element nodes:
.replace( new RegExp( '(' + hws + ')+', 'g' ), real$hwsElmt )
// Deal with:
// - '<hws/>字<hws/>' => '字'
// - "<hws/>字<hws/>" => "字"
.replace( /([\'"])\s(.+?)\s\1/g, apostrophe )
// Deal with:
// - <hws/>“字”<hws/>
// - <hws/>‘字’<hws/>
.replace( /\s[‘“]/g, curveQuote )
.replace( /[’”]\s/g, curveQuote )
.normalize()
// Return the finder instance for future usage
return finder
}
})
$.extend( Han.fn, {
renderHWS: function( strict ) {
Han.renderHWS( this.context, strict )
return this
},
revertHWS: function() {
$.tag( 'h-hws', this.context )
.forEach(function( hws ) {
$.remove( hws )
})
this.HWS = []
return this
}
})
var HANGABLE_CLASS = 'bd-hangable'
var HANGABLE_AVOID = 'h-char.bd-hangable'
var HANGABLE_CS_HTML = '<h-cs hidden class="jinze-outer hangable-outer"> </h-cs>'
var matches = Han.find.matches
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 )
return ret
}
function insertHangableCS( $jinze ) {
var $cs = $jinze.nextSibling
if ( $cs && matches( $cs, 'h-cs.jinze-outer' )) {
$cs.classList.add( 'hangable-outer' )
} else {
$jinze.insertAdjacentHTML(
'afterend',
HANGABLE_CS_HTML
)
}
}
Han.support['han-space'] = detectSpaceFont()
$.extend( Han, {
detectSpaceFont: detectSpaceFont,
isSpaceFontLoaded: detectSpaceFont(),
renderHanging: function( context ) {
var context = context || document
var finder = Han.find( context )
finder
.avoid( 'textarea, code, kbd, samp, pre' )
.avoid( HANGABLE_AVOID )
.replace(
TYPESET.jinze.hanging,
function( portion ) {
if ( /^[\x20\t\r\n\f]+$/.test( portion.text )) {
return ''
}
var $elmt = portion.node.parentNode
var $jinze, $new, $bd, biaodian
if ( $jinze = $.parent( $elmt, 'h-jinze' )) {
insertHangableCS( $jinze )
}
biaodian = portion.text.trim()
$new = Han.createBDChar( biaodian )
$new.innerHTML = '<h-inner>' + biaodian + '</h-inner>'
$new.classList.add( HANGABLE_CLASS )
$bd = $.parent( $elmt, 'h-char.biaodian' )
return !$bd
? $new
: (function() {
$bd.classList.add( HANGABLE_CLASS )
return matches( $elmt, 'h-inner, h-inner *' )
? biaodian
: $new.firstChild
})()
}
)
return finder
}
})
$.extend( Han.fn, {
renderHanging: function() {
var classList = this.condition.classList
Han.isSpaceFontLoaded = detectSpaceFont()
if (
Han.isSpaceFontLoaded &&
classList.contains( 'no-han-space' )
) {
classList.remove( 'no-han-space' )
classList.add( 'han-space' )
}
Han.renderHanging( this.context )
return this
},
revertHanging: function() {
$.qsa(
'h-char.bd-hangable, h-cs.hangable-outer',
this.context
).forEach(function( $elmt ) {
var classList = $elmt.classList
classList.remove( 'bd-hangable' )
classList.remove( 'hangable-outer' )
})
return this
}
})
var JIYA_CLASS = 'bd-jiya'
var JIYA_AVOID = 'h-char.bd-jiya'
var CONSECUTIVE_CLASS = 'bd-consecutive'
var JIYA_CS_HTML = '<h-cs hidden class="jinze-outer jiya-outer"> </h-cs>'
var matches = Han.find.matches
function trimBDClass( clazz ) {
return clazz.replace(
/(biaodian|cjk|bd-jiya|bd-consecutive|bd-hangable)/gi, ''
).trim()
}
function charifyBiaodian( portion ) {
var biaodian = portion.text
var $elmt = portion.node.parentNode
var $bd = $.parent( $elmt, 'h-char.biaodian' )
var $new = Han.createBDChar( biaodian )
var $jinze
$new.innerHTML = '<h-inner>' + biaodian + '</h-inner>'
$new.classList.add( JIYA_CLASS )
if ( $jinze = $.parent( $elmt, 'h-jinze' )) {
insertJiyaCS( $jinze )
}
return !$bd
? $new
: (function() {
$bd.classList.add( JIYA_CLASS )
return matches( $elmt, 'h-inner, h-inner *' )
? biaodian
: $new.firstChild
})()
}
var prevBDType, $$prevCS
function locateConsecutiveBD( portion ) {
var prev = prevBDType
var $elmt = portion.node.parentNode
var $bd = $.parent( $elmt, 'h-char.biaodian' )
var $jinze = $.parent( $bd, 'h-jinze' )
var classList
classList = $bd.classList
if ( prev ) {
$bd.setAttribute( 'prev', prev )
}
if ( $$prevCS && classList.contains( 'bd-open' )) {
$$prevCS.pop().setAttribute( 'next', 'bd-open' )
}
$$prevCS = undefined
if ( portion.isEnd ) {
prevBDType = undefined
classList.add( CONSECUTIVE_CLASS, 'end-portion' )
} else {
prevBDType = trimBDClass($bd.getAttribute( 'class' ))
classList.add( CONSECUTIVE_CLASS )
}
if ( $jinze ) {
$$prevCS = locateCS( $jinze, {
prev: prev,
'class': trimBDClass($bd.getAttribute( 'class' ))
})
}
return portion.text
}
function insertJiyaCS( $jinze ) {
if (
matches( $jinze, '.tou, .touwei' ) &&
!matches( $jinze.previousSibling, 'h-cs.jiya-outer' )
) {
$jinze.insertAdjacentHTML( 'beforebegin', JIYA_CS_HTML )
}
if (
matches( $jinze, '.wei, .touwei' ) &&
!matches( $jinze.nextSibling, 'h-cs.jiya-outer' )
) {
$jinze.insertAdjacentHTML( 'afterend', JIYA_CS_HTML )
}
}
function locateCS( $jinze, attr ) {
var $prev, $next
if (matches( $jinze, '.tou, .touwei' )) {
$prev = $jinze.previousSibling
if (matches( $prev, 'h-cs' )) {
$prev.className = 'jinze-outer jiya-outer'
$prev.setAttribute( 'prev', attr.prev )
}
}
if (matches( $jinze, '.wei, .touwei' )) {
$next = $jinze.nextSibling
if (matches( $next, 'h-cs' )) {
$next.className = 'jinze-outer jiya-outer ' + attr[ 'class' ]
$next.removeAttribute( 'prev' )
}
}
return [ $prev, $next ]
}
Han.renderJiya = function( context ) {
var context = context || document
var finder = Han.find( context )
finder
.avoid( 'textarea, code, kbd, samp, pre, h-cs' )
.avoid( JIYA_AVOID )
.charify({
avoid: false,
biaodian: charifyBiaodian
})
// End avoiding `JIYA_AVOID`:
.endAvoid()
.avoid( 'textarea, code, kbd, samp, pre, h-cs' )
.replace( TYPESET.group.biaodian[0], locateConsecutiveBD )
.replace( TYPESET.group.biaodian[1], locateConsecutiveBD )
return finder
}
$.extend( Han.fn, {
renderJiya: function() {
Han.renderJiya( this.context )
return this
},
revertJiya: function() {
$.qsa(
'h-char.bd-jiya, h-cs.jiya-outer',
this.context
).forEach(function( $elmt ) {
var classList = $elmt.classList
classList.remove( 'bd-jiya' )
classList.remove( 'jiya-outer' )
})
return this
}
})
var QUERY_RU_W_ANNO = 'h-ru[annotation]'
var SELECTOR_TO_IGNORE = 'textarea, code, kbd, samp, pre'
function createCompareFactory( font, treat, control ) {
return function() {
var a = Han.localize.writeOnCanvas( treat, font )
var b = Han.localize.writeOnCanvas( control, font )
return Han.localize.compareCanvases( a, b )
}
}
function isVowelCombLigaNormal() {
return createCompareFactory( '"Romanization Sans"', '\u0061\u030D', '\uDB80\uDC61' )
}
function isVowelICombLigaNormal() {
return createCompareFactory( '"Romanization Sans"', '\u0069\u030D', '\uDB80\uDC69' )
}
function isZhuyinCombLigaNormal() {
return createCompareFactory( '"Zhuyin Kaiti"', '\u31B4\u0358', '\uDB8C\uDDB4' )
}
function createSubstFactory( regexToSubst ) {
return function( context ) {
var context = context || document
var finder = Han.find( context ).avoid( SELECTOR_TO_IGNORE )
regexToSubst
.forEach(function( pattern ) {
finder
.replace(
new RegExp( pattern[ 0 ], 'ig' ),
function( portion, match ) {
var ret = $.clone( charCombLiga )
// Put the original content in an inner container
// for better presentational effect of hidden text
ret.innerHTML = '<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
});