WIP anthy engine.
authorHuang Peng <shawn.p.huang@gmail.com>
Wed, 11 Jun 2008 10:01:32 +0000 (18:01 +0800)
committerHuang Peng <shawn.p.huang@gmail.com>
Wed, 11 Jun 2008 10:01:32 +0000 (18:01 +0800)
engine/anthy/Makefile
engine/anthy/engine.py [new file with mode: 0644]
engine/anthy/factory.py [new file with mode: 0644]
engine/anthy/tables.py [new file with mode: 0644]

index 36dbf74..70d0428 100644 (file)
@@ -1,8 +1,10 @@
-all: _anthy.so
+all: _anthy.so test
 
 anthy.py anthy_wrap.c: anthy.i
        swig -python -I/usr/include $<
 _anthy.so: anthy_wrap.c
        $(CC) -shared -o $@ $^ -fPIC `python-config --cflags --libs` `pkg-config anthy --cflags --libs`
+test:
+       PYTHONPATH=../.. python main.py
 clean:
        $(RM) anthy.py anthy_wrap.c _anthy.so
diff --git a/engine/anthy/engine.py b/engine/anthy/engine.py
new file mode 100644 (file)
index 0000000..faf14d6
--- /dev/null
@@ -0,0 +1,257 @@
+#!/usr/bin/env python
+import gobject
+import gtk
+import pango
+import dbus
+import ibus
+import anthy
+from tables import *
+from ibus import keysyms
+from ibus import interface
+
+class Engine (interface.IEngine):
+       def __init__ (self, dbusconn, object_path):
+               interface.IEngine.__init__ (self, dbusconn, object_path)
+               self._dbusconn = dbusconn
+               
+               # create anthy context
+               self._context = anthy.anthy_context ()
+               self._context._set_encoding (2)
+
+               self._lookup_table = ibus.LookupTable ()
+               self._prop_list = ibus.PropList ()
+               
+               # use reset to init values
+               self._reset ()
+       
+       # reset values of engine
+       def _reset (self):
+               self._input_chars = u""
+               self._convert_chars = u""
+               self._cursor_pos = 0
+               self._need_update = False
+               self._convert_begined = False
+               self._segments = []
+               self._lookup_table.clean ()
+
+       # begine convert
+       def _begin_convert (self):
+               if self._convert_begined:
+                       return
+               self._convert_begined = True
+
+               self._context.set_string (self._input_chars.encode ("utf-8"))
+               conv_stat = anthy.anthy_conv_stat ()
+               self._context.get_stat (conv_stat)
+               
+               for i in xrange (0, conv_stat.nr_segment):
+                       buf = " " * 100
+                       l = self._context.get_segment (i, 0, buf, 100)
+                       text = unicode (buf[:l], "utf-8")
+                       self._segments.append ((0, text))
+               
+               self._cursor_pos = 0
+
+               self._fill_lookup_table ()
+
+       def _fill_lookup_table ():
+               # get segment stat
+               seg_stat = anthy.anthy_segment_stat ()
+               self._context.get_segment_stat (self._current_pos, seg_stat)
+               
+               # fill lookup_table
+               self._lookup_table.clean ()
+               for i in xrange (0, seg_stat.nr_candidate):
+                       buf = " " * 100
+                       l = self._context.get_segment (self._cursor_pos, i, buf, 100)
+                       candidate = unicode (buf[:l], "utf-8")
+                       self._lookup_table.append_candidate (candidate)
+
+       def _process_key_event (self, keyval, is_press, state):
+               # ignore key release events
+               if not is_press:
+                       return False
+
+               if self._input_chars:
+                       if keyval == keysyms.Return:
+                               self._commit_string (self._input_chars)
+                               return True
+                       elif keyval == keysyms.Escape:
+                               self._reset ()
+                               return True
+                       elif keyval == keysyms.BackSpace:
+                               self._input_chars = self._input_chars[:self._cursor_pos - 1] + self._input_chars [self._cursor_pos:]
+                               self._invalidate ()
+                               return True
+                       elif keyval == keysyms.space:
+                               if not self._convert_begined:
+                                       self._begine_convert ()
+                                       self._invalidate ()
+                               else:
+                                       self._cursor_down ()
+                               return True
+                       elif keyval >= keysyms._1 and keyval <= keysyms._9:
+                               index = keyval - keysyms._1
+                               candidates = self._lookup_table.get_canidates_in_current_page ()
+                               if index >= len (candidates):
+                                       return False
+                               candidate = candidates[index][0]
+                               self._commit_string (candidate)
+                               return True
+                       elif keyval == keysyms.Page_Up or keyval == keysyms.KP_Page_Up:
+                               if self._lookup_table.page_up ():
+                                       self._update_lookup_table ()
+                               return True
+                       elif keyval == keysyms.Up:
+                               self._cursor_up ()
+                               return True
+                       elif keyval == keysyms.Down:
+                               self._cursor_down ()
+                               return True
+                       elif keyval == keysyms.Left or keyval == keysyms.Right:
+                               return True
+                       elif keyval == keysyms.Page_Down or keyval == keysyms.KP_Page_Down:
+                               if self._lookup_table.page_down ():
+                                       self._update_lookup_table ()
+                               return True
+               if keyval in xrange (keysyms.a, keysyms.z + 1) or \
+                       keyval in xrange (keysyms.A, keysyms.Z + 1):
+                       if state & (keysyms.CONTROL_MASK | keysyms.ALT_MASK) == 0:
+                               self._input_chars += unichr (keyval)
+                               self._cursor_pos += 1
+                               self._invalidate ()
+                               return True
+               else:
+                       if keyval < 128 and self._input_chars:
+                               self._commit_string (self._input_chars)
+
+               return False
+
+       def _invalidate (self):
+               if self._need_update:
+                       return
+               self._need_update = True
+               gobject.idle_add (self._update, priority = gobject.PRIORITY_LOW)
+
+       def _cursor_up (self):
+               # only process cursor down in convert mode
+               if not self._convert_begined:
+                       return False
+               
+               if not self._lookup_table.cursor_up ():
+                       return False
+
+               candidate = self._lookup_table.get_current_candidate ()[0]
+               index = self._lookup_table.get_cursor_pos ()
+               self._segments[self._cursor_pos] = index, candidate
+               self._invalidate ()
+               return True
+
+       def _cursor_down (self):
+               # only process cursor down in convert mode
+               if not self._convert_begined:
+                       return False
+               
+               if not self._lookup_table.cursor_down ():
+                       return False
+
+               candidate = self._lookup_table.get_current_candidate ()[0]
+               index = self._lookup_table.get_cursor_pos ()
+               self._segments[self._cursor_pos] = index, candidate
+               self._invalidate ()
+               return True
+
+       def _commit_string (self, text):
+               self._reset ()
+               self.CommitString (text)
+               self._update ()
+
+       def _update_input_chars (self):
+               begin, end  = max (self._cursor_pos - 4, 0), self._cursor_pos
+               
+               for i in range (begin, end):
+                       text = self._input_chars[i:end]
+                       romja = romaji_typing_rule.get (text, None)
+                       if romja != None:
+                               self._input_chars = u"".join ((self._input_chars[:i], romja, self._input_chars[end:]))
+                               self._cursor_pos -= len(text)
+                               self._cursor_pos += len(romja)
+
+               attrs = ibus.AttrList ()
+               attrs.append (ibus.AttributeUnderline (pango.UNDERLINE_SINGLE, 0, len (self._input_chars.encode ("utf-8"))))
+       
+               self.UpdatePreedit (dbus.String (self._input_chars), 
+                               attrs.to_dbus_value (),
+                               dbus.Int32 (self._cursor_pos),
+                               len (self._input_chars) > 0)
+
+       def _update_convert_chars (self):
+               self._convert_chars = u""
+               buf = " " * 100
+               pos = 0
+               i = 0
+               for seg_index, text in self._segments:
+                       self._convert_chars += text
+                       if i < self._cursor_pos:
+                               pos += len (text)
+                       i += 1
+               
+               attrs = ibus.AttrList ()
+               attrs.append (ibus.AttributeUnderline (pango.UNDERLINE_SINGLE, 0, len (self._convert_chars.encode ("utf-8"))))
+               
+               self.UpdatePreedit (dbus.String (self._convert_chars), 
+                               attrs.to_dbus_value (),
+                               dbus.Int32 (pos),
+                               True)
+
+       def _update (self):
+               self._need_update = False
+               if self._convert_begined:
+                       self._update_input_chars ()
+               else:
+                       self._update_convert_chars ()
+
+       # methods for dbus rpc
+       def ProcessKeyEvent (self, keyval, is_press, state):
+               try:
+                       return self._process_key_event (keyval, is_press, state)
+               except Exception, e:
+                       print e
+               return False
+
+       def FocusIn (self):
+               self.RegisterProperties (self._prop_list.to_dbus_value ())
+               print "FocusIn"
+
+       def FocusOut (self):
+               print "FocusOut"
+
+       def SetCursorLocation (self, x, y, w, h):
+               pass
+
+       def Reset (self):
+               print "Reset"
+
+       def PageUp (self):
+               print "PageUp"
+
+       def PageDown (self):
+               print "PageDown"
+
+       def CursorUp (self):
+               self._cursor_up ()
+
+       def CursorDown (self):
+               self._cursor_down ()
+
+       def SetEnable (self, enable):
+               self._enable = enable
+               if self._enable:
+                       self.RegisterProperties (self._prop_list.to_dbus_value ())
+
+       def PropertyActivate (self, prop_name):
+               print "PropertyActivate (%s)" % prop_name
+
+       def Destroy (self):
+               print "Destroy"
+
diff --git a/engine/anthy/factory.py b/engine/anthy/factory.py
new file mode 100644 (file)
index 0000000..62f697c
--- /dev/null
@@ -0,0 +1,33 @@
+from ibus import interface
+import engine
+
+FACTORY_PATH = "/com/redhat/IBus/engines/Anthy/Factory"
+ENGINE_PATH = "/com/redhat/IBus/engines/Anthy/Engine/%d"
+
+class DemoEngineFactory (interface.IEngineFactory):
+       NAME = "AnthyEngine"
+       LANG = "ja"
+       ICON = ""
+       AUTHORS = "Huang Peng <shawn.p.huang@gmail.com>"
+       CREDITS = "GPLv2"
+
+       def __init__ (self, dbusconn):
+               interface.IEngineFactory.__init__ (self, dbusconn, object_path = FACTORY_PATH)
+               self._dbusconn = dbusconn
+               self._max_engine_id = 1
+       
+       def GetInfo (self):
+               return [
+                       self.NAME,
+                       self.LANG,
+                       self.ICON,
+                       self.AUTHORS,
+                       self.CREDITS
+                       ]
+
+       def CreateEngine (self):
+               engine_path = ENGINE_PATH % self._max_engine_id
+               self._max_engine_id += 1
+               return engine.Engine (self._dbusconn, engine_path)
+
+
diff --git a/engine/anthy/tables.py b/engine/anthy/tables.py
new file mode 100644 (file)
index 0000000..e01a464
--- /dev/null
@@ -0,0 +1,331 @@
+# -*- encoding: utf-8 -*-
+
+# string, result, cont
+romaji_typing_rule = {
+    u"-" : u"ー",
+    u"a" : u"あ",
+    u"i" : u"い",
+    u"u" : u"う",
+    u"e" : u"え",
+    u"o" : u"お",
+    u"xa" : u"ぁ",
+    u"xi" : u"ぃ",
+    u"xu" : u"ぅ",
+    u"xe" : u"ぇ",
+    u"xo" : u"ぉ",
+    u"la" : u"ぁ",
+    u"li" : u"ぃ",
+    u"lu" : u"ぅ",
+    u"le" : u"ぇ",
+    u"lo" : u"ぉ",
+    u"wi" : u"うぃ",
+    u"we" : u"うぇ",
+    u"wha" : u"うぁ",
+    u"whi" : u"うぃ",
+    u"whe" : u"うぇ",
+    u"who" : u"うぉ",
+    u"va" : u"ヴぁ",
+    u"vi" : u"ヴぃ",
+    u"vu" : u"ヴ",
+    u"ve" : u"ヴぇ",
+    u"vo" : u"ヴぉ",
+    u"ka" : u"か",
+    u"ki" : u"き",
+    u"ku" : u"く",
+    u"ke" : u"け",
+    u"ko" : u"こ",
+    u"ga" : u"が",
+    u"gi" : u"ぎ",
+    u"gu" : u"ぐ",
+    u"ge" : u"げ",
+    u"go" : u"ご",
+    u"kya" : u"きゃ",
+    u"kyi" : u"きぃ",
+    u"kyu" : u"きゅ",
+    u"kye" : u"きぇ",
+    u"kyo" : u"きょ",
+    u"gya" : u"ぎゃ",
+    u"gyi" : u"ぎぃ",
+    u"gyu" : u"ぎゅ",
+    u"gye" : u"ぎぇ",
+    u"gyo" : u"ぎょ",
+    u"sa" : u"さ",
+    u"si" : u"し",
+    u"su" : u"す",
+    u"se" : u"せ",
+    u"so" : u"そ",
+    u"za" : u"ざ",
+    u"zi" : u"じ",
+    u"zu" : u"ず",
+    u"ze" : u"ぜ",
+    u"zo" : u"ぞ",
+    u"sya" : u"しゃ",
+    u"syi" : u"しぃ",
+    u"syu" : u"しゅ",
+    u"sye" : u"しぇ",
+    u"syo" : u"しょ",
+    u"sha" : u"しゃ",
+    u"shi" : u"し",
+    u"shu" : u"しゅ",
+    u"she" : u"しぇ",
+    u"sho" : u"しょ",
+    u"zya" : u"じゃ",
+    u"zyi" : u"じぃ",
+    u"zyu" : u"じゅ",
+    u"zye" : u"じぇ",
+    u"zyo" : u"じょ",
+    u"ja" : u"じゃ",
+    u"jya" : u"じゃ",
+    u"ji" : u"じ",
+    u"jyi" : u"じぃ",
+    u"ju" : u"じゅ",
+    u"jyu" : u"じゅ",
+    u"je" : u"じぇ",
+    u"jye" : u"じぇ",
+    u"jo" : u"じょ",
+    u"jyo" : u"じょ",
+    u"ta" : u"た",
+    u"ti" : u"ち",
+    u"tu" : u"つ",
+    u"tsu" : u"つ",
+    u"te" : u"て",
+    u"to" : u"と",
+    u"da" : u"だ",
+    u"di" : u"ぢ",
+    u"du" : u"づ",
+    u"de" : u"で",
+    u"do" : u"ど",
+    u"xtu" : u"っ",
+    u"xtsu" : u"っ",
+    u"ltu" : u"っ",
+    u"ltsu" : u"っ",
+    u"tya" : u"ちゃ",
+    u"tyi" : u"ちぃ",
+    u"tyu" : u"ちゅ",
+    u"tye" : u"ちぇ",
+    u"tyo" : u"ちょ",
+    u"cha" : u"ちゃ",
+    u"chi" : u"ち",
+    u"chu" : u"ちゅ",
+    u"che" : u"ちぇ",
+    u"cho" : u"ちょ",
+    u"dya" : u"ぢゃ",
+    u"dyi" : u"ぢぃ",
+    u"dyu" : u"ぢゅ",
+    u"dye" : u"ぢぇ",
+    u"dyo" : u"ぢょ",
+    u"tha" : u"てゃ",
+    u"thi" : u"てぃ",
+    u"thu" : u"てゅ",
+    u"the" : u"てぇ",
+    u"tho" : u"てょ",
+    u"dha" : u"でゃ",
+    u"dhi" : u"でぃ",
+    u"dhu" : u"でゅ",
+    u"dhe" : u"でぇ",
+    u"dho" : u"でょ",
+    u"na" : u"な",
+    u"ni" : u"に",
+    u"nu" : u"ぬ",
+    u"ne" : u"ね",
+    u"no" : u"の",
+    u"nya" : u"にゃ",
+    u"nyi" : u"にぃ",
+    u"nyu" : u"にゅ",
+    u"nye" : u"にぇ",
+    u"nyo" : u"にょ",
+    u"ha" : u"は",
+    u"hi" : u"ひ",
+    u"hu" : u"ふ",
+    u"fu" : u"ふ",
+    u"he" : u"へ",
+    u"ho" : u"ほ",
+    u"ba" : u"ば",
+    u"bi" : u"び",
+    u"bu" : u"ぶ",
+    u"be" : u"べ",
+    u"bo" : u"ぼ",
+    u"pa" : u"ぱ",
+    u"pi" : u"ぴ",
+    u"pu" : u"ぷ",
+    u"pe" : u"ぺ",
+    u"po" : u"ぽ",
+    u"hya" : u"ひゃ",
+    u"hyi" : u"ひぃ",
+    u"hyu" : u"ひゅ",
+    u"hye" : u"ひぇ",
+    u"hyo" : u"ひょ",
+    u"bya" : u"びゃ",
+    u"byi" : u"びぃ",
+    u"byu" : u"びゅ",
+    u"bye" : u"びぇ",
+    u"byo" : u"びょ",
+    u"pya" : u"ぴゃ",
+    u"pyi" : u"ぴぃ",
+    u"pyu" : u"ぴゅ",
+    u"pye" : u"ぴぇ",
+    u"pyo" : u"ぴょ",
+    u"fa" : u"ふぁ",
+    u"fi" : u"ふぃ",
+    u"fu" : u"ふ",
+    u"fe" : u"ふぇ",
+    u"fo" : u"ふぉ",
+    u"ma" : u"ま",
+    u"mi" : u"み",
+    u"mu" : u"む",
+    u"me" : u"め",
+    u"mo" : u"も",
+    u"mya" : u"みゃ",
+    u"myi" : u"みぃ",
+    u"myu" : u"みゅ",
+    u"mye" : u"みぇ",
+    u"myo" : u"みょ",
+    u"lya" : u"ゃ",
+    u"xya" : u"ゃ",
+    u"ya" : u"や",
+    u"lyu" : u"ゅ",
+    u"xyu" : u"ゅ",
+    u"yu" : u"ゆ",
+    u"lyo" : u"ょ",
+    u"xyo" : u"ょ",
+    u"yo" : u"よ",
+    u"ra" : u"ら",
+    u"ri" : u"り",
+    u"ru" : u"る",
+    u"re" : u"れ",
+    u"ro" : u"ろ",
+    u"rya" : u"りゃ",
+    u"ryi" : u"りぃ",
+    u"ryu" : u"りゅ",
+    u"rye" : u"りぇ",
+    u"ryo" : u"りょ",
+    u"xwa" : u"ゎ",
+    u"wa" : u"わ",
+    u"wo" : u"を",
+# u"n'" : u"ん",
+    u"nn" : u"ん",
+    u"wyi" : u"ゐ",
+    u"wye" : u"ゑ",
+}
+
+#hiragana, katakana, half_katakana
+hiragana_katakana_table = { 
+    u"あ" : (u"ア", u"ア"),
+    u"い" : (u"イ", u"イ"),
+    u"う" : (u"ウ", u"ウ"),
+    u"え" : (u"エ", u"エ"),
+    u"お" : (u"オ", u"オ"),
+    u"か" : (u"カ", u"カ"),
+    u"き" : (u"キ", u"キ"),
+    u"く" : (u"ク", u"ク"),
+    u"け" : (u"ケ", u"ケ"),
+    u"こ" : (u"コ", u"コ"),
+    u"が" : (u"ガ", u"ガ"),
+    u"ぎ" : (u"ギ", u"ギ"),
+    u"ぐ" : (u"グ", u"グ"),
+    u"げ" : (u"ゲ", u"ゲ"),
+    u"ご" : (u"ゴ", u"ゴ"),
+    u"さ" : (u"サ", u"サ"),
+    u"し" : (u"シ", u"シ"),
+    u"す" : (u"ス", u"ス"),
+    u"せ" : (u"セ", u"セ"),
+    u"そ" : (u"ソ", u"ソ"),
+    u"ざ" : (u"ザ", u"ザ"),
+    u"じ" : (u"ジ", u"ジ"),
+    u"ず" : (u"ズ", u"ズ"),
+    u"ぜ" : (u"ゼ", u"ゼ"),
+    u"ぞ" : (u"ゾ", u"ゾ"),
+    u"た" : (u"タ", u"タ"),
+    u"ち" : (u"チ", u"チ"),
+    u"つ" : (u"ツ", u"ツ"),
+    u"て" : (u"テ", u"テ"),
+    u"と" : (u"ト", u"ト"),
+    u"だ" : (u"ダ", u"ダ"),
+    u"ぢ" : (u"ヂ", u"ヂ"),
+    u"づ" : (u"ヅ", u"ヅ"),
+    u"で" : (u"デ", u"デ"),
+    u"ど" : (u"ド", u"ド"),
+    u"な" : (u"ナ", u"ナ"),
+    u"に" : (u"ニ", u"ニ"),
+    u"ぬ" : (u"ヌ", u"ヌ"),
+    u"ね" : (u"ネ", u"ネ"),
+    u"の" : (u"ノ", u"ノ"),
+    u"は" : (u"ハ", u"ハ"),
+    u"ひ" : (u"ヒ", u"ヒ"),
+    u"ふ" : (u"フ", u"フ"),
+    u"へ" : (u"ヘ", u"ヘ"),
+    u"ほ" : (u"ホ", u"ホ"),
+    u"ば" : (u"バ", u"バ"),
+    u"び" : (u"ビ", u"ビ"),
+    u"ぶ" : (u"ブ", u"ブ"),
+    u"べ" : (u"ベ", u"ベ"),
+    u"ぼ" : (u"ボ", u"ボ"),
+    u"ぱ" : (u"パ", u"パ"),
+    u"ぴ" : (u"ピ", u"ピ"),
+    u"ぷ" : (u"プ", u"プ"),
+    u"ぺ" : (u"ペ", u"ペ"),
+    u"ぽ" : (u"ポ", u"ポ"),
+    u"ま" : (u"マ", u"マ"),
+    u"み" : (u"ミ", u"ミ"),
+    u"む" : (u"ム", u"ム"),
+    u"め" : (u"メ", u"メ"),
+    u"も" : (u"モ", u"モ"),
+    u"や" : (u"ヤ", u"ヤ"),
+    u"ゆ" : (u"ユ", u"ユ"),
+    u"よ" : (u"ヨ", u"ヨ"),
+    u"ら" : (u"ラ", u"ラ"),
+    u"り" : (u"リ", u"リ"),
+    u"る" : (u"ル", u"ル"),
+    u"れ" : (u"レ", u"レ"),
+    u"ろ" : (u"ロ", u"ロ"),
+    u"わ" : (u"ワ", u"ワ"),
+    u"を" : (u"ヲ", u"ヲ"),
+    u"ん" : (u"ン", u"ン"),
+    u"ぁ" : (u"ァ", u"ァ"),
+    u"ぃ" : (u"ィ", u"ィ"),
+    u"ぅ" : (u"ゥ", u"ゥ"),
+    u"ぇ" : (u"ェ", u"ェ"),
+    u"ぉ" : (u"ォ", u"ォ"),
+    u"っ" : (u"ッ", u"ッ"),
+    u"ゃ" : (u"ャ", u"ャ"),
+    u"ゅ" : (u"ュ", u"ュ"),
+    u"ょ" : (u"ョ", u"ョ"),
+    u"ヵ" : (u"ヵ", u"カ"),
+    u"ヶ" : (u"ヶ", u"ケ"),
+    u"ゎ" : (u"ヮ", u"ワ"),
+    u"ゐ" : (u"ヰ", u"ィ"),
+    u"ゑ" : (u"ヱ", u"ェ"),
+    u"ヴ" : (u"ヴ", u"ヴ"),
+    u"ー" : (u"ー", u"ー"),
+    u"、" : (u"、", u"、"),
+    u"。" : (u"。", u"。"),
+    u"!" : (u"!", u"!"),
+    u"”" : (u"”", u"\""),
+    u"#" : (u"#", u"#"),
+    u"$" : (u"$", u"$"),
+    u"%" : (u"%", u"%"),
+    u"&" : (u"&", u"&"),
+    u"’" : (u"’", u"'"),
+    u"(" : (u"(", u""),
+    u")" : (u")", u")"),
+    u"〜" : (u"〜", u"~"),
+    u"=" : (u"=", u"="),
+    u"^" : (u"^", u"u"),
+    u"\" : (u"\", u"\\"),
+    u"|" : (u"|", u"|"),
+    u"‘" : (u"‘", u"`"),
+    u"@" : (u"@", u"@"),
+    u"{" : (u"{", u""),
+    u"「" : (u"「", u"「"),
+    u"+" : (u"+", u"+"),
+    u";" : (u";", u";"),
+    u"*" : (u"*", u"*"),
+    u":" : (u":", u" : u"),
+    u"}" : (u"}", u")"),
+    u"」" : (u"」", u"」"),
+    u"<" : (u"<", u"<"),
+    u">" : (u">", u">"),
+    u"?" : (u"?", u"?"),
+    u"/" : (u"/", u"/"),
+    u"_" : (u"_", u"_"),
+}