dom = {}; (dom[a.id] = a for a in document.querySelectorAll("[id]")) debounce = (func, wait, immediate = false) -> timeout = null -> context = @ args = arguments later = -> timeout = null func.apply context, args unless immediate call_now = immediate and not timeout clearTimeout timeout timeout = setTimeout later, wait func.apply context, args if call_now hanzi_unicode_ranges = [ ["30A0", "30FF"] # katakana used for some components ["2E80", "2EFF"] ["31C0", "31EF"] ["4E00", "9FFF"] ["3400", "4DBF"] ["20000", "2A6DF"] ["2A700", "2B73F"] ["2B740", "2B81F"] ["2B820", "2CEAF"] ["2CEB0", "2EBEF"] ["30000", "3134F"] ["31350", "323AF"] ["2EBF0", "2EE5F"] ] unicode_ranges_pattern = (a, is_reject) -> "[" + (if is_reject then "^" else "") + a.map((a) -> a.map((b) -> "\\u{#{b}}").join("-")).join("") + "]" unicode_ranges_regexp = (a, is_reject, regexp_flags) -> new RegExp unicode_ranges_pattern(a, is_reject), "u" + (regexp_flags || "") non_hanzi_regexp = unicode_ranges_regexp hanzi_unicode_ranges, true latin_regexp = /([a-z]+)([0-5])?$/i class trie_node_class constructor: -> @children = {} @is_end_of_word = false class trie_class constructor: -> @root = new trie_node_class() insert: (word) -> node = @root for char in word node = node.children[char] ?= new trie_node_class() node.is_end_of_word = true search: (word) -> node = @root for char in word return false unless node.children[char]? node = node.children[char] node.is_end_of_word class character_search_class character_data: __character_data__ default_matches_limit: 20 is_syllable: (a) -> @syllable_trie.search a reset: -> dom.character_input.value = "" dom.character_results.innerHTML = "" make_svg: (svg_paths) -> result = '' result += "" for a in svg_paths # create text elements while ensuring that they do not overlap with each other min_distance = 5 placed_positions = [] for path, index in svg_paths match = /M\s*(-?\d+\.?\d*),\s*(-?\d+\.?\d*)/.exec path continue unless match x = parseFloat match[1] y = parseFloat match[2] x += 3 y -= 3 is_overlapping = (current_x, current_y) -> for pos in placed_positions dx = current_x - pos[0] dy = current_y - pos[1] distance = Math.sqrt dx * dx + dy * dy return true if distance < min_distance false original_y = y offset_step = 10 # pixels to move vertically each attempt max_attempts = 10 attempt = 0 while is_overlapping(x, y) and attempt < max_attempts y += offset_step # move the text down by offset_step pixels attempt += 1 continue if is_overlapping x, y result += "#{index + 1}" placed_positions.push [x, y] result + "" filter: => dom.character_results.innerHTML = "" values = dom.character_input.value.split(",").map (a) -> a.trim() latin_values = [] hanzi_values = [] for a in values continue unless 0 < a.length if non_hanzi_regexp.test a continue unless 1 < a.length syllable = a.match latin_regexp continue unless syllable [_, syllable, tone] = syllable if @is_syllable syllable latin_values.push new RegExp syllable + (tone || "[0-5]") else if 1 < a.length then hanzi_values = hanzi_values.concat Array.from a else hanzi_values.push a return unless latin_values.length or hanzi_values.length matches = [] for value in latin_values for data in @character_data matches.push data if value.test data[2] for value in hanzi_values data = @character_index[value] continue unless data [char, stroke_count, latin, compositions, decompositions, svg] = data unless dom.search_containing.checked || dom.search_contained.checked matches.push data continue if dom.search_containing.checked for decomposition in Array.from decompositions data = @character_index[decomposition] matches.push data if data if dom.search_contained.checked for composition in Array.from compositions data = @character_index[composition] matches.push data if data html = "" if matches.length for data in matches.slice 0, @matches_limit [char, stroke_count, latin, compositions, decompositions, svg_paths] = data if svg_paths svg = @make_svg svg_paths html += "
" html += "#{svg}
#{char}
#{latin}
" html += "
" else html += "
" html += "
#{char}
" html += "
#{stroke_count}
#{latin}
" html += "
" if @matches_limit < matches.length html += "
show #{matches.length - @matches_limit} more
" @matches_limit = @default_matches_limit dom.character_results.innerHTML = html || "no character results" constructor: (app) -> @matches_limit = @default_matches_limit filter_debounced = debounce @filter, 250 dom.character_reset.addEventListener "click", @reset dom.character_input.addEventListener "keyup", filter_debounced dom.character_input.addEventListener "change", @filter dom.search_contained.addEventListener "change", @filter dom.search_containing.addEventListener "change", @filter param_input = app.url_params.get "character_input" dom.character_input.value = param_input if param_input @character_index = {} @character_index[data[0]] = data for data in @character_data dom.character_results.addEventListener "click", (event) => # make a word search when clicking on character target = event.target if "character_show_remaining" == target.id @matches_limit = 1024 @filter() return if target.classList.contains("text_char") && !target.parentNode.classList.contains("nosvg") char = target.innerHTML return if dom.word_input.value.includes char dom.word_input.value = char app.word_search.filter() syllables = [ "a","ai","an","ang","ao","ba","bai","ban","bang","bao","bei","ben","beng","bi","bian","biang","biao","bie","bin","bing","bo","bu", "ca","cai","can","cang","cao","ce","cei","cen","ceng","cha","chai","chan","chang","chao","che","chen","cheng","chi","chong", "chou","chu","chua","chuai","chuan","chuang","chui","chun","chuo","ci","cong","cou","cu","cuan","cui","cun","cuo","da","dai","dan", "dang","dao","de","dei","den","deng","di","dian","diao","die","ding","diu","dong","dou","du","duan","dui","dun","duo","e","ei","en","eng","er", "fa","fan","fang","fei","fen","feng","fo","fou","fu","ga","gai","gan","gang","gao","ge","gei","gen","geng","gong","gou","gu","gua","guai", "guan","guang","gui","gun","guo","ha","hai","han","hang","hao","he","hei","hen","heng","hong","hou","hu","hua","huai","huan","huang", "hui","hun","huo","ji","jia","jian","jiang","jiao","jie","jin","jing","jiong","jiu","ju","juan","jue","jun","ka","kai","kan","kang", "kao","ke","kei","ken","keng","kong","kou","ku","kua","kuai","kuan","kuang","kui","kun","kuo","la","lai","lan","lang","lao","le","lei","leng", "li","lia","lian","liang","liao","lie","lin","ling","liu","lo","long","lou","lu","luan","lun","luo","lü","lüe","ma","mai","man","mang","mao", "me","mei","men","meng","mi","mian","miao","mie","min","ming","miu","mo","mou","mu","na","nai","nan","nang","nao","ne","nei","nen","neng","ni", "nian","niang","niao","nie","nin","ning","niu","nong","nou","nu","nuan","nuo","nü","nüe","o","ou","pa","pai","pan","pang","pao","pei","pen","peng", "pi","pian","piao","pie","pin","ping","po","pou","pu","qi","qia","qian","qiang","qiao","qie","qin","qing","qiong","qiu","qu","quan","que","qun", "ran","rang","rao","re","ren","reng","ri","rong","rou","ru","rua","ruan","rui","run","ruo","sa","sai","san","sang","sao","se","sen","seng", "sha","shai","shan","shang","shao","she","shei","shen","sheng","shi","shou","shu","shua","shuai","shuan","shuang","shui","shun", "shuo","si","song","sou","su","suan","sui","sun","suo","ta","tai","tan","tang","tao","te","teng","ti","tian","tiao","tie","ting","tong","tou", "tu","tuan","tui","tun","tuo","wa","wai","wan","wang","wei","wen","weng","wo","wu","xi","xia","xian","xiang","xiao","xie","xin","xing","xiong", "xiu","xu","xuan","xue","xun","ya","yan","yang","yao","ye","yi","yin","ying","yong","you","yu","yuan","yue","yun","za","zai","zan","zang", "zao","ze","zei","zen","zeng","zha","zhai","zhan","zhang","zhao","zhe","zhei","zhen","zheng","zhi","zhong","zhou","zhu","zhua","zhuai", "zhuan","zhuang","zhui","zhun","zhuo","zi","zong","zou","zu","zuan","zui","zun","zuo" ] @syllable_trie = new trie_class() @syllable_trie.insert a for a in syllables @filter() class word_search_class word_data: __word_data__ result_limit: 150 make_result_html: (data) -> glossary = data[2].join("; ").replace /\"/g, "'" attributes = (if 1 == data[0].length then " class=\"single\"" else "") "
#{data[0]}
#{data[1]} \"#{glossary}\"" reset: -> dom.word_input.value = "" dom.word_results.innerHTML = "" filter: => dom.word_results.innerHTML = "" values = dom.word_input.value.split(",").map (a) -> a.trim() values = values.filter (a) -> a.length > 0 return unless values.length regexps = values.map((value) -> if dom.search_split.checked and !dom.search_translations.checked characters = Array.from value.replace(/[^\u4E00-\u9FA5]/ig, "") words = [] i = 0 while i < characters.length j = i + 1 while j < Math.min(i + 5, characters.length) + 1 words.push characters.slice(i, j).join("") j += 1 i += 1 regexp = new RegExp("(^" + words.join("$)|(^") + "$)") (entry) -> regexp.test entry[0] else if /[a-zA-Z0-9]/.test(value) if dom.search_translations.checked if value.length > 2 regexp = new RegExp(value.replace(/u/ig, "(u|ü)"), "i") (entry) -> entry[2].some((a) -> regexp.test(a)) else length_limit = value.length * 2.5 regexp = new RegExp("\\b" + value, "i") (entry) -> length_limit >= entry[1].length and (regexp.test(entry[1]) or regexp.test(entry[1].replace(/[0-4]/g, ""))) else if !dom.search_translations.checked regexp = new RegExp(value) (entry) -> regexp.test(entry[0]) ).filter((a) -> a) html = "" matches_count = 0 for entry in @word_data break unless matches_count < @result_limit for matcher in regexps if matcher entry html += @make_result_html entry matches_count += 1 dom.word_results.innerHTML = html || "no word results" constructor: (app) -> param_input = app.url_params.get "word_input" dom.character_input.value = param_input if param_input filter_debounced = debounce @filter, 150 dom.word_reset.addEventListener "click", @reset dom.word_input.addEventListener "keyup", filter_debounced dom.word_input.addEventListener "change", @filter dom.search_translations.addEventListener "change", @filter dom.search_split.addEventListener "change", @filter dom.word_results.addEventListener "click", (event) -> # make a character search when clicking on single character words target = event.target if target.classList.contains "single" char = target.innerHTML unless dom.character_input.value.includes char dom.character_input.value = char app.character_search.filter() @filter class app_class constructor: -> dom.toggle_search_type.checked = false dom.about_link.addEventListener "click", -> dom.about.classList.toggle "hidden" dom.about_link_close.addEventListener "click", -> dom.about.classList.toggle "hidden" dom.toggle_search_type.addEventListener "change", (event) -> dom.filter.classList.toggle "search_character_active" @url_params = new URLSearchParams window.location.search @character_search = new character_search_class @ @word_search = new word_search_class @ new app_class()