3006 lines
80 KiB
JavaScript
3006 lines
80 KiB
JavaScript
/*!
|
||
* 漢字標準格式 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](位擴展A、B區)
|
||
Cyrillic combining diacritical marks (in extended-A, B)
|
||
*/
|
||
kirillica: {
|
||
base: '[0-9\u0400-\u0482\u048A-\u04FF\u0500-\u052F\uA640-\uA66E\uA67E-\uA697]',
|
||
combine: '[\u0483-\u0489\u2DE0-\u2DFF\uA66F-\uA67D\uA69F]'
|
||
},
|
||
|
||
/**
|
||
* Kana (假名)
|
||
*
|
||
* 1. 日文假名:[\u30A2\u30A4\u30A6\u30A8\u30AA-\u30FA\u3042\u3044\u3046\u3048\u304A-\u3094\u309F\u30FF]
|
||
Japanese Kana
|
||
* 2. 假名補充[\u1B000\u1B001](\uD82C[\uDC00-\uDC01])
|
||
Kana supplement
|
||
* 3. 日文假名小寫:[\u3041\u3043\u3045\u3047\u3049\u30A1\u30A3\u30A5\u30A7\u30A9\u3063\u3083\u3085\u3087\u308E\u3095\u3096\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6\u31F0-\u31FF]
|
||
Japanese small Kana
|
||
* 4. 假名組字符:[\u3099-\u309C]
|
||
Kana combining characters
|
||
* 5. 半形假名:[\uFF66-\uFF9F]
|
||
Halfwidth Kana
|
||
* 6. 符號:[\u309D\u309E\u30FB-\u30FE]
|
||
Marks
|
||
*/
|
||
kana: {
|
||
base: '[\u30A2\u30A4\u30A6\u30A8\u30AA-\u30FA\u3042\u3044\u3046\u3048\u304A-\u3094\u309F\u30FF]|\uD82C[\uDC00-\uDC01]',
|
||
small: '[\u3041\u3043\u3045\u3047\u3049\u30A1\u30A3\u30A5\u30A7\u30A9\u3063\u3083\u3085\u3087\u308E\u3095\u3096\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6\u31F0-\u31FF]',
|
||
combine: '[\u3099-\u309C]',
|
||
half: '[\uFF66-\uFF9F]',
|
||
mark: '[\u30A0\u309D\u309E\u30FB-\u30FE]'
|
||
},
|
||
|
||
/**
|
||
* Eonmun (Hangul, 諺文)
|
||
*
|
||
* 1. 諺文音節:[\uAC00-\uD7A3]
|
||
Eonmun (Hangul) syllables
|
||
* 2. 諺文字母:[\u1100-\u11FF\u314F-\u3163\u3131-\u318E\uA960-\uA97C\uD7B0-\uD7FB]
|
||
Eonmun (Hangul) letters
|
||
* 3. 半形諺文字母:[\uFFA1-\uFFDC]
|
||
Halfwidth Eonmun (Hangul) letters
|
||
*/
|
||
eonmun: {
|
||
base: '[\uAC00-\uD7A3]',
|
||
letter: '[\u1100-\u11FF\u314F-\u3163\u3131-\u318E\uA960-\uA97C\uD7B0-\uD7FB]',
|
||
half: '[\uFFA1-\uFFDC]'
|
||
},
|
||
|
||
/**
|
||
* Zhuyin (注音符號, Mandarin & Dialect Phonetic Symbols)
|
||
*
|
||
* 1. 國語注音、方言音符號:[\u3105-\u312D][\u31A0-\u31BA]
|
||
Bopomofo phonetic symbols
|
||
* 2. 平上去聲調號:[\u02D9\u02CA\u02C5\u02C7\u02EA\u02EB\u02CB] (**註:**國語三聲包含乙個不合規範的符號)
|
||
Level, rising, departing tones
|
||
* 3. 入聲調號:[\u31B4-\u31B7][\u0358\u030d]?
|
||
Checked (entering) tones
|
||
*/
|
||
zhuyin: {
|
||
base: '[\u3105-\u312D\u31A0-\u31BA]',
|
||
initial: '[\u3105-\u3119\u312A-\u312C\u31A0-\u31A3]',
|
||
medial: '[\u3127-\u3129]',
|
||
final: '[\u311A-\u3129\u312D\u31A4-\u31B3\u31B8-\u31BA]',
|
||
tone: '[\u02D9\u02CA\u02C5\u02C7\u02CB\u02EA\u02EB]',
|
||
checked: '[\u31B4-\u31B7][\u0358\u030d]?'
|
||
}
|
||
}
|
||
|
||
var TYPESET = (function() {
|
||
var rWhite = '[\\x20\\t\\r\\n\\f]'
|
||
// Whitespace characters
|
||
// http://www.w3.org/TR/css3-selectors/#whitespace
|
||
|
||
var rPtOpen = UNICODE.punct.open
|
||
var rPtClose = UNICODE.punct.close
|
||
var rPtEnd = UNICODE.punct.end
|
||
var rPtMid = UNICODE.punct.middle
|
||
var rPtSing = UNICODE.punct.sing
|
||
var rPt = rPtOpen + '|' + rPtEnd + '|' + rPtMid
|
||
|
||
var rBDOpen = UNICODE.biaodian.open
|
||
var rBDClose = UNICODE.biaodian.close
|
||
var rBDEnd = UNICODE.biaodian.end
|
||
var rBDMid = UNICODE.biaodian.middle
|
||
var rBDLiga = UNICODE.biaodian.liga + '{2}'
|
||
var rBD = rBDOpen + '|' + rBDEnd + '|' + rBDMid
|
||
|
||
var rKana = UNICODE.kana.base + UNICODE.kana.combine + '?'
|
||
var rKanaS = UNICODE.kana.small + UNICODE.kana.combine + '?'
|
||
var rKanaH = UNICODE.kana.half
|
||
var rEon = UNICODE.eonmun.base + '|' + UNICODE.eonmun.letter
|
||
var rEonH = UNICODE.eonmun.half
|
||
|
||
var rHan = UNICODE.hanzi.base + '|' + UNICODE.hanzi.desc + '|' + UNICODE.hanzi.radical + '|' + rKana
|
||
|
||
var rCbn = UNICODE.ellinika.combine
|
||
var rLatn = UNICODE.latin.base + rCbn + '*'
|
||
var rGk = UNICODE.ellinika.base + rCbn + '*'
|
||
|
||
var rCyCbn = UNICODE.kirillica.combine
|
||
var rCy = UNICODE.kirillica.base + rCyCbn + '*'
|
||
|
||
var rAlph = rLatn + '|' + rGk + '|' + rCy
|
||
|
||
// For words like `it's`, `Jones’s` or `'99`
|
||
var rApo = '[\u0027\u2019]'
|
||
var rChar = rHan + '|(?:' + rAlph + '|' + rApo + ')+'
|
||
|
||
var rZyS = UNICODE.zhuyin.initial
|
||
var rZyJ = UNICODE.zhuyin.medial
|
||
var rZyY = UNICODE.zhuyin.final
|
||
var rZyD = UNICODE.zhuyin.tone + '|' + UNICODE.zhuyin.checked
|
||
|
||
return {
|
||
/* Character-level selector (字級選擇器)
|
||
*/
|
||
char: {
|
||
punct: {
|
||
all: new RegExp( '(' + rPt + ')', 'g' ),
|
||
open: new RegExp( '(' + rPtOpen + ')', 'g' ),
|
||
end: new RegExp( '(' + rPtEnd + ')', 'g' ),
|
||
sing: new RegExp( '(' + rPtSing + ')', 'g' )
|
||
},
|
||
|
||
biaodian: {
|
||
all: new RegExp( '(' + rBD + ')', 'g' ),
|
||
open: new RegExp( '(' + rBDOpen + ')', 'g' ),
|
||
close: new RegExp( '(' + rBDClose + ')', 'g' ),
|
||
end: new RegExp( '(' + rBDEnd + ')', 'g' ),
|
||
liga: new RegExp( '(' + rBDLiga + ')', 'g' )
|
||
},
|
||
|
||
hanzi: new RegExp( '(' + rHan + ')', 'g' ),
|
||
|
||
latin: new RegExp( '(' + rLatn + ')', 'ig' ),
|
||
ellinika: new RegExp( '(' + rGk + ')', 'ig' ),
|
||
kirillica: new RegExp( '(' + rCy + ')', 'ig' ),
|
||
|
||
kana: new RegExp( '(' + rKana + '|' + rKanaS + '|' + rKanaH + ')', 'g' ),
|
||
eonmun: new RegExp( '(' + rEon + '|' + rEonH + ')', 'g' )
|
||
},
|
||
|
||
/* Word-level selectors (詞級選擇器)
|
||
*/
|
||
group: {
|
||
biaodian: [
|
||
new RegExp( '((' + rBD + '){2,})', 'g' ),
|
||
new RegExp( '(' + rBDLiga + rBDOpen + ')', 'g' )
|
||
],
|
||
punct: null,
|
||
hanzi: new RegExp( '(' + rHan + ')+', 'g' ),
|
||
western: new RegExp( '(' + rLatn + '|' + rGk + '|' + rCy + '|' + rPt + ')+', 'ig' ),
|
||
kana: new RegExp( '(' + rKana + '|' + rKanaS + '|' + rKanaH + ')+', 'g' ),
|
||
eonmun: new RegExp( '(' + rEon + '|' + rEonH + '|' + rPt + ')+', 'g' )
|
||
},
|
||
|
||
/* Punctuation Rules (禁則)
|
||
*/
|
||
jinze: {
|
||
hanging: new RegExp( rWhite + '*([、,。.])(?!' + rBDEnd + ')', 'ig' ),
|
||
touwei: new RegExp( '(' + rBDOpen + '+)(' + rChar + ')(' + rBDEnd + '+)', 'ig' ),
|
||
tou: new RegExp( '(' + rBDOpen + '+)(' + rChar + ')', 'ig' ),
|
||
wei: new RegExp( '(' + rChar + ')(' + rBDEnd + '+)', 'ig' ),
|
||
middle: new RegExp( '(' + rChar + ')(' + rBDMid + ')(' + rChar + ')', 'ig' )
|
||
},
|
||
|
||
zhuyin: {
|
||
form: new RegExp( '^\u02D9?(' + rZyS + ')?(' + rZyJ + ')?(' + rZyY + ')?(' + rZyD + ')?$' ),
|
||
diao: new RegExp( '(' + rZyD + ')', 'g' )
|
||
},
|
||
|
||
/* Hanzi and Western mixed spacing (漢字西文混排間隙)
|
||
* - Basic mode
|
||
* - Strict mode
|
||
*/
|
||
hws: {
|
||
base: [
|
||
new RegExp( '('+ rHan + ')(' + rAlph + '|' + rPtOpen + ')', 'ig' ),
|
||
new RegExp( '('+ rAlph + '|' + rPtEnd + ')(' + rHan + ')', 'ig' )
|
||
],
|
||
|
||
strict: [
|
||
new RegExp( '('+ rHan + ')' + rWhite + '?(' + rAlph + '|' + rPtOpen + ')', 'ig' ),
|
||
new RegExp( '('+ rAlph + '|' + rPtEnd + ')' + rWhite + '?(' + rHan + ')', 'ig' )
|
||
]
|
||
},
|
||
|
||
// The feature displays the following characters
|
||
// in its variant form for font consistency and
|
||
// presentational reason. Meanwhile, this won't
|
||
// alter the original character in the DOM.
|
||
'display-as': {
|
||
'ja-font-for-hant': [
|
||
// '夠 够',
|
||
'查 査',
|
||
'啟 啓',
|
||
'鄉 鄕',
|
||
'值 値',
|
||
'污 汚'
|
||
],
|
||
|
||
'comb-liga-pua': [
|
||
[ '\u0061[\u030d\u0358]', '\uDB80\uDC61' ],
|
||
[ '\u0065[\u030d\u0358]', '\uDB80\uDC65' ],
|
||
[ '\u0069[\u030d\u0358]', '\uDB80\uDC69' ],
|
||
[ '\u006F[\u030d\u0358]', '\uDB80\uDC6F' ],
|
||
[ '\u0075[\u030d\u0358]', '\uDB80\uDC75' ],
|
||
|
||
[ '\u31B4[\u030d\u0358]', '\uDB8C\uDDB4' ],
|
||
[ '\u31B5[\u030d\u0358]', '\uDB8C\uDDB5' ],
|
||
[ '\u31B6[\u030d\u0358]', '\uDB8C\uDDB6' ],
|
||
[ '\u31B7[\u030d\u0358]', '\uDB8C\uDDB7' ]
|
||
],
|
||
|
||
'comb-liga-vowel': [
|
||
[ '\u0061[\u030d\u0358]', '\uDB80\uDC61' ],
|
||
[ '\u0065[\u030d\u0358]', '\uDB80\uDC65' ],
|
||
[ '\u0069[\u030d\u0358]', '\uDB80\uDC69' ],
|
||
[ '\u006F[\u030d\u0358]', '\uDB80\uDC6F' ],
|
||
[ '\u0075[\u030d\u0358]', '\uDB80\uDC75' ]
|
||
],
|
||
|
||
'comb-liga-zhuyin': [
|
||
[ '\u31B4[\u030d\u0358]', '\uDB8C\uDDB4' ],
|
||
[ '\u31B5[\u030d\u0358]', '\uDB8C\uDDB5' ],
|
||
[ '\u31B6[\u030d\u0358]', '\uDB8C\uDDB6' ],
|
||
[ '\u31B7[\u030d\u0358]', '\uDB8C\uDDB7' ]
|
||
]
|
||
},
|
||
|
||
// The feature actually *converts* the character
|
||
// in the DOM for semantic reason.
|
||
//
|
||
// Note that this could be aggressive.
|
||
'inaccurate-char': [
|
||
[ '[\u2022\u2027]', '\u00B7' ],
|
||
[ '\u22EF\u22EF', '\u2026\u2026' ],
|
||
[ '\u2500\u2500', '\u2014\u2014' ],
|
||
[ '\u2035', '\u2018' ],
|
||
[ '\u2032', '\u2019' ],
|
||
[ '\u2036', '\u201C' ],
|
||
[ '\u2033', '\u201D' ]
|
||
]
|
||
}
|
||
})()
|
||
|
||
Han.UNICODE = UNICODE
|
||
Han.TYPESET = TYPESET
|
||
|
||
// Aliases
|
||
Han.UNICODE.cjk = Han.UNICODE.hanzi
|
||
Han.UNICODE.greek = Han.UNICODE.ellinika
|
||
Han.UNICODE.cyrillic = Han.UNICODE.kirillica
|
||
Han.UNICODE.hangul = Han.UNICODE.eonmun
|
||
Han.UNICODE.zhuyin.ruyun = Han.UNICODE.zhuyin.checked
|
||
|
||
Han.TYPESET.char.cjk = Han.TYPESET.char.hanzi
|
||
Han.TYPESET.char.greek = Han.TYPESET.char.ellinika
|
||
Han.TYPESET.char.cyrillic = Han.TYPESET.char.kirillica
|
||
Han.TYPESET.char.hangul = Han.TYPESET.char.eonmun
|
||
|
||
Han.TYPESET.group.hangul = Han.TYPESET.group.eonmun
|
||
Han.TYPESET.group.cjk = Han.TYPESET.group.hanzi
|
||
|
||
var $ = {
|
||
/**
|
||
* Query selectors which return arrays of the resulted
|
||
* node lists.
|
||
*/
|
||
id: function( selector, $context ) {
|
||
return ( $context || document ).getElementById( selector )
|
||
},
|
||
|
||
tag: function( selector, $context ) {
|
||
return this.makeArray(
|
||
( $context || document ).getElementsByTagName( selector )
|
||
)
|
||
},
|
||
|
||
qs: function( selector, $context ) {
|
||
return ( $context || document ).querySelector( selector )
|
||
},
|
||
|
||
qsa: function( selector, $context ) {
|
||
return this.makeArray(
|
||
( $context || document ).querySelectorAll( selector )
|
||
)
|
||
},
|
||
|
||
parent: function( $node, selector ) {
|
||
return selector
|
||
? (function() {
|
||
if ( typeof $.matches !== 'function' ) return
|
||
|
||
while (!$.matches( $node, selector )) {
|
||
if (
|
||
!$node ||
|
||
$node === document.documentElement
|
||
) {
|
||
$node = undefined
|
||
break
|
||
}
|
||
$node = $node.parentNode
|
||
}
|
||
return $node
|
||
})()
|
||
: $node
|
||
? $node.parentNode : undefined
|
||
},
|
||
|
||
/**
|
||
* Create a document fragment, a text node with text
|
||
* or an element with/without classes.
|
||
*/
|
||
create: function( name, clazz ) {
|
||
var $elmt = '!' === name
|
||
? document.createDocumentFragment()
|
||
: '' === name
|
||
? document.createTextNode( clazz || '' )
|
||
: document.createElement( name )
|
||
|
||
try {
|
||
if ( clazz ) {
|
||
$elmt.className = clazz
|
||
}
|
||
} catch (e) {}
|
||
|
||
return $elmt
|
||
},
|
||
|
||
/**
|
||
* Clone a DOM node (text, element or fragment) deeply
|
||
* or childlessly.
|
||
*/
|
||
clone: function( $node, deep ) {
|
||
return $node.cloneNode(
|
||
typeof deep === 'boolean'
|
||
? deep
|
||
: true
|
||
)
|
||
},
|
||
|
||
/**
|
||
* Remove a node (text, element or fragment).
|
||
*/
|
||
remove: function( $node ) {
|
||
return $node.parentNode.removeChild( $node )
|
||
},
|
||
|
||
/**
|
||
* Set attributes all in once with an object.
|
||
*/
|
||
setAttr: function( target, attr ) {
|
||
if ( typeof attr !== 'object' ) return
|
||
var len = attr.length
|
||
|
||
// Native `NamedNodeMap``:
|
||
if (
|
||
typeof attr[0] === 'object' &&
|
||
'name' in attr[0]
|
||
) {
|
||
for ( var i = 0; i < len; i++ ) {
|
||
if ( attr[ i ].value !== undefined ) {
|
||
target.setAttribute( attr[ i ].name, attr[ i ].value )
|
||
}
|
||
}
|
||
|
||
// Plain object:
|
||
} else {
|
||
for ( var name in attr ) {
|
||
if (
|
||
attr.hasOwnProperty( name ) &&
|
||
attr[ name ] !== undefined
|
||
) {
|
||
target.setAttribute( name, attr[ name ] )
|
||
}
|
||
}
|
||
}
|
||
return target
|
||
},
|
||
|
||
/**
|
||
* Indicate whether or not the given node is an
|
||
* element.
|
||
*/
|
||
isElmt: function( $node ) {
|
||
return $node && $node.nodeType === Node.ELEMENT_NODE
|
||
},
|
||
|
||
/**
|
||
* Indicate whether or not the given node should
|
||
* be ignored (`<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
|
||
});
|
||
|